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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +41 -0
- data/.gitignore +3 -1
- data/CHANGELOG.md +76 -5
- data/Gemfile.lock +18 -4
- data/LICENSE +21 -0
- data/README.md +33 -53
- data/Rakefile +29 -2
- data/benchmarks/components.rb +191 -0
- data/benchmarks/concurrent.rb +110 -0
- data/benchmarks/helpers.rb +88 -0
- data/benchmarks/quicsilver_server.rb +46 -0
- data/benchmarks/rails.rb +170 -0
- data/benchmarks/throughput.rb +113 -0
- data/examples/minimal_http3_server.rb +0 -6
- data/examples/rack_http3_server.rb +0 -6
- data/examples/simple_client_test.rb +26 -0
- data/ext/quicsilver/quicsilver.c +615 -138
- data/lib/quicsilver/client/client.rb +250 -0
- data/lib/quicsilver/client/request.rb +98 -0
- data/lib/quicsilver/protocol/frames.rb +327 -0
- data/lib/quicsilver/protocol/qpack/decoder.rb +165 -0
- data/lib/quicsilver/protocol/qpack/encoder.rb +189 -0
- data/lib/quicsilver/protocol/qpack/header_block_decoder.rb +125 -0
- data/lib/quicsilver/protocol/qpack/huffman.rb +459 -0
- data/lib/quicsilver/protocol/request_encoder.rb +47 -0
- data/lib/quicsilver/protocol/request_parser.rb +387 -0
- data/lib/quicsilver/protocol/response_encoder.rb +72 -0
- data/lib/quicsilver/protocol/response_parser.rb +249 -0
- data/lib/quicsilver/server/listener_data.rb +14 -0
- data/lib/quicsilver/server/request_handler.rb +86 -0
- data/lib/quicsilver/server/request_registry.rb +50 -0
- data/lib/quicsilver/server/server.rb +336 -0
- data/lib/quicsilver/transport/configuration.rb +132 -0
- data/lib/quicsilver/transport/connection.rb +350 -0
- data/lib/quicsilver/transport/event_loop.rb +38 -0
- data/lib/quicsilver/transport/inbound_stream.rb +33 -0
- data/lib/quicsilver/transport/stream.rb +28 -0
- data/lib/quicsilver/transport/stream_event.rb +26 -0
- data/lib/quicsilver/version.rb +1 -1
- data/lib/quicsilver.rb +49 -9
- data/lib/rackup/handler/quicsilver.rb +77 -0
- data/quicsilver.gemspec +10 -3
- metadata +122 -17
- data/examples/minimal_http3_client.rb +0 -89
- data/lib/quicsilver/client.rb +0 -191
- data/lib/quicsilver/http3/request_encoder.rb +0 -112
- data/lib/quicsilver/http3/request_parser.rb +0 -158
- data/lib/quicsilver/http3/response_encoder.rb +0 -73
- data/lib/quicsilver/http3.rb +0 -68
- data/lib/quicsilver/listener_data.rb +0 -29
- data/lib/quicsilver/server.rb +0 -258
- data/lib/quicsilver/server_configuration.rb +0 -49
data/ext/quicsilver/quicsilver.c
CHANGED
|
@@ -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
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
static
|
|
20
|
-
|
|
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
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
if (event == NULL) return;
|
|
99
|
+
struct dispatch_ruby_args* a = (struct dispatch_ruby_args*)arg;
|
|
61
100
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
84
|
-
{
|
|
85
|
-
|
|
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
|
-
|
|
197
|
+
// 1. ExecutionPoll — process MsQuic timers/state, may fire callbacks (has GVL)
|
|
198
|
+
uint32_t wait_ms = MsQuic->ExecutionPoll(ExecContext);
|
|
101
199
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
142
|
-
size_t total_len = sizeof(HQUIC) +
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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("
|
|
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
|
|
356
|
-
VALUE
|
|
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
|
|
367
|
-
uint32_t
|
|
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 =
|
|
684
|
+
Settings.PeerBidiStreamCount = max_concurrent_requests;
|
|
376
685
|
Settings.IsSet.PeerBidiStreamCount = TRUE;
|
|
377
|
-
Settings.PeerUnidiStreamCount =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
539
|
-
|
|
540
|
-
|
|
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
|
-
//
|
|
633
|
-
|
|
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
|
-
|
|
639
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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, "
|
|
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
|
}
|