winloop
A native Windows Fiber Scheduler for MRI Ruby, built on I/O Completion Ports.
winloop makes ordinary blocking code — socket reads and writes, sleep,
Timeout.timeout, Mutex, Queue, Thread#join, DNS lookups — run
cooperatively on a single thread. Wrap your code in Winloop.run { ... },
spawn fibers with Fiber.schedule, and thousands of connections share one
thread without you touching a single nonblocking API.
require "winloop"
require "socket"
Winloop.run do
server = TCPServer.new("127.0.0.1", 9292)
loop do
client = server.accept # readiness via AFD poll on the IOCP
Fiber.schedule do # one lightweight fiber per connection
while (line = client.gets) # recv driven by the completion port
client.write(line) # send, never blocking the loop
end
client.close
end
end
end
Why this exists
Ruby's Fiber::Scheduler (the engine behind socketry/async and friends) is the
modern way to write concurrent I/O. But on Windows the selectors that back it —
socketry/io-event ships select, epoll, kqueue, and io_uring — have no
IOCP backend. The result has long been the weakest async story of any major
platform: select()-bound, capped at 64 handles, and slow.
winloop fills that hole the same way libuv, Rust's mio, and wepoll do it:
- The event loop is a real I/O Completion Port. One
GetQueuedCompletionStatusExcall per turn reaps every ready event at once. - Readiness (accept, connect,
IO#wait_readable) — which IOCP doesn't natively provide — is obtained by submittingIOCTL_AFD_POLLagainst\Device\Afdas an overlapped operation on the port. This is the undocumented mechanism libuv/mio rely on, and it is the only way to get readiness semantics onto a completion port. - Reads and writes are driven by
recv/sendonce the port reports readiness. - Timers (
sleep,Timeout.timeout) use a monotonic min-heap that sets the port's wait timeout — no kernel timer objects, no extra threads. Mutex/Queue/Thread#joinpark the fiber on an in-process waiter list; a wakeup from another OS thread breaks the loop's wait viaPostQueuedCompletionStatus.
Requirements
- A native Windows MSVC build of Ruby (
x64-mswin64). winloop compiles a C extension withcl.exeand linksws2_32. It will refuse to install on any non-mswinplatform. - Ruby >= 3.1 (for a stable
Fiber::SchedulerandIO::Buffer). \Device\Afdreadiness polling is a Windows NT facility; winloop is not expected to work under Wine.
Installation
gem install winloop
Or build from a checkout (it dogfoods the vcvars
gem so it can find the MSVC toolchain without a Developer Command Prompt):
rake compile
rake test
What runs cooperatively
Inside Winloop.run { ... } (and any Fiber.schedule started within it):
| Operation | Hook | Mechanism |
|---|---|---|
TCPServer#accept, IO#wait_readable |
io_wait |
IOCTL_AFD_POLL overlapped on the IOCP |
IO#read / gets / recv on a socket |
io_read |
recv_nonblock + io_wait |
IO#write / puts on a socket |
io_write |
write_nonblock + io_wait |
sleep |
kernel_sleep |
monotonic timer heap |
Timeout.timeout |
timeout_after |
timer heap + Fiber#raise |
Mutex, ConditionVariable, Queue, Thread#join |
block/unblock |
waiter lists + PostQueuedCompletionStatus |
Addrinfo.getaddrinfo, DNS in TCPSocket.new |
address_resolve |
resolved on a worker thread |
Process.wait |
process_wait |
reaped on a worker thread |
API
Winloop.run { ... } # install a scheduler, run the block in a fiber, drive the
# loop until every scheduled fiber finishes; returns the
# block's value (and re-raises anything it raised).
Winloop.supported? # true when the native IOCP/AFD backend loaded.
For full control you can drive it yourself, exactly like any other
Fiber::Scheduler:
scheduler = Winloop::Scheduler.new
Fiber.set_scheduler(scheduler)
Fiber.schedule { ... }
scheduler.close # runs the event loop to completion
Scope and limitations (v0.1)
- Sockets are the focus. TCP/UDP sockets get the full IOCP/AFD path. Pipes and
regular files fall back to a blocking read inside
io_read(async file I/O on Windows is a separate mechanism, planned for later). - Single thread-of-use. One thread runs the loop and resumes fibers; other OS
threads may safely wake it (e.g. a background
Queueproducer), but the loop itself is not a multi-threaded worker pool. IOCTL_AFD_POLLis undocumented; winloop resolves itsntdllentry points at runtime and degrades by raising a clear error if\Device\Afdis unavailable.
How it compares
winloop is a drop-in Fiber::Scheduler, so it works with anything that targets
the scheduler interface. It is not a reimplementation of async — it's the
missing Windows engine such libraries can run on. The reference scheduler shipped
with Ruby is intended for testing and is IO.select-bound; winloop replaces that
selector with a true completion port.
License
MIT. See LICENSE.txt.