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