quicsilver 0.1.0 → 0.3.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +41 -0
  3. data/.gitignore +3 -1
  4. data/CHANGELOG.md +76 -5
  5. data/Gemfile.lock +18 -4
  6. data/LICENSE +21 -0
  7. data/README.md +33 -53
  8. data/Rakefile +29 -2
  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 +46 -0
  13. data/benchmarks/rails.rb +170 -0
  14. data/benchmarks/throughput.rb +113 -0
  15. data/examples/minimal_http3_server.rb +0 -6
  16. data/examples/rack_http3_server.rb +0 -6
  17. data/examples/simple_client_test.rb +26 -0
  18. data/ext/quicsilver/quicsilver.c +615 -138
  19. data/lib/quicsilver/client/client.rb +250 -0
  20. data/lib/quicsilver/client/request.rb +98 -0
  21. data/lib/quicsilver/protocol/frames.rb +327 -0
  22. data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
  23. data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
  24. data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
  25. data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
  26. data/lib/quicsilver/protocol/request_encoder.rb +47 -0
  27. data/lib/quicsilver/protocol/request_parser.rb +387 -0
  28. data/lib/quicsilver/protocol/response_encoder.rb +72 -0
  29. data/lib/quicsilver/protocol/response_parser.rb +249 -0
  30. data/lib/quicsilver/server/listener_data.rb +14 -0
  31. data/lib/quicsilver/server/request_handler.rb +86 -0
  32. data/lib/quicsilver/server/request_registry.rb +50 -0
  33. data/lib/quicsilver/server/server.rb +336 -0
  34. data/lib/quicsilver/transport/configuration.rb +132 -0
  35. data/lib/quicsilver/transport/connection.rb +350 -0
  36. data/lib/quicsilver/transport/event_loop.rb +38 -0
  37. data/lib/quicsilver/transport/inbound_stream.rb +33 -0
  38. data/lib/quicsilver/transport/stream.rb +28 -0
  39. data/lib/quicsilver/transport/stream_event.rb +26 -0
  40. data/lib/quicsilver/version.rb +1 -1
  41. data/lib/quicsilver.rb +49 -9
  42. data/lib/rackup/handler/quicsilver.rb +77 -0
  43. data/quicsilver.gemspec +10 -3
  44. metadata +122 -17
  45. data/examples/minimal_http3_client.rb +0 -89
  46. data/lib/quicsilver/client.rb +0 -191
  47. data/lib/quicsilver/http3/request_encoder.rb +0 -112
  48. data/lib/quicsilver/http3/request_parser.rb +0 -158
  49. data/lib/quicsilver/http3/response_encoder.rb +0 -73
  50. data/lib/quicsilver/http3.rb +0 -68
  51. data/lib/quicsilver/listener_data.rb +0 -29
  52. data/lib/quicsilver/server.rb +0 -258
  53. data/lib/quicsilver/server_configuration.rb +0 -49
@@ -1,23 +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
- char* event_type;
12
- uint64_t stream_id;
13
- char* data;
14
- size_t data_len;
15
- struct CallbackEvent* next;
16
- } 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)
17
26
 
18
- static CallbackEvent* event_queue_head = NULL;
19
- static CallbackEvent* event_queue_tail = NULL;
20
- 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
+ }
21
43
 
22
44
  // Global MSQUIC API table
23
45
  static const QUIC_API_TABLE* MsQuic = NULL;
@@ -34,6 +56,7 @@ typedef struct {
34
56
  int failed;
35
57
  QUIC_STATUS error_status;
36
58
  uint64_t error_code;
59
+ VALUE client_obj; // Ruby client object (Qnil for server connections)
37
60
  } ConnectionContext;
38
61
 
39
62
  // Listener state tracking
@@ -47,72 +70,198 @@ typedef struct {
47
70
 
48
71
  // Stream state tracking
49
72
  typedef struct {
73
+ HQUIC connection;
74
+ void* connection_ctx; // ConnectionContext pointer (for building connection_data)
75
+ VALUE client_obj; // Ruby client object (copied from connection context)
50
76
  int started;
51
77
  int shutdown;
78
+ int early_data; // Set when stream received 0-RTT data
52
79
  QUIC_STATUS error_status;
53
80
  } StreamContext;
54
81
 
55
- // Enqueue a callback event (thread-safe)
56
- static void
57
- enqueue_callback_event(const char* event_type, uint64_t stream_id, const char* data, size_t data_len)
82
+ // rb_protect wrapper catches Ruby exceptions so they don't longjmp
83
+ // through MsQuic callback frames (which would corrupt MsQuic state).
84
+ // All Ruby object construction AND the funcall happen inside rb_protect.
85
+ struct dispatch_ruby_args {
86
+ HQUIC connection;
87
+ void* connection_ctx;
88
+ VALUE client_obj;
89
+ const char* event_type;
90
+ uint64_t stream_id;
91
+ const char* data;
92
+ size_t data_len;
93
+ int early_data;
94
+ };
95
+
96
+ static VALUE
97
+ dispatch_ruby_body(VALUE arg)
58
98
  {
59
- CallbackEvent* event = (CallbackEvent*)malloc(sizeof(CallbackEvent));
60
- if (event == NULL) return;
99
+ struct dispatch_ruby_args* a = (struct dispatch_ruby_args*)arg;
61
100
 
62
- event->event_type = strdup(event_type);
63
- event->stream_id = stream_id;
64
- event->data = (char*)malloc(data_len);
65
- if (event->data != NULL) {
66
- memcpy(event->data, data, data_len);
101
+ if (NIL_P(a->client_obj)) {
102
+ VALUE server_class = rb_const_get_at(mQuicsilver, rb_intern("Server"));
103
+ if (rb_class_real(CLASS_OF(server_class)) == rb_cClass) {
104
+ VALUE connection_data = rb_ary_new2(2);
105
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection));
106
+ rb_ary_push(connection_data, ULL2NUM((uintptr_t)a->connection_ctx));
107
+ VALUE argv[5] = {
108
+ connection_data,
109
+ ULL2NUM(a->stream_id),
110
+ rb_str_new_cstr(a->event_type),
111
+ rb_str_new(a->data, a->data_len),
112
+ a->early_data ? Qtrue : Qfalse
113
+ };
114
+ rb_funcallv(server_class, rb_intern("handle_stream"), 5, argv);
115
+ }
116
+ } else {
117
+ if (RB_TYPE_P(a->client_obj, T_OBJECT)) {
118
+ VALUE argv[4] = {
119
+ ULL2NUM(a->stream_id),
120
+ rb_str_new_cstr(a->event_type),
121
+ rb_str_new(a->data, a->data_len),
122
+ a->early_data ? Qtrue : Qfalse
123
+ };
124
+ rb_funcallv(a->client_obj, rb_intern("handle_stream_event"), 4, argv);
125
+ }
67
126
  }
68
- event->data_len = data_len;
69
- event->next = NULL;
70
127
 
71
- pthread_mutex_lock(&queue_mutex);
72
- if (event_queue_tail == NULL) {
73
- event_queue_head = event_queue_tail = event;
74
- } else {
75
- event_queue_tail->next = event;
76
- event_queue_tail = event;
128
+ return Qnil;
129
+ }
130
+
131
+ // Dispatch event to Ruby — entire body wrapped in rb_protect so no Ruby call
132
+ // (object construction or funcall) can longjmp through MsQuic callback frames.
133
+ static void
134
+ dispatch_to_ruby(HQUIC connection, void* connection_ctx, VALUE client_obj,
135
+ const char* event_type, uint64_t stream_id,
136
+ const char* data, size_t data_len, int early_data)
137
+ {
138
+ struct dispatch_ruby_args args;
139
+ args.connection = connection;
140
+ args.connection_ctx = connection_ctx;
141
+ args.client_obj = client_obj;
142
+ args.event_type = event_type;
143
+ args.stream_id = stream_id;
144
+ args.data = data;
145
+ args.data_len = data_len;
146
+ args.early_data = early_data;
147
+
148
+ int state = 0;
149
+ rb_protect(dispatch_ruby_body, (VALUE)&args, &state);
150
+ if (state) {
151
+ rb_set_errinfo(Qnil);
152
+ fprintf(stderr, "Quicsilver: exception in callback\n");
77
153
  }
78
- pthread_mutex_unlock(&queue_mutex);
79
154
  }
80
155
 
81
- // Process all pending callback events (called from Ruby)
156
+ // Platform I/O wait called without GVL so other Ruby threads can run
157
+ struct poll_args {
158
+ QUIC_EVENTQ eq;
159
+ QUIC_CQE events[64];
160
+ int max_events;
161
+ int timeout_ms;
162
+ int count;
163
+ };
164
+
165
+ static void*
166
+ eventq_wait_nogvl(void* arg)
167
+ {
168
+ struct poll_args* a = (struct poll_args*)arg;
169
+ #if __linux__
170
+ a->count = epoll_wait(a->eq, a->events, a->max_events, a->timeout_ms);
171
+ #elif __APPLE__ || __FreeBSD__
172
+ struct timespec ts;
173
+ ts.tv_sec = a->timeout_ms / 1000;
174
+ ts.tv_nsec = (a->timeout_ms % 1000) * 1000000;
175
+ a->count = kevent(a->eq, NULL, 0, a->events, a->max_events, &ts);
176
+ #endif
177
+ return NULL;
178
+ }
179
+
180
+ static inline QUIC_SQE*
181
+ cqe_get_sqe(QUIC_CQE* cqe)
182
+ {
183
+ #if __linux__
184
+ return (QUIC_SQE*)cqe->data.ptr;
185
+ #elif __APPLE__ || __FreeBSD__
186
+ return (QUIC_SQE*)cqe->udata;
187
+ #endif
188
+ }
189
+
190
+ // Drive MsQuic execution: poll internal timers, wait for I/O, fire completions.
191
+ // Callbacks (StreamCallback, ConnectionCallback) fire HERE on the Ruby thread.
82
192
  static VALUE
83
- quicsilver_process_events(VALUE self)
84
- {
85
- CallbackEvent* event;
86
- VALUE server_class = rb_const_get(mQuicsilver, rb_intern("Server"));
87
- int processed = 0;
88
-
89
- while (1) {
90
- pthread_mutex_lock(&queue_mutex);
91
- event = event_queue_head;
92
- if (event != NULL) {
93
- event_queue_head = event->next;
94
- if (event_queue_head == NULL) {
95
- event_queue_tail = NULL;
96
- }
97
- }
98
- pthread_mutex_unlock(&queue_mutex);
193
+ quicsilver_poll(VALUE self)
194
+ {
195
+ if (ExecContext == NULL) return INT2NUM(0);
99
196
 
100
- if (event == NULL) break;
197
+ // 1. ExecutionPoll — process MsQuic timers/state, may fire callbacks (has GVL)
198
+ uint32_t wait_ms = MsQuic->ExecutionPoll(ExecContext);
101
199
 
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));
107
- processed++;
108
- }
200
+ // 2. Wait for I/O completions (releases GVL)
201
+ struct poll_args args;
202
+ args.eq = EventQ;
203
+ args.max_events = 64;
204
+ // With wake_event_loop(), Ruby threads instantly unblock us when work is
205
+ // queued. Cap at 1s as a safety net for shutdown responsiveness.
206
+ uint32_t actual_wait = (wait_ms == UINT32_MAX) ? 1000 : wait_ms;
207
+ args.timeout_ms = (int)actual_wait;
208
+ args.count = 0;
209
+
210
+ rb_thread_call_without_gvl(eventq_wait_nogvl, &args, RUBY_UBF_IO, NULL);
109
211
 
110
- free(event->event_type);
111
- free(event->data);
112
- free(event);
212
+ // 3. Fire completions — MsQuic callbacks run here (has GVL)
213
+ for (int i = 0; i < args.count; i++) {
214
+ #if __linux__
215
+ if (args.events[i].data.ptr == NULL) {
216
+ uint64_t val;
217
+ read(WakeFd, &val, sizeof(val)); // drain eventfd
218
+ continue;
219
+ }
220
+ #elif __APPLE__ || __FreeBSD__
221
+ if (args.events[i].filter == EVFILT_USER && args.events[i].ident == WAKE_IDENT) continue;
222
+ #endif
223
+ QUIC_SQE* sqe = cqe_get_sqe(&args.events[i]);
224
+ if (sqe && sqe->Completion) {
225
+ sqe->Completion(&args.events[i]);
226
+ }
113
227
  }
114
228
 
115
- return INT2NUM(processed);
229
+ return INT2NUM(args.count);
230
+ }
231
+
232
+ // Inline poll for use during synchronous waits (e.g. wait_for_connection).
233
+ // Short non-blocking poll — keeps MsQuic alive while we spin.
234
+ static void
235
+ poll_inline(int timeout_ms)
236
+ {
237
+ if (ExecContext == NULL) return;
238
+
239
+ MsQuic->ExecutionPoll(ExecContext);
240
+
241
+ QUIC_CQE events[8];
242
+ #if __linux__
243
+ int count = epoll_wait(EventQ, events, 8, timeout_ms);
244
+ #elif __APPLE__ || __FreeBSD__
245
+ struct timespec ts;
246
+ ts.tv_sec = timeout_ms / 1000;
247
+ ts.tv_nsec = (timeout_ms % 1000) * 1000000;
248
+ int count = kevent(EventQ, NULL, 0, events, 8, &ts);
249
+ #endif
250
+ for (int i = 0; i < count; i++) {
251
+ #if __linux__
252
+ if (events[i].data.ptr == NULL) {
253
+ uint64_t val;
254
+ read(WakeFd, &val, sizeof(val)); // drain eventfd
255
+ continue;
256
+ }
257
+ #elif __APPLE__ || __FreeBSD__
258
+ if (events[i].filter == EVFILT_USER && events[i].ident == WAKE_IDENT) continue;
259
+ #endif
260
+ QUIC_SQE* sqe = cqe_get_sqe(&events[i]);
261
+ if (sqe && sqe->Completion) {
262
+ sqe->Completion(&events[i]);
263
+ }
264
+ }
116
265
  }
117
266
 
118
267
  QUIC_STATUS
@@ -125,44 +274,111 @@ StreamCallback(HQUIC Stream, void* Context, QUIC_STREAM_EVENT* Event)
125
274
  }
126
275
 
127
276
  switch (Event->Type) {
128
- case QUIC_STREAM_EVENT_RECEIVE:
129
- // Client sent data - enqueue for Ruby processing
277
+ case QUIC_STREAM_EVENT_RECEIVE: {
278
+ int has_fin = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) != 0;
279
+
280
+ // Track 0-RTT early data for replay protection
281
+ if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_0_RTT) {
282
+ ctx->early_data = 1;
283
+ }
284
+
285
+ if (Event->RECEIVE.BufferCount == 0 && has_fin) {
286
+ // Empty FIN — headers-only request/response with no body
287
+ uint64_t stream_id = 0;
288
+ uint32_t stream_id_len = sizeof(stream_id);
289
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
290
+
291
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj,
292
+ "RECEIVE_FIN", stream_id, (const char*)&Stream, sizeof(HQUIC), ctx->early_data);
293
+ break;
294
+ }
295
+
130
296
  if (Event->RECEIVE.BufferCount > 0) {
131
- const QUIC_BUFFER* buffer = &Event->RECEIVE.Buffers[0];
132
- const char* event_type = (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) ? "RECEIVE_FIN" : "RECEIVE";
297
+ const char* event_type = has_fin ? "RECEIVE_FIN" : "RECEIVE";
133
298
 
134
- // Get the QUIC protocol stream ID (0, 4, 8, 12...)
135
299
  uint64_t stream_id = 0;
136
300
  uint32_t stream_id_len = sizeof(stream_id);
137
301
  MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
138
302
 
139
- // Pack stream handle pointer along with data for RECEIVE_FIN
303
+ size_t total_data_len = 0;
304
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
305
+ total_data_len += Event->RECEIVE.Buffers[b].Length;
306
+ }
307
+
140
308
  if (Event->RECEIVE.Flags & QUIC_RECEIVE_FLAG_FIN) {
141
- // Create combined buffer: [stream_handle(8 bytes)][data]
142
- size_t total_len = sizeof(HQUIC) + buffer->Length;
309
+ // Create combined buffer: [stream_handle(8)][all data]
310
+ size_t total_len = sizeof(HQUIC) + total_data_len;
143
311
  char* combined = (char*)malloc(total_len);
144
312
  if (combined != NULL) {
145
313
  memcpy(combined, &Stream, sizeof(HQUIC));
146
- memcpy(combined + sizeof(HQUIC), buffer->Buffer, buffer->Length);
147
- enqueue_callback_event(event_type, stream_id, combined, total_len);
314
+ size_t offset = sizeof(HQUIC);
315
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
316
+ memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
317
+ offset += Event->RECEIVE.Buffers[b].Length;
318
+ }
319
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_len, ctx->early_data);
148
320
  free(combined);
149
321
  }
322
+ } else if (Event->RECEIVE.BufferCount == 1) {
323
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id,
324
+ (const char*)Event->RECEIVE.Buffers[0].Buffer, Event->RECEIVE.Buffers[0].Length, 0);
150
325
  } else {
151
- enqueue_callback_event(event_type, stream_id, (const char*)buffer->Buffer, buffer->Length);
326
+ char* combined = (char*)malloc(total_data_len);
327
+ if (combined != NULL) {
328
+ size_t offset = 0;
329
+ for (uint32_t b = 0; b < Event->RECEIVE.BufferCount; b++) {
330
+ memcpy(combined + offset, Event->RECEIVE.Buffers[b].Buffer, Event->RECEIVE.Buffers[b].Length);
331
+ offset += Event->RECEIVE.Buffers[b].Length;
332
+ }
333
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, event_type, stream_id, combined, total_data_len, 0);
334
+ free(combined);
335
+ }
152
336
  }
153
337
  }
154
338
  break;
339
+ }
155
340
  case QUIC_STREAM_EVENT_SEND_COMPLETE:
341
+ // Free the send buffer that was allocated in quicsilver_send_stream
342
+ if (Event->SEND_COMPLETE.ClientContext != NULL) {
343
+ free(Event->SEND_COMPLETE.ClientContext);
344
+ }
156
345
  break;
157
346
  case QUIC_STREAM_EVENT_SHUTDOWN_COMPLETE:
158
347
  ctx->shutdown = 1;
348
+ free(ctx);
349
+ MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, NULL);
350
+ if (Event->SHUTDOWN_COMPLETE.AppCloseInProgress == FALSE) {
351
+ MsQuic->StreamClose(Stream);
352
+ }
159
353
  break;
160
354
  case QUIC_STREAM_EVENT_PEER_SEND_SHUTDOWN:
161
355
  break;
162
- case QUIC_STREAM_EVENT_PEER_SEND_ABORTED:
356
+ case QUIC_STREAM_EVENT_PEER_SEND_ABORTED: {
357
+ // Peer sent RESET_STREAM — pack [stream_handle(8)][error_code(8)]
358
+ uint64_t stream_id = 0;
359
+ uint32_t stream_id_len = sizeof(stream_id);
360
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
361
+ uint64_t error_code = Event->PEER_SEND_ABORTED.ErrorCode;
362
+ char combined[sizeof(HQUIC) + sizeof(uint64_t)];
363
+ memcpy(combined, &Stream, sizeof(HQUIC));
364
+ memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
365
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STREAM_RESET", stream_id, combined, sizeof(combined), 0);
366
+ break;
367
+ }
368
+ case QUIC_STREAM_EVENT_PEER_RECEIVE_ABORTED: {
369
+ // Peer sent STOP_SENDING — pack [stream_handle(8)][error_code(8)]
370
+ uint64_t stream_id = 0;
371
+ uint32_t stream_id_len = sizeof(stream_id);
372
+ MsQuic->GetParam(Stream, QUIC_PARAM_STREAM_ID, &stream_id_len, &stream_id);
373
+ uint64_t error_code = Event->PEER_RECEIVE_ABORTED.ErrorCode;
374
+ char combined[sizeof(HQUIC) + sizeof(uint64_t)];
375
+ memcpy(combined, &Stream, sizeof(HQUIC));
376
+ memcpy(combined + sizeof(HQUIC), &error_code, sizeof(uint64_t));
377
+ dispatch_to_ruby(ctx->connection, ctx->connection_ctx, ctx->client_obj, "STOP_SENDING", stream_id, combined, sizeof(combined), 0);
163
378
  break;
379
+ }
164
380
  }
165
-
381
+
166
382
  return QUIC_STATUS_SUCCESS;
167
383
  }
168
384
 
@@ -182,8 +398,8 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
182
398
  case QUIC_CONNECTION_EVENT_CONNECTED:
183
399
  ctx->connected = 1;
184
400
  ctx->failed = 0;
185
- // Notify Ruby about new connection
186
- enqueue_callback_event("CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC));
401
+ // Notify Ruby about new connection - pass ctx pointer for building connection_data
402
+ dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_ESTABLISHED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
187
403
  break;
188
404
  case QUIC_CONNECTION_EVENT_SHUTDOWN_INITIATED_BY_TRANSPORT:
189
405
  ctx->connected = 0;
@@ -199,18 +415,31 @@ ConnectionCallback(HQUIC Connection, void* Context, QUIC_CONNECTION_EVENT* Event
199
415
  break;
200
416
  case QUIC_CONNECTION_EVENT_SHUTDOWN_COMPLETE:
201
417
  ctx->connected = 0;
418
+ dispatch_to_ruby(Connection, ctx, ctx->client_obj, "CONNECTION_CLOSED", 0, (const char*)&Connection, sizeof(HQUIC), 0);
419
+ // Free context for all connections (both client and server).
420
+ // Client GC registration must be removed before freeing.
421
+ if (!NIL_P(ctx->client_obj)) {
422
+ rb_gc_unregister_address(&ctx->client_obj);
423
+ }
424
+ free(ctx);
202
425
  break;
203
426
  case QUIC_CONNECTION_EVENT_PEER_STREAM_STARTED:
204
427
  // Client opened a stream
205
428
  Stream = Event->PEER_STREAM_STARTED.Stream;
206
429
  stream_ctx = (StreamContext*)malloc(sizeof(StreamContext));
207
430
  if (stream_ctx != NULL) {
431
+ stream_ctx->connection = Connection;
432
+ stream_ctx->connection_ctx = ctx; // Store connection context pointer
433
+ stream_ctx->client_obj = ctx->client_obj; // Copy from connection context
208
434
  stream_ctx->started = 1;
209
435
  stream_ctx->shutdown = 0;
436
+ stream_ctx->early_data = 0;
210
437
  stream_ctx->error_status = QUIC_STATUS_SUCCESS;
211
438
 
212
439
  // Set the stream callback handler to handle data events
213
440
  MsQuic->SetCallbackHandler(Stream, (void*)StreamCallback, stream_ctx);
441
+ } else {
442
+ MsQuic->StreamClose(Stream);
214
443
  }
215
444
  break;
216
445
  default:
@@ -240,16 +469,18 @@ ListenerCallback(HQUIC Listener, void* Context, QUIC_LISTENER_EVENT* Event)
240
469
  conn_ctx->failed = 0;
241
470
  conn_ctx->error_status = QUIC_STATUS_SUCCESS;
242
471
  conn_ctx->error_code = 0;
243
-
472
+ conn_ctx->client_obj = Qnil; // Server connections have no client object
473
+
244
474
  // Set the connection callback
245
475
  MsQuic->SetCallbackHandler(Event->NEW_CONNECTION.Connection, (void*)ConnectionCallback, conn_ctx);
246
-
476
+
247
477
  // Accept the new connection with the server configuration
248
478
  QUIC_STATUS Status = MsQuic->ConnectionSetConfiguration(Event->NEW_CONNECTION.Connection, ctx->Configuration);
249
479
  if (QUIC_FAILED(Status)) {
250
480
  free(conn_ctx);
251
481
  return Status;
252
482
  }
483
+
253
484
  } else {
254
485
  // Reject the connection if we can't allocate context
255
486
  return QUIC_STATUS_OUT_OF_MEMORY;
@@ -283,15 +514,67 @@ quicsilver_open(VALUE self)
283
514
  rb_raise(rb_eRuntimeError, "MsQuicOpenVersion failed, 0x%x!", Status);
284
515
  return Qfalse;
285
516
  }
286
-
287
- // Create a registration for the app's connections
517
+
518
+ // Custom execution MUST be set up BEFORE RegistrationOpen.
519
+ // RegistrationOpen triggers MsQuic lazy init (LazyInitComplete=TRUE),
520
+ // after which ExecutionCreate returns QUIC_STATUS_INVALID_STATE.
521
+ #if __linux__
522
+ EventQ = epoll_create1(0);
523
+ #elif __APPLE__ || __FreeBSD__
524
+ EventQ = kqueue();
525
+ #endif
526
+ if (EventQ == -1) {
527
+ MsQuicClose(MsQuic);
528
+ MsQuic = NULL;
529
+ rb_raise(rb_eRuntimeError, "Failed to create event queue for custom execution");
530
+ return Qfalse;
531
+ }
532
+
533
+ QUIC_EXECUTION_CONFIG exec_config = { 0, &EventQ };
534
+ Status = MsQuic->ExecutionCreate(
535
+ QUIC_GLOBAL_EXECUTION_CONFIG_FLAG_NONE,
536
+ 0, // PollingIdleTimeoutUs
537
+ 1, // 1 execution context
538
+ &exec_config,
539
+ &ExecContext
540
+ );
541
+ if (QUIC_FAILED(Status)) {
542
+ close(EventQ);
543
+ EventQ = -1;
544
+ MsQuicClose(MsQuic);
545
+ MsQuic = NULL;
546
+ rb_raise(rb_eRuntimeError, "ExecutionCreate failed, 0x%x!", Status);
547
+ return Qfalse;
548
+ }
549
+
550
+ // Now open registration — MsQuic lazy init will see the custom execution
551
+ // context and skip spawning its own worker threads.
288
552
  if (QUIC_FAILED(Status = MsQuic->RegistrationOpen(&RegConfig, &Registration))) {
289
- rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
553
+ MsQuic->ExecutionDelete(1, &ExecContext);
554
+ ExecContext = NULL;
555
+ close(EventQ);
556
+ EventQ = -1;
290
557
  MsQuicClose(MsQuic);
291
558
  MsQuic = NULL;
559
+ rb_raise(rb_eRuntimeError, "RegistrationOpen failed, 0x%x!", Status);
292
560
  return Qfalse;
293
561
  }
294
-
562
+
563
+ // Register wake source — Ruby threads can unblock the event loop
564
+ #if __linux__
565
+ WakeFd = eventfd(0, EFD_NONBLOCK);
566
+ if (WakeFd != -1) {
567
+ struct epoll_event ev = { .events = EPOLLIN, .data.ptr = NULL };
568
+ epoll_ctl(EventQ, EPOLL_CTL_ADD, WakeFd, &ev);
569
+ }
570
+ #elif __APPLE__ || __FreeBSD__
571
+ {
572
+ struct kevent kev;
573
+ EV_SET(&kev, WAKE_IDENT, EVFILT_USER, EV_ADD | EV_CLEAR, 0, 0, NULL);
574
+ kevent(EventQ, &kev, 1, NULL, 0, NULL);
575
+ }
576
+ #endif
577
+
295
578
  return Qtrue;
296
579
  }
297
580
 
@@ -331,11 +614,11 @@ quicsilver_create_configuration(VALUE self, VALUE unsecure)
331
614
  }
332
615
 
333
616
  if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
334
- rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
335
617
  MsQuic->ConfigurationClose(Configuration);
618
+ rb_raise(rb_eRuntimeError, "ConfigurationLoadCredential failed, 0x%x!", Status);
336
619
  return Qnil;
337
620
  }
338
-
621
+
339
622
  // Return the configuration handle as a Ruby integer (pointer)
340
623
  return ULL2NUM((uintptr_t)Configuration);
341
624
  }
@@ -350,12 +633,25 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
350
633
  }
351
634
  VALUE cert_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("cert_file")));
352
635
  VALUE key_file_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("key_file")));
353
- VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout")));
636
+ VALUE idle_timeout_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("idle_timeout_ms")));
354
637
  VALUE server_resumption_level_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("server_resumption_level")));
355
- VALUE peer_bidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_bidi_stream_count")));
356
- VALUE peer_unidi_stream_count_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("peer_unidi_stream_count")));
638
+ VALUE max_concurrent_requests_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_concurrent_requests")));
639
+ VALUE max_unidirectional_streams_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_unidirectional_streams")));
357
640
  VALUE alpn_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("alpn")));
358
-
641
+ VALUE stream_receive_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_window")));
642
+ VALUE stream_receive_buffer_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("stream_receive_buffer")));
643
+ VALUE connection_flow_control_window_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("connection_flow_control_window")));
644
+ VALUE pacing_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("pacing_enabled")));
645
+ VALUE send_buffering_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("send_buffering_enabled")));
646
+ VALUE initial_rtt_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_rtt_ms")));
647
+ VALUE initial_window_packets_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("initial_window_packets")));
648
+ VALUE max_ack_delay_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("max_ack_delay_ms")));
649
+ VALUE keep_alive_interval_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("keep_alive_interval_ms")));
650
+ VALUE congestion_control_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("congestion_control_algorithm")));
651
+ VALUE migration_enabled_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("migration_enabled")));
652
+ VALUE disconnect_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("disconnect_timeout_ms")));
653
+ VALUE handshake_idle_timeout_ms_val = rb_hash_aref(config_hash, ID2SYM(rb_intern("handshake_idle_timeout_ms")));
654
+
359
655
  QUIC_STATUS Status;
360
656
  HQUIC Configuration = NULL;
361
657
 
@@ -363,20 +659,65 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
363
659
  const char* key_path = StringValueCStr(key_file_val);
364
660
  uint32_t idle_timeout_ms = NUM2INT(idle_timeout_val);
365
661
  uint32_t server_resumption_level = NUM2INT(server_resumption_level_val);
366
- uint32_t peer_bidi_stream_count = NUM2INT(peer_bidi_stream_count_val);
367
- uint32_t peer_unidi_stream_count = NUM2INT(peer_unidi_stream_count_val);
662
+ uint32_t max_concurrent_requests = NUM2INT(max_concurrent_requests_val);
663
+ uint32_t max_unidirectional_streams = NUM2INT(max_unidirectional_streams_val);
368
664
  const char* alpn_str = StringValueCStr(alpn_val);
665
+ uint32_t stream_receive_window = NUM2UINT(stream_receive_window_val);
666
+ uint32_t stream_receive_buffer = NUM2UINT(stream_receive_buffer_val);
667
+ uint32_t connection_flow_control_window = NUM2UINT(connection_flow_control_window_val);
668
+ uint8_t pacing_enabled = (uint8_t)NUM2INT(pacing_enabled_val);
669
+ uint8_t send_buffering_enabled = (uint8_t)NUM2INT(send_buffering_enabled_val);
670
+ uint32_t initial_rtt_ms = NUM2UINT(initial_rtt_ms_val);
671
+ uint32_t initial_window_packets = NUM2UINT(initial_window_packets_val);
672
+ uint32_t max_ack_delay_ms = NUM2UINT(max_ack_delay_ms_val);
673
+ uint32_t keep_alive_interval_ms = NUM2UINT(keep_alive_interval_ms_val);
674
+ uint16_t congestion_control = (uint16_t)NUM2INT(congestion_control_val);
675
+ uint8_t migration_enabled = (uint8_t)NUM2INT(migration_enabled_val);
676
+ uint32_t disconnect_timeout_ms = NUM2UINT(disconnect_timeout_ms_val);
677
+ uint64_t handshake_idle_timeout_ms = NUM2ULL(handshake_idle_timeout_ms_val);
369
678
 
370
679
  QUIC_SETTINGS Settings = {0};
371
- Settings.IdleTimeoutMs = idle_timeout_ms;
680
+ Settings.IdleTimeoutMs = idle_timeout_ms;
372
681
  Settings.IsSet.IdleTimeoutMs = TRUE;
373
682
  Settings.ServerResumptionLevel = server_resumption_level;
374
683
  Settings.IsSet.ServerResumptionLevel = TRUE;
375
- Settings.PeerBidiStreamCount = peer_bidi_stream_count;
684
+ Settings.PeerBidiStreamCount = max_concurrent_requests;
376
685
  Settings.IsSet.PeerBidiStreamCount = TRUE;
377
- Settings.PeerUnidiStreamCount = peer_unidi_stream_count;
686
+ Settings.PeerUnidiStreamCount = max_unidirectional_streams;
378
687
  Settings.IsSet.PeerUnidiStreamCount = TRUE;
379
688
 
689
+ // Flow control / backpressure settings
690
+ Settings.StreamRecvWindowDefault = stream_receive_window;
691
+ Settings.IsSet.StreamRecvWindowDefault = TRUE;
692
+ Settings.StreamRecvBufferDefault = stream_receive_buffer;
693
+ Settings.IsSet.StreamRecvBufferDefault = TRUE;
694
+ Settings.ConnFlowControlWindow = connection_flow_control_window;
695
+ Settings.IsSet.ConnFlowControlWindow = TRUE;
696
+
697
+ // Throughput settings
698
+ Settings.PacingEnabled = pacing_enabled;
699
+ Settings.IsSet.PacingEnabled = TRUE;
700
+ Settings.SendBufferingEnabled = send_buffering_enabled;
701
+ Settings.IsSet.SendBufferingEnabled = TRUE;
702
+ Settings.InitialRttMs = initial_rtt_ms;
703
+ Settings.IsSet.InitialRttMs = TRUE;
704
+ Settings.InitialWindowPackets = initial_window_packets;
705
+ Settings.IsSet.InitialWindowPackets = TRUE;
706
+ Settings.MaxAckDelayMs = max_ack_delay_ms;
707
+ Settings.IsSet.MaxAckDelayMs = TRUE;
708
+
709
+ // Connection management
710
+ Settings.KeepAliveIntervalMs = keep_alive_interval_ms;
711
+ Settings.IsSet.KeepAliveIntervalMs = TRUE;
712
+ Settings.CongestionControlAlgorithm = congestion_control;
713
+ Settings.IsSet.CongestionControlAlgorithm = TRUE;
714
+ Settings.MigrationEnabled = migration_enabled;
715
+ Settings.IsSet.MigrationEnabled = TRUE;
716
+ Settings.DisconnectTimeoutMs = disconnect_timeout_ms;
717
+ Settings.IsSet.DisconnectTimeoutMs = TRUE;
718
+ Settings.HandshakeIdleTimeoutMs = handshake_idle_timeout_ms;
719
+ Settings.IsSet.HandshakeIdleTimeoutMs = TRUE;
720
+
380
721
  QUIC_BUFFER Alpn = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
381
722
 
382
723
  // Create configuration
@@ -397,8 +738,8 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
397
738
  CredConfig.Flags = QUIC_CREDENTIAL_FLAG_NO_CERTIFICATE_VALIDATION;
398
739
 
399
740
  if (QUIC_FAILED(Status = MsQuic->ConfigurationLoadCredential(Configuration, &CredConfig))) {
400
- rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
401
741
  MsQuic->ConfigurationClose(Configuration);
742
+ rb_raise(rb_eRuntimeError, "Server ConfigurationLoadCredential failed, 0x%x!", Status);
402
743
  return Qnil;
403
744
  }
404
745
 
@@ -408,35 +749,44 @@ quicsilver_create_server_configuration(VALUE self, VALUE config_hash)
408
749
 
409
750
  // Create a QUIC connection with context
410
751
  static VALUE
411
- quicsilver_create_connection(VALUE self)
752
+ quicsilver_create_connection(VALUE self, VALUE client_obj)
412
753
  {
413
754
  if (MsQuic == NULL) {
414
755
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized. Call Quicsilver.open_connection first.");
415
756
  return Qnil;
416
757
  }
417
-
758
+
418
759
  QUIC_STATUS Status;
419
760
  HQUIC Connection = NULL;
420
-
761
+
421
762
  // Allocate and initialize connection context
422
763
  ConnectionContext* ctx = (ConnectionContext*)malloc(sizeof(ConnectionContext));
423
764
  if (ctx == NULL) {
424
765
  rb_raise(rb_eRuntimeError, "Failed to allocate connection context");
425
766
  return Qnil;
426
767
  }
427
-
768
+
428
769
  ctx->connected = 0;
429
770
  ctx->failed = 0;
430
771
  ctx->error_status = QUIC_STATUS_SUCCESS;
431
772
  ctx->error_code = 0;
432
-
773
+ ctx->client_obj = client_obj; // Store Ruby client object (Qnil for server)
774
+
775
+ // Protect from GC if it's a Ruby object
776
+ if (!NIL_P(client_obj)) {
777
+ rb_gc_register_address(&ctx->client_obj);
778
+ }
779
+
433
780
  // Create connection with enhanced callback and context
434
781
  if (QUIC_FAILED(Status = MsQuic->ConnectionOpen(Registration, ConnectionCallback, ctx, &Connection))) {
782
+ if (!NIL_P(client_obj)) {
783
+ rb_gc_unregister_address(&ctx->client_obj);
784
+ }
435
785
  free(ctx);
436
786
  rb_raise(rb_eRuntimeError, "ConnectionOpen failed, 0x%x!", Status);
437
787
  return Qnil;
438
788
  }
439
-
789
+
440
790
  // Return both the connection handle and context as an array
441
791
  VALUE result = rb_ary_new2(2);
442
792
  rb_ary_push(result, ULL2NUM((uintptr_t)Connection));
@@ -463,7 +813,8 @@ quicsilver_start_connection(VALUE self, VALUE connection_handle, VALUE config_ha
463
813
  rb_raise(rb_eRuntimeError, "ConnectionStart failed, 0x%x!", Status);
464
814
  return Qfalse;
465
815
  }
466
-
816
+
817
+ wake_event_loop();
467
818
  return Qtrue;
468
819
  }
469
820
 
@@ -477,7 +828,7 @@ quicsilver_wait_for_connection(VALUE self, VALUE context_handle, VALUE timeout_m
477
828
  const int sleep_interval = 10; // 10ms
478
829
 
479
830
  while (elapsed < timeout && !ctx->connected && !ctx->failed) {
480
- usleep(sleep_interval * 1000); // Convert to microseconds
831
+ poll_inline(sleep_interval); // Drive MsQuic execution while waiting
481
832
  elapsed += sleep_interval;
482
833
  }
483
834
 
@@ -522,27 +873,64 @@ quicsilver_close_connection_handle(VALUE self, VALUE connection_data)
522
873
  if (MsQuic == NULL) {
523
874
  return Qnil;
524
875
  }
525
-
876
+
526
877
  // Extract connection handle and context from array
527
878
  VALUE connection_handle = rb_ary_entry(connection_data, 0);
528
879
  VALUE context_handle = rb_ary_entry(connection_data, 1);
529
-
880
+
530
881
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
531
- ConnectionContext* ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
532
-
533
- // Only close if connection is valid
882
+ (void)context_handle; // ctx freed by SHUTDOWN_COMPLETE, not here
883
+
534
884
  if (Connection != NULL) {
535
885
  MsQuic->ConnectionClose(Connection);
536
886
  }
537
-
538
- // Free context if valid
539
- if (ctx != NULL) {
540
- free(ctx);
887
+
888
+ // Don't free ctx here — ConnectionClose is async and SHUTDOWN_COMPLETE
889
+ // will fire on the next event loop poll, which still needs ctx.
890
+ // SHUTDOWN_COMPLETE handles cleanup for both client and server.
891
+
892
+ return Qnil;
893
+ }
894
+
895
+ // Close a server-side connection handle (context already freed in C callback)
896
+ static VALUE
897
+ quicsilver_close_server_connection(VALUE self, VALUE connection_handle)
898
+ {
899
+ if (MsQuic == NULL) return Qnil;
900
+
901
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
902
+ if (Connection != NULL) {
903
+ MsQuic->ConnectionClose(Connection);
541
904
  }
542
-
543
905
  return Qnil;
544
906
  }
545
907
 
908
+ // Gracefully shutdown a QUIC connection (sends CONNECTION_CLOSE frame to peer)
909
+ // silent = true: immediate shutdown without notifying peer
910
+ // silent = false: graceful shutdown with CONNECTION_CLOSE frame
911
+ static VALUE
912
+ quicsilver_connection_shutdown(VALUE self, VALUE connection_handle, VALUE error_code, VALUE silent)
913
+ {
914
+ if (MsQuic == NULL) {
915
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
916
+ return Qnil;
917
+ }
918
+
919
+ HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
920
+ uint64_t ErrorCode = NUM2ULL(error_code);
921
+
922
+ if (Connection != NULL) {
923
+ QUIC_CONNECTION_SHUTDOWN_FLAGS flags = RTEST(silent)
924
+ ? QUIC_CONNECTION_SHUTDOWN_FLAG_SILENT
925
+ : QUIC_CONNECTION_SHUTDOWN_FLAG_NONE;
926
+
927
+ MsQuic->ConnectionShutdown(Connection, flags, ErrorCode);
928
+ wake_event_loop();
929
+ }
930
+
931
+ return Qtrue;
932
+ }
933
+
546
934
  // Close a QUIC configuration
547
935
  static VALUE
548
936
  quicsilver_close_configuration(VALUE self, VALUE config_handle)
@@ -556,20 +944,35 @@ quicsilver_close_configuration(VALUE self, VALUE config_handle)
556
944
  return Qnil;
557
945
  }
558
946
 
559
- // Close MSQUIC
560
947
  static VALUE
561
948
  quicsilver_close(VALUE self)
562
949
  {
563
950
  if (MsQuic != NULL) {
564
951
  if (Registration != NULL) {
565
- // This will block until all outstanding child objects have been closed
566
952
  MsQuic->RegistrationClose(Registration);
567
953
  Registration = NULL;
568
954
  }
955
+
956
+ if (ExecContext != NULL) {
957
+ MsQuic->ExecutionDelete(1, &ExecContext);
958
+ ExecContext = NULL;
959
+ }
960
+
569
961
  MsQuicClose(MsQuic);
570
962
  MsQuic = NULL;
571
963
  }
572
-
964
+
965
+ #if __linux__
966
+ if (WakeFd != -1) {
967
+ close(WakeFd);
968
+ WakeFd = -1;
969
+ }
970
+ #endif
971
+ if (EventQ != -1) {
972
+ close(EventQ);
973
+ EventQ = -1;
974
+ }
975
+
573
976
  return Qnil;
574
977
  }
575
978
 
@@ -615,34 +1018,42 @@ quicsilver_create_listener(VALUE self, VALUE config_handle)
615
1018
 
616
1019
  // Start listener on specific address and port
617
1020
  static VALUE
618
- quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port)
1021
+ quicsilver_start_listener(VALUE self, VALUE listener_handle, VALUE address, VALUE port, VALUE alpn)
619
1022
  {
620
1023
  if (MsQuic == NULL) {
621
1024
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
622
1025
  return Qfalse;
623
1026
  }
624
-
1027
+
625
1028
  HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
626
1029
  uint16_t Port = (uint16_t)NUM2INT(port);
627
-
1030
+ const char* alpn_str = StringValueCStr(alpn);
1031
+
628
1032
  // Setup address - properly initialize the entire structure
629
1033
  QUIC_ADDR Address;
630
1034
  memset(&Address, 0, sizeof(Address));
631
-
632
- // Set up for localhost/any address
633
- QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET);
1035
+
1036
+ // Parse address string to determine family
1037
+ const char* addr_str = StringValueCStr(address);
1038
+ if (strchr(addr_str, ':') != NULL) {
1039
+ // IPv6 address (contains ':')
1040
+ QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_INET6);
1041
+ } else {
1042
+ // IPv4 address or unspecified - use UNSPEC for dual-stack
1043
+ QuicAddrSetFamily(&Address, QUIC_ADDRESS_FAMILY_UNSPEC);
1044
+ }
634
1045
  QuicAddrSetPort(&Address, Port);
635
-
1046
+
636
1047
  QUIC_STATUS Status;
637
-
638
- // Create QUIC_BUFFER for the address
639
- QUIC_BUFFER AlpnBuffer = { sizeof("h3") - 1, (uint8_t*)"h3" };
640
-
1048
+
1049
+ QUIC_BUFFER AlpnBuffer = { (uint32_t)strlen(alpn_str), (uint8_t*)alpn_str };
1050
+
641
1051
  if (QUIC_FAILED(Status = MsQuic->ListenerStart(Listener, &AlpnBuffer, 1, &Address))) {
642
1052
  rb_raise(rb_eRuntimeError, "ListenerStart failed, 0x%x!", Status);
643
1053
  return Qfalse;
644
1054
  }
645
-
1055
+
1056
+ wake_event_loop();
646
1057
  return Qtrue;
647
1058
  }
648
1059
 
@@ -653,9 +1064,10 @@ quicsilver_stop_listener(VALUE self, VALUE listener_handle)
653
1064
  if (MsQuic == NULL) {
654
1065
  return Qfalse;
655
1066
  }
656
-
1067
+
657
1068
  HQUIC Listener = (HQUIC)(uintptr_t)NUM2ULL(listener_handle);
658
1069
  MsQuic->ListenerStop(Listener);
1070
+ wake_event_loop();
659
1071
  return Qtrue;
660
1072
  }
661
1073
 
@@ -683,15 +1095,22 @@ quicsilver_close_listener(VALUE self, VALUE listener_data)
683
1095
  }
684
1096
 
685
1097
  // Open a QUIC stream
1098
+ // Accepts connection_data array [connection_handle, context_handle]
1099
+ // Works uniformly for both client and server
686
1100
  static VALUE
687
- quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional)
1101
+ quicsilver_open_stream(VALUE self, VALUE connection_data, VALUE unidirectional)
688
1102
  {
689
1103
  if (MsQuic == NULL) {
690
1104
  rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
691
1105
  return Qnil;
692
1106
  }
693
1107
 
1108
+ // Extract connection handle and context from array
1109
+ VALUE connection_handle = rb_ary_entry(connection_data, 0);
1110
+ VALUE context_handle = rb_ary_entry(connection_data, 1);
1111
+
694
1112
  HQUIC Connection = (HQUIC)(uintptr_t)NUM2ULL(connection_handle);
1113
+ ConnectionContext* conn_ctx = (ConnectionContext*)(uintptr_t)NUM2ULL(context_handle);
695
1114
  HQUIC Stream = NULL;
696
1115
 
697
1116
  StreamContext* ctx = (StreamContext*)malloc(sizeof(StreamContext));
@@ -699,9 +1118,13 @@ quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional
699
1118
  rb_raise(rb_eRuntimeError, "Failed to allocate stream context");
700
1119
  return Qnil;
701
1120
  }
702
-
1121
+
1122
+ ctx->connection = Connection;
1123
+ ctx->connection_ctx = conn_ctx; // Store connection context pointer
1124
+ ctx->client_obj = conn_ctx ? conn_ctx->client_obj : Qnil;
703
1125
  ctx->started = 1;
704
1126
  ctx->shutdown = 0;
1127
+ ctx->early_data = 0;
705
1128
  ctx->error_status = QUIC_STATUS_SUCCESS;
706
1129
 
707
1130
  // Use flag based on parameter
@@ -720,12 +1143,13 @@ quicsilver_open_stream(VALUE self, VALUE connection_handle, VALUE unidirectional
720
1143
  // Start the stream
721
1144
  Status = MsQuic->StreamStart(Stream, QUIC_STREAM_START_FLAG_NONE);
722
1145
  if (QUIC_FAILED(Status)) {
723
- free(ctx);
1146
+ // StreamClose fires SHUTDOWN_COMPLETE synchronously which frees ctx
724
1147
  MsQuic->StreamClose(Stream);
725
1148
  rb_raise(rb_eRuntimeError, "StreamStart failed, 0x%x!", Status);
726
1149
  return Qnil;
727
1150
  }
728
-
1151
+
1152
+ wake_event_loop();
729
1153
  return ULL2NUM((uintptr_t)Stream);
730
1154
  }
731
1155
 
@@ -766,9 +1190,57 @@ quicsilver_send_stream(VALUE self, VALUE stream_handle, VALUE data, VALUE send_f
766
1190
  rb_raise(rb_eRuntimeError, "StreamSend failed, 0x%x!", Status);
767
1191
  return Qfalse;
768
1192
  }
769
-
1193
+
1194
+ wake_event_loop();
770
1195
  return Qtrue;
771
- }
1196
+ }
1197
+
1198
+ // Reset a QUIC stream (RESET_STREAM frame - abruptly terminates sending)
1199
+ static VALUE
1200
+ quicsilver_stream_reset(VALUE self, VALUE stream_handle, VALUE error_code)
1201
+ {
1202
+ if (MsQuic == NULL) {
1203
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1204
+ return Qnil;
1205
+ }
1206
+
1207
+ HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1208
+ if (Stream == NULL) return Qnil;
1209
+
1210
+ uint64_t ErrorCode = NUM2ULL(error_code);
1211
+
1212
+ MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_SEND, ErrorCode);
1213
+
1214
+ wake_event_loop();
1215
+ return Qtrue;
1216
+ }
1217
+
1218
+ // Stop sending on a QUIC stream (STOP_SENDING frame - requests peer to stop)
1219
+ static VALUE
1220
+ quicsilver_stream_stop_sending(VALUE self, VALUE stream_handle, VALUE error_code)
1221
+ {
1222
+ if (MsQuic == NULL) {
1223
+ rb_raise(rb_eRuntimeError, "MSQUIC not initialized.");
1224
+ return Qnil;
1225
+ }
1226
+
1227
+ HQUIC Stream = (HQUIC)(uintptr_t)NUM2ULL(stream_handle);
1228
+ if (Stream == NULL) return Qnil;
1229
+
1230
+ uint64_t ErrorCode = NUM2ULL(error_code);
1231
+
1232
+ MsQuic->StreamShutdown(Stream, QUIC_STREAM_SHUTDOWN_FLAG_ABORT_RECEIVE, ErrorCode);
1233
+
1234
+ wake_event_loop();
1235
+ return Qtrue;
1236
+ }
1237
+
1238
+ static VALUE
1239
+ quicsilver_wake(VALUE self)
1240
+ {
1241
+ wake_event_loop();
1242
+ return Qnil;
1243
+ }
772
1244
 
773
1245
  // Initialize the extension
774
1246
  void
@@ -785,23 +1257,28 @@ Init_quicsilver(void)
785
1257
  rb_define_singleton_method(mQuicsilver, "create_server_configuration", quicsilver_create_server_configuration, 1);
786
1258
  rb_define_singleton_method(mQuicsilver, "close_configuration", quicsilver_close_configuration, 1);
787
1259
 
788
- // Connection management
789
- rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 0);
1260
+ // Connection management
1261
+ rb_define_singleton_method(mQuicsilver, "create_connection", quicsilver_create_connection, 1);
790
1262
  rb_define_singleton_method(mQuicsilver, "start_connection", quicsilver_start_connection, 4);
791
1263
  rb_define_singleton_method(mQuicsilver, "wait_for_connection", quicsilver_wait_for_connection, 2);
792
1264
  rb_define_singleton_method(mQuicsilver, "connection_status", quicsilver_connection_status, 1);
1265
+ rb_define_singleton_method(mQuicsilver, "connection_shutdown", quicsilver_connection_shutdown, 3);
793
1266
  rb_define_singleton_method(mQuicsilver, "close_connection_handle", quicsilver_close_connection_handle, 1);
1267
+ rb_define_singleton_method(mQuicsilver, "close_server_connection", quicsilver_close_server_connection, 1);
794
1268
 
795
1269
  // Listener management
796
1270
  rb_define_singleton_method(mQuicsilver, "create_listener", quicsilver_create_listener, 1);
797
- rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 3);
1271
+ rb_define_singleton_method(mQuicsilver, "start_listener", quicsilver_start_listener, 4);
798
1272
  rb_define_singleton_method(mQuicsilver, "stop_listener", quicsilver_stop_listener, 1);
799
1273
  rb_define_singleton_method(mQuicsilver, "close_listener", quicsilver_close_listener, 1);
800
1274
 
801
1275
  // Stream management
802
1276
  rb_define_singleton_method(mQuicsilver, "open_stream", quicsilver_open_stream, 2);
803
1277
  rb_define_singleton_method(mQuicsilver, "send_stream", quicsilver_send_stream, 3);
1278
+ rb_define_singleton_method(mQuicsilver, "stream_reset", quicsilver_stream_reset, 2);
1279
+ rb_define_singleton_method(mQuicsilver, "stream_stop_sending", quicsilver_stream_stop_sending, 2);
804
1280
 
805
- // Event processing
806
- rb_define_singleton_method(mQuicsilver, "process_events", quicsilver_process_events, 0);
1281
+ // Event processing (custom execution — app drives MsQuic)
1282
+ rb_define_singleton_method(mQuicsilver, "poll", quicsilver_poll, 0);
1283
+ rb_define_singleton_method(mQuicsilver, "wake", quicsilver_wake, 0);
807
1284
  }