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 with the minimum additional
7//! CPU and memory cost: a completion hands the caller's buffer back without the
8//! crate having allocated anything to track it.
9//!
10//! # Scope: a file data plane, not a general completion backend
11//!
12//! The kernel's operation table is fixed at seven entries -- no-op, read, write,
13//! flush, register-files, register-buffers, and cancel. There is no ioctl
14//! operation, no socket operation, and no directory-change operation, and unlike
15//! Linux's `io_uring` -- which grew to roughly fifty opcodes including full
16//! socket support -- Windows `IoRing` has not grown beyond file I/O.
17//!
18//! So this crate is not a replacement for
19//! [`windows-overlapped-io-sys`](https://docs.rs/windows-overlapped-io-sys),
20//! which remains the crate for arbitrary I/O on arbitrary handles. Use this one
21//! for high-volume file reads and writes; use that one for everything else.
22//! Neither can subsume the other.
23//!
24//! # Availability is a runtime question
25//!
26//! `IoRing` is present from Windows 11 and Server 2022, but three separate facts
27//! about it are decided at runtime rather than at compile time: which ring
28//! version the system supports, whether the ring is a real kernel ring or a
29//! user-mode emulation with no kernel benefit, and whether the completion-event
30//! feature the thread-pool delivery path depends on is available at all. All
31//! three are answerable without creating a ring.
32//!
33//! # Choosing a delivery architecture
34//!
35//! There are two coherent high-performance shapes for consuming completions,
36//! and picking the wrong one costs more than any API detail in this crate.
37//!
38//! **Model A -- shared queue, kernel load-balances.** A pool of threads waits;
39//! work is handed to whichever thread the system picks next. This is
40//! `EventDelivery`: the ring's completion event wired to a thread-pool wait,
41//! so no thread of yours ever blocks on I/O. Load balancing is automatic and
42//! locality is incidental -- start here. It lives behind the default-on
43//! `threadpool` feature, so a Model B consumer can drop the thread-pool
44//! dependency entirely with `default-features = false` (D-22); it is named
45//! here rather than linked because a link would dangle in that
46//! configuration. See `examples/model_a_delivery.rs` for a full worked
47//! example (M6.2).
48//!
49//! **Model B -- shared-nothing execution domains.** One pinned thread per
50//! domain, owning its ring, its buffer pool, and its shard of the
51//! application's state, with no cross-thread synchronization on the data
52//! path -- a pinned thread parked directly in [`Batch::submit_and_wait`] *is*
53//! the event loop. This is what `IoRing`'s own API is shaped for: the
54//! submission queue is not thread-safe, registration is per-ring, and there is
55//! exactly one completion event per ring, none of which are limitations to
56//! work around. That fused submit-and-wait is Model B's *usual* wakeup source
57//! rather than its defining one -- see the next paragraph, which is still
58//! Model B.
59//!
60//! **Model B with a multiplexed wakeup.** [`IoRing::completion_event`] hands
61//! back an owned duplicate of the ring's own completion event, so a caller
62//! keeps its ring and still waits on it alongside unrelated handles with
63//! `WaitForMultipleObjects`. This is not a third architecture -- it changes
64//! only what the domain thread blocks on, never who owns, submits, or drains
65//! -- but it is what any consumer mixing ring I/O with non-ring I/O needs,
66//! since `IOSQE_FLAGS_DRAIN_PRECEDING_OPS` orders SQEs against SQEs and
67//! cannot order across the two paths. **The event is edge-triggered on the
68//! completion queue going empty to non-empty**, which is measured rather than
69//! documented by Win32 and hangs rather than merely slows if assumed
70//! otherwise; see that method's docs before using it, and
71//! `examples/model_b_multiplexed.rs` for the whole shape end to end,
72//! including shutdown with I/O still outstanding (M11.6).
73//!
74//! Most real applications want Model B on the hot data path and Model A
75//! everywhere else -- the control plane, background work, cold paths -- where
76//! the thread pool's quiescence is worth more than locality. Both are
77//! first-class here; neither is a degraded form of the other. The full
78//! trade-off -- including why the NUMA node is the wrong key for sizing a
79//! Model B execution domain, and why buffer placement likely dominates thread
80//! placement -- is in "Two delivery architectures" in `DESIGN-NOTES.md`.
81//!
82//! # Durability
83//!
84//! Three facts, all measured rather than documented by Win32, and all of them
85//! things a consumer gets wrong by default. They are stated in full on
86//! [`Batch::flush`], [`FlushCoverage`], [`WriteCaching`] and [`FlushMode`];
87//! this is the summary that stops a reader from never looking.
88//!
89//! 1. **There is no FUA.** `BuildIoRingWriteFile`'s entire flag set is
90//!    `{FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH}`, and
91//!    write-through is a cache directive to the OS, not a device-level
92//!    guarantee -- whether it becomes a Force Unit Access bit depends on the
93//!    driver, the volume, and the device's write-cache setting. Treating a
94//!    completed write-through write as durable is a mistake, not a shortcut.
95//! 2. **The flush operation is the only durability primitive the ring has**,
96//!    and only in a mode that syncs the device -- [`FlushMode::NoSync`]
97//!    deliberately does not.
98//! 3. **A flush without the barrier covers nothing.** An unflagged flush is an
99//!    ordinary operation competing with the writes before it, and it
100//!    frequently wins, so its completion proves nothing about them. This is
101//!    why [`Batch::flush`] requires a [`FlushCoverage`] instead of defaulting:
102//!    the obvious spelling was a silent data-loss bug, invisible until power
103//!    is lost.
104//!
105//! What follows from all three is that **durability is a property of an epoch,
106//! never of an individual write**, because the ring offers no per-write
107//! primitive to make it one: stream the writes, close the epoch with one
108//! covering flush, and wait on the flush rather than on the writes. The
109//! barrier that makes this correct is also a ring-wide stall
110//! ([`PushOptions::drain_preceding`] documents its reach), so the correct
111//! construction is also the expensive one. "Durability on the ring" in
112//! `DESIGN-NOTES.md` has the full shape and the three ways to pay for it.
113//!
114//! # Topology guidance
115//!
116//! This crate does not partition anything for you (D-8 in `DESIGN-NOTES.md`):
117//! it makes a ring cheap and correct, makes its affinity explicit, and leaves
118//! sizing a Model B execution domain to the caller. Three pointers, not a
119//! partitioning policy:
120//!
121//! - **Size a domain by last-level (L3) cache, not by NUMA node.** Node count
122//!   is a firmware setting a process cannot see (AMD NPS, Intel Sub-NUMA
123//!   Clustering), and most real deployments are virtualized, where NUMA
124//!   topology is often invisible entirely. `GetLogicalProcessorInformationEx`
125//!   filtered to `RelationCache` / `CacheLevel == 3` degrades sanely instead:
126//!   one reported domain on a VM is correct. See `examples/l3_domains.rs` for
127//!   a runnable enumeration (M6.3), built on the safe wrapper in
128//!   [`windows-topology-sys`](https://docs.rs/windows-topology-sys).
129//! - **Processor groups are a hard floor.** A thread's affinity is a
130//!   `GROUP_AFFINITY` and a ring's waiter lives in exactly one group, so above
131//!   64 logical processors the partition is forced whether or not it is
132//!   wanted.
133//! - **Buffer placement likely dominates thread placement.** A registered
134//!   buffer on a node remote from the device means every byte crosses the
135//!   interconnect, on every operation, forever; where the completion callback
136//!   happens to run is a one-time cache-warmth question by comparison.
137//!   `VirtualAllocExNuma`, on the node closest to the device, registered once
138//!   into that domain's ring (see [`Batch::register_buffers`]), is very
139//!   likely the highest-leverage locality decision available.
140//!
141//! # Status
142//!
143//! Under construction. The design, including the delivery-architecture guidance
144//! this crate exists to make usable, is recorded in `DESIGN-NOTES.md` beside the
145//! source; the build-out is tracked in `CHECKLIST.md`.
146
147#![warn(missing_docs)]
148
149#[cfg(windows)]
150mod batch;
151#[cfg(windows)]
152mod buf;
153#[cfg(windows)]
154mod capability;
155/// This crate's conservation rules, made executable (M16.1).
156///
157/// A `pub mod` rather than a re-export, because the module documentation *is*
158/// the statement of what the oracle does and does not check -- and a consumer
159/// reaching for it needs that, not just the type.
160#[cfg(windows)]
161pub mod contract;
162#[cfg(windows)]
163mod error;
164#[cfg(all(windows, feature = "threadpool"))]
165mod event_delivery;
166#[cfg(windows)]
167mod ring;
168#[cfg(windows)]
169mod token;
170
171#[cfg(windows)]
172pub use batch::{
173    Batch, FileRef, FileTarget, FlushCoverage, FlushMode, PendingBufferRegistration,
174    PendingFileRegistration, PushOptions, RegisteredBuffers, RegisteredFile, RegisteredFiles,
175    RegisteredSpan, RegisteredUse, SharedFile, WriteCaching,
176};
177#[cfg(windows)]
178pub use buf::{IoBuf, IoBufMut};
179#[cfg(windows)]
180pub use capability::{Capabilities, RingVersion, capabilities};
181#[cfg(windows)]
182pub use error::{IoRingError, IoRingErrorExt, RingCondition};
183#[cfg(all(windows, feature = "threadpool"))]
184pub use event_delivery::{EventDelivery, RingScope};
185/// The fault-injection seam (M16.3), for exercising failure paths a healthy
186/// machine will not produce on demand. See
187/// [`Completion::with_injected_failure`] for why transforming a real
188/// completion is sound where fabricating one would not be.
189#[cfg(all(windows, any(test, feature = "fault-injection")))]
190pub use ring::InjectedFailure;
191#[cfg(windows)]
192pub use ring::{Completion, IoRing, Op, RingInfo};
193#[cfg(windows)]
194pub use token::Token;