quicsilver 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,9 @@ static VALUE mQuicsilver;
8
8
 
9
9
  // Event queue for callbacks
10
10
  typedef struct CallbackEvent {
11
+ HQUIC connection;
12
+ void* connection_ctx; // ConnectionContext pointer for building connection_data
13
+ VALUE client_obj; // Ruby client object (for routing callbacks)
11
14
  char* event_type;
12
15
  uint64_t stream_id;
13
16
  char* data;
@@ -34,6 +37,7 @@ typedef struct {
34
37
  int failed;
35
38
  QUIC_STATUS error_status;
36
39
  uint64_t error_code;
40
+ VALUE client_obj; // Ruby client object (Qnil for server connections)
37
41
  } ConnectionContext;
38
42
 
39
43
  // Listener state tracking
@@ -47,18 +51,25 @@ typedef struct {
47
51
 
48
52
  // Stream state tracking
49
53
  typedef struct {
54
+ HQUIC connection;
55
+ void* connection_ctx; // ConnectionContext pointer (for building connection_data)
56
+ VALUE client_obj; // Ruby client object (copied from connection context)
50
57
  int started;
51
58
  int shutdown;
52
59
  QUIC_STATUS error_status;
53
60
  } StreamContext;
54
61
 
55
62
  // Enqueue a callback event (thread-safe)
63
+ // NOTE: Called from QUIC callback threads without GVL - cannot use Ruby API here
56
64
  static void
57
- enqueue_callback_event(const char* event_type, uint64_t stream_id, const char* data, size_t data_len)
65
+ enqueue_callback_event(HQUIC connection, void* connection_ctx, VALUE client_obj, const char* event_type, uint64_t stream_id, const char* data, size_t data_len)
58
66
  {
59
67
  CallbackEvent* event = (CallbackEvent*)malloc(sizeof(CallbackEvent));
60
68
  if (event == NULL) return;
61
69
 
70
+ event->connection = connection;
71
+ event->connection_ctx = connection_ctx;
72
+ event->client_obj = client_obj; // Store raw VALUE - will validate before use
62
73
  event->event_type = strdup(event_type);
63
74
  event->stream_id = stream_id;
64
75
  event->data = (char*)malloc(data_len);
@@ -78,15 +89,35 @@ enqueue_callback_event(const char* event_type, uint64_t stream_id, const char* d
78
89
  pthread_mutex_unlock(&queue_mutex);
79
90
  }
80
91
 
92
+ // Free a single event
93
+ static void
94
+ free_event(CallbackEvent* event)
95
+ {
96
+ if (event == NULL) return;
97
+ free(event->event_type);
98
+ free(event->data);
99
+ free(event);
100
+ }
101
+
81
102
  // Process all pending callback events (called from Ruby)
82
103
  static VALUE
83
104
  quicsilver_process_events(VALUE self)
84
105
  {
85
106
  CallbackEvent* event;
86
- VALUE server_class = rb_const_get(mQuicsilver, rb_intern("Server"));
107
+ VALUE server_class = Qnil;
87
108
  int processed = 0;
88
109
 
89
- while (1) {
110
+ // Get Server class for server events (client_obj == Qnil)
111
+ server_class = rb_const_get_at(mQuicsilver, rb_intern("Server"));
112
+ if (rb_class_real(CLASS_OF(server_class)) != rb_cClass) {
113
+ server_class = Qnil;
114
+ }
115
+
116
+ // Process events in a loop, but limit iterations to avoid GVL starvation
117
+ int max_iterations = 100;
118
+ int iteration = 0;
119
+
120
+ while (iteration++ < max_iterations) {
90
121
  pthread_mutex_lock(&queue_mutex);
91
122
  event = event_queue_head;
92
123
  if (event != NULL) {
@@ -99,17 +130,42 @@ quicsilver_process_events(VALUE self)
99
130
 
100
131
  if (event == NULL) break;
101
132
 
102
- if (!NIL_P(server_class)) {
103
- rb_funcall(server_class, rb_intern("handle_stream"), 3,
104
- ULL2NUM(event->stream_id),
105
- rb_str_new_cstr(event->event_type),
106
- rb_str_new(event->data, event->data_len));
133
+ // Route based on client_obj:
134
+ // - If Qnil: server event, route to Server.handle_stream with connection_data
135
+ // - If not Qnil: client event, route to client_obj.handle_stream_event
136
+ int handled = 0;
137
+
138
+ if (NIL_P(event->client_obj)) {
139
+ // Server event - build connection_data array [connection_handle, context_ptr]
140
+ if (!NIL_P(server_class)) {
141
+ VALUE connection_data = rb_ary_new2(2);
142
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)event->connection));
143
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)event->connection_ctx));
144
+
145
+ rb_funcall(server_class, rb_intern("handle_stream"), 4,
146
+ connection_data,
147
+ ULL2NUM(event->stream_id),
148
+ rb_str_new_cstr(event->event_type),
149
+ rb_str_new(event->data, event->data_len));
150
+ handled = 1;
151
+ }
152
+ } else {
153
+ // Client event - validate object is a real Ruby object before calling
154
+ // This catches use-after-free when connection was closed but events still queued
155
+ if (RB_TYPE_P(event->client_obj, T_OBJECT)) {
156
+ rb_funcall(event->client_obj, rb_intern("handle_stream_event"), 3,
157
+ ULL2NUM(event->stream_id),
158
+ rb_str_new_cstr(event->event_type),
159
+ rb_str_new(event->data, event->data_len));
160
+ handled = 1;
161
+ }
162
+ }
163
+
164
+ if (handled) {
107
165
  processed++;
108
166
  }
109
167
 
110
- free(event->event_type);
111
- free(event->data);
112
- free(event);
168
+ free_event(event);
113
169
  }
114
170
 
115
171
  return INT2NUM(processed);
@@ -144,15 +200,19 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
144
200
  if (combined != NULL) {
145
201
  memcpy(combined, &Stream, sizeof(HQUIC));
146
202
  memcpy(combined + sizeof(HQUIC), buffer->Buffer, buffer->Length);
147
- enqueue_callback_event(event_type, stream_id, combined, total_len);
203
+ enqueue_callback_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_len);
148
204
  free(combined);
149
205
  }
150
206
  } else {
151
- enqueue_callback_event(event_type, stream_id, (const char*)buffer->Buffer, buffer->Length);
207
+ enqueue_callback_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, (const char*)buffer->Buffer, buffer->Length);
152
208
  }
153
209
  }
154
210
  break;
155
211
  case QUIC_STREAM_EVENT_SEND_COMPLETE:
212
+ // Free the send buffer that was allocated in quicsilver_send_stream
213
+ if (Event->SEND_COMPLETE.ClientContext != NULL) {
214
+ free(Event->SEND_COMPLETE.ClientContext);
215
+ }
156
216
  break;
157
217
  case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
158
218
  ctx->shutdown = 1;
@@ -162,7 +222,7 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
162
222
  case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
163
223
  break;
164
224
  }
165
-
225
+
166
226
  return QUIC_STATUS_SUCCESS;
167
227
  }
168
228
 
@@ -182,8 +242,8 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
182
242
  case QUIC_CONNECTION_EVENT_CONNECTED:
183
243
  ctx->connected = 1;
184
244
  ctx->failed = 0;
185
- // Notify Ruby about new connection
186
- enqueue_callback_event("CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC));
245
+ // Notify Ruby about new connection - pass ctx pointer for building connection_data
246
+ enqueue_callback_event(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC));
187
247
  break;
188
248
  case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
189
249
  ctx->connected = 0;
@@ -199,12 +259,17 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
199
259
  break;
200
260
  case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
201
261
  ctx->connected = 0;
262
+ // Notify Ruby to clean up connection resources
263
+ enqueue_callback_event(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0, (const char*)&Connection, sizeof(HQUIC));
202
264
  break;
203
265
  case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
204
266
  // Client opened a stream
205
267
  Stream = Event->PEER_STREAM_STARTED.Stream;
206
268
  stream_ctx = (StreamContext*)malloc(sizeof(StreamContext));
207
269
  if (stream_ctx != NULL) {
270
+ stream_ctx->connection = Connection;
271
+ stream_ctx->connection_ctx = ctx; // Store connection context pointer
272
+ stream_ctx->client_obj = ctx->client_obj; // Copy from connection context
208
273
  stream_ctx->started = 1;
209
274
  stream_ctx->shutdown = 0;
210
275
  stream_ctx->error_status = QUIC_STATUS_SUCCESS;
@@ -240,16 +305,18 @@ ListenerCallback(HQUIC Listener, void* Context, QUIC_LISTENER_EVENT* Event)
240
305
  conn_ctx->failed = 0;
241
306
  conn_ctx->error_status = QUIC_STATUS_SUCCESS;
242
307
  conn_ctx->error_code = 0;
243
-
308
+ conn_ctx->client_obj = Qnil; // Server connections have no client object
309
+
244
310
  // Set the connection callback
245
311
  MsQuic->SetCallbackHandler(Event->NEW_CONNECTION.Connection, (void*)ConnectionCallback, conn_ctx);
246
-
312
+
247
313
  // Accept the new connection with the server configuration
248
314
  QUIC_STATUS Status = MsQuic->ConnectionSetConfiguration(Event->NEW_CONNECTION.Connection, ctx->Configuration);
249
315
  if (QUIC_FAILED(Status)) {
250
316
  free(conn_ctx);
251
317
  return Status;
252
318
  }
319
+
253
320
  } else {
254
321
  // Reject the connection if we can't allocate context
255
322
  return QUIC_STATUS_OUT_OF_MEMORY;
@@ -355,7 +422,10 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
355
422
  VALUE peer_bidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_bidi_stream_count")));
356
423
  VALUE peer_unidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_unidi_stream_count")));
357
424
  VALUE alpn_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("alpn")));
358
-
425
+ VALUE stream_recv_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_recv_window")));
426
+ VALUE stream_recv_buffer_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_recv_buffer")));
427
+ VALUE conn_flow_control_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("conn_flow_control_window")));
428
+
359
429
  QUIC_STATUS Status;
360
430
  HQUIC Configuration = NULL;
361
431
 
@@ -366,9 +436,12 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
366
436
  uint32_t peer_bidi_stream_count = NUM2INT(peer_bidi_stream_count_val);
367
437
  uint32_t peer_unidi_stream_count = NUM2INT(peer_unidi_stream_count_val);
368
438
  const char* alpn_str = StringValueCStr(alpn_val);
439
+ uint32_t stream_recv_window = NUM2UINT(stream_recv_window_val);
440
+ uint32_t stream_recv_buffer = NUM2UINT(stream_recv_buffer_val);
441
+ uint32_t conn_flow_control_window = NUM2UINT(conn_flow_control_window_val);
369
442
 
370
443
  QUIC_SETTINGS Settings = {0};
371
- Settings.IdleTimeoutMs = idle_timeout_ms;
444
+ Settings.IdleTimeoutMs = idle_timeout_ms;
372
445
  Settings.IsSet.IdleTimeoutMs = TRUE;
373
446
  Settings.ServerResumptionLevel = server_resumption_level;
374
447
  Settings.IsSet.ServerResumptionLevel = TRUE;
@@ -377,6 +450,14 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
377
450
  Settings.PeerUnidiStreamCount = peer_unidi_stream_count;
378
451
  Settings.IsSet.PeerUnidiStreamCount = TRUE;
379
452
 
453
+ // Flow control / backpressure settings
454
+ Settings.StreamRecvWindowDefault = stream_recv_window;
455
+ Settings.IsSet.StreamRecvWindowDefault = TRUE;
456
+ Settings.StreamRecvBufferDefault = stream_recv_buffer;
457
+ Settings.IsSet.StreamRecvBufferDefault = TRUE;
458
+ Settings.ConnFlowControlWindow = conn_flow_control_window;
459
+ Settings.IsSet.ConnFlowControlWindow = TRUE;
460
+
380
461
  QUIC_BUFFER Alpn = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
381
462
 
382
463
  // Create configuration
@@ -408,35 +489,44 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
408
489
 
409
490
  // Create a QUIC connection with context
410
491
  static VALUE
411
- quicsilver_create_connection(VALUE self)
492
+ quicsilver_create_connection(VALUE self, VALUE client_obj)
412
493
  {
413
494
  if (MsQuic == NULL) {
414
495
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
415
496
  return Qnil;
416
497
  }
417
-
498
+
418
499
  QUIC_STATUS Status;
419
500
  HQUIC Connection = NULL;
420
-
501
+
421
502
  // Allocate and initialize connection context
422
503
  ConnectionContext* ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
423
504
  if (ctx == NULL) {
424
505
  rb_raise(rb_eRuntimeError, "Failed to allocate connection context");
425
506
  return Qnil;
426
507
  }
427
-
508
+
428
509
  ctx->connected = 0;
429
510
  ctx->failed = 0;
430
511
  ctx->error_status = QUIC_STATUS_SUCCESS;
431
512
  ctx->error_code = 0;
432
-
513
+ ctx->client_obj = client_obj; // Store Ruby client object (Qnil for server)
514
+
515
+ // Protect from GC if it's a Ruby object
516
+ if (!NIL_P(client_obj)) {
517
+ rb_gc_register_address(&ctx->client_obj);
518
+ }
519
+
433
520
  // Create connection with enhanced callback and context
434
521
  if (QUIC_FAILED(Status = MsQuic->ConnectionOpen(Registration, ConnectionCallback, ctx, &Connection))) {
522
+ if (!NIL_P(client_obj)) {
523
+ rb_gc_unregister_address(&ctx->client_obj);
524
+ }
435
525
  free(ctx);
436
526
  rb_raise(rb_eRuntimeError, "ConnectionOpen failed, 0x%x!", Status);
437
527
  return Qnil;
438
528
  }
439
-
529
+
440
530
  // Return both the connection handle and context as an array
441
531
  VALUE result = rb_ary_new2(2);
442
532
  rb_ary_push(result, ULL2NUM((uintptr_t)Connection));
@@ -522,27 +612,55 @@ quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
522
612
  if (MsQuic == NULL) {
523
613
  return Qnil;
524
614
  }
525
-
615
+
526
616
  // Extract connection handle and context from array
527
617
  VALUE connection_handle = rb_ary_entry(connection_data, 0);
528
618
  VALUE context_handle = rb_ary_entry(connection_data, 1);
529
-
619
+
530
620
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
531
621
  ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
532
-
533
- // Only close if connection is valid
622
+
534
623
  if (Connection != NULL) {
535
624
  MsQuic->ConnectionClose(Connection);
536
625
  }
537
-
626
+
538
627
  // Free context if valid
539
628
  if (ctx != NULL) {
629
+ // Unregister from GC if client object was set
630
+ if (!NIL_P(ctx->client_obj)) {
631
+ rb_gc_unregister_address(&ctx->client_obj);
632
+ }
540
633
  free(ctx);
541
634
  }
542
-
635
+
543
636
  return Qnil;
544
637
  }
545
638
 
639
+ // Gracefully shutdown a QUIC connection (sends CONNECTION_CLOSE frame to peer)
640
+ // silent = true: immediate shutdown without notifying peer
641
+ // silent = false: graceful shutdown with CONNECTION_CLOSE frame
642
+ static VALUE
643
+ quicsilver_connection_shutdown(VALUE self, VALUE connection_handle, VALUE error_code, VALUE silent)
644
+ {
645
+ if (MsQuic == NULL) {
646
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
647
+ return Qnil;
648
+ }
649
+
650
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
651
+ uint64_t ErrorCode = NUM2ULL(error_code);
652
+
653
+ if (Connection != NULL) {
654
+ QUIC_CONNECTION_SHUTDOWN_FLAGS flags = RTEST(silent)
655
+ ? QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT
656
+ : QUIC_CONNECTION_SHUTDOWN_FLAG_NONE;
657
+
658
+ MsQuic->ConnectionShutdown(Connection, flags, ErrorCode);
659
+ }
660
+
661
+ return Qtrue;
662
+ }
663
+
546
664
  // Close a QUIC configuration
547
665
  static VALUE
548
666
  quicsilver_close_configuration(VALUE self, VALUE config_handle)
@@ -683,15 +801,22 @@ quicsilver_close_listener(VALUE self, VALUE listener_data)
683
801
  }
684
802
 
685
803
  // Open a QUIC stream
804
+ // Accepts connection_data array [connection_handle, context_handle]
805
+ // Works uniformly for both client and server
686
806
  static VALUE
687
- quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional)
807
+ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
688
808
  {
689
809
  if (MsQuic == NULL) {
690
810
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
691
811
  return Qnil;
692
812
  }
693
813
 
814
+ // Extract connection handle and context from array
815
+ VALUE connection_handle = rb_ary_entry(connection_data, 0);
816
+ VALUE context_handle = rb_ary_entry(connection_data, 1);
817
+
694
818
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
819
+ ConnectionContext* conn_ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
695
820
  HQUIC Stream = NULL;
696
821
 
697
822
  StreamContext* ctx = (StreamContext*)malloc(sizeof(StreamContext));
@@ -699,7 +824,10 @@ quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional
699
824
  rb_raise(rb_eRuntimeError, "Failed to allocate stream context");
700
825
  return Qnil;
701
826
  }
702
-
827
+
828
+ ctx->connection = Connection;
829
+ ctx->connection_ctx = conn_ctx; // Store connection context pointer
830
+ ctx->client_obj = conn_ctx ? conn_ctx->client_obj : Qnil;
703
831
  ctx->started = 1;
704
832
  ctx->shutdown = 0;
705
833
  ctx->error_status = QUIC_STATUS_SUCCESS;
@@ -785,11 +913,12 @@ Init_quicsilver(void)
785
913
  rb_define_singleton_method(mQuicsilver, "create_server_configuration", quicsilver_create_server_configuration, 1);
786
914
  rb_define_singleton_method(mQuicsilver, "close_configuration", quicsilver_close_configuration, 1);
787
915
 
788
- // Connection management
789
- rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 0);
916
+ // Connection management
917
+ rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 1);
790
918
  rb_define_singleton_method(mQuicsilver, "start_connection", quicsilver_start_connection, 4);
791
919
  rb_define_singleton_method(mQuicsilver, "wait_for_connection", quicsilver_wait_for_connection, 2);
792
920
  rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
921
+ rb_define_singleton_method(mQuicsilver, "connection_shutdown", quicsilver_connection_shutdown, 3);
793
922
  rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
794
923
 
795
924
  // Listener management