quicsilver 0.2.0 → 0.4.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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +4 -5
  3. data/.github/workflows/cibuildgem.yaml +93 -0
  4. data/.gitignore +3 -1
  5. data/CHANGELOG.md +81 -0
  6. data/Gemfile.lock +26 -4
  7. data/README.md +95 -31
  8. data/Rakefile +95 -3
  9. data/benchmarks/components.rb +191 -0
  10. data/benchmarks/concurrent.rb +110 -0
  11. data/benchmarks/helpers.rb +88 -0
  12. data/benchmarks/quicsilver_server.rb +1 -1
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/README.md +44 -91
  16. data/examples/benchmark.rb +111 -0
  17. data/examples/connection_pool_demo.rb +47 -0
  18. data/examples/example_helper.rb +18 -0
  19. data/examples/falcon_middleware.rb +44 -0
  20. data/examples/feature_demo.rb +125 -0
  21. data/examples/grpc_style.rb +97 -0
  22. data/examples/minimal_http3_server.rb +6 -18
  23. data/examples/priorities.rb +60 -0
  24. data/examples/protocol_http_server.rb +31 -0
  25. data/examples/rack_http3_server.rb +8 -20
  26. data/examples/rails_feature_test.rb +260 -0
  27. data/examples/simple_client_test.rb +2 -2
  28. data/examples/streaming_sse.rb +33 -0
  29. data/examples/trailers.rb +69 -0
  30. data/ext/quicsilver/extconf.rb +14 -0
  31. data/ext/quicsilver/quicsilver.c +568 -181
  32. data/lib/quicsilver/client/client.rb +349 -0
  33. data/lib/quicsilver/client/connection_pool.rb +106 -0
  34. data/lib/quicsilver/client/request.rb +98 -0
  35. data/lib/quicsilver/libmsquic.2.dylib +0 -0
  36. data/lib/quicsilver/protocol/adapter.rb +176 -0
  37. data/lib/quicsilver/protocol/control_stream_parser.rb +106 -0
  38. data/lib/quicsilver/protocol/frame_parser.rb +142 -0
  39. data/lib/quicsilver/protocol/frame_reader.rb +55 -0
  40. data/lib/quicsilver/{http3.rb → protocol/frames.rb} +146 -30
  41. data/lib/quicsilver/protocol/priority.rb +56 -0
  42. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  43. data/lib/quicsilver/protocol/qpack/encoder.rb +227 -0
  44. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +140 -0
  45. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  46. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  47. data/lib/quicsilver/protocol/request_parser.rb +275 -0
  48. data/lib/quicsilver/protocol/response_encoder.rb +97 -0
  49. data/lib/quicsilver/protocol/response_parser.rb +141 -0
  50. data/lib/quicsilver/protocol/stream_input.rb +98 -0
  51. data/lib/quicsilver/protocol/stream_output.rb +59 -0
  52. data/lib/quicsilver/quicsilver.bundle +0 -0
  53. data/lib/quicsilver/server/listener_data.rb +14 -0
  54. data/lib/quicsilver/server/request_handler.rb +138 -0
  55. data/lib/quicsilver/server/request_registry.rb +50 -0
  56. data/lib/quicsilver/server/server.rb +610 -0
  57. data/lib/quicsilver/transport/configuration.rb +141 -0
  58. data/lib/quicsilver/transport/connection.rb +379 -0
  59. data/lib/quicsilver/transport/event_loop.rb +38 -0
  60. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  61. data/lib/quicsilver/transport/stream.rb +28 -0
  62. data/lib/quicsilver/transport/stream_event.rb +26 -0
  63. data/lib/quicsilver/version.rb +1 -1
  64. data/lib/quicsilver.rb +55 -14
  65. data/lib/rackup/handler/quicsilver.rb +1 -2
  66. data/quicsilver.gemspec +13 -3
  67. metadata +125 -21
  68. data/benchmarks/benchmark.rb +0 -68
  69. data/examples/setup_certs.sh +0 -57
  70. data/lib/quicsilver/client.rb +0 -261
  71. data/lib/quicsilver/connection.rb +0 -42
  72. data/lib/quicsilver/event_loop.rb +0 -38
  73. data/lib/quicsilver/http3/request_encoder.rb +0 -133
  74. data/lib/quicsilver/http3/request_parser.rb +0 -176
  75. data/lib/quicsilver/http3/response_encoder.rb +0 -186
  76. data/lib/quicsilver/http3/response_parser.rb +0 -160
  77. data/lib/quicsilver/listener_data.rb +0 -29
  78. data/lib/quicsilver/quic_stream.rb +0 -36
  79. data/lib/quicsilver/request_registry.rb +0 -48
  80. data/lib/quicsilver/server.rb +0 -355
  81. data/lib/quicsilver/server_configuration.rb +0 -78
@@ -1,26 +1,45 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/thread.h>
3
+ #define QUIC_API_ENABLE_PREVIEW_FEATURES 1
2
4
  #include "msquic.h"
3
- #include <pthread.h>
4
5
  #include <stdlib.h>
5
6
  #include <string.h>
7
+ #include <unistd.h>
8
+
9
+ #if __linux__
10
+ #include <sys/epoll.h>
11
+ #elif __APPLE__ || __FreeBSD__
12
+ #include <sys/event.h>
13
+ #endif
6
14
 
7
15
  static VALUE mQuicsilver;
8
16
 
9
- // Event queue for callbacks
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)
14
- char* event_type;
15
- uint64_t stream_id;
16
- char* data;
17
- size_t data_len;
18
- struct CallbackEvent* next;
19
- } CallbackEvent;
17
+ // Custom execution: app owns the event loop, MsQuic spawns no threads
18
+ static QUIC_EVENTQ EventQ = -1; // kqueue (macOS) / epoll (Linux)
19
+ static QUIC_EXECUTION* ExecContext = NULL;
20
+
21
+ #if __linux__
22
+ #include <sys/eventfd.h>
23
+ static int WakeFd = -1; // eventfd for waking epoll
24
+ #endif
25
+ #define WAKE_IDENT 0xCAFE // kqueue EVFILT_USER identifier (macOS only)
20
26
 
21
- static CallbackEvent* event_queue_head = NULL;
22
- static CallbackEvent* event_queue_tail = NULL;
23
- static pthread_mutex_t queue_mutex = PTHREAD_MUTEX_INITIALIZER;
27
+ // Wake the event loop from another thread (e.g. after StreamSend)
28
+ static void
29
+ wake_event_loop(void)
30
+ {
31
+ if (EventQ == -1) return;
32
+ #if __linux__
33
+ if (WakeFd != -1) {
34
+ uint64_t val = 1;
35
+ write(WakeFd, &val, sizeof(val));
36
+ }
37
+ #elif __APPLE__ || __FreeBSD__
38
+ struct kevent kev;
39
+ EV_SET(&kev, WAKE_IDENT, EVFILT_USER, 0, NOTE_TRIGGER, 0, NULL);
40
+ kevent(EventQ, &kev, 1, NULL, 0, NULL);
41
+ #endif
42
+ }
24
43
 
25
44
  // Global MSQUIC API table
26
45
  static const QUIC_API_TABLE* MsQuic = NULL;
@@ -56,119 +75,199 @@ typedef struct {
56
75
  VALUE client_obj; // Ruby client object (copied from connection context)
57
76
  int started;
58
77
  int shutdown;
78
+ int early_data; // Set when stream received 0-RTT data
59
79
  QUIC_STATUS error_status;
60
80
  } StreamContext;
61
81
 
62
- // Enqueue a callback event (thread-safe)
63
- // NOTE: Called from QUIC callback threads without GVL - cannot use Ruby API here
64
- static void
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)
66
- {
67
- CallbackEvent* event = (CallbackEvent*)malloc(sizeof(CallbackEvent));
68
- if (event == NULL) return;
69
-
70
- event->connection = connection;
71
- event->connection_ctx = connection_ctx;
72
- event->client_obj = client_obj; // Store raw VALUE - will validate before use
73
- event->event_type = strdup(event_type);
74
- event->stream_id = stream_id;
75
- event->data = (char*)malloc(data_len);
76
- if (event->data != NULL) {
77
- memcpy(event->data, data, data_len);
78
- }
79
- event->data_len = data_len;
80
- event->next = NULL;
81
-
82
- pthread_mutex_lock(&queue_mutex);
83
- if (event_queue_tail == NULL) {
84
- event_queue_head = event_queue_tail = event;
82
+ // Pending stream priorities — set from Ruby threads, applied on MsQuic event thread.
83
+ // Simple array-based storage (max 256 pending). Key = stream handle, value = priority + 1.
84
+ #define MAX_PENDING_PRIORITIES 256
85
+ static struct { HQUIC stream; uint16_t priority_plus_one; } PendingPriorities[MAX_PENDING_PRIORITIES];
86
+ static int PendingPriorityCount = 0;
87
+
88
+ // rb_protect wrapper catches Ruby exceptions so they don't longjmp
89
+ // through MsQuic callback frames (which would corrupt MsQuic state).
90
+ // All Ruby object construction AND the funcall happen inside rb_protect.
91
+ struct dispatch_ruby_args {
92
+ HQUIC connection;
93
+ void* connection_ctx;
94
+ VALUE client_obj;
95
+ const char* event_type;
96
+ uint64_t stream_id;
97
+ const char* data;
98
+ size_t data_len;
99
+ int early_data;
100
+ };
101
+
102
+ static VALUE
103
+ dispatch_ruby_body(VALUE arg)
104
+ {
105
+ struct dispatch_ruby_args* a = (struct dispatch_ruby_args*)arg;
106
+
107
+ if (NIL_P(a->client_obj)) {
108
+ VALUE server_class = rb_const_get_at(mQuicsilver, rb_intern("Server"));
109
+ if (rb_class_real(CLASS_OF(server_class)) == rb_cClass) {
110
+ VALUE connection_data = rb_ary_new2(2);
111
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection));
112
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection_ctx));
113
+ VALUE argv[5] = {
114
+ connection_data,
115
+ ULL2NUM(a->stream_id),
116
+ rb_str_new_cstr(a->event_type),
117
+ rb_str_new(a->data, a->data_len),
118
+ a->early_data ? Qtrue : Qfalse
119
+ };
120
+ rb_funcallv(server_class, rb_intern("handle_stream"), 5, argv);
121
+ }
85
122
  } else {
86
- event_queue_tail->next = event;
87
- event_queue_tail = event;
123
+ if (RB_TYPE_P(a->client_obj, T_OBJECT)) {
124
+ VALUE argv[4] = {
125
+ ULL2NUM(a->stream_id),
126
+ rb_str_new_cstr(a->event_type),
127
+ rb_str_new(a->data, a->data_len),
128
+ a->early_data ? Qtrue : Qfalse
129
+ };
130
+ rb_funcallv(a->client_obj, rb_intern("handle_stream_event"), 4, argv);
131
+ }
88
132
  }
89
- pthread_mutex_unlock(&queue_mutex);
133
+
134
+ return Qnil;
90
135
  }
91
136
 
92
- // Free a single event
137
+ // Dispatch event to Ruby — entire body wrapped in rb_protect so no Ruby call
138
+ // (object construction or funcall) can longjmp through MsQuic callback frames.
93
139
  static void
94
- free_event(CallbackEvent* event)
140
+ dispatch_to_ruby(HQUIC connection, void* connection_ctx, VALUE client_obj,
141
+ const char* event_type, uint64_t stream_id,
142
+ const char* data, size_t data_len, int early_data)
95
143
  {
96
- if (event == NULL) return;
97
- free(event->event_type);
98
- free(event->data);
99
- free(event);
144
+ struct dispatch_ruby_args args;
145
+ args.connection = connection;
146
+ args.connection_ctx = connection_ctx;
147
+ args.client_obj = client_obj;
148
+ args.event_type = event_type;
149
+ args.stream_id = stream_id;
150
+ args.data = data;
151
+ args.data_len = data_len;
152
+ args.early_data = early_data;
153
+
154
+ int state = 0;
155
+ rb_protect(dispatch_ruby_body, (VALUE)&args, &state);
156
+ if (state) {
157
+ rb_set_errinfo(Qnil);
158
+ fprintf(stderr, "Quicsilver: exception in callback\n");
159
+ }
100
160
  }
101
161
 
102
- // Process all pending callback events (called from Ruby)
103
- static VALUE
104
- quicsilver_process_events(VALUE self)
162
+ // Platform I/O wait called without GVL so other Ruby threads can run
163
+ struct poll_args {
164
+ QUIC_EVENTQ eq;
165
+ QUIC_CQE events[64];
166
+ int max_events;
167
+ int timeout_ms;
168
+ int count;
169
+ };
170
+
171
+ static void*
172
+ eventq_wait_nogvl(void* arg)
105
173
  {
106
- CallbackEvent* event;
107
- VALUE server_class = Qnil;
108
- int processed = 0;
109
-
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
- }
174
+ struct poll_args* a = (struct poll_args*)arg;
175
+ #if __linux__
176
+ a->count = epoll_wait(a->eq, a->events, a->max_events, a->timeout_ms);
177
+ #elif __APPLE__ || __FreeBSD__
178
+ struct timespec ts;
179
+ ts.tv_sec = a->timeout_ms / 1000;
180
+ ts.tv_nsec = (a->timeout_ms % 1000) * 1000000;
181
+ a->count = kevent(a->eq, NULL, 0, a->events, a->max_events, &ts);
182
+ #endif
183
+ return NULL;
184
+ }
115
185
 
116
- // Process events in a loop, but limit iterations to avoid GVL starvation
117
- int max_iterations = 100;
118
- int iteration = 0;
186
+ static inline QUIC_SQE*
187
+ cqe_get_sqe(QUIC_CQE* cqe)
188
+ {
189
+ #if __linux__
190
+ return (QUIC_SQE*)cqe->data.ptr;
191
+ #elif __APPLE__ || __FreeBSD__
192
+ return (QUIC_SQE*)cqe->udata;
193
+ #endif
194
+ }
119
195
 
120
- while (iteration++ < max_iterations) {
121
- pthread_mutex_lock(&queue_mutex);
122
- event = event_queue_head;
123
- if (event != NULL) {
124
- event_queue_head = event->next;
125
- if (event_queue_head == NULL) {
126
- event_queue_tail = NULL;
127
- }
196
+ // Drive MsQuic execution: poll internal timers, wait for I/O, fire completions.
197
+ // Callbacks (StreamCallback, ConnectionCallback) fire HERE on the Ruby thread.
198
+ static VALUE
199
+ quicsilver_poll(VALUE self)
200
+ {
201
+ if (ExecContext == NULL) return INT2NUM(0);
202
+
203
+ // 1. ExecutionPoll — process MsQuic timers/state, may fire callbacks (has GVL)
204
+ uint32_t wait_ms = MsQuic->ExecutionPoll(ExecContext);
205
+
206
+ // 2. Wait for I/O completions (releases GVL)
207
+ struct poll_args args;
208
+ args.eq = EventQ;
209
+ args.max_events = 64;
210
+ // With wake_event_loop(), Ruby threads instantly unblock us when work is
211
+ // queued. Cap at 1s as a safety net for shutdown responsiveness.
212
+ uint32_t actual_wait = (wait_ms == UINT32_MAX) ? 1000 : wait_ms;
213
+ args.timeout_ms = (int)actual_wait;
214
+ args.count = 0;
215
+
216
+ rb_thread_call_without_gvl(eventq_wait_nogvl, &args, RUBY_UBF_IO, NULL);
217
+
218
+ // 3. Fire completions — MsQuic callbacks run here (has GVL)
219
+ for (int i = 0; i < args.count; i++) {
220
+ #if __linux__
221
+ if (args.events[i].data.ptr == NULL) {
222
+ uint64_t val;
223
+ read(WakeFd, &val, sizeof(val)); // drain eventfd
224
+ continue;
128
225
  }
129
- pthread_mutex_unlock(&queue_mutex);
130
-
131
- if (event == NULL) break;
132
-
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
- }
226
+ #elif __APPLE__ || __FreeBSD__
227
+ if (args.events[i].filter == EVFILT_USER && args.events[i].ident == WAKE_IDENT) continue;
228
+ #endif
229
+ QUIC_SQE* sqe = cqe_get_sqe(&args.events[i]);
230
+ if (sqe && sqe->Completion) {
231
+ sqe->Completion(&args.events[i]);
162
232
  }
233
+ }
163
234
 
164
- if (handled) {
165
- processed++;
166
- }
235
+ return INT2NUM(args.count);
236
+ }
167
237
 
168
- free_event(event);
238
+ // Inline poll for use during synchronous waits (e.g. wait_for_connection).
239
+ // Short non-blocking poll — keeps MsQuic alive while we spin.
240
+ static void
241
+ poll_inline(int timeout_ms)
242
+ {
243
+ if (ExecContext == NULL) return;
244
+
245
+ MsQuic->ExecutionPoll(ExecContext);
246
+
247
+ QUIC_CQE events[8];
248
+ #if __linux__
249
+ int count = epoll_wait(EventQ, events, 8, timeout_ms);
250
+ #elif __APPLE__ || __FreeBSD__
251
+ struct timespec ts;
252
+ ts.tv_sec = timeout_ms / 1000;
253
+ ts.tv_nsec = (timeout_ms % 1000) * 1000000;
254
+ int count = kevent(EventQ, NULL, 0, events, 8, &ts);
255
+ #endif
256
+ for (int i = 0; i < count; i++) {
257
+ #if __linux__
258
+ if (events[i].data.ptr == NULL) {
259
+ uint64_t val;
260
+ read(WakeFd, &val, sizeof(val)); // drain eventfd
261
+ continue;
262
+ }
263
+ #elif __APPLE__ || __FreeBSD__
264
+ if (events[i].filter == EVFILT_USER && events[i].ident == WAKE_IDENT) continue;
265
+ #endif
266
+ QUIC_SQE* sqe = cqe_get_sqe(&events[i]);
267
+ if (sqe && sqe->Completion) {
268
+ sqe->Completion(&events[i]);
269
+ }
169
270
  }
170
-
171
- return INT2NUM(processed);
172
271
  }
173
272
 
174
273
  QUIC_STATUS
@@ -180,34 +279,81 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
180
279
  return QUIC_STATUS_SUCCESS;
181
280
  }
182
281
 
282
+ // Apply pending priority on the event loop thread (safe context for SetParam)
283
+ for (int i = 0; i < PendingPriorityCount; i++) {
284
+ if (PendingPriorities[i].stream == Stream) {
285
+ uint16_t priority = PendingPriorities[i].priority_plus_one - 1;
286
+ // Remove by swapping with last
287
+ PendingPriorities[i] = PendingPriorities[--PendingPriorityCount];
288
+ MsQuic->SetParam(Stream, QUIC_PARAM_STREAM_PRIORITY, sizeof(priority), &priority);
289
+ break;
290
+ }
291
+ }
292
+
183
293
  switch (Event->Type) {
184
- case QUIC_STREAM_EVENT_RECEIVE:
185
- // Client sent data - enqueue for Ruby processing
294
+ case QUIC_STREAM_EVENT_RECEIVE: {
295
+ int has_fin = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) != 0;
296
+
297
+ // Track 0-RTT early data for replay protection
298
+ if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_0_RTT) {
299
+ ctx->early_data = 1;
300
+ }
301
+
302
+ if (Event->RECEIVE.BufferCount == 0 && has_fin) {
303
+ // Empty FIN — headers-only request/response with no body
304
+ uint64_t stream_id = 0;
305
+ uint32_t stream_id_len = sizeof(stream_id);
306
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
307
+
308
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj,
309
+ "RECEIVE_FIN", stream_id, (const char*)&Stream, sizeof(HQUIC), ctx->early_data);
310
+ break;
311
+ }
312
+
186
313
  if (Event->RECEIVE.BufferCount > 0) {
187
- const QUIC_BUFFER* buffer = &Event->RECEIVE.Buffers[0];
188
- const char* event_type = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) ? "RECEIVE_FIN" : "RECEIVE";
314
+ const char* event_type = has_fin ? "RECEIVE_FIN" : "RECEIVE";
189
315
 
190
- // Get the QUIC protocol stream ID (0, 4, 8, 12...)
191
316
  uint64_t stream_id = 0;
192
317
  uint32_t stream_id_len = sizeof(stream_id);
193
318
  MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
194
319
 
195
- // Pack stream handle pointer along with data for RECEIVE_FIN
320
+ size_t total_data_len = 0;
321
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
322
+ total_data_len += Event->RECEIVE.Buffers[b].Length;
323
+ }
324
+
196
325
  if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) {
197
- // Create combined buffer: [stream_handle(8 bytes)][data]
198
- size_t total_len = sizeof(HQUIC) + buffer->Length;
326
+ // Create combined buffer: [stream_handle(8)][all data]
327
+ size_t total_len = sizeof(HQUIC) + total_data_len;
199
328
  char* combined = (char*)malloc(total_len);
200
329
  if (combined != NULL) {
201
330
  memcpy(combined, &Stream, sizeof(HQUIC));
202
- memcpy(combined + sizeof(HQUIC), buffer->Buffer, buffer->Length);
203
- enqueue_callback_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_len);
331
+ size_t offset = sizeof(HQUIC);
332
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
333
+ memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
334
+ offset += Event->RECEIVE.Buffers[b].Length;
335
+ }
336
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_len, ctx->early_data);
204
337
  free(combined);
205
338
  }
339
+ } else if (Event->RECEIVE.BufferCount == 1) {
340
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id,
341
+ (const char*)Event->RECEIVE.Buffers[0].Buffer, Event->RECEIVE.Buffers[0].Length, 0);
206
342
  } else {
207
- enqueue_callback_event(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, (const char*)buffer->Buffer, buffer->Length);
343
+ char* combined = (char*)malloc(total_data_len);
344
+ if (combined != NULL) {
345
+ size_t offset = 0;
346
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
347
+ memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
348
+ offset += Event->RECEIVE.Buffers[b].Length;
349
+ }
350
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_data_len, 0);
351
+ free(combined);
352
+ }
208
353
  }
209
354
  }
210
355
  break;
356
+ }
211
357
  case QUIC_STREAM_EVENT_SEND_COMPLETE:
212
358
  // Free the send buffer that was allocated in quicsilver_send_stream
213
359
  if (Event->SEND_COMPLETE.ClientContext != NULL) {
@@ -216,11 +362,38 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
216
362
  break;
217
363
  case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
218
364
  ctx->shutdown = 1;
365
+ free(ctx);
366
+ MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, NULL);
367
+ if (Event->SHUTDOWN_COMPLETE.AppCloseInProgress == FALSE) {
368
+ MsQuic->StreamClose(Stream);
369
+ }
219
370
  break;
220
371
  case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
221
372
  break;
222
- case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
373
+ case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: {
374
+ // Peer sent RESET_STREAM — pack [stream_handle(8)][error_code(8)]
375
+ uint64_t stream_id = 0;
376
+ uint32_t stream_id_len = sizeof(stream_id);
377
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
378
+ uint64_t error_code = Event->PEER_SEND_ABORTED.ErrorCode;
379
+ char combined[sizeof(HQUIC) + sizeof(uint64_t)];
380
+ memcpy(combined, &Stream, sizeof(HQUIC));
381
+ memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
382
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STREAM_RESET", stream_id, combined, sizeof(combined), 0);
383
+ break;
384
+ }
385
+ case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: {
386
+ // Peer sent STOP_SENDING — pack [stream_handle(8)][error_code(8)]
387
+ uint64_t stream_id = 0;
388
+ uint32_t stream_id_len = sizeof(stream_id);
389
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
390
+ uint64_t error_code = Event->PEER_RECEIVE_ABORTED.ErrorCode;
391
+ char combined[sizeof(HQUIC) + sizeof(uint64_t)];
392
+ memcpy(combined, &Stream, sizeof(HQUIC));
393
+ memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
394
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STOP_SENDING", stream_id, combined, sizeof(combined), 0);
223
395
  break;
396
+ }
224
397
  }
225
398
 
226
399
  return QUIC_STATUS_SUCCESS;
@@ -243,7 +416,7 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
243
416
  ctx->connected = 1;
244
417
  ctx->failed = 0;
245
418
  // 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));
419
+ dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
247
420
  break;
248
421
  case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
249
422
  ctx->connected = 0;
@@ -259,8 +432,13 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
259
432
  break;
260
433
  case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
261
434
  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));
435
+ dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
436
+ // Free context for all connections (both client and server).
437
+ // Client GC registration must be removed before freeing.
438
+ if (!NIL_P(ctx->client_obj)) {
439
+ rb_gc_unregister_address(&ctx->client_obj);
440
+ }
441
+ free(ctx);
264
442
  break;
265
443
  case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
266
444
  // Client opened a stream
@@ -272,10 +450,13 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
272
450
  stream_ctx->client_obj = ctx->client_obj; // Copy from connection context
273
451
  stream_ctx->started = 1;
274
452
  stream_ctx->shutdown = 0;
453
+ stream_ctx->early_data = 0;
275
454
  stream_ctx->error_status = QUIC_STATUS_SUCCESS;
276
455
 
277
456
  // Set the stream callback handler to handle data events
278
457
  MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, stream_ctx);
458
+ } else {
459
+ MsQuic->StreamClose(Stream);
279
460
  }
280
461
  break;
281
462
  default:
@@ -350,15 +531,67 @@ quicsilver_open(VALUE self)
350
531
  rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
351
532
  return Qfalse;
352
533
  }
353
-
354
- // Create a registration for the app's connections
534
+
535
+ // Custom execution MUST be set up BEFORE RegistrationOpen.
536
+ // RegistrationOpen triggers MsQuic lazy init (LazyInitComplete=TRUE),
537
+ // after which ExecutionCreate returns QUIC_STATUS_INVALID_STATE.
538
+ #if __linux__
539
+ EventQ = epoll_create1(0);
540
+ #elif __APPLE__ || __FreeBSD__
541
+ EventQ = kqueue();
542
+ #endif
543
+ if (EventQ == -1) {
544
+ MsQuicClose(MsQuic);
545
+ MsQuic = NULL;
546
+ rb_raise(rb_eRuntimeError, "Failed to create event queue for custom execution");
547
+ return Qfalse;
548
+ }
549
+
550
+ QUIC_EXECUTION_CONFIG exec_config = { 0, &EventQ };
551
+ Status = MsQuic->ExecutionCreate(
552
+ QUIC_GLOBAL_EXECUTION_CONFIG_FLAG_NONE,
553
+ 0, // PollingIdleTimeoutUs
554
+ 1, // 1 execution context
555
+ &exec_config,
556
+ &ExecContext
557
+ );
558
+ if (QUIC_FAILED(Status)) {
559
+ close(EventQ);
560
+ EventQ = -1;
561
+ MsQuicClose(MsQuic);
562
+ MsQuic = NULL;
563
+ rb_raise(rb_eRuntimeError, "ExecutionCreate failed, 0x%x!", Status);
564
+ return Qfalse;
565
+ }
566
+
567
+ // Now open registration — MsQuic lazy init will see the custom execution
568
+ // context and skip spawning its own worker threads.
355
569
  if (QUIC_FAILED(Status = MsQuic->RegistrationOpen(&RegConfig, &Registration))) {
356
- rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
570
+ MsQuic->ExecutionDelete(1, &ExecContext);
571
+ ExecContext = NULL;
572
+ close(EventQ);
573
+ EventQ = -1;
357
574
  MsQuicClose(MsQuic);
358
575
  MsQuic = NULL;
576
+ rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
359
577
  return Qfalse;
360
578
  }
361
-
579
+
580
+ // Register wake source — Ruby threads can unblock the event loop
581
+ #if __linux__
582
+ WakeFd = eventfd(0, EFD_NONBLOCK);
583
+ if (WakeFd != -1) {
584
+ struct epoll_event ev = { .events = EPOLLIN, .data.ptr = NULL };
585
+ epoll_ctl(EventQ, EPOLL_CTL_ADD, WakeFd, &ev);
586
+ }
587
+ #elif __APPLE__ || __FreeBSD__
588
+ {
589
+ struct kevent kev;
590
+ EV_SET(&kev, WAKE_IDENT, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, NULL);
591
+ kevent(EventQ, &kev, 1, NULL, 0, NULL);
592
+ }
593
+ #endif
594
+
362
595
  return Qtrue;
363
596
  }
364
597
 
@@ -398,11 +631,11 @@ quicsilver_create_configuration(VALUE self, VALUE unsecure)
398
631
  }
399
632
 
400
633
  if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
401
- rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
402
634
  MsQuic->ConfigurationClose(Configuration);
635
+ rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
403
636
  return Qnil;
404
637
  }
405
-
638
+
406
639
  // Return the configuration handle as a Ruby integer (pointer)
407
640
  return ULL2NUM((uintptr_t)Configuration);
408
641
  }
@@ -417,14 +650,24 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
417
650
  }
418
651
  VALUE cert_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("cert_file")));
419
652
  VALUE key_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("key_file")));
420
- VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout")));
653
+ VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout_ms")));
421
654
  VALUE server_resumption_level_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("server_resumption_level")));
422
- VALUE peer_bidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_bidi_stream_count")));
423
- VALUE peer_unidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_unidi_stream_count")));
655
+ VALUE max_concurrent_requests_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_concurrent_requests")));
656
+ VALUE max_unidirectional_streams_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_unidirectional_streams")));
424
657
  VALUE alpn_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("alpn")));
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")));
658
+ VALUE stream_receive_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_window")));
659
+ VALUE stream_receive_buffer_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_buffer")));
660
+ VALUE connection_flow_control_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("connection_flow_control_window")));
661
+ VALUE pacing_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("pacing_enabled")));
662
+ VALUE send_buffering_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("send_buffering_enabled")));
663
+ VALUE initial_rtt_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_rtt_ms")));
664
+ VALUE initial_window_packets_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_window_packets")));
665
+ VALUE max_ack_delay_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_ack_delay_ms")));
666
+ VALUE keep_alive_interval_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("keep_alive_interval_ms")));
667
+ VALUE congestion_control_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("congestion_control_algorithm")));
668
+ VALUE migration_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("migration_enabled")));
669
+ VALUE disconnect_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("disconnect_timeout_ms")));
670
+ VALUE handshake_idle_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("handshake_idle_timeout_ms")));
428
671
 
429
672
  QUIC_STATUS Status;
430
673
  HQUIC Configuration = NULL;
@@ -433,31 +676,65 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
433
676
  const char* key_path = StringValueCStr(key_file_val);
434
677
  uint32_t idle_timeout_ms = NUM2INT(idle_timeout_val);
435
678
  uint32_t server_resumption_level = NUM2INT(server_resumption_level_val);
436
- uint32_t peer_bidi_stream_count = NUM2INT(peer_bidi_stream_count_val);
437
- uint32_t peer_unidi_stream_count = NUM2INT(peer_unidi_stream_count_val);
679
+ uint32_t max_concurrent_requests = NUM2INT(max_concurrent_requests_val);
680
+ uint32_t max_unidirectional_streams = NUM2INT(max_unidirectional_streams_val);
438
681
  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);
682
+ uint32_t stream_receive_window = NUM2UINT(stream_receive_window_val);
683
+ uint32_t stream_receive_buffer = NUM2UINT(stream_receive_buffer_val);
684
+ uint32_t connection_flow_control_window = NUM2UINT(connection_flow_control_window_val);
685
+ uint8_t pacing_enabled = (uint8_t)NUM2INT(pacing_enabled_val);
686
+ uint8_t send_buffering_enabled = (uint8_t)NUM2INT(send_buffering_enabled_val);
687
+ uint32_t initial_rtt_ms = NUM2UINT(initial_rtt_ms_val);
688
+ uint32_t initial_window_packets = NUM2UINT(initial_window_packets_val);
689
+ uint32_t max_ack_delay_ms = NUM2UINT(max_ack_delay_ms_val);
690
+ uint32_t keep_alive_interval_ms = NUM2UINT(keep_alive_interval_ms_val);
691
+ uint16_t congestion_control = (uint16_t)NUM2INT(congestion_control_val);
692
+ uint8_t migration_enabled = (uint8_t)NUM2INT(migration_enabled_val);
693
+ uint32_t disconnect_timeout_ms = NUM2UINT(disconnect_timeout_ms_val);
694
+ uint64_t handshake_idle_timeout_ms = NUM2ULL(handshake_idle_timeout_ms_val);
442
695
 
443
696
  QUIC_SETTINGS Settings = {0};
444
697
  Settings.IdleTimeoutMs = idle_timeout_ms;
445
698
  Settings.IsSet.IdleTimeoutMs = TRUE;
446
699
  Settings.ServerResumptionLevel = server_resumption_level;
447
700
  Settings.IsSet.ServerResumptionLevel = TRUE;
448
- Settings.PeerBidiStreamCount = peer_bidi_stream_count;
701
+ Settings.PeerBidiStreamCount = max_concurrent_requests;
449
702
  Settings.IsSet.PeerBidiStreamCount = TRUE;
450
- Settings.PeerUnidiStreamCount = peer_unidi_stream_count;
703
+ Settings.PeerUnidiStreamCount = max_unidirectional_streams;
451
704
  Settings.IsSet.PeerUnidiStreamCount = TRUE;
452
705
 
453
706
  // Flow control / backpressure settings
454
- Settings.StreamRecvWindowDefault = stream_recv_window;
707
+ Settings.StreamRecvWindowDefault = stream_receive_window;
455
708
  Settings.IsSet.StreamRecvWindowDefault = TRUE;
456
- Settings.StreamRecvBufferDefault = stream_recv_buffer;
709
+ Settings.StreamRecvBufferDefault = stream_receive_buffer;
457
710
  Settings.IsSet.StreamRecvBufferDefault = TRUE;
458
- Settings.ConnFlowControlWindow = conn_flow_control_window;
711
+ Settings.ConnFlowControlWindow = connection_flow_control_window;
459
712
  Settings.IsSet.ConnFlowControlWindow = TRUE;
460
713
 
714
+ // Throughput settings
715
+ Settings.PacingEnabled = pacing_enabled;
716
+ Settings.IsSet.PacingEnabled = TRUE;
717
+ Settings.SendBufferingEnabled = send_buffering_enabled;
718
+ Settings.IsSet.SendBufferingEnabled = TRUE;
719
+ Settings.InitialRttMs = initial_rtt_ms;
720
+ Settings.IsSet.InitialRttMs = TRUE;
721
+ Settings.InitialWindowPackets = initial_window_packets;
722
+ Settings.IsSet.InitialWindowPackets = TRUE;
723
+ Settings.MaxAckDelayMs = max_ack_delay_ms;
724
+ Settings.IsSet.MaxAckDelayMs = TRUE;
725
+
726
+ // Connection management
727
+ Settings.KeepAliveIntervalMs = keep_alive_interval_ms;
728
+ Settings.IsSet.KeepAliveIntervalMs = TRUE;
729
+ Settings.CongestionControlAlgorithm = congestion_control;
730
+ Settings.IsSet.CongestionControlAlgorithm = TRUE;
731
+ Settings.MigrationEnabled = migration_enabled;
732
+ Settings.IsSet.MigrationEnabled = TRUE;
733
+ Settings.DisconnectTimeoutMs = disconnect_timeout_ms;
734
+ Settings.IsSet.DisconnectTimeoutMs = TRUE;
735
+ Settings.HandshakeIdleTimeoutMs = handshake_idle_timeout_ms;
736
+ Settings.IsSet.HandshakeIdleTimeoutMs = TRUE;
737
+
461
738
  QUIC_BUFFER Alpn = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
462
739
 
463
740
  // Create configuration
@@ -478,8 +755,8 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
478
755
  CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
479
756
 
480
757
  if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
481
- rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
482
758
  MsQuic->ConfigurationClose(Configuration);
759
+ rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
483
760
  return Qnil;
484
761
  }
485
762
 
@@ -553,7 +830,8 @@ quicsilver_start_connection(VALUE self, VALUE connection_handle, VALUE config_ha
553
830
  rb_raise(rb_eRuntimeError, "ConnectionStart failed, 0x%x!", Status);
554
831
  return Qfalse;
555
832
  }
556
-
833
+
834
+ wake_event_loop();
557
835
  return Qtrue;
558
836
  }
559
837
 
@@ -567,7 +845,7 @@ quicsilver_wait_for_connection(VALUE self, VALUE context_handle, VALUE timeout_m
567
845
  const int sleep_interval = 10; // 10ms
568
846
 
569
847
  while (elapsed < timeout && !ctx->connected && !ctx->failed) {
570
- usleep(sleep_interval * 1000); // Convert to microseconds
848
+ poll_inline(sleep_interval); // Drive MsQuic execution while waiting
571
849
  elapsed += sleep_interval;
572
850
  }
573
851
 
@@ -618,21 +896,29 @@ quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
618
896
  VALUE context_handle = rb_ary_entry(connection_data, 1);
619
897
 
620
898
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
621
- ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
899
+ (void)context_handle; // ctx freed by SHUTDOWN_COMPLETE, not here
622
900
 
623
901
  if (Connection != NULL) {
624
902
  MsQuic->ConnectionClose(Connection);
625
903
  }
626
904
 
627
- // Free context if valid
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
- }
633
- free(ctx);
634
- }
905
+ // Don't free ctx here — ConnectionClose is async and SHUTDOWN_COMPLETE
906
+ // will fire on the next event loop poll, which still needs ctx.
907
+ // SHUTDOWN_COMPLETE handles cleanup for both client and server.
908
+
909
+ return Qnil;
910
+ }
911
+
912
+ // Close a server-side connection handle (context already freed in C callback)
913
+ static VALUE
914
+ quicsilver_close_server_connection(VALUE self, VALUE connection_handle)
915
+ {
916
+ if (MsQuic == NULL) return Qnil;
635
917
 
918
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
919
+ if (Connection != NULL) {
920
+ MsQuic->ConnectionClose(Connection);
921
+ }
636
922
  return Qnil;
637
923
  }
638
924
 
@@ -656,6 +942,7 @@ quicsilver_connection_shutdown(VALUE self, VALUE connection_handle, VALUE error_
656
942
  : QUIC_CONNECTION_SHUTDOWN_FLAG_NONE;
657
943
 
658
944
  MsQuic->ConnectionShutdown(Connection, flags, ErrorCode);
945
+ wake_event_loop();
659
946
  }
660
947
 
661
948
  return Qtrue;
@@ -674,20 +961,35 @@ quicsilver_close_configuration(VALUE self, VALUE config_handle)
674
961
  return Qnil;
675
962
  }
676
963
 
677
- // Close MSQUIC
678
964
  static VALUE
679
965
  quicsilver_close(VALUE self)
680
966
  {
681
967
  if (MsQuic != NULL) {
682
968
  if (Registration != NULL) {
683
- // This will block until all outstanding child objects have been closed
684
969
  MsQuic->RegistrationClose(Registration);
685
970
  Registration = NULL;
686
971
  }
972
+
973
+ if (ExecContext != NULL) {
974
+ MsQuic->ExecutionDelete(1, &ExecContext);
975
+ ExecContext = NULL;
976
+ }
977
+
687
978
  MsQuicClose(MsQuic);
688
979
  MsQuic = NULL;
689
980
  }
690
-
981
+
982
+ #if __linux__
983
+ if (WakeFd != -1) {
984
+ close(WakeFd);
985
+ WakeFd = -1;
986
+ }
987
+ #endif
988
+ if (EventQ != -1) {
989
+ close(EventQ);
990
+ EventQ = -1;
991
+ }
992
+
691
993
  return Qnil;
692
994
  }
693
995
 
@@ -733,34 +1035,42 @@ quicsilver_create_listener(VALUE self, VALUE config_handle)
733
1035
 
734
1036
  // Start listener on specific address and port
735
1037
  static VALUE
736
- quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port)
1038
+ quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port, VALUE alpn)
737
1039
  {
738
1040
  if (MsQuic == NULL) {
739
1041
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
740
1042
  return Qfalse;
741
1043
  }
742
-
1044
+
743
1045
  HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
744
1046
  uint16_t Port = (uint16_t)NUM2INT(port);
745
-
1047
+ const char* alpn_str = StringValueCStr(alpn);
1048
+
746
1049
  // Setup address - properly initialize the entire structure
747
1050
  QUIC_ADDR Address;
748
1051
  memset(&Address, 0, sizeof(Address));
749
-
750
- // Set up for localhost/any address
751
- QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET);
1052
+
1053
+ // Parse address string to determine family
1054
+ const char* addr_str = StringValueCStr(address);
1055
+ if (strchr(addr_str, ':') != NULL) {
1056
+ // IPv6 address (contains ':')
1057
+ QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET6);
1058
+ } else {
1059
+ // IPv4 address or unspecified - use UNSPEC for dual-stack
1060
+ QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_UNSPEC);
1061
+ }
752
1062
  QuicAddrSetPort(&Address, Port);
753
-
1063
+
754
1064
  QUIC_STATUS Status;
755
-
756
- // Create QUIC_BUFFER for the address
757
- QUIC_BUFFER AlpnBuffer = { sizeof("h3") - 1, (uint8_t*)"h3" };
758
-
1065
+
1066
+ QUIC_BUFFER AlpnBuffer = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
1067
+
759
1068
  if (QUIC_FAILED(Status = MsQuic->ListenerStart(Listener, &AlpnBuffer, 1, &Address))) {
760
1069
  rb_raise(rb_eRuntimeError, "ListenerStart failed, 0x%x!", Status);
761
1070
  return Qfalse;
762
1071
  }
763
-
1072
+
1073
+ wake_event_loop();
764
1074
  return Qtrue;
765
1075
  }
766
1076
 
@@ -771,9 +1081,10 @@ quicsilver_stop_listener(VALUE self, VALUE listener_handle)
771
1081
  if (MsQuic == NULL) {
772
1082
  return Qfalse;
773
1083
  }
774
-
1084
+
775
1085
  HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
776
1086
  MsQuic->ListenerStop(Listener);
1087
+ wake_event_loop();
777
1088
  return Qtrue;
778
1089
  }
779
1090
 
@@ -830,6 +1141,7 @@ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
830
1141
  ctx->client_obj = conn_ctx ? conn_ctx->client_obj : Qnil;
831
1142
  ctx->started = 1;
832
1143
  ctx->shutdown = 0;
1144
+ ctx->early_data = 0;
833
1145
  ctx->error_status = QUIC_STATUS_SUCCESS;
834
1146
 
835
1147
  // Use flag based on parameter
@@ -848,12 +1160,13 @@ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
848
1160
  // Start the stream
849
1161
  Status = MsQuic->StreamStart(Stream, QUIC_STREAM_START_FLAG_NONE);
850
1162
  if (QUIC_FAILED(Status)) {
851
- free(ctx);
1163
+ // StreamClose fires SHUTDOWN_COMPLETE synchronously which frees ctx
852
1164
  MsQuic->StreamClose(Stream);
853
1165
  rb_raise(rb_eRuntimeError, "StreamStart failed, 0x%x!", Status);
854
1166
  return Qnil;
855
1167
  }
856
-
1168
+
1169
+ wake_event_loop();
857
1170
  return ULL2NUM((uintptr_t)Stream);
858
1171
  }
859
1172
 
@@ -894,9 +1207,78 @@ quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_f
894
1207
  rb_raise(rb_eRuntimeError, "StreamSend failed, 0x%x!", Status);
895
1208
  return Qfalse;
896
1209
  }
897
-
1210
+
1211
+ wake_event_loop();
1212
+ return Qtrue;
1213
+ }
1214
+
1215
+ // Reset a QUIC stream (RESET_STREAM frame - abruptly terminates sending)
1216
+ static VALUE
1217
+ quicsilver_stream_reset(VALUE self, VALUE stream_handle, VALUE error_code)
1218
+ {
1219
+ if (MsQuic == NULL) {
1220
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1221
+ return Qnil;
1222
+ }
1223
+
1224
+ HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1225
+ if (Stream == NULL) return Qnil;
1226
+
1227
+ uint64_t ErrorCode = NUM2ULL(error_code);
1228
+
1229
+ MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND, ErrorCode);
1230
+
1231
+ wake_event_loop();
898
1232
  return Qtrue;
899
- }
1233
+ }
1234
+
1235
+ // Queue a stream priority change. Called from Ruby threads — just stores the
1236
+ // priority. The actual SetParam happens on the MsQuic event thread in StreamCallback.
1237
+ static VALUE
1238
+ quicsilver_set_stream_priority(VALUE self, VALUE stream_handle, VALUE priority)
1239
+ {
1240
+ if (MsQuic == NULL) return Qnil;
1241
+
1242
+ HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1243
+ if (Stream == NULL) return Qnil;
1244
+
1245
+ if (PendingPriorityCount >= MAX_PENDING_PRIORITIES) return Qfalse;
1246
+
1247
+ uint16_t Priority = (uint16_t)NUM2UINT(priority);
1248
+ PendingPriorities[PendingPriorityCount].stream = Stream;
1249
+ PendingPriorities[PendingPriorityCount].priority_plus_one = Priority + 1;
1250
+ PendingPriorityCount++;
1251
+
1252
+ wake_event_loop();
1253
+ return Qtrue;
1254
+ }
1255
+
1256
+ // Stop sending on a QUIC stream (STOP_SENDING frame - requests peer to stop)
1257
+ static VALUE
1258
+ quicsilver_stream_stop_sending(VALUE self, VALUE stream_handle, VALUE error_code)
1259
+ {
1260
+ if (MsQuic == NULL) {
1261
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1262
+ return Qnil;
1263
+ }
1264
+
1265
+ HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1266
+ if (Stream == NULL) return Qnil;
1267
+
1268
+ uint64_t ErrorCode = NUM2ULL(error_code);
1269
+
1270
+ MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE, ErrorCode);
1271
+
1272
+ wake_event_loop();
1273
+ return Qtrue;
1274
+ }
1275
+
1276
+ static VALUE
1277
+ quicsilver_wake(VALUE self)
1278
+ {
1279
+ wake_event_loop();
1280
+ return Qnil;
1281
+ }
900
1282
 
901
1283
  // Initialize the extension
902
1284
  void
@@ -920,17 +1302,22 @@ Init_quicsilver(void)
920
1302
  rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
921
1303
  rb_define_singleton_method(mQuicsilver, "connection_shutdown", quicsilver_connection_shutdown, 3);
922
1304
  rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
1305
+ rb_define_singleton_method(mQuicsilver, "close_server_connection", quicsilver_close_server_connection, 1);
923
1306
 
924
1307
  // Listener management
925
1308
  rb_define_singleton_method(mQuicsilver, "create_listener", quicsilver_create_listener, 1);
926
- rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 3);
1309
+ rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 4);
927
1310
  rb_define_singleton_method(mQuicsilver, "stop_listener", quicsilver_stop_listener, 1);
928
1311
  rb_define_singleton_method(mQuicsilver, "close_listener", quicsilver_close_listener, 1);
929
1312
 
930
1313
  // Stream management
931
1314
  rb_define_singleton_method(mQuicsilver, "open_stream", quicsilver_open_stream, 2);
932
1315
  rb_define_singleton_method(mQuicsilver, "send_stream", quicsilver_send_stream, 3);
1316
+ rb_define_singleton_method(mQuicsilver, "stream_reset", quicsilver_stream_reset, 2);
1317
+ rb_define_singleton_method(mQuicsilver, "stream_stop_sending", quicsilver_stream_stop_sending, 2);
1318
+ rb_define_singleton_method(mQuicsilver, "set_stream_priority", quicsilver_set_stream_priority, 2);
933
1319
 
934
- // Event processing
935
- rb_define_singleton_method(mQuicsilver, "process_events", quicsilver_process_events, 0);
1320
+ // Event processing (custom execution — app drives MsQuic)
1321
+ rb_define_singleton_method(mQuicsilver, "poll", quicsilver_poll, 0);
1322
+ rb_define_singleton_method(mQuicsilver, "wake", quicsilver_wake, 0);
936
1323
  }