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 GetQueuedCompletionStatusEx call per turn reaps every ready event at once.
  • Readiness (accept, connect, IO#wait_readable) — which IOCP doesn't natively provide — is obtained by submitting IOCTL_AFD_POLL against \Device\Afd as 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/send once 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#join park the fiber on an in-process waiter list; a wakeup from another OS thread breaks the loop's wait via PostQueuedCompletionStatus.

Requirements

  • A native Windows MSVC build of Ruby (x64-mswin64). winloop compiles a C extension with cl.exe and links ws2_32. It will refuse to install on any non-mswin platform.
  • Ruby >= 3.1 (for a stable Fiber::Scheduler and IO::Buffer).
  • \Device\Afd readiness 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 Queue producer), but the loop itself is not a multi-threaded worker pool.
  • IOCTL_AFD_POLL is undocumented; winloop resolves its ntdll entry points at runtime and degrades by raising a clear error if \Device\Afd is 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.