winloop 0.2.0 → 0.2.1
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/README.md +11 -278
- data/lib/winloop.rb +4 -81
- metadata +11 -67
- data/CHANGELOG.md +0 -88
- data/ext/winloop/extconf.rb +0 -19
- data/ext/winloop/winloop.c +0 -712
- data/lib/winloop/ops.rb +0 -36
- data/lib/winloop/scheduler.rb +0 -656
- data/lib/winloop/version.rb +0 -5
data/ext/winloop/winloop.c
DELETED
|
@@ -1,712 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* winloop — a Windows IOCP event-loop backend for a Ruby Fiber Scheduler.
|
|
3
|
-
*
|
|
4
|
-
* This C extension owns one I/O Completion Port plus one \Device\Afd handle and
|
|
5
|
-
* exposes the minimum the Ruby scheduler needs:
|
|
6
|
-
*
|
|
7
|
-
* Winloop::Backend.new -> create the IOCP + AFD handle
|
|
8
|
-
* #poll(io, events) -> id arm a one-shot AFD readiness poll for `io`
|
|
9
|
-
* #cancel(id) cancel a pending poll (CancelIoEx)
|
|
10
|
-
* #wait(timeout_ms) -> [[id, ready_events], ...] reap completions (one GQCSEx)
|
|
11
|
-
* #wakeup PostQueuedCompletionStatus (break the wait)
|
|
12
|
-
* #shutdown cancel + drain + close (idempotent)
|
|
13
|
-
*
|
|
14
|
-
* Readiness over a *completion* port is done the libuv/mio/wepoll way: submit
|
|
15
|
-
* IOCTL_AFD_POLL on \Device\Afd as an overlapped op whose ApcContext is the
|
|
16
|
-
* OVERLAPPED pointer (THE make-or-break detail — without it no completion packet
|
|
17
|
-
* is queued). Each in-flight poll is a winloop_req whose OVERLAPPED is the first
|
|
18
|
-
* member, so a completion's lpOverlapped casts straight back to the request.
|
|
19
|
-
*
|
|
20
|
-
* Pure C (no C++/-EHsc), so rb_raise (longjmp) is the normal, safe mechanism.
|
|
21
|
-
* include <ruby.h> before the Windows headers; never name a variable IN/OUT.
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
#include <ruby.h>
|
|
25
|
-
#include <ruby/io.h>
|
|
26
|
-
#include <ruby/win32.h>
|
|
27
|
-
#include <ruby/st.h>
|
|
28
|
-
#include <ruby/thread.h>
|
|
29
|
-
|
|
30
|
-
#include <winsock2.h>
|
|
31
|
-
#include <ws2tcpip.h>
|
|
32
|
-
#include <windows.h>
|
|
33
|
-
#include <winternl.h>
|
|
34
|
-
#include <stdint.h>
|
|
35
|
-
|
|
36
|
-
/* ---- Ruby IO event bits (verified: READABLE=1, WRITABLE=4, PRIORITY=2) ---- */
|
|
37
|
-
#define RB_READABLE 1
|
|
38
|
-
#define RB_PRIORITY 2
|
|
39
|
-
#define RB_WRITABLE 4
|
|
40
|
-
|
|
41
|
-
/* ---- AFD poll flags / IOCTL (verified on the target machine) ---- */
|
|
42
|
-
#define AFD_POLL_RECEIVE 0x0001
|
|
43
|
-
#define AFD_POLL_RECEIVE_EXPEDITED 0x0002
|
|
44
|
-
#define AFD_POLL_SEND 0x0004
|
|
45
|
-
#define AFD_POLL_DISCONNECT 0x0008
|
|
46
|
-
#define AFD_POLL_ABORT 0x0010
|
|
47
|
-
#define AFD_POLL_LOCAL_CLOSE 0x0020
|
|
48
|
-
#define AFD_POLL_ACCEPT 0x0080
|
|
49
|
-
#define AFD_POLL_CONNECT_FAIL 0x0100
|
|
50
|
-
#define IOCTL_AFD_POLL 0x00012024
|
|
51
|
-
#ifndef SIO_BASE_HANDLE
|
|
52
|
-
#define SIO_BASE_HANDLE 0x48000022
|
|
53
|
-
#endif
|
|
54
|
-
#ifndef STATUS_PENDING
|
|
55
|
-
#define STATUS_PENDING ((NTSTATUS)0x00000103L)
|
|
56
|
-
#endif
|
|
57
|
-
#define AFD_FILE_OPEN 0x00000001
|
|
58
|
-
|
|
59
|
-
#define WAKE_KEY ((ULONG_PTR)0x57414B45ull) /* "WAKE" — PostQueuedCompletionStatus marker */
|
|
60
|
-
#define AFD_KEY ((ULONG_PTR)0x41464430ull) /* "AFD0" — the AFD handle's completion key */
|
|
61
|
-
#define EXT_KEY ((ULONG_PTR)0x45585431ull) /* "EXT1" — ALL generic client ops (winloop 0.2) */
|
|
62
|
-
#define REAP_CAP 64
|
|
63
|
-
|
|
64
|
-
/* Generic op state machine: prepare -> submit -> complete (reaped) -> retire (freed).
|
|
65
|
-
* Only a reaped completion ends kernel ownership of the OVERLAPPED + buffer; the
|
|
66
|
-
* sole exception is op_abandon on a PREPARED op the kernel never saw. */
|
|
67
|
-
enum { OP_PREPARED = 0, OP_SUBMITTED = 1, OP_COMPLETED = 2 };
|
|
68
|
-
|
|
69
|
-
typedef struct _AFD_POLL_HANDLE_INFO {
|
|
70
|
-
HANDLE Handle;
|
|
71
|
-
ULONG Events;
|
|
72
|
-
NTSTATUS Status;
|
|
73
|
-
} AFD_POLL_HANDLE_INFO;
|
|
74
|
-
typedef struct _AFD_POLL_INFO {
|
|
75
|
-
LARGE_INTEGER Timeout;
|
|
76
|
-
ULONG NumberOfHandles;
|
|
77
|
-
ULONG Exclusive;
|
|
78
|
-
AFD_POLL_HANDLE_INFO Handles[1];
|
|
79
|
-
} AFD_POLL_INFO;
|
|
80
|
-
|
|
81
|
-
typedef NTSTATUS (NTAPI *PFN_NtCreateFile)(PHANDLE, ACCESS_MASK, POBJECT_ATTRIBUTES,
|
|
82
|
-
PIO_STATUS_BLOCK, PLARGE_INTEGER, ULONG, ULONG, ULONG, ULONG, PVOID, ULONG);
|
|
83
|
-
typedef NTSTATUS (NTAPI *PFN_NtDeviceIoControlFile)(HANDLE, HANDLE, PIO_APC_ROUTINE,
|
|
84
|
-
PVOID, PIO_STATUS_BLOCK, ULONG, PVOID, ULONG, PVOID, ULONG);
|
|
85
|
-
typedef VOID (NTAPI *PFN_RtlInitUnicodeString)(PUNICODE_STRING, PCWSTR);
|
|
86
|
-
typedef ULONG (NTAPI *PFN_RtlNtStatusToDosError)(NTSTATUS);
|
|
87
|
-
|
|
88
|
-
static PFN_NtCreateFile pNtCreateFile;
|
|
89
|
-
static PFN_NtDeviceIoControlFile pNtDeviceIoControlFile;
|
|
90
|
-
static PFN_RtlInitUnicodeString pRtlInitUnicodeString;
|
|
91
|
-
static PFN_RtlNtStatusToDosError pRtlNtStatusToDosError;
|
|
92
|
-
|
|
93
|
-
#ifndef InitializeObjectAttributes
|
|
94
|
-
#define InitializeObjectAttributes(p, n, a, r, s) do { \
|
|
95
|
-
(p)->Length = sizeof(OBJECT_ATTRIBUTES); (p)->RootDirectory = (r); \
|
|
96
|
-
(p)->Attributes = (a); (p)->ObjectName = (n); (p)->SecurityDescriptor = (s); \
|
|
97
|
-
(p)->SecurityQualityOfService = NULL; } while (0)
|
|
98
|
-
#endif
|
|
99
|
-
|
|
100
|
-
/* One in-flight AFD poll. OVERLAPPED MUST be first so lpOverlapped casts back. */
|
|
101
|
-
typedef struct {
|
|
102
|
-
OVERLAPPED ov;
|
|
103
|
-
uint64_t id;
|
|
104
|
-
AFD_POLL_INFO in;
|
|
105
|
-
AFD_POLL_INFO out;
|
|
106
|
-
} winloop_req;
|
|
107
|
-
|
|
108
|
-
/* One generic op (winloop 0.2). OVERLAPPED MUST be first: a completion's
|
|
109
|
-
* lpOverlapped casts back to the record — the same proven pattern as winloop_req. */
|
|
110
|
-
typedef struct {
|
|
111
|
-
OVERLAPPED ov; /* zeroed at prepare; kernel writes Internal/InternalHigh */
|
|
112
|
-
uint64_t id; /* Ruby-visible op id (shares next_id with AFD reqs) */
|
|
113
|
-
uint64_t tag; /* user value, echoed in the completion tuple */
|
|
114
|
-
HANDLE handle; /* associated client handle for CancelIoEx; never NULL */
|
|
115
|
-
int state; /* OP_PREPARED -> OP_SUBMITTED -> OP_COMPLETED */
|
|
116
|
-
DWORD bytes; /* filled at reap */
|
|
117
|
-
DWORD error; /* Win32 code at reap (RtlNtStatusToDosError(ov.Internal)) */
|
|
118
|
-
size_t cap; /* embedded buffer capacity (0 = none) */
|
|
119
|
-
/* unsigned char buf[cap] co-allocated at OP_BUF_OFFSET (8-byte aligned) */
|
|
120
|
-
} winloop_op;
|
|
121
|
-
|
|
122
|
-
#define OP_BUF_OFFSET ((sizeof(winloop_op) + 7) & ~(size_t)7)
|
|
123
|
-
|
|
124
|
-
typedef struct {
|
|
125
|
-
HANDLE iocp;
|
|
126
|
-
HANDLE afd;
|
|
127
|
-
uint64_t next_id;
|
|
128
|
-
st_table *reqs; /* id -> winloop_req* */
|
|
129
|
-
st_table *ops_by_id; /* id -> winloop_op* (Ruby-facing op methods) */
|
|
130
|
-
st_table *ops_by_addr; /* OVERLAPPED addr -> winloop_op* (defensive reap) */
|
|
131
|
-
st_table *assoc_handles; /* set of HANDLE values passed to #associate (op_prepare
|
|
132
|
-
validation ONLY — never used for blanket cancels, so
|
|
133
|
-
a recycled handle value can't trigger a wrong cancel) */
|
|
134
|
-
size_t op_bytes; /* live op allocations, reported by backend_memsize */
|
|
135
|
-
int closed;
|
|
136
|
-
} winloop_backend;
|
|
137
|
-
|
|
138
|
-
static VALUE mWinloop, cBackend, eError;
|
|
139
|
-
|
|
140
|
-
/* ---- ntdll resolution ---- */
|
|
141
|
-
static int resolve_ntdll(void) {
|
|
142
|
-
HMODULE nt = GetModuleHandleW(L"ntdll.dll");
|
|
143
|
-
if (!nt) return 0;
|
|
144
|
-
pNtCreateFile = (PFN_NtCreateFile) GetProcAddress(nt, "NtCreateFile");
|
|
145
|
-
pNtDeviceIoControlFile = (PFN_NtDeviceIoControlFile) GetProcAddress(nt, "NtDeviceIoControlFile");
|
|
146
|
-
pRtlInitUnicodeString = (PFN_RtlInitUnicodeString) GetProcAddress(nt, "RtlInitUnicodeString");
|
|
147
|
-
pRtlNtStatusToDosError = (PFN_RtlNtStatusToDosError) GetProcAddress(nt, "RtlNtStatusToDosError");
|
|
148
|
-
return pNtCreateFile && pNtDeviceIoControlFile && pRtlInitUnicodeString &&
|
|
149
|
-
pRtlNtStatusToDosError;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
static HANDLE afd_open(void) {
|
|
153
|
-
HANDLE h = INVALID_HANDLE_VALUE;
|
|
154
|
-
UNICODE_STRING name; OBJECT_ATTRIBUTES oa; IO_STATUS_BLOCK iosb; NTSTATUS st;
|
|
155
|
-
pRtlInitUnicodeString(&name, L"\\Device\\Afd\\Winloop");
|
|
156
|
-
InitializeObjectAttributes(&oa, &name, 0, NULL, NULL);
|
|
157
|
-
st = pNtCreateFile(&h, SYNCHRONIZE | FILE_READ_DATA | FILE_WRITE_DATA, &oa, &iosb,
|
|
158
|
-
NULL, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, AFD_FILE_OPEN, 0, NULL, 0);
|
|
159
|
-
return (st == 0) ? h : INVALID_HANDLE_VALUE;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
static SOCKET base_socket(SOCKET s) {
|
|
163
|
-
SOCKET base = INVALID_SOCKET; DWORD bytes = 0;
|
|
164
|
-
if (WSAIoctl(s, SIO_BASE_HANDLE, NULL, 0, &base, sizeof(base), &bytes, NULL, NULL) == 0)
|
|
165
|
-
return base;
|
|
166
|
-
return s;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
static ULONG ruby_to_afd(int events) {
|
|
170
|
-
ULONG a = AFD_POLL_ABORT | AFD_POLL_CONNECT_FAIL | AFD_POLL_LOCAL_CLOSE; /* always report errors */
|
|
171
|
-
if (events & RB_READABLE) a |= AFD_POLL_RECEIVE | AFD_POLL_ACCEPT | AFD_POLL_DISCONNECT;
|
|
172
|
-
if (events & RB_PRIORITY) a |= AFD_POLL_RECEIVE_EXPEDITED;
|
|
173
|
-
if (events & RB_WRITABLE) a |= AFD_POLL_SEND;
|
|
174
|
-
return a;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
static int afd_to_ruby(ULONG a) {
|
|
178
|
-
int e = 0;
|
|
179
|
-
if (a & (AFD_POLL_RECEIVE | AFD_POLL_RECEIVE_EXPEDITED | AFD_POLL_ACCEPT |
|
|
180
|
-
AFD_POLL_DISCONNECT | AFD_POLL_LOCAL_CLOSE)) e |= RB_READABLE;
|
|
181
|
-
if (a & AFD_POLL_RECEIVE_EXPEDITED) e |= RB_PRIORITY;
|
|
182
|
-
if (a & (AFD_POLL_SEND | AFD_POLL_CONNECT_FAIL)) e |= RB_WRITABLE;
|
|
183
|
-
/* On abort/error, wake for both directions so the follow-up recv/send surfaces it. */
|
|
184
|
-
if (a & (AFD_POLL_ABORT | AFD_POLL_CONNECT_FAIL)) e |= RB_READABLE | RB_WRITABLE;
|
|
185
|
-
return e;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/* ---- TypedData ---- */
|
|
189
|
-
static int free_req_i(st_data_t key, st_data_t val, st_data_t arg) {
|
|
190
|
-
(void)key; (void)arg;
|
|
191
|
-
xfree((void *)val);
|
|
192
|
-
return ST_CONTINUE;
|
|
193
|
-
}
|
|
194
|
-
/* Shutdown sweep step 1: cancel every still-SUBMITTED generic op, per op
|
|
195
|
-
* (CancelIoEx(handle, ov) — precise; a blanket per-handle cancel would be exposed
|
|
196
|
-
* to the recycled-handle-value hazard if a client closed a handle early). */
|
|
197
|
-
static int cancel_submitted_op_i(st_data_t key, st_data_t val, st_data_t arg) {
|
|
198
|
-
winloop_op *op = (winloop_op *)val;
|
|
199
|
-
(void)key; (void)arg;
|
|
200
|
-
if (op->state == OP_SUBMITTED) CancelIoEx(op->handle, &op->ov);
|
|
201
|
-
/* failures ignored: ERROR_NOT_FOUND = already completed; a (contract-
|
|
202
|
-
* violating) closed handle simply fails the lookup */
|
|
203
|
-
return ST_CONTINUE;
|
|
204
|
-
}
|
|
205
|
-
static int count_submitted_op_i(st_data_t key, st_data_t val, st_data_t arg) {
|
|
206
|
-
winloop_op *op = (winloop_op *)val;
|
|
207
|
-
(void)key;
|
|
208
|
-
if (op->state == OP_SUBMITTED) (*(size_t *)arg)++;
|
|
209
|
-
return ST_CONTINUE;
|
|
210
|
-
}
|
|
211
|
-
/* Shutdown sweep step 4: free every op the kernel can no longer touch. Ops still
|
|
212
|
-
* OP_SUBMITTED (their packet never surfaced inside the bounded drain) are
|
|
213
|
-
* DELIBERATELY LEAKED — the kernel may still write their OVERLAPPED/IOSB, and a
|
|
214
|
-
* use-after-free beats a bounded leak (the libuv tradeoff). In practice the
|
|
215
|
-
* per-op CancelIoEx + the 100 ms drain retires them. */
|
|
216
|
-
static int free_op_unless_submitted_i(st_data_t key, st_data_t val, st_data_t arg) {
|
|
217
|
-
winloop_op *op = (winloop_op *)val;
|
|
218
|
-
(void)key; (void)arg;
|
|
219
|
-
if (op->state != OP_SUBMITTED) xfree(op);
|
|
220
|
-
return ST_CONTINUE;
|
|
221
|
-
}
|
|
222
|
-
/* During the shutdown drain, mark reaped EXT packets' ops OP_COMPLETED so the
|
|
223
|
-
* sweep may free them. Pure C — also runs from the GC free hook (no Ruby calls). */
|
|
224
|
-
static void drain_mark_ext(winloop_backend *b, OVERLAPPED_ENTRY *ents, ULONG n) {
|
|
225
|
-
ULONG i;
|
|
226
|
-
for (i = 0; i < n; i++) {
|
|
227
|
-
st_data_t val;
|
|
228
|
-
if (ents[i].lpCompletionKey != EXT_KEY || ents[i].lpOverlapped == NULL) continue;
|
|
229
|
-
if (!st_lookup(b->ops_by_addr, (st_data_t)(uintptr_t)ents[i].lpOverlapped, &val)) continue;
|
|
230
|
-
((winloop_op *)val)->state = OP_COMPLETED;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
static void backend_drain_and_close(winloop_backend *b) {
|
|
234
|
-
if (b->closed) return;
|
|
235
|
-
b->closed = 1;
|
|
236
|
-
if (b->reqs) {
|
|
237
|
-
/* Cancel everything still pending so the kernel stops owning our OVERLAPPEDs. */
|
|
238
|
-
if (b->afd != INVALID_HANDLE_VALUE) CancelIoEx(b->afd, NULL);
|
|
239
|
-
}
|
|
240
|
-
if (b->ops_by_id) st_foreach(b->ops_by_id, cancel_submitted_op_i, 0);
|
|
241
|
-
/* Drain queued completions (best-effort) so no packet references freed memory. */
|
|
242
|
-
if (b->iocp) {
|
|
243
|
-
OVERLAPPED_ENTRY ents[REAP_CAP]; ULONG n = 0; int spins = 0;
|
|
244
|
-
while (GetQueuedCompletionStatusEx(b->iocp, ents, REAP_CAP, &n, 0, FALSE) && n && spins++ < 4096) {
|
|
245
|
-
if (b->ops_by_addr) drain_mark_ext(b, ents, n);
|
|
246
|
-
}
|
|
247
|
-
/* One bounded 100 ms drain — ONLY when generic ops are still in flight
|
|
248
|
-
* (their cancel completions are usually instants away). Existing AFD-only
|
|
249
|
-
* workloads never take this branch; the GVL stays held (this also runs
|
|
250
|
-
* from the GC free hook, where releasing the GVL is not an option). */
|
|
251
|
-
if (b->ops_by_id) {
|
|
252
|
-
size_t still = 0;
|
|
253
|
-
st_foreach(b->ops_by_id, count_submitted_op_i, (st_data_t)&still);
|
|
254
|
-
if (still &&
|
|
255
|
-
GetQueuedCompletionStatusEx(b->iocp, ents, REAP_CAP, &n, 100, FALSE) && n) {
|
|
256
|
-
drain_mark_ext(b, ents, n);
|
|
257
|
-
spins = 0;
|
|
258
|
-
while (GetQueuedCompletionStatusEx(b->iocp, ents, REAP_CAP, &n, 0, FALSE) && n && spins++ < 4096)
|
|
259
|
-
drain_mark_ext(b, ents, n);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (b->afd != INVALID_HANDLE_VALUE) { CloseHandle(b->afd); b->afd = INVALID_HANDLE_VALUE; }
|
|
264
|
-
if (b->iocp) { CloseHandle(b->iocp); b->iocp = NULL; }
|
|
265
|
-
if (b->reqs) { st_foreach(b->reqs, free_req_i, 0); st_free_table(b->reqs); b->reqs = NULL; }
|
|
266
|
-
if (b->ops_by_id) { st_foreach(b->ops_by_id, free_op_unless_submitted_i, 0); st_free_table(b->ops_by_id); b->ops_by_id = NULL; }
|
|
267
|
-
if (b->ops_by_addr) { st_free_table(b->ops_by_addr); b->ops_by_addr = NULL; }
|
|
268
|
-
if (b->assoc_handles) { st_free_table(b->assoc_handles); b->assoc_handles = NULL; }
|
|
269
|
-
b->op_bytes = 0;
|
|
270
|
-
}
|
|
271
|
-
static void backend_free(void *p) {
|
|
272
|
-
winloop_backend *b = (winloop_backend *)p;
|
|
273
|
-
if (!b) return;
|
|
274
|
-
backend_drain_and_close(b);
|
|
275
|
-
xfree(b);
|
|
276
|
-
}
|
|
277
|
-
static size_t backend_memsize(const void *p) {
|
|
278
|
-
const winloop_backend *b = (const winloop_backend *)p;
|
|
279
|
-
return sizeof(winloop_backend) + (b ? b->op_bytes : 0);
|
|
280
|
-
}
|
|
281
|
-
static const rb_data_type_t backend_type = {
|
|
282
|
-
"Winloop::Backend", { 0, backend_free, backend_memsize }, 0, 0, RUBY_TYPED_FREE_IMMEDIATELY
|
|
283
|
-
};
|
|
284
|
-
|
|
285
|
-
static winloop_backend *get_backend(VALUE self) {
|
|
286
|
-
winloop_backend *b;
|
|
287
|
-
TypedData_Get_Struct(self, winloop_backend, &backend_type, b);
|
|
288
|
-
if (!b || b->closed) rb_raise(eError, "winloop: backend is closed");
|
|
289
|
-
return b;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/* The op methods additionally need the op tables, which only #initialize creates
|
|
293
|
-
* (a Backend.allocate'd-but-never-initialized object must raise, not crash). */
|
|
294
|
-
static winloop_backend *get_backend_ops(VALUE self) {
|
|
295
|
-
winloop_backend *b = get_backend(self);
|
|
296
|
-
if (!b->ops_by_id) rb_raise(eError, "winloop: backend is closed");
|
|
297
|
-
return b;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
static VALUE backend_alloc(VALUE klass) {
|
|
301
|
-
winloop_backend *b;
|
|
302
|
-
VALUE obj = TypedData_Make_Struct(klass, winloop_backend, &backend_type, b);
|
|
303
|
-
b->afd = INVALID_HANDLE_VALUE;
|
|
304
|
-
return obj;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
static VALUE backend_initialize(VALUE self) {
|
|
308
|
-
winloop_backend *b;
|
|
309
|
-
TypedData_Get_Struct(self, winloop_backend, &backend_type, b);
|
|
310
|
-
if (!resolve_ntdll()) rb_raise(eError, "winloop: could not resolve ntdll entry points");
|
|
311
|
-
b->iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
|
|
312
|
-
if (!b->iocp) rb_raise(eError, "winloop: CreateIoCompletionPort failed (%lu)", GetLastError());
|
|
313
|
-
b->afd = afd_open();
|
|
314
|
-
if (b->afd == INVALID_HANDLE_VALUE) {
|
|
315
|
-
CloseHandle(b->iocp); b->iocp = NULL;
|
|
316
|
-
rb_raise(eError, "winloop: could not open \\Device\\Afd (unsupported platform?)");
|
|
317
|
-
}
|
|
318
|
-
if (!CreateIoCompletionPort(b->afd, b->iocp, AFD_KEY, 0)) {
|
|
319
|
-
CloseHandle(b->afd); CloseHandle(b->iocp); b->afd = INVALID_HANDLE_VALUE; b->iocp = NULL;
|
|
320
|
-
rb_raise(eError, "winloop: could not associate AFD handle (%lu)", GetLastError());
|
|
321
|
-
}
|
|
322
|
-
b->next_id = 0;
|
|
323
|
-
b->reqs = st_init_numtable();
|
|
324
|
-
/* st_data_t is pointer-sized; numtables hold ids/addresses/handles arm64-clean. */
|
|
325
|
-
b->ops_by_id = st_init_numtable();
|
|
326
|
-
b->ops_by_addr = st_init_numtable();
|
|
327
|
-
b->assoc_handles = st_init_numtable();
|
|
328
|
-
b->op_bytes = 0;
|
|
329
|
-
b->closed = 0;
|
|
330
|
-
return self;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/* Arm a one-shot readiness poll on `io` for `events`; returns the request id. */
|
|
334
|
-
static VALUE backend_poll(VALUE self, VALUE io, VALUE vevents) {
|
|
335
|
-
winloop_backend *b = get_backend(self);
|
|
336
|
-
int events = NUM2INT(vevents);
|
|
337
|
-
int fd = rb_io_descriptor(io);
|
|
338
|
-
SOCKET sock = rb_w32_get_osfhandle(fd);
|
|
339
|
-
if (sock == INVALID_SOCKET || !rb_w32_is_socket(fd))
|
|
340
|
-
rb_raise(eError, "winloop: fd %d is not a socket (winloop polls sockets)", fd);
|
|
341
|
-
SOCKET base = base_socket(sock);
|
|
342
|
-
|
|
343
|
-
winloop_req *req = ALLOC(winloop_req);
|
|
344
|
-
memset(req, 0, sizeof(*req));
|
|
345
|
-
req->id = ++b->next_id;
|
|
346
|
-
req->in.Timeout.QuadPart = INT64_MAX; /* stays pending until an event fires */
|
|
347
|
-
req->in.NumberOfHandles = 1;
|
|
348
|
-
req->in.Exclusive = FALSE;
|
|
349
|
-
req->in.Handles[0].Handle = (HANDLE)base;
|
|
350
|
-
req->in.Handles[0].Events = ruby_to_afd(events);
|
|
351
|
-
|
|
352
|
-
NTSTATUS st = pNtDeviceIoControlFile(b->afd, NULL, NULL, &req->ov,
|
|
353
|
-
(PIO_STATUS_BLOCK)&req->ov.Internal, IOCTL_AFD_POLL,
|
|
354
|
-
&req->in, sizeof(req->in), &req->out, sizeof(req->out));
|
|
355
|
-
if (st != STATUS_PENDING && st != 0 /*STATUS_SUCCESS*/) {
|
|
356
|
-
xfree(req);
|
|
357
|
-
rb_raise(eError, "winloop: AFD poll failed (status 0x%08lX)", (unsigned long)st);
|
|
358
|
-
}
|
|
359
|
-
/* STATUS_SUCCESS == satisfied synchronously, but a completion packet is still
|
|
360
|
-
queued (we never set FILE_SKIP_COMPLETION_PORT_ON_SUCCESS) — handle uniformly. */
|
|
361
|
-
st_insert(b->reqs, (st_data_t)req->id, (st_data_t)req);
|
|
362
|
-
return ULL2NUM(req->id);
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
/* Cancel a pending poll. The cancelled op still posts a (STATUS_CANCELLED)
|
|
366
|
-
completion, which #wait reaps and frees — so we do NOT free here. */
|
|
367
|
-
static VALUE backend_cancel(VALUE self, VALUE vid) {
|
|
368
|
-
winloop_backend *b = get_backend(self);
|
|
369
|
-
uint64_t id = NUM2ULL(vid);
|
|
370
|
-
st_data_t val;
|
|
371
|
-
if (st_lookup(b->reqs, (st_data_t)id, &val)) {
|
|
372
|
-
winloop_req *req = (winloop_req *)val;
|
|
373
|
-
CancelIoEx(b->afd, &req->ov);
|
|
374
|
-
}
|
|
375
|
-
return Qnil;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/* ===== Generic OVERLAPPED ops — "bring your own OVERLAPPED" (winloop 0.2) =====
|
|
379
|
-
*
|
|
380
|
-
* The whole feature is Integer-in/Integer-out: handles, addresses and ids cross
|
|
381
|
-
* the API as Ruby Integers (the cross-gem ABI — other gems never link winloop).
|
|
382
|
-
* winloop owns op memory (OVERLAPPED + embedded buffer, one heap allocation);
|
|
383
|
-
* clients own their handles — winloop NEVER calls CloseHandle on them. */
|
|
384
|
-
|
|
385
|
-
static const char *op_state_name(int state) {
|
|
386
|
-
switch (state) {
|
|
387
|
-
case OP_PREPARED: return "prepared";
|
|
388
|
-
case OP_SUBMITTED: return "submitted";
|
|
389
|
-
default: return "completed";
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
static winloop_op *op_lookup(winloop_backend *b, uint64_t id) {
|
|
394
|
-
st_data_t val;
|
|
395
|
-
if (!st_lookup(b->ops_by_id, (st_data_t)id, &val))
|
|
396
|
-
rb_raise(eError, "winloop: unknown op id %llu", (unsigned long long)id);
|
|
397
|
-
return (winloop_op *)val;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* Remove an op from both tables and free it (the ONLY free path besides the
|
|
401
|
-
* shutdown sweep). Callers must have established the kernel no longer owns it. */
|
|
402
|
-
static void op_retire(winloop_backend *b, winloop_op *op) {
|
|
403
|
-
st_data_t key = (st_data_t)op->id;
|
|
404
|
-
st_delete(b->ops_by_id, &key, NULL);
|
|
405
|
-
key = (st_data_t)(uintptr_t)&op->ov;
|
|
406
|
-
st_delete(b->ops_by_addr, &key, NULL);
|
|
407
|
-
b->op_bytes -= OP_BUF_OFFSET + op->cap;
|
|
408
|
-
xfree(op);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/* Associate `handle` (opened FILE_FLAG_OVERLAPPED) with the backend's IOCP under
|
|
412
|
-
* EXT_KEY. Permanent for the handle's lifetime (Win32: one port per handle, no
|
|
413
|
-
* disassociation); the caller keeps ownership. */
|
|
414
|
-
static VALUE backend_associate(VALUE self, VALUE vhandle) {
|
|
415
|
-
winloop_backend *b = get_backend_ops(self);
|
|
416
|
-
uint64_t hv = NUM2ULL(vhandle); /* coercion first: a raise here owns nothing */
|
|
417
|
-
DWORD gle;
|
|
418
|
-
if (hv == 0 || hv == UINT64_MAX)
|
|
419
|
-
rb_raise(rb_eArgError, "winloop: %llu is not a usable HANDLE value",
|
|
420
|
-
(unsigned long long)hv);
|
|
421
|
-
if (!CreateIoCompletionPort((HANDLE)(uintptr_t)hv, b->iocp, EXT_KEY, 0)) {
|
|
422
|
-
gle = GetLastError(); /* captured before any Ruby allocation */
|
|
423
|
-
rb_raise(eError, "winloop: could not associate handle %llu with the completion "
|
|
424
|
-
"port (error %lu: already associated with a completion port, or not "
|
|
425
|
-
"opened with FILE_FLAG_OVERLAPPED)", (unsigned long long)hv, gle);
|
|
426
|
-
}
|
|
427
|
-
st_insert(b->assoc_handles, (st_data_t)hv, (st_data_t)1);
|
|
428
|
-
return Qtrue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/* Private primitive behind Winloop::Backend#op_prepare (lib/winloop/ops.rb owns
|
|
432
|
-
* the kwargs + range validation). Allocates one op record: zeroed OVERLAPPED +
|
|
433
|
-
* tag + optional embedded buffer; returns [op_id, ov_addr, buf_addr]. */
|
|
434
|
-
static VALUE backend__op_prepare(VALUE self, VALUE vhandle, VALUE vtag, VALUE vcap) {
|
|
435
|
-
winloop_backend *b = get_backend_ops(self);
|
|
436
|
-
/* All coercion before any allocation (raise hygiene). */
|
|
437
|
-
uint64_t hv = NUM2ULL(vhandle);
|
|
438
|
-
uint64_t tag = NUM2ULL(vtag);
|
|
439
|
-
uint64_t cap = NUM2ULL(vcap);
|
|
440
|
-
size_t total;
|
|
441
|
-
winloop_op *op;
|
|
442
|
-
if (cap > 16ull * 1024 * 1024) /* defensive twin of OP_CAPACITY_MAX (send bypass) */
|
|
443
|
-
rb_raise(rb_eArgError, "winloop: capacity %llu exceeds OP_CAPACITY_MAX",
|
|
444
|
-
(unsigned long long)cap);
|
|
445
|
-
/* The anti-hang check (§3.4 rule 8): an op on a handle that is not on the
|
|
446
|
-
* port never completes — not an error, a silent loop freeze. Honestly: a
|
|
447
|
-
* handle that was associated, closed and recycled to the same value passes. */
|
|
448
|
-
if (!st_lookup(b->assoc_handles, (st_data_t)hv, NULL))
|
|
449
|
-
rb_raise(eError, "winloop: handle %llu was never associated with this backend "
|
|
450
|
-
"— call #associate first", (unsigned long long)hv);
|
|
451
|
-
|
|
452
|
-
total = OP_BUF_OFFSET + (size_t)cap;
|
|
453
|
-
op = (winloop_op *)xmalloc(total); /* a raise above this line leaks nothing */
|
|
454
|
-
memset(op, 0, total); /* zeroed OVERLAPPED (offset 0 reads); buffer never leaks heap */
|
|
455
|
-
op->id = ++b->next_id;
|
|
456
|
-
op->tag = tag;
|
|
457
|
-
op->handle = (HANDLE)(uintptr_t)hv;
|
|
458
|
-
op->state = OP_PREPARED;
|
|
459
|
-
op->cap = (size_t)cap;
|
|
460
|
-
st_insert(b->ops_by_id, (st_data_t)op->id, (st_data_t)op);
|
|
461
|
-
/* An OOM longjmp from this second insert leaks one record until the shutdown
|
|
462
|
-
* sweep — the identical, accepted exposure as backend_poll's st_insert. */
|
|
463
|
-
st_insert(b->ops_by_addr, (st_data_t)(uintptr_t)&op->ov, (st_data_t)op);
|
|
464
|
-
b->op_bytes += total;
|
|
465
|
-
return rb_ary_new_from_args(3, ULL2NUM(op->id), ULL2NUM((uintptr_t)&op->ov),
|
|
466
|
-
cap ? ULL2NUM((uintptr_t)((char *)op + OP_BUF_OFFSET))
|
|
467
|
-
: ULL2NUM(0));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/* Call after the client's native call returned success OR ERROR_IO_PENDING —
|
|
471
|
-
* both queue exactly one completion packet (no skip-on-success, ever). */
|
|
472
|
-
static VALUE backend_op_submitted(VALUE self, VALUE vid) {
|
|
473
|
-
winloop_backend *b = get_backend_ops(self);
|
|
474
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
475
|
-
if (op->state != OP_PREPARED)
|
|
476
|
-
rb_raise(eError, "winloop: op %llu is %s; op_submitted is only legal on a "
|
|
477
|
-
"prepared op", (unsigned long long)op->id, op_state_name(op->state));
|
|
478
|
-
op->state = OP_SUBMITTED;
|
|
479
|
-
return Qtrue;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/* For a native call that failed SYNCHRONOUSLY (GetLastError != ERROR_IO_PENDING):
|
|
483
|
-
* the kernel never saw the op, no packet will ever arrive — free immediately. */
|
|
484
|
-
static VALUE backend_op_abandon(VALUE self, VALUE vid) {
|
|
485
|
-
winloop_backend *b = get_backend_ops(self);
|
|
486
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
487
|
-
if (op->state == OP_SUBMITTED)
|
|
488
|
-
rb_raise(eError, "winloop: op %llu is submitted; a submitted op is retired by "
|
|
489
|
-
"its completion, not op_abandon", (unsigned long long)op->id);
|
|
490
|
-
if (op->state == OP_COMPLETED)
|
|
491
|
-
rb_raise(eError, "winloop: op %llu is completed; retire it with op_result or "
|
|
492
|
-
"op_free, not op_abandon", (unsigned long long)op->id);
|
|
493
|
-
op_retire(b, op);
|
|
494
|
-
return Qtrue;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/* CancelIoEx(handle, ov) — targeted, cross-thread-safe. NEVER frees: a cancelled
|
|
498
|
-
* op still posts a packet (usually error 995) which #wait reaps. Returns true if
|
|
499
|
-
* a cancel was issued; false on ERROR_NOT_FOUND (already completed — benign) or
|
|
500
|
-
* ANY other CancelIoEx failure (rb_warn'ed, never raised: await_op's ensure path
|
|
501
|
-
* must never mask an in-flight Timeout/Fiber#raise unwind with a Winloop::Error). */
|
|
502
|
-
static VALUE backend_op_cancel(VALUE self, VALUE vid) {
|
|
503
|
-
winloop_backend *b = get_backend_ops(self);
|
|
504
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
505
|
-
DWORD gle;
|
|
506
|
-
if (op->state == OP_PREPARED)
|
|
507
|
-
rb_raise(eError, "winloop: op %llu is prepared; only a submitted op can be "
|
|
508
|
-
"cancelled (a prepared op is retired with op_abandon)",
|
|
509
|
-
(unsigned long long)op->id);
|
|
510
|
-
if (CancelIoEx(op->handle, &op->ov)) return Qtrue;
|
|
511
|
-
gle = GetLastError(); /* captured immediately, before any Ruby call */
|
|
512
|
-
if (gle != ERROR_NOT_FOUND)
|
|
513
|
-
rb_warn("winloop: CancelIoEx for op %llu failed (error %lu) — treated as "
|
|
514
|
-
"no-cancel, never an exception", (unsigned long long)op->id, gle);
|
|
515
|
-
return Qfalse;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/* [bytes, error, data] for a COMPLETED op; frees the record. The String is built
|
|
519
|
-
* BEFORE the free so an OOM raise leaves the op intact and retryable. `data` is
|
|
520
|
-
* clamped to min(bytes, capacity): a stray packet's bogus byte count cannot make
|
|
521
|
-
* winloop read past its own buffer. */
|
|
522
|
-
static VALUE backend_op_result(VALUE self, VALUE vid) {
|
|
523
|
-
winloop_backend *b = get_backend_ops(self);
|
|
524
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
525
|
-
VALUE data = Qnil, result;
|
|
526
|
-
if (op->state != OP_COMPLETED)
|
|
527
|
-
rb_raise(eError, "winloop: op %llu is %s; op_result is only legal once its "
|
|
528
|
-
"completion has been reaped by #wait",
|
|
529
|
-
(unsigned long long)op->id, op_state_name(op->state));
|
|
530
|
-
if (op->cap) {
|
|
531
|
-
size_t n = (size_t)op->bytes < op->cap ? (size_t)op->bytes : op->cap;
|
|
532
|
-
data = rb_str_new((const char *)op + OP_BUF_OFFSET, (long)n); /* ASCII-8BIT */
|
|
533
|
-
}
|
|
534
|
-
result = rb_ary_new_from_args(3, ULONG2NUM(op->bytes), ULONG2NUM(op->error), data);
|
|
535
|
-
op_retire(b, op);
|
|
536
|
-
return result;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/* Retire a COMPLETED op without materializing its data (orphaned completions). */
|
|
540
|
-
static VALUE backend_op_free(VALUE self, VALUE vid) {
|
|
541
|
-
winloop_backend *b = get_backend_ops(self);
|
|
542
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
543
|
-
if (op->state != OP_COMPLETED)
|
|
544
|
-
rb_raise(eError, "winloop: op %llu is %s; op_free is only legal once its "
|
|
545
|
-
"completion has been reaped by #wait",
|
|
546
|
-
(unsigned long long)op->id, op_state_name(op->state));
|
|
547
|
-
op_retire(b, op);
|
|
548
|
-
return Qtrue;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
static VALUE backend_op_state(VALUE self, VALUE vid) {
|
|
552
|
-
winloop_backend *b = get_backend_ops(self);
|
|
553
|
-
winloop_op *op = op_lookup(b, NUM2ULL(vid));
|
|
554
|
-
switch (op->state) {
|
|
555
|
-
case OP_PREPARED: return ID2SYM(rb_intern("prepared"));
|
|
556
|
-
case OP_SUBMITTED: return ID2SYM(rb_intern("submitted"));
|
|
557
|
-
default: return ID2SYM(rb_intern("completed"));
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
/* The raw IOCP HANDLE value — read-only, for tests and diagnostics. Hard rules:
|
|
562
|
-
* never wait on it, never close it, never associate handles behind winloop's
|
|
563
|
-
* back, never post after shutdown, at most ONE post per op. */
|
|
564
|
-
static VALUE backend_port_handle(VALUE self) {
|
|
565
|
-
winloop_backend *b = get_backend_ops(self);
|
|
566
|
-
return ULL2NUM((uintptr_t)b->iocp);
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/* Reap completions in one GetQueuedCompletionStatusEx. Returns an Array of
|
|
570
|
-
[id, ready_events]; wakeup packets are skipped. Empty array on timeout. */
|
|
571
|
-
/* The blocking GQCSEx call, run WITHOUT the GVL so other threads (and the very
|
|
572
|
-
producer that will call #unblock) can run while the loop sleeps on the port. */
|
|
573
|
-
typedef struct {
|
|
574
|
-
HANDLE iocp;
|
|
575
|
-
OVERLAPPED_ENTRY *ents;
|
|
576
|
-
ULONG cap, n;
|
|
577
|
-
DWORD ms, err;
|
|
578
|
-
BOOL ok;
|
|
579
|
-
} gqcs_args;
|
|
580
|
-
|
|
581
|
-
static void *gqcs_call(void *p) {
|
|
582
|
-
gqcs_args *a = (gqcs_args *)p;
|
|
583
|
-
a->ok = GetQueuedCompletionStatusEx(a->iocp, a->ents, a->cap, &a->n, a->ms, FALSE);
|
|
584
|
-
a->err = a->ok ? 0 : GetLastError();
|
|
585
|
-
return NULL;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
/* Ruby's interrupt path (signals, Thread#kill): wake the GQCSEx so the loop
|
|
589
|
-
thread can return to Ruby and check interrupts. */
|
|
590
|
-
static void gqcs_ubf(void *p) {
|
|
591
|
-
PostQueuedCompletionStatus((HANDLE)p, 0, WAKE_KEY, NULL);
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
static VALUE backend_wait(VALUE self, VALUE vtimeout_ms) {
|
|
595
|
-
winloop_backend *b = get_backend(self);
|
|
596
|
-
DWORD ms = NIL_P(vtimeout_ms) ? INFINITE : (DWORD)NUM2ULONG(vtimeout_ms);
|
|
597
|
-
OVERLAPPED_ENTRY ents[REAP_CAP];
|
|
598
|
-
|
|
599
|
-
gqcs_args a = { b->iocp, ents, REAP_CAP, 0, ms, 0, FALSE };
|
|
600
|
-
rb_thread_call_without_gvl(gqcs_call, &a, gqcs_ubf, b->iocp);
|
|
601
|
-
BOOL ok = a.ok;
|
|
602
|
-
DWORD err = a.err;
|
|
603
|
-
ULONG n = a.n;
|
|
604
|
-
|
|
605
|
-
VALUE result = rb_ary_new();
|
|
606
|
-
if (!ok) {
|
|
607
|
-
if (err == WAIT_TIMEOUT) return result;
|
|
608
|
-
rb_raise(eError, "winloop: GetQueuedCompletionStatusEx failed (%lu)", err);
|
|
609
|
-
}
|
|
610
|
-
for (ULONG i = 0; i < n; i++) {
|
|
611
|
-
if (ents[i].lpCompletionKey == WAKE_KEY || ents[i].lpOverlapped == NULL) continue;
|
|
612
|
-
if (ents[i].lpCompletionKey == EXT_KEY) {
|
|
613
|
-
/* Generic-op path (winloop 0.2). Defensive dispatch: verify membership
|
|
614
|
-
* in ops_by_addr AND op state before acting — an unknown lpOverlapped
|
|
615
|
-
* is never cast, an already-COMPLETED op (double post) is never
|
|
616
|
-
* re-completed and never yields a second tuple. The only accepted
|
|
617
|
-
* anomaly is a packet for a still-PREPARED op (a standalone embedder
|
|
618
|
-
* called #wait between the native submit and op_submitted): the packet
|
|
619
|
-
* is real and the memory is ours, so accept it with a warning. */
|
|
620
|
-
st_data_t val;
|
|
621
|
-
winloop_op *op;
|
|
622
|
-
if (!st_lookup(b->ops_by_addr, (st_data_t)(uintptr_t)ents[i].lpOverlapped, &val)) {
|
|
623
|
-
rb_warn("winloop: dropped a completion packet for an unknown OVERLAPPED "
|
|
624
|
-
"0x%llx (stray or duplicate post)",
|
|
625
|
-
(unsigned long long)(uintptr_t)ents[i].lpOverlapped);
|
|
626
|
-
continue;
|
|
627
|
-
}
|
|
628
|
-
op = (winloop_op *)val;
|
|
629
|
-
if (op->state == OP_COMPLETED) {
|
|
630
|
-
rb_warn("winloop: dropped a duplicate completion packet for op %llu "
|
|
631
|
-
"(already completed; bytes/error preserved)",
|
|
632
|
-
(unsigned long long)op->id);
|
|
633
|
-
continue;
|
|
634
|
-
}
|
|
635
|
-
if (op->state == OP_PREPARED)
|
|
636
|
-
rb_warn("winloop: completion packet reaped for op %llu before "
|
|
637
|
-
"op_submitted (protocol violation; accepting the packet)",
|
|
638
|
-
(unsigned long long)op->id);
|
|
639
|
-
op->state = OP_COMPLETED;
|
|
640
|
-
op->bytes = ents[i].dwNumberOfBytesTransferred;
|
|
641
|
-
/* Status lives in the OVERLAPPED's Internal (an NTSTATUS), readable
|
|
642
|
-
* only after the packet left the port; map it to a familiar Win32
|
|
643
|
-
* code (0 ok, 995 cancelled, 1022 rescan, 38 EOF, ...). */
|
|
644
|
-
op->error = pRtlNtStatusToDosError((NTSTATUS)op->ov.Internal);
|
|
645
|
-
rb_ary_push(result, rb_ary_new_from_args(4, ULL2NUM(op->id),
|
|
646
|
-
ULONG2NUM(op->bytes), ULONG2NUM(op->error), ULL2NUM(op->tag)));
|
|
647
|
-
continue;
|
|
648
|
-
}
|
|
649
|
-
/* AFD path — byte-for-byte the pre-0.2 code (only the AFD handle carries
|
|
650
|
-
* AFD_KEY on this port). */
|
|
651
|
-
winloop_req *req = (winloop_req *)ents[i].lpOverlapped;
|
|
652
|
-
st_data_t key = (st_data_t)req->id;
|
|
653
|
-
st_delete(b->reqs, &key, NULL);
|
|
654
|
-
int events = afd_to_ruby(req->out.Handles[0].Events);
|
|
655
|
-
VALUE pair = rb_ary_new_from_args(2, ULL2NUM(req->id), INT2NUM(events));
|
|
656
|
-
rb_ary_push(result, pair);
|
|
657
|
-
xfree(req);
|
|
658
|
-
}
|
|
659
|
-
return result;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
static VALUE backend_wakeup(VALUE self) {
|
|
663
|
-
winloop_backend *b = get_backend(self);
|
|
664
|
-
PostQueuedCompletionStatus(b->iocp, 0, WAKE_KEY, NULL);
|
|
665
|
-
return Qnil;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
static VALUE backend_shutdown(VALUE self) {
|
|
669
|
-
winloop_backend *b;
|
|
670
|
-
TypedData_Get_Struct(self, winloop_backend, &backend_type, b);
|
|
671
|
-
if (b) backend_drain_and_close(b);
|
|
672
|
-
return Qnil;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
void Init_winloop(void) {
|
|
676
|
-
WSADATA wsa;
|
|
677
|
-
WSAStartup(MAKEWORD(2, 2), &wsa);
|
|
678
|
-
|
|
679
|
-
mWinloop = rb_define_module("Winloop");
|
|
680
|
-
cBackend = rb_define_class_under(mWinloop, "Backend", rb_cObject);
|
|
681
|
-
eError = rb_define_class_under(mWinloop, "Error", rb_eStandardError);
|
|
682
|
-
|
|
683
|
-
/* Export the Ruby IO event bits the scheduler/backend agree on. */
|
|
684
|
-
rb_define_const(mWinloop, "READABLE", INT2NUM(RB_READABLE));
|
|
685
|
-
rb_define_const(mWinloop, "WRITABLE", INT2NUM(RB_WRITABLE));
|
|
686
|
-
rb_define_const(mWinloop, "PRIORITY", INT2NUM(RB_PRIORITY));
|
|
687
|
-
|
|
688
|
-
/* The completion key winloop assigns to ALL generic ops on the port
|
|
689
|
-
* (internal dispatch; exposed for tests and a possible future injection
|
|
690
|
-
* protocol — PQCS injection is NOT a v1 protocol). */
|
|
691
|
-
rb_define_const(mWinloop, "EXTERNAL_KEY", ULL2NUM(0x45585431ull));
|
|
692
|
-
|
|
693
|
-
rb_define_alloc_func(cBackend, backend_alloc);
|
|
694
|
-
rb_define_method(cBackend, "initialize", RUBY_METHOD_FUNC(backend_initialize), 0);
|
|
695
|
-
rb_define_method(cBackend, "poll", RUBY_METHOD_FUNC(backend_poll), 2);
|
|
696
|
-
rb_define_method(cBackend, "cancel", RUBY_METHOD_FUNC(backend_cancel), 1);
|
|
697
|
-
rb_define_method(cBackend, "wait", RUBY_METHOD_FUNC(backend_wait), 1);
|
|
698
|
-
rb_define_method(cBackend, "wakeup", RUBY_METHOD_FUNC(backend_wakeup), 0);
|
|
699
|
-
rb_define_method(cBackend, "shutdown", RUBY_METHOD_FUNC(backend_shutdown), 0);
|
|
700
|
-
|
|
701
|
-
/* Generic OVERLAPPED ops (winloop 0.2). `_op_prepare` is the private bridge
|
|
702
|
-
* behind the validated Winloop::Backend#op_prepare in lib/winloop/ops.rb. */
|
|
703
|
-
rb_define_method(cBackend, "associate", RUBY_METHOD_FUNC(backend_associate), 1);
|
|
704
|
-
rb_define_method(cBackend, "_op_prepare", RUBY_METHOD_FUNC(backend__op_prepare), 3);
|
|
705
|
-
rb_define_method(cBackend, "op_submitted", RUBY_METHOD_FUNC(backend_op_submitted), 1);
|
|
706
|
-
rb_define_method(cBackend, "op_abandon", RUBY_METHOD_FUNC(backend_op_abandon), 1);
|
|
707
|
-
rb_define_method(cBackend, "op_cancel", RUBY_METHOD_FUNC(backend_op_cancel), 1);
|
|
708
|
-
rb_define_method(cBackend, "op_result", RUBY_METHOD_FUNC(backend_op_result), 1);
|
|
709
|
-
rb_define_method(cBackend, "op_free", RUBY_METHOD_FUNC(backend_op_free), 1);
|
|
710
|
-
rb_define_method(cBackend, "op_state", RUBY_METHOD_FUNC(backend_op_state), 1);
|
|
711
|
-
rb_define_method(cBackend, "port_handle", RUBY_METHOD_FUNC(backend_port_handle), 0);
|
|
712
|
-
}
|