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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 421971bb70dfb847bc33075448fd61cb6b5821f8c8a4d2db1b9af90f40ecc151
|
|
4
|
+
data.tar.gz: 93cb1c5234f1c337fb9978725672b8607490bf904302e06e3044b5171882df63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9fc6ac85100e03148872e24cef8beadd10e4989d34f748fb30c94d0b0f08b55a672483eb60f0e5edce7fca605acbfb12053c7bd6d68a7d135f0c8a60ed1eb92
|
|
7
|
+
data.tar.gz: c8b7b095198daecccc195c3b69eb4c67812f653911a552301bc191e4e010ab57f4f88aaa6ffe7a751af50ce1e13b056779a8a6dc88085566a2fc15aa8de7b199
|
data/README.md
CHANGED
|
@@ -1,278 +1,11 @@
|
|
|
1
|
-
# winloop
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
require "winloop"
|
|
13
|
-
require "socket"
|
|
14
|
-
|
|
15
|
-
Winloop.run do
|
|
16
|
-
server = TCPServer.new("127.0.0.1", 9292)
|
|
17
|
-
loop do
|
|
18
|
-
client = server.accept # readiness via AFD poll on the IOCP
|
|
19
|
-
Fiber.schedule do # one lightweight fiber per connection
|
|
20
|
-
while (line = client.gets) # recv driven by the completion port
|
|
21
|
-
client.write(line) # send, never blocking the loop
|
|
22
|
-
end
|
|
23
|
-
client.close
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Why this exists
|
|
30
|
-
|
|
31
|
-
Ruby's `Fiber::Scheduler` (the engine behind `socketry/async` and friends) is the
|
|
32
|
-
modern way to write concurrent I/O. But on Windows the selectors that back it —
|
|
33
|
-
`socketry/io-event` ships `select`, `epoll`, `kqueue`, and `io_uring` — have **no
|
|
34
|
-
IOCP backend**. The result has long been the weakest async story of any major
|
|
35
|
-
platform: `select()`-bound, capped at 64 handles, and slow.
|
|
36
|
-
|
|
37
|
-
`winloop` fills that hole the same way libuv, Rust's mio, and wepoll do it:
|
|
38
|
-
|
|
39
|
-
* The event loop is a real **I/O Completion Port**. One
|
|
40
|
-
`GetQueuedCompletionStatusEx` call per turn reaps every ready event at once.
|
|
41
|
-
* **Readiness** (accept, connect, `IO#wait_readable`) — which IOCP doesn't natively
|
|
42
|
-
provide — is obtained by submitting `IOCTL_AFD_POLL` against `\Device\Afd` as an
|
|
43
|
-
overlapped operation on the port. This is the undocumented mechanism libuv/mio
|
|
44
|
-
rely on, and it is the only way to get readiness semantics onto a completion port.
|
|
45
|
-
* **Reads and writes** are driven by `recv`/`send` once the port reports readiness.
|
|
46
|
-
* **Timers** (`sleep`, `Timeout.timeout`) use a monotonic min-heap that sets the
|
|
47
|
-
port's wait timeout — no kernel timer objects, no extra threads.
|
|
48
|
-
* **`Mutex`/`Queue`/`Thread#join`** park the fiber on an in-process waiter list; a
|
|
49
|
-
wakeup from another OS thread breaks the loop's wait via
|
|
50
|
-
`PostQueuedCompletionStatus`.
|
|
51
|
-
|
|
52
|
-
## Requirements
|
|
53
|
-
|
|
54
|
-
* A **native Windows MSVC build of Ruby** (`x64-mswin64`). winloop compiles a C
|
|
55
|
-
extension with `cl.exe` and links `ws2_32`. It will refuse to install on any
|
|
56
|
-
non-`mswin` platform.
|
|
57
|
-
* Ruby >= 3.1 (for a stable `Fiber::Scheduler` and `IO::Buffer`).
|
|
58
|
-
* `\Device\Afd` readiness polling is a Windows NT facility; winloop is **not**
|
|
59
|
-
expected to work under Wine.
|
|
60
|
-
|
|
61
|
-
## Installation
|
|
62
|
-
|
|
63
|
-
```sh
|
|
64
|
-
gem install winloop
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Or build from a checkout (it dogfoods the [`vcvars`](https://rubygems.org/gems/vcvars)
|
|
68
|
-
gem so it can find the MSVC toolchain without a Developer Command Prompt):
|
|
69
|
-
|
|
70
|
-
```sh
|
|
71
|
-
rake compile
|
|
72
|
-
rake test
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
## What runs cooperatively
|
|
76
|
-
|
|
77
|
-
Inside `Winloop.run { ... }` (and any `Fiber.schedule` started within it):
|
|
78
|
-
|
|
79
|
-
| Operation | Hook | Mechanism |
|
|
80
|
-
| ------------------------------------------ | --------------- | -------------------------------------- |
|
|
81
|
-
| `TCPServer#accept`, `IO#wait_readable` | `io_wait` | `IOCTL_AFD_POLL` overlapped on the IOCP |
|
|
82
|
-
| `IO#read` / `gets` / `recv` on a socket | `io_read` | `recv_nonblock` + `io_wait` |
|
|
83
|
-
| `IO#write` / `puts` on a socket | `io_write` | `write_nonblock` + `io_wait` |
|
|
84
|
-
| `sleep` | `kernel_sleep` | monotonic timer heap |
|
|
85
|
-
| `Timeout.timeout` | `timeout_after` | timer heap + `Fiber#raise` |
|
|
86
|
-
| `Mutex`, `ConditionVariable`, `Queue`, `Thread#join` | `block`/`unblock` | waiter lists + `PostQueuedCompletionStatus` |
|
|
87
|
-
| `Addrinfo.getaddrinfo`, DNS in `TCPSocket.new` | `address_resolve` | resolved on a worker thread |
|
|
88
|
-
| `Process.wait` | `process_wait` | reaped on a worker thread |
|
|
89
|
-
| your own OVERLAPPED ops (any gem) | `await_op` | generic completion packets on the same IOCP |
|
|
90
|
-
|
|
91
|
-
## API
|
|
92
|
-
|
|
93
|
-
```ruby
|
|
94
|
-
Winloop.run { ... } # install a scheduler, run the block in a fiber, drive the
|
|
95
|
-
# loop until every scheduled fiber finishes; returns the
|
|
96
|
-
# block's value (and re-raises anything it raised).
|
|
97
|
-
|
|
98
|
-
Winloop.supported? # true when the native IOCP/AFD backend loaded.
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
For full control you can drive it yourself, exactly like any other
|
|
102
|
-
`Fiber::Scheduler`:
|
|
103
|
-
|
|
104
|
-
```ruby
|
|
105
|
-
scheduler = Winloop::Scheduler.new
|
|
106
|
-
Fiber.set_scheduler(scheduler)
|
|
107
|
-
Fiber.schedule { ... }
|
|
108
|
-
scheduler.close # runs the event loop to completion
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
The generic-op surface (winloop 0.2 — see the next section):
|
|
112
|
-
|
|
113
|
-
```ruby
|
|
114
|
-
# Scheduler (the cross-gem protocol; loop-thread-only):
|
|
115
|
-
sched.op_associate(handle) # => true (permanent; handle stays yours)
|
|
116
|
-
sched.op_prepare(handle, tag: 0, capacity: 0) # => [op_id, ov_addr, buf_addr]
|
|
117
|
-
sched.op_submitted(op_id) # => true (native call returned TRUE or ERROR_IO_PENDING)
|
|
118
|
-
sched.op_abandon(op_id) # => true (native call failed synchronously)
|
|
119
|
-
sched.op_cancel(op_id) # => true | false (false: already completed, or CancelIoEx failed — warned, never raised)
|
|
120
|
-
sched.op_state(op_id) # => :prepared | :submitted | :completed
|
|
121
|
-
sched.await_op(op_id, timeout: nil) # => [bytes, error, data] | nil (timeout; op auto-cancelled)
|
|
122
|
-
|
|
123
|
-
# Backend (the power layer; also the standalone-embedding layer):
|
|
124
|
-
backend.associate(handle) # => true
|
|
125
|
-
backend.op_prepare(handle, tag: 0, capacity: 0) # => [op_id, ov_addr, buf_addr]
|
|
126
|
-
backend.op_submitted(op_id) # => true
|
|
127
|
-
backend.op_abandon(op_id) # => true
|
|
128
|
-
backend.op_cancel(op_id) # => true | false
|
|
129
|
-
backend.op_result(op_id) # => [bytes, error, data] (frees the record)
|
|
130
|
-
backend.op_free(op_id) # => true (retire a completion nobody wants)
|
|
131
|
-
backend.op_state(op_id) # => :prepared | :submitted | :completed
|
|
132
|
-
backend.port_handle # => Integer (read-only; tests/diagnostics)
|
|
133
|
-
backend.wait(timeout_ms) # => [[id, events], ..., [op_id, bytes, error, tag], ...]
|
|
134
|
-
Winloop::EXTERNAL_KEY # => 0x45585431 — completion key of all generic ops
|
|
135
|
-
Winloop::OP_CAPACITY_MAX # => 16 MiB — op_prepare capacity ceiling
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Bring your own OVERLAPPED (generic completions)
|
|
139
|
-
|
|
140
|
-
**winloop 0.2 lets any gem associate a Windows handle with the loop's completion
|
|
141
|
-
port, submit its own overlapped operation, and park the calling fiber until the
|
|
142
|
-
completion packet arrives — cancel and synchronous-completion semantics done
|
|
143
|
-
right, with zero change to everything winloop already does.** This is a seam for
|
|
144
|
-
**gem authors** (file watchers, named pipes, mailslots…), not app code.
|
|
145
|
-
|
|
146
|
-
The contract loop: `op_associate` → `op_prepare` → your native submit →
|
|
147
|
-
`op_submitted` (success **or** `ERROR_IO_PENDING` — both queue a packet) /
|
|
148
|
-
`op_abandon` (synchronous failure — the only no-packet case) → `await_op` →
|
|
149
|
-
`[bytes, error, data]`.
|
|
150
|
-
|
|
151
|
-
A real overlapped file read driven from plain Ruby via Fiddle (stdlib):
|
|
152
|
-
|
|
153
|
-
```ruby
|
|
154
|
-
require "winloop"
|
|
155
|
-
require "fiddle/import"
|
|
156
|
-
|
|
157
|
-
module K32
|
|
158
|
-
extend Fiddle::Importer
|
|
159
|
-
dlload "kernel32.dll"
|
|
160
|
-
extern "void* CreateFileW(void*, unsigned long, unsigned long, void*, unsigned long, unsigned long, void*)"
|
|
161
|
-
extern "int ReadFile(void*, void*, unsigned long, void*, void*)"
|
|
162
|
-
extern "int CloseHandle(void*)"
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
GENERIC_READ = 0x8000_0000
|
|
166
|
-
FILE_SHARE_READ = 0x1
|
|
167
|
-
OPEN_EXISTING = 3
|
|
168
|
-
FILE_FLAG_OVERLAPPED = 0x4000_0000
|
|
169
|
-
ERROR_IO_PENDING = 997
|
|
170
|
-
|
|
171
|
-
Winloop.run do
|
|
172
|
-
sched = Fiber.scheduler # a Winloop::Scheduler
|
|
173
|
-
wpath = (File.expand_path(__FILE__) + "\0").encode("UTF-16LE")
|
|
174
|
-
h = K32.CreateFileW(wpath, GENERIC_READ, FILE_SHARE_READ, nil,
|
|
175
|
-
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nil).to_i
|
|
176
|
-
sched.op_associate(h)
|
|
177
|
-
op_id, ov, buf = sched.op_prepare(h, capacity: 4096)
|
|
178
|
-
ok = K32.ReadFile(h, buf, 4096, nil, ov)
|
|
179
|
-
if ok != 0 || Fiddle.win32_last_error == ERROR_IO_PENDING
|
|
180
|
-
sched.op_submitted(op_id) # success AND pending both queue a packet
|
|
181
|
-
bytes, error, data = sched.await_op(op_id, timeout: 5) # fiber parks HERE
|
|
182
|
-
raise Winloop::Error, "read failed (#{error})" unless error.zero?
|
|
183
|
-
puts "read #{bytes} bytes: #{data[0, 40].inspect}"
|
|
184
|
-
else
|
|
185
|
-
sched.op_abandon(op_id) # synchronous failure: no packet will come
|
|
186
|
-
raise Winloop::Error, "ReadFile failed (#{Fiddle.win32_last_error})"
|
|
187
|
-
end
|
|
188
|
-
K32.CloseHandle(h)
|
|
189
|
-
end
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Rules (read this twice)
|
|
193
|
-
|
|
194
|
-
* **Handles stay yours.** winloop owns the op memory (OVERLAPPED + buffer, one
|
|
195
|
-
backend-owned heap allocation); you open and close your handles — winloop
|
|
196
|
-
never calls `CloseHandle` on them.
|
|
197
|
-
* **Association is permanent** — one port per handle, no disassociation (Win32
|
|
198
|
-
rule). After the loop shuts down an associated handle can never join another
|
|
199
|
-
winloop loop; re-open per `Winloop.run` session.
|
|
200
|
-
* **Associate before prepare.** `op_prepare` rejects handles it has never seen,
|
|
201
|
-
because an op on a handle that is not on the port never completes: not an
|
|
202
|
-
error, a **silent hang**. The one case the check cannot catch — closing a
|
|
203
|
-
handle mid-flight so the OS recycles its value — is on you: cancel → await →
|
|
204
|
-
only then `CloseHandle`.
|
|
205
|
-
* **Use `await_op` timeouts while first integrating a new native API**; switch
|
|
206
|
-
to `nil` once the submit path is proven.
|
|
207
|
-
* Never set `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` on an associated handle (it
|
|
208
|
-
is irreversible and per-handle): winloop's invariant is exactly one packet per
|
|
209
|
-
submitted op, always. Don't use `ReadFileEx`/`WriteFileEx` (APC I/O) on
|
|
210
|
-
associated handles, and don't duplicate/inherit them.
|
|
211
|
-
* **Cancel never frees** — the cancelled op still posts a packet (usually error
|
|
212
|
-
995), which is reaped normally.
|
|
213
|
-
* **After `await_op` returns — value or `nil` — the op id is dead.** Don't use
|
|
214
|
-
it again.
|
|
215
|
-
* Loop thread only: ops are submitted on the loop thread (fibers run there
|
|
216
|
-
anyway); the scheduler wrappers enforce it.
|
|
217
|
-
* **Completion errors are values, not exceptions**: `995` cancelled, `1022`
|
|
218
|
-
RDCW rescan, `38` EOF, … A zero-byte successful completion is a real signal
|
|
219
|
-
(RDCW overflow ⇒ rescan), delivered verbatim.
|
|
220
|
-
* No PQCS injection — out of scope in v1. `Backend#port_handle` exists for
|
|
221
|
-
tests and diagnostics only.
|
|
222
|
-
|
|
223
|
-
### Standalone (no scheduler)
|
|
224
|
-
|
|
225
|
-
Client gems never hard-depend on winloop. The protocol is duck-typed on the
|
|
226
|
-
scheduler:
|
|
227
|
-
|
|
228
|
-
```ruby
|
|
229
|
-
sched = Fiber.scheduler
|
|
230
|
-
if sched&.respond_to?(:await_op)
|
|
231
|
-
# winloop (or compatible) IOCP path: op_associate / op_prepare / native
|
|
232
|
-
# submit / op_submitted / await_op — as above.
|
|
233
|
-
else
|
|
234
|
-
# the client gem's own standalone mechanism, e.g. the winipc pattern:
|
|
235
|
-
# stack OVERLAPPED + manual-reset hEvent, WaitForSingleObject without the
|
|
236
|
-
# GVL with ubf = CancelIoEx, GetOverlappedResult; or a gem-owned wait thread
|
|
237
|
-
# for continuous streams.
|
|
238
|
-
end
|
|
239
|
-
```
|
|
240
|
-
|
|
241
|
-
winloop ships **no** shared standalone wait thread or service (dependency
|
|
242
|
-
direction, irreversible association, ownership — clients own their threads).
|
|
243
|
-
`Winloop::Backend` itself does work standalone — create, `associate`,
|
|
244
|
-
prepare/submit, `wait`-poll (dispatch on tuple size: AFD 2-tuples vs op
|
|
245
|
-
4-tuples in the same array), `op_result`, `shutdown` — which is how an embedder
|
|
246
|
-
or a test drives it deterministically.
|
|
247
|
-
|
|
248
|
-
## Scope and limitations (v0.2)
|
|
249
|
-
|
|
250
|
-
* **Sockets are the focus.** TCP/UDP sockets get the full IOCP/AFD path. Pipes and
|
|
251
|
-
regular files fall back to a blocking read inside `io_read` (async file I/O on
|
|
252
|
-
Windows is a separate mechanism, planned for later).
|
|
253
|
-
* **Single thread-of-use.** One thread runs the loop and resumes fibers; other OS
|
|
254
|
-
threads may safely wake it (e.g. a background `Queue` producer), but the loop
|
|
255
|
-
itself is not a multi-threaded worker pool.
|
|
256
|
-
* `IOCTL_AFD_POLL` is undocumented; winloop resolves its `ntdll` entry points at
|
|
257
|
-
runtime and degrades by raising a clear error if `\Device\Afd` is unavailable.
|
|
258
|
-
* **The ops API is for gem authors.** Buffers are backend-owned only (no
|
|
259
|
-
IO::Buffer/String pinning — copy-out removes the whole lifetime/compaction bug
|
|
260
|
-
class). Ops still in flight at shutdown are cancelled and drained for a
|
|
261
|
-
bounded interval; stragglers may be deliberately leaked rather than freed
|
|
262
|
-
under possible kernel ownership.
|
|
263
|
-
* **x64 only.** arm64-mswin is expected to work (all code is `_WIN64`/
|
|
264
|
-
`uintptr_t`-clean, no arch-specific anything — AFD + IOCP are
|
|
265
|
-
production-proven on arm64 via libuv/Node and mio/Rust) but is untested and
|
|
266
|
-
unsupported until an arm64-mswin Ruby distribution exists.
|
|
267
|
-
|
|
268
|
-
## How it compares
|
|
269
|
-
|
|
270
|
-
`winloop` is a drop-in `Fiber::Scheduler`, so it works with anything that targets
|
|
271
|
-
the scheduler interface. It is *not* a reimplementation of `async` — it's the
|
|
272
|
-
missing Windows engine such libraries can run on. The reference scheduler shipped
|
|
273
|
-
with Ruby is intended for testing and is `IO.select`-bound; winloop replaces that
|
|
274
|
-
selector with a true completion port.
|
|
275
|
-
|
|
276
|
-
## License
|
|
277
|
-
|
|
278
|
-
MIT. See [LICENSE.txt](LICENSE.txt).
|
|
1
|
+
# winloop (discontinued)
|
|
2
|
+
|
|
3
|
+
**winloop is no longer maintained.** This final release (0.2.1) contains no
|
|
4
|
+
code: installing it compiles nothing, and `require "winloop"` raises a
|
|
5
|
+
`LoadError` explaining the discontinuation.
|
|
6
|
+
|
|
7
|
+
Use async / io-event (native Windows IOCP support is in progress upstream) instead.
|
|
8
|
+
|
|
9
|
+
Earlier releases remain on RubyGems only because RubyGems does not allow
|
|
10
|
+
versions older than 30 days to be yanked. They are unmaintained and not
|
|
11
|
+
recommended.
|
data/lib/winloop.rb
CHANGED
|
@@ -1,81 +1,4 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
# winloop — a native Windows (IOCP) Fiber Scheduler for MRI Ruby.
|
|
6
|
-
#
|
|
7
|
-
# require "winloop"
|
|
8
|
-
#
|
|
9
|
-
# Winloop.run do
|
|
10
|
-
# server = TCPServer.new("127.0.0.1", 9292)
|
|
11
|
-
# loop do
|
|
12
|
-
# client = server.accept # io_wait via AFD poll
|
|
13
|
-
# Fiber.schedule do # one fiber per connection
|
|
14
|
-
# while (line = client.gets) # io_read via recv_nonblock + io_wait
|
|
15
|
-
# client.write(line)
|
|
16
|
-
# end
|
|
17
|
-
# client.close
|
|
18
|
-
# end
|
|
19
|
-
# end
|
|
20
|
-
# end
|
|
21
|
-
#
|
|
22
|
-
# All blocking socket I/O, sleeps, timeouts and Mutex/Queue/Thread#join inside
|
|
23
|
-
# the block run cooperatively on a single thread, driven by one I/O Completion
|
|
24
|
-
# Port. See Winloop::Scheduler for the hook implementations.
|
|
25
|
-
module Winloop
|
|
26
|
-
class Error < StandardError; end unless const_defined?(:Error)
|
|
27
|
-
|
|
28
|
-
# Loaded for its side effect of defining Winloop::Backend + the IO event
|
|
29
|
-
# constants (READABLE/WRITABLE/PRIORITY). mswin-only.
|
|
30
|
-
begin
|
|
31
|
-
require "winloop/winloop"
|
|
32
|
-
rescue LoadError => e
|
|
33
|
-
raise LoadError, "winloop's native backend failed to load — winloop needs a " \
|
|
34
|
-
"Windows MSVC (mswin) Ruby. (#{e.message})"
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
require_relative "winloop/scheduler"
|
|
38
|
-
require_relative "winloop/ops" # generic-op validation layer (OP_CAPACITY_MAX, op_prepare)
|
|
39
|
-
|
|
40
|
-
module_function
|
|
41
|
-
|
|
42
|
-
# True if the IOCP/AFD backend is available on this platform.
|
|
43
|
-
def supported?
|
|
44
|
-
const_defined?(:Backend)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Install a fresh Winloop::Scheduler on the current thread, run `block` inside
|
|
48
|
-
# a non-blocking fiber, drive the event loop until every scheduled fiber has
|
|
49
|
-
# finished, and return the block's value.
|
|
50
|
-
#
|
|
51
|
-
# Nesting is not supported; if a scheduler is already installed, the block is
|
|
52
|
-
# simply scheduled on it.
|
|
53
|
-
def run
|
|
54
|
-
raise ArgumentError, "Winloop.run requires a block" unless block_given?
|
|
55
|
-
|
|
56
|
-
# Already inside a scheduler (e.g. a nested Winloop.run within a fiber): just
|
|
57
|
-
# run the block here — blocking operations already cooperate with that loop.
|
|
58
|
-
return yield if Fiber.scheduler
|
|
59
|
-
|
|
60
|
-
scheduler = Scheduler.new
|
|
61
|
-
Fiber.set_scheduler(scheduler)
|
|
62
|
-
result = nil
|
|
63
|
-
error = nil
|
|
64
|
-
Fiber.schedule do
|
|
65
|
-
begin
|
|
66
|
-
result = yield
|
|
67
|
-
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
68
|
-
error = e
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
scheduler.close # drives the loop to completion
|
|
72
|
-
raise error if error
|
|
73
|
-
result
|
|
74
|
-
ensure
|
|
75
|
-
# Detach our (now-closed) scheduler so subsequent ordinary I/O on this
|
|
76
|
-
# thread doesn't hit a closed backend. Safe here: we are on the main fiber
|
|
77
|
-
# and close has already returned (the reentrant-close trap only bites when
|
|
78
|
-
# set_scheduler(nil) is called from *inside* #close during finalization).
|
|
79
|
-
Fiber.set_scheduler(nil) if Fiber.scheduler.equal?(scheduler)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
raise LoadError, "winloop is discontinued and this release contains no code. Use async / io-event (native Windows IOCP support is in progress upstream) instead." \
|
|
4
|
+
" Earlier winloop releases are unmaintained and not recommended."
|
metadata
CHANGED
|
@@ -1,87 +1,30 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: winloop
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ned
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '13.0'
|
|
19
|
-
type: :development
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '13.0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: rake-compiler
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - "~>"
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '1.2'
|
|
33
|
-
type: :development
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - "~>"
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '1.2'
|
|
40
|
-
- !ruby/object:Gem::Dependency
|
|
41
|
-
name: minitest
|
|
42
|
-
requirement: !ruby/object:Gem::Requirement
|
|
43
|
-
requirements:
|
|
44
|
-
- - "~>"
|
|
45
|
-
- !ruby/object:Gem::Version
|
|
46
|
-
version: '5.0'
|
|
47
|
-
type: :development
|
|
48
|
-
prerelease: false
|
|
49
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
-
requirements:
|
|
51
|
-
- - "~>"
|
|
52
|
-
- !ruby/object:Gem::Version
|
|
53
|
-
version: '5.0'
|
|
54
|
-
description: |
|
|
55
|
-
winloop is a Ruby Fiber::Scheduler built on Win32 I/O Completion Ports. It
|
|
56
|
-
makes ordinary socket I/O, sleeps, timeouts and Mutex/Queue/Thread#join run
|
|
57
|
-
cooperatively on a single thread — the async-runtime story that has always
|
|
58
|
-
been weak on Windows, done the way libuv/mio/wepoll do it: readiness over an
|
|
59
|
-
IOCP via \Device\Afd polling, with recv/send driven by the completion port.
|
|
60
|
-
Requires a native Windows MSVC (mswin) build of Ruby.
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: winloop is discontinued. This final release contains no code and exists
|
|
13
|
+
only so that installing winloop explains the discontinuation instead of building
|
|
14
|
+
an unmaintained native extension.
|
|
61
15
|
executables: []
|
|
62
|
-
extensions:
|
|
63
|
-
- ext/winloop/extconf.rb
|
|
16
|
+
extensions: []
|
|
64
17
|
extra_rdoc_files: []
|
|
65
18
|
files:
|
|
66
|
-
- CHANGELOG.md
|
|
67
19
|
- LICENSE.txt
|
|
68
20
|
- README.md
|
|
69
|
-
- ext/winloop/extconf.rb
|
|
70
|
-
- ext/winloop/winloop.c
|
|
71
21
|
- lib/winloop.rb
|
|
72
|
-
- lib/winloop/ops.rb
|
|
73
|
-
- lib/winloop/scheduler.rb
|
|
74
|
-
- lib/winloop/version.rb
|
|
75
|
-
homepage: https://github.com/main-path/winloop
|
|
76
22
|
licenses:
|
|
77
23
|
- MIT
|
|
78
24
|
metadata:
|
|
79
|
-
homepage_uri: https://github.com/main-path/winloop
|
|
80
|
-
source_code_uri: https://github.com/main-path/winloop
|
|
81
|
-
changelog_uri: https://github.com/main-path/winloop/blob/main/CHANGELOG.md
|
|
82
|
-
bug_tracker_uri: https://github.com/main-path/winloop/issues
|
|
83
|
-
allowed_push_host: https://rubygems.org
|
|
84
25
|
rubygems_mfa_required: 'true'
|
|
26
|
+
post_install_message: winloop is discontinued and this release contains no code. Use
|
|
27
|
+
async / io-event (native Windows IOCP support is in progress upstream) instead.
|
|
85
28
|
rdoc_options: []
|
|
86
29
|
require_paths:
|
|
87
30
|
- lib
|
|
@@ -89,7 +32,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
89
32
|
requirements:
|
|
90
33
|
- - ">="
|
|
91
34
|
- !ruby/object:Gem::Version
|
|
92
|
-
version: '
|
|
35
|
+
version: '2.0'
|
|
93
36
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
37
|
requirements:
|
|
95
38
|
- - ">="
|
|
@@ -98,5 +41,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
98
41
|
requirements: []
|
|
99
42
|
rubygems_version: 3.6.9
|
|
100
43
|
specification_version: 4
|
|
101
|
-
summary:
|
|
44
|
+
summary: Discontinued. Use async / io-event (native Windows IOCP support is in progress
|
|
45
|
+
upstream) instead.
|
|
102
46
|
test_files: []
|
data/CHANGELOG.md
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.2.0
|
|
4
|
-
|
|
5
|
-
**Bring your own OVERLAPPED** — a generic OVERLAPPED/IOCP completion API, so any
|
|
6
|
-
gem can associate a Windows handle with the loop's completion port, submit its
|
|
7
|
-
own overlapped operation, and park the calling fiber until the completion packet
|
|
8
|
-
arrives. Strictly additive: nothing in the 0.1 API changes signature or behavior,
|
|
9
|
-
and all 34 pre-existing tests pass byte-unmodified.
|
|
10
|
-
|
|
11
|
-
### Added
|
|
12
|
-
|
|
13
|
-
* **`Winloop::Backend` op API** (the power layer; also works standalone, with no
|
|
14
|
-
scheduler): `#associate(handle)`, `#op_prepare(handle, tag:, capacity:)` →
|
|
15
|
-
`[op_id, ov_addr, buf_addr]` (backend-owned OVERLAPPED + 8-byte-aligned
|
|
16
|
-
embedded buffer), `#op_submitted`, `#op_abandon` (synchronous-failure path —
|
|
17
|
-
the only no-packet case), `#op_cancel` (`CancelIoEx`; **never frees** — the
|
|
18
|
-
cancelled op still posts a packet, which is reaped normally; CancelIoEx
|
|
19
|
-
failures warn and return `false`, never raise), `#op_result` →
|
|
20
|
-
`[bytes, error, data]`, `#op_free`, `#op_state`, and `#port_handle`
|
|
21
|
-
(tests/diagnostics only).
|
|
22
|
-
* **`Winloop::Scheduler` op protocol** (the misuse-resistant cross-gem surface,
|
|
23
|
-
feature-gated by `Fiber.scheduler.respond_to?(:await_op)`): `op_associate`,
|
|
24
|
-
`op_prepare`, `op_submitted`, `op_abandon`, `op_cancel`, `op_state`, and
|
|
25
|
-
`await_op(op_id, timeout: nil)`, which parks the calling fiber until the
|
|
26
|
-
completion is reaped (returns the `[bytes, error, data]` triple, or `nil` on
|
|
27
|
-
timeout after auto-cancelling and orphaning the op). Loop-thread-only,
|
|
28
|
-
enforced.
|
|
29
|
-
* Constants `Winloop::EXTERNAL_KEY` (the completion key carried by all generic
|
|
30
|
-
ops) and `Winloop::OP_CAPACITY_MAX` (16 MiB embedded-buffer ceiling).
|
|
31
|
-
* **Completion errors are data, not exceptions**: the `error` element carries
|
|
32
|
-
the Win32 code mapped from the final NTSTATUS via `RtlNtStatusToDosError`
|
|
33
|
-
(`0` success, `995` cancelled, `1022` RDCW rescan, `38` EOF, …).
|
|
34
|
-
* `op_prepare` rejects handles never passed to `#associate` — an op on a handle
|
|
35
|
-
that is not on the port never completes (a silent hang, not an error).
|
|
36
|
-
* Defensive reap: unknown EXT packets and duplicate posts for already-completed
|
|
37
|
-
ops are skipped with a warning, never cast, never re-completed.
|
|
38
|
-
* `ObjectSpace.memsize_of(backend)` now reports live op-buffer bytes.
|
|
39
|
-
* Gemspec metadata tightened to suite grade: `bug_tracker_uri`,
|
|
40
|
-
`rubygems_mfa_required`.
|
|
41
|
-
|
|
42
|
-
### Changed
|
|
43
|
-
|
|
44
|
-
* `Backend#shutdown` (and the GC free hook) now also cancels every in-flight
|
|
45
|
-
generic op per op (`CancelIoEx(handle, ov)`), drains their packets — including
|
|
46
|
-
one bounded 100 ms wait taken **only** when generic ops are still in flight —
|
|
47
|
-
and frees retired records. Ops whose packet never surfaced inside the bounded
|
|
48
|
-
drain are deliberately leaked (the kernel may still write their
|
|
49
|
-
OVERLAPPED/IOSB; a use-after-free beats a bounded leak). Existing AFD-only
|
|
50
|
-
workloads see byte-identical shutdown.
|
|
51
|
-
* `Backend#wait` may now return 4-element arrays `[op_id, bytes, error, tag]`
|
|
52
|
-
alongside the existing AFD 2-tuples — when, and only when, the new op API is
|
|
53
|
-
used in the process. Callers dispatch on tuple size; the AFD contract
|
|
54
|
-
(2-tuples, `[]` on timeout, wakeups skipped) is untouched.
|
|
55
|
-
|
|
56
|
-
## 0.1.0
|
|
57
|
-
|
|
58
|
-
Initial release.
|
|
59
|
-
|
|
60
|
-
* A `Fiber::Scheduler` for MRI on Windows backed by I/O Completion Ports.
|
|
61
|
-
* C backend (`Winloop::Backend`): one IOCP plus a `\Device\Afd` handle, with
|
|
62
|
-
one-shot readiness polls (`IOCTL_AFD_POLL`) submitted as overlapped operations
|
|
63
|
-
on the port, reaped in batches via `GetQueuedCompletionStatusEx`. The wait runs
|
|
64
|
-
without the GVL so background threads can wake the loop with
|
|
65
|
-
`PostQueuedCompletionStatus`.
|
|
66
|
-
* Scheduler hooks: `io_wait`, `io_read`, `io_write`, `kernel_sleep`,
|
|
67
|
-
`timeout_after`, `block`/`unblock`, `fiber`, `address_resolve`, `process_wait`,
|
|
68
|
-
and `close` (which drives the event loop).
|
|
69
|
-
* Monotonic timer min-heap for `sleep` and `Timeout.timeout`.
|
|
70
|
-
* `Winloop.run { ... }` convenience entry point.
|
|
71
|
-
* Verified with TCP echo over many concurrent connections, large multi-chunk
|
|
72
|
-
transfers, EOF handling, `Timeout` (both firing and not), cross-thread `Queue`,
|
|
73
|
-
inter-fiber `Mutex` contention, `Thread#join`, DNS resolution, and `IO.popen`.
|
|
74
|
-
|
|
75
|
-
Hardened over three rounds of adversarial review (the GVL is released around the
|
|
76
|
-
completion wait so background threads can wake the loop):
|
|
77
|
-
* AFD readiness polls are **coalesced per socket** — multiple fibers waiting the
|
|
78
|
-
same socket for the same event all wake (the AFD driver only completes one poll
|
|
79
|
-
per readiness edge, so independent polls would lose wakeups).
|
|
80
|
-
* Cross-thread wakeups are unified through a single per-fiber guard, so a timeout
|
|
81
|
-
timer racing an `unblock` (e.g. `ConditionVariable#wait(mutex, timeout)` racing a
|
|
82
|
-
signal, or `Thread#join(timeout)` near its deadline) can never double-resume a
|
|
83
|
-
fiber and cut its next wait short.
|
|
84
|
-
* Settled timers are dropped from the heap so an early-resolved wait can't pin the
|
|
85
|
-
loop asleep until its original (possibly long) timeout.
|
|
86
|
-
* An unhandled exception in a scheduled fiber is reported, not fatal to the loop.
|
|
87
|
-
* The non-socket read fallback runs on a worker thread that is killed if its fiber
|
|
88
|
-
unwinds, so it can't leak or steal data; pipe EOF is silent.
|
data/ext/winloop/extconf.rb
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
#
|
|
3
|
-
# extconf.rb for the winloop C backend (IOCP + \Device\Afd readiness).
|
|
4
|
-
|
|
5
|
-
require "mkmf"
|
|
6
|
-
|
|
7
|
-
unless RbConfig::CONFIG["target_os"] =~ /mswin/
|
|
8
|
-
abort <<~MSG
|
|
9
|
-
winloop requires a native Windows MSVC (mswin) Ruby — its event loop is built
|
|
10
|
-
on Win32 I/O Completion Ports and \\Device\\Afd readiness polling (cl.exe).
|
|
11
|
-
Your Ruby is "#{RbConfig::CONFIG['arch']}".
|
|
12
|
-
MSG
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
# Winsock. (ntdll's Nt*/Rtl* entry points are resolved at runtime via
|
|
16
|
-
# GetProcAddress, so no ntdll import lib is required.) Pure C — no -EHsc.
|
|
17
|
-
$libs = [$libs, "ws2_32.lib"].join(" ")
|
|
18
|
-
|
|
19
|
-
create_makefile("winloop/winloop")
|