Skip to main content

windows_ioring_sys/
lib.rs

1// Copyright (c) 2026 Mike Grier
2//! Memory-safe Rust over the Windows `IoRing` submission/completion ring.
3//!
4//! Windows 11 and Server 2022 added `IoRing`, a submission/completion ring for
5//! file I/O closer in shape to `io_uring` than to anything else Windows offers.
6//! This crate raises those primitives into safe Rust. The ring holds each
7//! in-flight operation's buffer on the caller's behalf and hands it back at the
8//! pop that observes its completion, so no buffer the kernel may still be using
9//! is reachable from safe code. The tracking that makes this possible has a
10//! cost, which may allocate: an entry in a hash set for every operation in
11//! flight, and for an owned push an entry in a hash map holding its payload
12//! ([D-78](../DESIGN-NOTES.md#d-78)).
13//!
14//! # Scope: a file data plane, not a general completion backend
15//!
16//! The kernel's operation table is fixed at seven entries -- no-op, read, write,
17//! flush, register-files, register-buffers, and cancel. There is no ioctl
18//! operation, no socket operation, and no directory-change operation, and unlike
19//! Linux's `io_uring` -- which grew to roughly fifty opcodes including full
20//! socket support -- Windows `IoRing` has not grown beyond file I/O.
21//!
22//! So this crate is not a replacement for
23//! [`windows-overlapped-io-sys`](https://docs.rs/windows-overlapped-io-sys),
24//! which remains the crate for arbitrary I/O on arbitrary handles. Use this one
25//! for high-volume file reads and writes; use that one for everything else.
26//! Neither can subsume the other.
27//!
28//! # Availability is a runtime question
29//!
30//! `IoRing` is present from Windows 11 and Server 2022, but three separate facts
31//! about it are decided at runtime rather than at compile time: which ring
32//! version the system supports, whether the ring is a real kernel ring or a
33//! user-mode emulation with no kernel benefit, and whether the completion-event
34//! feature the thread-pool delivery path depends on is available at all. All
35//! three are answerable without creating a ring.
36//!
37//! # Choosing a delivery architecture
38//!
39//! There are two coherent high-performance shapes for consuming completions,
40//! and picking the wrong one costs more than any API detail in this crate.
41//!
42//! **Model A, threadless dispatch -- shared queue, kernel load-balances.** A
43//! pool of threads waits;
44//! work is handed to whichever thread the system picks next. This is
45//! `EventDelivery`: the ring's completion event wired to a thread-pool wait,
46//! so no thread of yours ever blocks on I/O. Load balancing is automatic and
47//! locality is incidental -- start here. A caller that keeps its ring behind
48//! its own lock takes `CompletionWake` instead, which only tells it when to
49//! drain; `EventDelivery` is built on it (D-84). Both live behind the
50//! default-on `threadpool` feature, so a Model B consumer can drop the thread-pool
51//! dependency entirely with `default-features = false` (D-22); it is named
52//! here rather than linked because a link would dangle in that
53//! configuration. See `examples/model_a_delivery.rs` for a full worked
54//! example (M6.2).
55//!
56//! **Model B, dedicated domain thread -- shared-nothing execution domains.**
57//! One pinned thread per
58//! domain, owning its ring, its buffer pool, and its shard of the
59//! application's state, with no cross-thread synchronization on the data
60//! path -- a pinned thread parked directly in [`Batch::submit_and_wait`] *is*
61//! the event loop. This is what `IoRing`'s own API is shaped for: the
62//! submission queue is not thread-safe, registration is per-ring, and there is
63//! exactly one completion event per ring, none of which are limitations to
64//! work around. That fused submit-and-wait is Model B's *usual* wakeup source
65//! rather than its defining one -- see the next paragraph, which is still
66//! Model B.
67//!
68//! **Model B with a multiplexed wakeup.** [`IoRing::completion_event`] hands
69//! back an owned duplicate of the ring's own completion event, so a caller
70//! keeps its ring and still waits on it alongside unrelated handles with
71//! `WaitForMultipleObjects`. This is not a third architecture -- it changes
72//! only what the domain thread blocks on, never who owns, submits, or drains
73//! -- but it is what any consumer mixing ring I/O with non-ring I/O needs,
74//! since `IOSQE_FLAGS_DRAIN_PRECEDING_OPS` orders SQEs against SQEs and
75//! cannot order across the two paths. **The event is edge-triggered on the
76//! completion queue going empty to non-empty**, which is measured rather than
77//! documented by Win32 and hangs rather than merely slows if assumed
78//! otherwise; see that method's docs before using it, and
79//! `examples/model_b_multiplexed.rs` for the whole shape end to end,
80//! including shutdown with I/O still outstanding (M11.6).
81//!
82//! Most real applications want Model B on the hot data path and Model A
83//! everywhere else -- the control plane, background work, cold paths -- where
84//! the thread pool's quiescence is worth more than locality. Both are
85//! first-class here; neither is a degraded form of the other. The full
86//! trade-off -- including why the NUMA node is the wrong key for sizing a
87//! Model B execution domain, and why buffer placement likely dominates thread
88//! placement -- is in "Two delivery architectures" in `DESIGN-NOTES.md`.
89//!
90//! # Durability
91//!
92//! Three facts, all measured rather than documented by Win32, and all of them
93//! things a consumer gets wrong by default. They are stated in full on
94//! [`Batch::flush_owned`], [`FlushCoverage`], [`WriteCaching`] and [`FlushMode`];
95//! this is the summary that stops a reader from never looking.
96//!
97//! 1. **There is no FUA.** `BuildIoRingWriteFile`'s entire flag set is
98//!    `{FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH}`, and
99//!    write-through is a cache directive to the OS, not a device-level
100//!    guarantee -- whether it becomes a Force Unit Access bit depends on the
101//!    driver, the volume, and the device's write-cache setting. Treating a
102//!    completed write-through write as durable is a mistake, not a shortcut.
103//! 2. **The flush operation is the only durability primitive the ring has**,
104//!    and only in a mode that syncs the device -- [`FlushMode::NoSync`]
105//!    deliberately does not.
106//! 3. **A flush without the barrier covers nothing.** An unflagged flush is an
107//!    ordinary operation competing with the writes before it, and it
108//!    frequently wins, so its completion proves nothing about them. This is
109//!    why [`Batch::flush_owned`] requires a [`FlushCoverage`] instead of defaulting:
110//!    the obvious spelling was a silent data-loss bug, invisible until power
111//!    is lost.
112//!
113//! What follows from all three is that **durability is a property of an epoch,
114//! never of an individual write**, because the ring offers no per-write
115//! primitive to make it one: stream the writes, close the epoch with one
116//! covering flush, and wait on the flush rather than on the writes. The
117//! barrier that makes this correct waits for everything outstanding, so the
118//! flush itself is expensive -- but it is **one-sided** and does not hold back
119//! work submitted after it ([`PushOptions::drain_preceding`] documents both
120//! halves). "Durability on the ring" in `DESIGN-NOTES.md` has the full shape
121//! and the ways to pay for it.
122//!
123//! # Topology guidance
124//!
125//! This crate does not partition anything for you (D-8 in `DESIGN-NOTES.md`):
126//! it makes a ring cheap and correct, makes its affinity explicit, and leaves
127//! sizing a Model B execution domain to the caller. Three pointers, not a
128//! partitioning policy:
129//!
130//! - **Size a domain by the outermost cache level that partitions the
131//!   machine, not by NUMA node.** Node count is a firmware setting a process
132//!   cannot see (AMD NPS, Intel Sub-NUMA Clustering), and most real
133//!   deployments are virtualized, where NUMA topology is often invisible
134//!   entirely. A cache partition degrades sanely instead: a machine whose
135//!   caches partition nothing yields one ring, which is correct.
136//!
137//!   **Ask which level partitions; do not filter on `CacheLevel == 3`.**
138//!   `MachineMemoryTopology::outermost_partitioning_cache` is the one
139//!   definition, and it is not a convenience wrapper over that filter: a
140//!   shipping ARM part reports no L3 at all, and a machine can report an L3
141//!   spanning every processor above a real L2 partition -- where filtering on
142//!   level 3 does not even degrade, because it matched something. It returns
143//!   one whole-machine domain and calls it a cache-aware partition. See
144//!   `examples/cache_domains.rs` for a runnable enumeration (M6.3), built on
145//!   the safe wrapper in
146//!   [`windows-topology-sys`](https://docs.rs/windows-topology-sys).
147//! - **Processor groups are a hard floor.** A thread's affinity is a
148//!   `GROUP_AFFINITY` and a ring's waiter lives in exactly one group, so above
149//!   64 logical processors the partition is forced whether or not it is
150//!   wanted.
151//! - **Buffer placement likely dominates thread placement.** A registered
152//!   buffer on a node remote from the device means every byte crosses the
153//!   interconnect, on every operation, forever; where the completion callback
154//!   happens to run is a one-time cache-warmth question by comparison.
155//!   `VirtualAllocExNuma`, on the node closest to the device, registered once
156//!   into that domain's ring (see [`Batch::register_buffers`]), is very
157//!   likely the highest-leverage locality decision available. [`NumaBuffer`]
158//!   is that allocation; *which* node is a question about a caller's storage
159//!   layout, and this crate does not answer it for them.
160//!
161//! # Where the design lives
162//!
163//! The design, including the delivery-architecture guidance this crate exists
164//! to make usable, is recorded in `DESIGN-NOTES.md` beside the source, and the
165//! work still open against it is tracked in `CHECKLIST.md`.
166
167#![warn(missing_docs)]
168
169#[cfg(windows)]
170mod accounting;
171#[cfg(windows)]
172mod batch;
173#[cfg(windows)]
174mod capability;
175#[cfg(all(windows, feature = "threadpool"))]
176mod completion_wake;
177/// This crate's conservation rules, made executable (M16.1).
178///
179/// A `pub mod` rather than a re-export, because the module documentation *is*
180/// the statement of what the oracle does and does not check -- and a consumer
181/// reaching for it needs that, not just the type.
182#[cfg(windows)]
183pub mod contract;
184#[cfg(windows)]
185mod error;
186#[cfg(all(windows, feature = "threadpool"))]
187mod event_delivery;
188// M23.3 SPIKE -- exported so a real consumer can be converted, which is the
189// only way to validate whether one type fits the twelve hand-rolled shapes.
190// Whether it stays public is the decision M23.3 has not yet taken.
191#[cfg(windows)]
192mod ring;
193/// The seam the kernel-response resolver sits under (M26.2).
194///
195/// Private without the `kernel-seam` feature, where it is nothing but
196/// `#[inline(always)]` forwards to the same `windows-sys` calls this crate
197/// made before. With the feature on it additionally publishes
198/// `sys::Responses` and `sys::register` -- a responder for one ring, on any
199/// thread ([D-85](../DESIGN-NOTES.md#d-85)) -- so a test can answer the calls
200/// that carry an operation instead of the kernel.
201/// With `fault-injection` as well it publishes `sys::call_faults` and
202/// `sys::submit_faults`, a safe responder that fails a ring's calls,
203/// completions and submits on demand ([D-86](../DESIGN-NOTES.md#d-86)), for a
204/// consumer that forbids `unsafe`.
205#[cfg(windows)]
206pub mod sys;
207#[cfg(windows)]
208mod token;
209
210#[cfg(windows)]
211pub use batch::{
212    Batch, FileRef, FileTarget, FlushCoverage, FlushMode, PendingBufferRegistration,
213    PendingFileRegistration, PushOptions, RegisteredBuffers, RegisteredFile, RegisteredFiles,
214    RegisteredUse, WriteCaching,
215};
216#[cfg(windows)]
217pub use capability::{Capabilities, RingVersion, capabilities};
218#[cfg(all(windows, feature = "threadpool"))]
219pub use completion_wake::CompletionWake;
220#[cfg(windows)]
221pub use error::{IoRingError, IoRingErrorExt, PushRefused, RingCondition};
222#[cfg(all(windows, feature = "threadpool"))]
223pub use event_delivery::{EventDelivery, RingScope};
224/// The fault-injection seam (M16.3), for exercising failure paths a healthy
225/// machine will not produce on demand. See
226/// [`Completion::with_injected_failure`] for why transforming a real
227/// completion is sound where fabricating one would not be.
228#[cfg(all(windows, any(test, feature = "fault-injection")))]
229pub use ring::InjectedFailure;
230#[cfg(windows)]
231pub use ring::{
232    AllowsRaw, Completion, CompletionWait, HeldCompletion, IoRing, Op, Open, Owned, RingInfo,
233    RingKind, RingWait, SubmitWait,
234};
235#[cfg(windows)]
236pub use token::OperationId;
237/// The file type a safe push takes: a shared owner of a handle, from
238/// `win-shared-os-owned-handle`, re-exported so it can be named without that
239/// crate. It replaced this crate's own `SharedFile` (D-82).
240#[cfg(windows)]
241pub use win_shared_os_owned_handle::SharedHandle;
242// Re-exported rather than defined here: the buffer contract is `io-buffers`', defined once in a
243// portable crate, and re-exporting keeps `windows_ioring_sys::IoBuf`, `IoBufMut` and
244// `RegisteredSpan` resolving for anyone who already bound to them (IB-1.3).
245#[cfg(windows)]
246pub use io_buffers::{IoBuf, IoBufMut, RegisteredSpan};
247// Re-exported rather than defined here: the allocator moved to `win-numa-sys`, and re-exporting
248// keeps `windows_ioring_sys::NumaBuffer` resolving for anyone who already bound to it. Its
249// `IoBuf`/`IoBufMut` impls are that crate's own, of `io-buffers`' traits.
250#[cfg(windows)]
251pub use win_numa_sys::NumaBuffer;
252
253// The crate's markdown documentation is compiled as doctests, so an example that
254// a contract change invalidates breaks the build instead of quietly teaching the
255// old answer. `cfg(doctest)` means these items exist only while rustdoc collects
256// tests, so they cost an ordinary build nothing.
257#[cfg(all(doctest, windows))]
258#[doc = include_str!("../README.md")]
259struct ReadmeDoctests;