Skip to main content

Crate windows_ioring_sys

Crate windows_ioring_sys 

Source
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.

  1. 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.
  2. The flush operation is the only durability primitive the ring has, and only in a mode that syncs the device – FlushMode::NoSync deliberately does not.
  3. 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_owned requires a FlushCoverage instead 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_cache is 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. See examples/cache_domains.rs for a runnable enumeration (M6.3), built on the safe wrapper in windows-topology-sys.

  • Processor groups are a hard floor. A thread’s affinity is a GROUP_AFFINITY and 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 (see Batch::register_buffers), is very likely the highest-leverage locality decision available. NumaBuffer is 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 IoRing implementation supports, queried without creating a ring.
Completion
One popped completion (M3.7): the operation’s identity, from IORING_CQE::UserData, and its result.
CompletionWake
Tells the owner of an IoRing to 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).
EventDelivery
Delivers an IoRing’s completions on thread-pool callback threads via its completion event, rather than a caller polling IoRing::try_pop itself (Model A, D-3).
IoRing
An owned IoRing, closed with CloseIoRing on drop.
IoRingError
An error from an IoRing API call.
NumaBuffer
An owned buffer allocated with VirtualAllocExNuma, freed with VirtualFree on drop.
OperationId
The name of one in-flight operation, carrying no claim on anything.
PendingBufferRegistration
A Batch::register_buffers push not yet matched to its completion.
PendingFileRegistration
A Batch::register_files push not yet matched to its completion.
PushOptions
Per-push options shared across every op builder (M3.2).
PushRefused
A push the ring refused, carrying everything the push was given (M30.1, D-80).
RegisteredBuffers
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).
RegisteredFile
One index a Batch::register_files registration assigned (M5.1).
RegisteredFiles
The confirmed result of a Batch::register_files push: count contiguous indices starting at base_index.
RegisteredSpan
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.
RegisteredUse
A marker the ring holds while a registered-buffer-indexed read or write is outstanding (M5.2, M5.3).
RingInfo
What IoRing::info reports back from GetIoRingInfo.
RingScope
Exclusive access to an EventDelivery’s ring, narrowed to what a submitting caller needs (M18.6).
RingVersion
An IORING_VERSION value.
RingWait
The ring, narrowed to what a CompletionWait needs (M21.2).
SharedHandle
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 own SharedFile (D-82). A shared owner of a Windows handle: an OwnedHandle behind a reference count.
SubmitWait
The default CompletionWait: block inside the ring’s own SubmitIoRing wait (M21.2).

Enums§

FileRef
A file, addressed either by a raw HANDLE or by the index a prior Batch::register_files assigned it (M5.1).
FlushCoverage
Whether a flush covers the operations queued before it (M12.1, D-23 in DESIGN-NOTES.md).
FlushMode
How much a flush is asked to push, and whether it syncs the device at all (M12.4, D-25 in DESIGN-NOTES.md).
InjectedFailure
The fault-injection seam (M16.3), for exercising failure paths a healthy machine will not produce on demand. See Completion::with_injected_failure for 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 IoRing operation.
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 Open ring by IoRing::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.
RingCondition
A named IORING_E_* condition (M10.5).
WriteCaching
Where a write is allowed to stop: in the system cache, or past it (M12.3, D-25 in DESIGN-NOTES.md).

Traits§

AllowsRaw
The kinds that may queue an operation the ring holds nothing for: Open. A method with this bound is one an Owned ring does not have.
CompletionWait
How IoRing::pop_within_with blocks between checks of the completion queue (M21.2).
FileTarget
A file a safe push can address: either a SharedHandle or a RegisteredFile (M10.4; D-29, D-33 and D-82 in DESIGN-NOTES.md).
IoBuf
An owned buffer an operation reads bytes from (a write).
IoBufMut
An owned buffer an operation writes bytes into (a read).
IoRingErrorExt
Ask an io::Error this crate produced about the ring condition inside it (M10.5, D-30).
RingKind
What a ring’s kind decides: the shape a pop returns it in. Implemented by Open and Owned only.

Functions§

capabilities
Query the running system’s IoRing capabilities.

Type Aliases§

HeldCompletion
A popped completion and whatever the ring was holding for it.