Expand description
Memory-safe Rust over the Windows IoRing submission/completion ring.
Windows 11 and Server 2022 added IoRing, a submission/completion ring for
file I/O closer in shape to io_uring than to anything else Windows offers.
This crate raises those primitives into safe Rust. The ring holds each
in-flight operation’s buffer on the caller’s behalf and hands it back at the
pop that observes its completion, so no buffer the kernel may still be using
is reachable from safe code. The tracking that makes this possible has a
cost, which may allocate: an entry in a hash set for every operation in
flight, and for an owned push an entry in a hash map holding its payload
(D-78).
§Scope: a file data plane, not a general completion backend
The kernel’s operation table is fixed at seven entries – no-op, read, write,
flush, register-files, register-buffers, and cancel. There is no ioctl
operation, no socket operation, and no directory-change operation, and unlike
Linux’s io_uring – which grew to roughly fifty opcodes including full
socket support – Windows IoRing has not grown beyond file I/O.
So this crate is not a replacement for
windows-overlapped-io-sys,
which remains the crate for arbitrary I/O on arbitrary handles. Use this one
for high-volume file reads and writes; use that one for everything else.
Neither can subsume the other.
§Availability is a runtime question
IoRing is present from Windows 11 and Server 2022, but three separate facts
about it are decided at runtime rather than at compile time: which ring
version the system supports, whether the ring is a real kernel ring or a
user-mode emulation with no kernel benefit, and whether the completion-event
feature the thread-pool delivery path depends on is available at all. All
three are answerable without creating a ring.
§Choosing a delivery architecture
There are two coherent high-performance shapes for consuming completions, and picking the wrong one costs more than any API detail in this crate.
Model A, threadless dispatch – shared queue, kernel load-balances. A
pool of threads waits;
work is handed to whichever thread the system picks next. This is
EventDelivery: the ring’s completion event wired to a thread-pool wait,
so no thread of yours ever blocks on I/O. Load balancing is automatic and
locality is incidental – start here. A caller that keeps its ring behind
its own lock takes CompletionWake instead, which only tells it when to
drain; EventDelivery is built on it (D-84). Both live behind the
default-on threadpool feature, so a Model B consumer can drop the thread-pool
dependency entirely with default-features = false (D-22); it is named
here rather than linked because a link would dangle in that
configuration. See examples/model_a_delivery.rs for a full worked
example (M6.2).
Model B, dedicated domain thread – shared-nothing execution domains.
One pinned thread per
domain, owning its ring, its buffer pool, and its shard of the
application’s state, with no cross-thread synchronization on the data
path – a pinned thread parked directly in Batch::submit_and_wait is
the event loop. This is what IoRing’s own API is shaped for: the
submission queue is not thread-safe, registration is per-ring, and there is
exactly one completion event per ring, none of which are limitations to
work around. That fused submit-and-wait is Model B’s usual wakeup source
rather than its defining one – see the next paragraph, which is still
Model B.
Model B with a multiplexed wakeup. IoRing::completion_event hands
back an owned duplicate of the ring’s own completion event, so a caller
keeps its ring and still waits on it alongside unrelated handles with
WaitForMultipleObjects. This is not a third architecture – it changes
only what the domain thread blocks on, never who owns, submits, or drains
– but it is what any consumer mixing ring I/O with non-ring I/O needs,
since IOSQE_FLAGS_DRAIN_PRECEDING_OPS orders SQEs against SQEs and
cannot order across the two paths. The event is edge-triggered on the
completion queue going empty to non-empty, which is measured rather than
documented by Win32 and hangs rather than merely slows if assumed
otherwise; see that method’s docs before using it, and
examples/model_b_multiplexed.rs for the whole shape end to end,
including shutdown with I/O still outstanding (M11.6).
Most real applications want Model B on the hot data path and Model A
everywhere else – the control plane, background work, cold paths – where
the thread pool’s quiescence is worth more than locality. Both are
first-class here; neither is a degraded form of the other. The full
trade-off – including why the NUMA node is the wrong key for sizing a
Model B execution domain, and why buffer placement likely dominates thread
placement – is in “Two delivery architectures” in DESIGN-NOTES.md.
§Durability
Three facts, all measured rather than documented by Win32, and all of them
things a consumer gets wrong by default. They are stated in full on
Batch::flush_owned, FlushCoverage, WriteCaching and FlushMode;
this is the summary that stops a reader from never looking.
- There is no FUA.
BuildIoRingWriteFile’s entire flag set is{FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH}, and write-through is a cache directive to the OS, not a device-level guarantee – whether it becomes a Force Unit Access bit depends on the driver, the volume, and the device’s write-cache setting. Treating a completed write-through write as durable is a mistake, not a shortcut. - The flush operation is the only durability primitive the ring has,
and only in a mode that syncs the device –
FlushMode::NoSyncdeliberately does not. - A flush without the barrier covers nothing. An unflagged flush is an
ordinary operation competing with the writes before it, and it
frequently wins, so its completion proves nothing about them. This is
why
Batch::flush_ownedrequires aFlushCoverageinstead of defaulting: the obvious spelling was a silent data-loss bug, invisible until power is lost.
What follows from all three is that durability is a property of an epoch,
never of an individual write, because the ring offers no per-write
primitive to make it one: stream the writes, close the epoch with one
covering flush, and wait on the flush rather than on the writes. The
barrier that makes this correct waits for everything outstanding, so the
flush itself is expensive – but it is one-sided and does not hold back
work submitted after it (PushOptions::drain_preceding documents both
halves). “Durability on the ring” in DESIGN-NOTES.md has the full shape
and the ways to pay for it.
§Topology guidance
This crate does not partition anything for you (D-8 in DESIGN-NOTES.md):
it makes a ring cheap and correct, makes its affinity explicit, and leaves
sizing a Model B execution domain to the caller. Three pointers, not a
partitioning policy:
-
Size a domain by the outermost cache level that partitions the machine, not by NUMA node. Node count is a firmware setting a process cannot see (AMD NPS, Intel Sub-NUMA Clustering), and most real deployments are virtualized, where NUMA topology is often invisible entirely. A cache partition degrades sanely instead: a machine whose caches partition nothing yields one ring, which is correct.
Ask which level partitions; do not filter on
CacheLevel == 3.MachineMemoryTopology::outermost_partitioning_cacheis the one definition, and it is not a convenience wrapper over that filter: a shipping ARM part reports no L3 at all, and a machine can report an L3 spanning every processor above a real L2 partition – where filtering on level 3 does not even degrade, because it matched something. It returns one whole-machine domain and calls it a cache-aware partition. Seeexamples/cache_domains.rsfor a runnable enumeration (M6.3), built on the safe wrapper inwindows-topology-sys. -
Processor groups are a hard floor. A thread’s affinity is a
GROUP_AFFINITYand a ring’s waiter lives in exactly one group, so above 64 logical processors the partition is forced whether or not it is wanted. -
Buffer placement likely dominates thread placement. A registered buffer on a node remote from the device means every byte crosses the interconnect, on every operation, forever; where the completion callback happens to run is a one-time cache-warmth question by comparison.
VirtualAllocExNuma, on the node closest to the device, registered once into that domain’s ring (seeBatch::register_buffers), is very likely the highest-leverage locality decision available.NumaBufferis that allocation; which node is a question about a caller’s storage layout, and this crate does not answer it for them.
§Where the design lives
The design, including the delivery-architecture guidance this crate exists
to make usable, is recorded in DESIGN-NOTES.md beside the source, and the
work still open against it is tracked in CHECKLIST.md.
Modules§
- contract
- This crate’s conservation rules, made executable (M16.1).
- sys
- The seam the kernel-response resolver sits under (M26.2).
Structs§
- Batch
- A scoped, exclusive submission window over one
IoRing(D-5). - Capabilities
- What the running system’s
IoRingimplementation supports, queried without creating a ring. - Completion
- One popped completion (M3.7): the operation’s identity, from
IORING_CQE::UserData, and its result. - Completion
Wake - Tells the owner of an
IoRingto drain it, on a thread-pool thread, whenever the ring’s completion queue may have gone from empty to non-empty (Model A, D-3; D-84). - Event
Delivery - Delivers an
IoRing’s completions on thread-pool callback threads via its completion event, rather than a caller pollingIoRing::try_popitself (Model A, D-3). - IoRing
- An owned
IoRing, closed withCloseIoRingon drop. - IoRing
Error - An error from an
IoRingAPI call. - Numa
Buffer - An owned buffer allocated with
VirtualAllocExNuma, freed withVirtualFreeon drop. - Operation
Id - The name of one in-flight operation, carrying no claim on anything.
- Pending
Buffer Registration - A
Batch::register_bufferspush not yet matched to its completion. - Pending
File Registration - A
Batch::register_filespush not yet matched to its completion. - Push
Options - Per-push options shared across every op builder (M3.2).
- Push
Refused - A push the ring refused, carrying everything the push was given (M30.1, D-80).
- Registered
Buffers - Buffers registered with a ring so later reads and writes can address them by index instead of handing over a fresh owned buffer each call (M5.2).
- Registered
File - One index a
Batch::register_filesregistration assigned (M5.1). - Registered
Files - The confirmed result of a
Batch::register_filespush:countcontiguous indices starting atbase_index. - Registered
Span - Which bytes of one registered buffer an operation reads or writes: bundled into one value so a registered read or write stays under a sane argument count.
- Registered
Use - A marker the ring holds while a registered-buffer-indexed read or write is outstanding (M5.2, M5.3).
- Ring
Info - What
IoRing::inforeports back fromGetIoRingInfo. - Ring
Scope - Exclusive access to an
EventDelivery’s ring, narrowed to what a submitting caller needs (M18.6). - Ring
Version - An
IORING_VERSIONvalue. - Ring
Wait - The ring, narrowed to what a
CompletionWaitneeds (M21.2). - Shared
Handle - The file type a safe push takes: a shared owner of a handle, from
win-shared-os-owned-handle, re-exported so it can be named without that crate. It replaced this crate’s ownSharedFile(D-82). A shared owner of a Windows handle: anOwnedHandlebehind a reference count. - Submit
Wait - The default
CompletionWait: block inside the ring’s ownSubmitIoRingwait (M21.2).
Enums§
- FileRef
- A file, addressed either by a raw
HANDLEor by the index a priorBatch::register_filesassigned it (M5.1). - Flush
Coverage - Whether a flush covers the operations queued before it (M12.1, D-23 in
DESIGN-NOTES.md). - Flush
Mode - How much a flush is asked to push, and whether it syncs the device at all
(M12.4, D-25 in
DESIGN-NOTES.md). - Injected
Failure - The fault-injection seam (M16.3), for exercising failure paths a healthy
machine will not produce on demand. See
Completion::with_injected_failurefor why transforming a real completion is sound where fabricating one would not be. A failure to report in place of a completion’s real result (M16.3). - Op
- One
IoRingoperation. - Open
- A ring that may have an operation in flight that the ring holds nothing for: the kind every constructor makes. All of the ring’s calls exist for it.
- Owned
- A ring none of whose in-flight operations can be one the ring holds nothing for, reached from
an
Openring byIoRing::into_owned. The calls that queue such an operation do not exist for it, and a pop returns what the ring held, without the case of there being nothing. - Ring
Condition - A named
IORING_E_*condition (M10.5). - Write
Caching - Where a write is allowed to stop: in the system cache, or past it (M12.3,
D-25 in
DESIGN-NOTES.md).
Traits§
- Allows
Raw - The kinds that may queue an operation the ring holds nothing for:
Open. A method with this bound is one anOwnedring does not have. - Completion
Wait - How
IoRing::pop_within_withblocks between checks of the completion queue (M21.2). - File
Target - A file a safe push can address: either a
SharedHandleor aRegisteredFile(M10.4; D-29, D-33 and D-82 inDESIGN-NOTES.md). - IoBuf
- An owned buffer an operation reads bytes from (a write).
- IoBuf
Mut - An owned buffer an operation writes bytes into (a read).
- IoRing
Error Ext - Ask an
io::Errorthis crate produced about the ring condition inside it (M10.5, D-30). - Ring
Kind - What a ring’s kind decides: the shape a pop returns it in. Implemented by
OpenandOwnedonly.
Functions§
- capabilities
- Query the running system’s
IoRingcapabilities.
Type Aliases§
- Held
Completion - A popped completion and whatever the ring was holding for it.