Skip to main content

windows_ioring_sys/
batch.rs

1// Copyright (c) 2026 Mike Grier
2//! `Batch`: a scoped, exclusive submission window (M3.1-M3.3, M3.5, M5).
3
4use std::ffi::c_void;
5use std::io;
6use std::mem::ManuallyDrop;
7use std::os::windows::io::{AsRawHandle, OwnedHandle};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicUsize, Ordering};
10
11use windows_sys::Win32::Foundation::HANDLE;
12use windows_sys::Win32::Storage::FileSystem::{
13    BuildIoRingCancelRequest, BuildIoRingFlushFile, BuildIoRingReadFile,
14    BuildIoRingRegisterBuffers, BuildIoRingRegisterFileHandles, BuildIoRingWriteFile,
15    FILE_FLUSH_DATA, FILE_FLUSH_DEFAULT, FILE_FLUSH_MIN_METADATA, FILE_FLUSH_MODE,
16    FILE_FLUSH_NO_SYNC, FILE_WRITE_FLAGS, FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH,
17    IORING_BUFFER_INFO, IORING_BUFFER_REF, IORING_BUFFER_REF_0, IORING_HANDLE_REF,
18    IORING_HANDLE_REF_0, IORING_REF_RAW, IORING_REF_REGISTERED, IORING_REGISTERED_BUFFER,
19    IORING_SQE_FLAGS, IOSQE_FLAGS_DRAIN_PRECEDING_OPS, IOSQE_FLAGS_NONE, SubmitIoRing,
20};
21
22use crate::buf::{IoBuf, IoBufMut};
23use crate::error::check;
24use crate::ring::{Completion, IoRing, Op, RingId};
25use crate::token::Token;
26
27/// Per-push options shared across every op builder (M3.2).
28#[derive(Clone, Copy, Debug, Default)]
29#[must_use]
30pub struct PushOptions {
31    drain_preceding: bool,
32}
33
34impl PushOptions {
35    /// The default options: no barrier.
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Set `IOSQE_FLAGS_DRAIN_PRECEDING_OPS`: this op does not start until
41    /// every op already queued on this batch's ring has completed. A
42    /// barrier, not a cheap tag -- it forces the ring to drain before
43    /// continuing.
44    ///
45    /// # Ring-wide, and it spans submissions
46    ///
47    /// Measured, not inferred (D-24 in `DESIGN-NOTES.md`). The barrier
48    /// reaches every operation outstanding on the *ring*, not only the ones
49    /// queued in this batch, and it holds back ops pushed after it even when
50    /// they target an entirely different file -- which rules out
51    /// filesystem-level serialization as the explanation. Results were
52    /// identical whether the sequence went in one [`Batch::submit`] or three.
53    ///
54    /// The consequence to plan for: **cross-epoch pipelining through a single
55    /// ring is not available.** A consumer that closes an epoch with a
56    /// drained flush stalls that whole ring for the flush's duration, so the
57    /// way to overlap epochs is more rings, not more batches.
58    ///
59    /// # The barrier stops at the ring's edge
60    ///
61    /// It orders SQEs against SQEs, and nothing else. Any non-ring I/O -- an
62    /// overlapped `DeviceIoControl`, anything issued through
63    /// `windows-overlapped-io-sys`, a plain blocking write -- is outside it in
64    /// *both* directions: this flag can neither make a ring op wait for a
65    /// non-ring op nor make a non-ring op wait for ring ops.
66    ///
67    /// A consumer mixing both paths is the normal case rather than an exotic
68    /// one, and it must enforce that ordering in its own code. What this
69    /// crate offers toward it is [`IoRing::completion_event`], which makes
70    /// waiting on the ring *alongside* other handles expressible without
71    /// giving up the ring or blocking a thread in a drain.
72    ///
73    /// # Ordering, never durability by itself
74    ///
75    /// This flag orders operations; it does not flush anything. It is,
76    /// however, what makes a flush cover the writes before it: a flush pushed
77    /// after a batch of writes *without* this flag routinely completes while
78    /// many of those writes are still outstanding (D-23 in `DESIGN-NOTES.md`,
79    /// measured at 17 and 23 of 32 writes finishing after the flush did). So
80    /// the obvious spelling -- push the writes, then push a flush -- silently
81    /// does not make those writes durable.
82    ///
83    /// A flush does not take its barrier decision from here, for exactly that
84    /// reason: [`Batch::flush`] requires a [`FlushCoverage`] instead, so the
85    /// choice cannot be inherited from a default (M12.1, D-25).
86    pub fn drain_preceding(mut self, drain: bool) -> Self {
87        self.drain_preceding = drain;
88        self
89    }
90
91    fn sqe_flags(self) -> IORING_SQE_FLAGS {
92        if self.drain_preceding {
93            IOSQE_FLAGS_DRAIN_PRECEDING_OPS
94        } else {
95            IOSQE_FLAGS_NONE
96        }
97    }
98}
99
100/// Whether a flush covers the operations queued before it (M12.1, D-23 in
101/// `DESIGN-NOTES.md`).
102///
103/// This is a **required argument** on [`Batch::flush`] and
104/// [`Batch::flush_raw`] rather than a field of [`PushOptions`], because there
105/// is no defensible default. An unflagged flush does *not* cover preceding
106/// writes: it is an ordinary operation competing with them, and it frequently
107/// wins. A default would make the obvious spelling -- push the writes, then
108/// push a flush -- a silent data-loss bug rather than a missing feature, so
109/// the decision is taken at the call site instead (D-25).
110///
111/// It is an enum rather than a `bool` for the same reason: `flush(&file,
112/// true)` does not say what the `true` decides, and this is not a parameter
113/// anyone should have to look up.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum FlushCoverage {
116    /// Wait for every operation already outstanding on the ring, then flush.
117    ///
118    /// This is what makes preceding writes durable when the flush's
119    /// completion is observed, and it is what a caller closing an epoch
120    /// wants.
121    ///
122    /// It sets `IOSQE_FLAGS_DRAIN_PRECEDING_OPS`, which is a **ring-wide**
123    /// barrier rather than a per-file one (D-24, measured): operations pushed
124    /// after it are held until it completes even when they target unrelated
125    /// files, and its reach is every operation outstanding on the ring, not
126    /// only the ones in this batch. The whole ring stalls for the flush's
127    /// duration. That cost is real, and it is the only reason the other
128    /// variant exists.
129    CoversPrecedingOperations,
130    /// Queue the flush with no barrier: it may start, and complete, while
131    /// writes pushed before it are still in flight.
132    ///
133    /// **Almost never what a caller wants.** Its completion proves nothing
134    /// about any preceding write, so using it to close an epoch loses data
135    /// that the caller believes is committed -- invisibly, until power is
136    /// lost.
137    ///
138    /// Two uses are legitimate. One is *host sequencing*: the caller has
139    /// already observed the completions of every write in the epoch before
140    /// pushing this, so the ordering is established outside the ring and the
141    /// barrier would only add a stall. The other is a flush that is not being
142    /// used for durability at all.
143    Unordered,
144}
145
146impl FlushCoverage {
147    fn sqe_flags(self) -> IORING_SQE_FLAGS {
148        match self {
149            Self::CoversPrecedingOperations => IOSQE_FLAGS_DRAIN_PRECEDING_OPS,
150            Self::Unordered => IOSQE_FLAGS_NONE,
151        }
152    }
153}
154
155/// Where a write is allowed to stop: in the system cache, or past it (M12.3,
156/// D-25 in `DESIGN-NOTES.md`).
157///
158/// This is `BuildIoRingWriteFile`'s `FILE_WRITE_FLAGS` parameter, which the
159/// crate previously hardcoded to `FILE_WRITE_FLAGS_NONE` -- so a consumer
160/// reading this API saw ordering but no way to express anything about caching
161/// at all, and could reasonably conclude the ring did not expose it. It does;
162/// the crate was narrowing the platform to what its own examples needed.
163///
164/// # What write-through is, and what it is not
165///
166/// Worth stating precisely, because conflating these produced a wrong
167/// recommendation in the exchange that prompted this work.
168///
169/// It **is** a first-level cache directive: it tells the OS not to leave the
170/// data sitting in the system cache. Its practical value is latency shaping --
171/// data already at the device makes a later flush shorter, which can matter
172/// when the flush is on a commit path.
173///
174/// It is **not** a durability guarantee, and it is **not** FUA. Whether it
175/// becomes a Force Unit Access bit on the underlying command depends on the
176/// driver, the volume, and whether the device's write cache is enabled -- none
177/// of which this API can see or promise. A write that completes with
178/// [`WriteCaching::WriteThrough`] may still be sitting in a volatile device
179/// cache, and will be lost on power failure like any other.
180///
181/// **The flush operation is the only durability primitive the ring has**, and
182/// only with [`FlushCoverage::CoversPrecedingOperations`]. Reaching for
183/// write-through instead of a covering flush is the mistake this doc exists to
184/// prevent; reaching for it *as well* is a legitimate latency optimization.
185#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
186pub enum WriteCaching {
187    /// `FILE_WRITE_FLAGS_NONE`: the write may be satisfied into the system
188    /// cache, and reaching the device is a later flush's job.
189    ///
190    /// The default, and the right choice for the streaming writes of an epoch
191    /// that a covering flush will close.
192    #[default]
193    Cached,
194    /// `FILE_WRITE_FLAGS_WRITE_THROUGH`: ask the OS not to leave the data in
195    /// the system cache.
196    ///
197    /// A latency-shaping knob, not a durability marker -- see this type's
198    /// docs. It does not remove the need for a flush.
199    WriteThrough,
200}
201
202impl WriteCaching {
203    fn raw(self) -> FILE_WRITE_FLAGS {
204        match self {
205            Self::Cached => FILE_WRITE_FLAGS_NONE,
206            Self::WriteThrough => FILE_WRITE_FLAGS_WRITE_THROUGH,
207        }
208    }
209}
210
211/// How much a flush is asked to push, and whether it syncs the device at all
212/// (M12.4, D-25 in `DESIGN-NOTES.md`).
213///
214/// This is `BuildIoRingFlushFile`'s `FILE_FLUSH_MODE` parameter, which the
215/// crate previously hardcoded to `FILE_FLUSH_DEFAULT`. Exposing it is the
216/// other half of D-25: the kernel offers a durability parameter here and a
217/// wrapper that hides it narrows the platform to what its own examples needed.
218///
219/// [`FlushMode::Default`] is what a caller closing an epoch wants, and is
220/// what the crate did before this existed.
221///
222/// # `NoSync` is not a durability operation
223///
224/// Three of these four modes issue the device sync; [`FlushMode::NoSync`] does
225/// not. That is worth stating rather than inferring, and the inference runs
226/// the other way too: **the existence of a distinct "no sync" mode is the
227/// evidence that the other three do sync.** Nothing in the Win32
228/// documentation says so directly.
229#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
230pub enum FlushMode {
231    /// `FILE_FLUSH_DEFAULT`: flush the file's data and metadata, and sync the
232    /// device.
233    ///
234    /// The default, and the mode a durability barrier wants. Pair it with
235    /// [`FlushCoverage::CoversPrecedingOperations`] or it covers nothing.
236    #[default]
237    Default,
238    /// `FILE_FLUSH_DATA`: flush the file's data, and sync the device.
239    ///
240    /// Metadata that is not required to read the data back may be left
241    /// unflushed. The analogue of `fdatasync`, and the usual choice for a log
242    /// whose records are self-describing.
243    Data,
244    /// `FILE_FLUSH_MIN_METADATA`: flush the data plus the minimum metadata
245    /// needed to retrieve it, and sync the device.
246    MinMetadata,
247    /// `FILE_FLUSH_NO_SYNC`: flush to the device **without** issuing the sync.
248    ///
249    /// **This is the one mode that makes nothing durable.** It pushes data out
250    /// of the system cache and stops there, so anything still held in a
251    /// volatile device cache is lost on power failure exactly as if no flush
252    /// had been issued at all. A completion from this mode is not a commit
253    /// point and must never be reported to a caller as one.
254    ///
255    /// It is useful for shaping when cache pressure is paid -- moving data
256    /// deviceward early so a later real flush is short -- which is the same
257    /// role [`WriteCaching::WriteThrough`] plays on the write side, and it
258    /// carries the same warning: a latency knob, not a durability primitive.
259    NoSync,
260}
261
262impl FlushMode {
263    fn raw(self) -> FILE_FLUSH_MODE {
264        match self {
265            Self::Default => FILE_FLUSH_DEFAULT,
266            Self::Data => FILE_FLUSH_DATA,
267            Self::MinMetadata => FILE_FLUSH_MIN_METADATA,
268            Self::NoSync => FILE_FLUSH_NO_SYNC,
269        }
270    }
271}
272
273/// Build the raw handle reference for `file`, refusing a [`RegisteredFile`]
274/// that did not come from `ring_id`'s own ring (PR #20 review response): the
275/// index alone names a slot in *some* ring's file table, and a slot from a
276/// different ring can legitimately hold an entirely different file, so
277/// crossing rings must be an explicit, reported error rather than silently
278/// addressing the wrong handle.
279fn handle_ref(file: FileRef, ring_id: RingId) -> io::Result<IORING_HANDLE_REF> {
280    match file {
281        FileRef::Raw(handle) => Ok(IORING_HANDLE_REF {
282            Kind: IORING_REF_RAW,
283            Handle: IORING_HANDLE_REF_0 { Handle: handle },
284        }),
285        FileRef::Registered(file) => {
286            if file.ring_id != ring_id {
287                return Err(io::Error::new(
288                    io::ErrorKind::InvalidInput,
289                    "this RegisteredFile was registered on a different IoRing",
290                ));
291            }
292            Ok(IORING_HANDLE_REF {
293                Kind: IORING_REF_REGISTERED,
294                Handle: IORING_HANDLE_REF_0 {
295                    Index: file.index(),
296                },
297            })
298        }
299    }
300}
301
302fn raw_buffer_ref(address: *mut c_void) -> IORING_BUFFER_REF {
303    IORING_BUFFER_REF {
304        Kind: IORING_REF_RAW,
305        Buffer: IORING_BUFFER_REF_0 { Address: address },
306    }
307}
308
309fn registered_buffer_ref(index: u32, offset: u32) -> IORING_BUFFER_REF {
310    IORING_BUFFER_REF {
311        Kind: IORING_REF_REGISTERED,
312        Buffer: IORING_BUFFER_REF_0 {
313            IndexAndOffset: IORING_REGISTERED_BUFFER {
314                BufferIndex: index,
315                Offset: offset,
316            },
317        },
318    }
319}
320
321fn checked_len(len: usize) -> io::Result<u32> {
322    u32::try_from(len).map_err(|_| {
323        io::Error::new(
324            io::ErrorKind::InvalidInput,
325            format!("an IoRing buffer is limited to u32::MAX bytes; {len} does not fit"),
326        )
327    })
328}
329
330/// A file, addressed either by a raw `HANDLE` or by the index a prior
331/// [`Batch::register_files`] assigned it (M5.1).
332///
333/// This enum is the `_raw` pushes' addressing parameter. A safe push takes a
334/// [`FileTarget`] instead -- a [`SharedFile`] or a [`RegisteredFile`] -- so
335/// reaching a registered file has not required `unsafe` since M10.4 (D-29,
336/// D-33). Constructing a `FileRef` directly is only necessary for the `_raw`
337/// pushes, whose reason to exist is a bare `HANDLE` this crate cannot keep
338/// alive for you.
339///
340/// A distinct type per addressing mode -- rather than one method accepting
341/// either a `HANDLE` or a bare `u32` -- is what makes "read against a
342/// registered file" and "read against an unregistered one" impossible to
343/// confuse by accident: a [`RegisteredFile`] cannot be mistaken for a raw
344/// `HANDLE`, or vice versa, because they are different types.
345#[derive(Clone, Copy, Debug)]
346pub enum FileRef {
347    /// `IORING_REF_RAW`: the caller's own open handle.
348    Raw(HANDLE),
349    /// `IORING_REF_REGISTERED`: an index from [`Batch::register_files`].
350    Registered(RegisteredFile),
351}
352
353impl From<HANDLE> for FileRef {
354    fn from(handle: HANDLE) -> Self {
355        FileRef::Raw(handle)
356    }
357}
358
359impl From<RegisteredFile> for FileRef {
360    fn from(file: RegisteredFile) -> Self {
361        FileRef::Registered(file)
362    }
363}
364
365/// A file handle this crate can read/write through without the caller
366/// having to prove it outlives every operation pushed against it (M8, PR
367/// #20 review response).
368///
369/// Backed by `Arc<OwnedHandle>` rather than [`Token`]'s exclusive-ownership
370/// shape: unlike a buffer, one handle is legitimately the target of many
371/// concurrent pushes, so what must survive until every one of them
372/// completes is a *reference*, not sole ownership. Every safe push method
373/// (e.g. [`Batch::read`], as opposed to its `_raw` sibling) clones this
374/// `Arc` into the same [`Token`] that already tracks the operation's own
375/// payload, so the underlying handle survives until that token is claimed
376/// or leaked (D-4 in `DESIGN-NOTES.md`), regardless of what the caller does
377/// with its own clone.
378#[derive(Clone, Debug)]
379pub struct SharedFile(Arc<OwnedHandle>);
380
381impl SharedFile {
382    /// Wrap an owned handle so it can be read/written through the ring
383    /// safely.
384    #[must_use]
385    pub fn new(handle: OwnedHandle) -> Self {
386        Self(Arc::new(handle))
387    }
388
389    fn raw_handle(&self) -> HANDLE {
390        self.0.as_raw_handle()
391    }
392}
393
394impl From<OwnedHandle> for SharedFile {
395    fn from(handle: OwnedHandle) -> Self {
396        Self::new(handle)
397    }
398}
399
400/// One index a [`Batch::register_files`] registration assigned (M5.1).
401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
402pub struct RegisteredFile {
403    index: u32,
404    ring_id: RingId,
405}
406
407impl RegisteredFile {
408    /// The raw registered-file index.
409    #[must_use]
410    pub fn index(self) -> u32 {
411        self.index
412    }
413}
414
415mod sealed {
416    pub trait Sealed {}
417    impl Sealed for super::SharedFile {}
418    impl Sealed for super::RegisteredFile {}
419}
420
421/// A file a *safe* push can address: either a [`SharedFile`] or a
422/// [`RegisteredFile`] (M10.4; D-29 and D-33 in `DESIGN-NOTES.md`).
423///
424/// This exists because the two carry different lifetime obligations, and
425/// exactly one of them has none. A `SharedFile` is a raw `HANDLE` underneath,
426/// so something must keep it open until the kernel is finished; a
427/// `RegisteredFile` is an index into a table the ring itself owns, minted by
428/// this crate and checked against the minting ring, so there is nothing for a
429/// caller to keep alive. Both are therefore safe to push, and neither needs
430/// `unsafe` -- which is why [`Batch::read`] and its siblings are generic over
431/// this trait rather than hardcoding `SharedFile`.
432///
433/// [`Guard`](FileTarget::Guard) is what the returned [`Token`] holds until the
434/// operation's completion is observed. For `SharedFile` that is a clone of its
435/// `Arc`, which is what makes the handle outlive the operation; for
436/// `RegisteredFile` there is nothing to keep alive, so it is the (`Copy`)
437/// index itself, handed back for symmetry rather than out of necessity.
438///
439/// **Sealed**: this crate implements it for exactly those two types and no
440/// others. An outside implementation could return an arbitrary raw `HANDLE`
441/// from [`as_file_ref`](FileTarget::as_file_ref) with no guard keeping it
442/// alive, which is precisely the unsoundness the `_raw` pushes make a caller
443/// take responsibility for with `unsafe`.
444pub trait FileTarget: sealed::Sealed {
445    /// What the operation's [`Token`] must hold until its completion is
446    /// observed.
447    type Guard: Send + 'static;
448
449    /// How this target addresses its file.
450    fn as_file_ref(&self) -> FileRef;
451
452    /// Produce the value the [`Token`] will hold for the operation's
453    /// duration.
454    fn guard(&self) -> Self::Guard;
455}
456
457impl FileTarget for SharedFile {
458    type Guard = SharedFile;
459
460    fn as_file_ref(&self) -> FileRef {
461        FileRef::Raw(self.raw_handle())
462    }
463
464    fn guard(&self) -> Self::Guard {
465        self.clone()
466    }
467}
468
469impl FileTarget for RegisteredFile {
470    type Guard = RegisteredFile;
471
472    fn as_file_ref(&self) -> FileRef {
473        FileRef::Registered(*self)
474    }
475
476    fn guard(&self) -> Self::Guard {
477        *self
478    }
479}
480
481/// The confirmed result of a [`Batch::register_files`] push: `count`
482/// contiguous indices starting at `base_index`.
483///
484/// `base_index` is currently always zero, because a ring accepts at most one
485/// registration that assigns an index -- do not read its presence as evidence
486/// that several registrations compose. It is kept rather than folded away
487/// because it is the correct shape if that rule is ever relaxed (M10.3, D-31).
488#[derive(Clone, Copy, Debug, PartialEq, Eq)]
489pub struct RegisteredFiles {
490    base_index: u32,
491    count: u32,
492    ring_id: RingId,
493}
494
495impl RegisteredFiles {
496    /// How many handles this registration covers.
497    #[must_use]
498    pub fn len(&self) -> u32 {
499        self.count
500    }
501
502    /// Whether this registration covers no handles.
503    #[must_use]
504    pub fn is_empty(&self) -> bool {
505        self.count == 0
506    }
507
508    /// The [`RegisteredFile`] for the `i`-th handle passed to
509    /// [`Batch::register_files`], or `None` if `i` is out of range.
510    #[must_use]
511    pub fn get(&self, i: u32) -> Option<RegisteredFile> {
512        (i < self.count).then(|| RegisteredFile {
513            index: self.base_index + i,
514            ring_id: self.ring_id,
515        })
516    }
517}
518
519/// A [`Batch::register_files`] push not yet matched to its completion.
520#[derive(Debug)]
521pub struct PendingFileRegistration {
522    user_data: usize,
523    base_index: u32,
524    count: u32,
525    ring_id: RingId,
526}
527
528impl PendingFileRegistration {
529    /// This push's `UserData` identity, to match against a popped
530    /// [`Completion`].
531    #[must_use]
532    pub fn user_data(&self) -> usize {
533        self.user_data
534    }
535
536    /// Turn this pending registration into usable [`RegisteredFiles`] once
537    /// `completion` names it, or hand it back unchanged if `completion`
538    /// names a different operation.
539    ///
540    /// # Errors
541    ///
542    /// The inner `Result` is `Err` if the registration itself failed --
543    /// nothing is lost, since no owned resource was ever handed over for
544    /// this op (M5.1, unlike [`Token`]).
545    pub fn claim_if(self, completion: &Completion) -> Result<io::Result<RegisteredFiles>, Self> {
546        if completion.user_data() != self.user_data || completion.ring_id() != self.ring_id {
547            return Err(self);
548        }
549        Ok(completion.result().map(|_| RegisteredFiles {
550            base_index: self.base_index,
551            count: self.count,
552            ring_id: self.ring_id,
553        }))
554    }
555}
556
557/// Per-buffer state a registration keeps for its whole life.
558///
559/// The registered length is captured at registration time and never
560/// recomputed. That matters because `IoBuf::bytes_len` reports the buffer's
561/// *current* length, and what the kernel holds is the length it was given --
562/// validating a span against the former would let a buffer that had grown
563/// address memory the kernel does not know about.
564struct BufferState {
565    /// Operations outstanding against this one buffer, in either direction.
566    outstanding: AtomicUsize,
567    /// Of those, the ones the kernel **writes into** this buffer -- that is,
568    /// reads. Always a subset of `outstanding`.
569    ///
570    /// Tracked separately because the two hazards are not the same. Mutating a
571    /// buffer races the kernel whichever way the kernel is touching it, so
572    /// [`RegisteredBuffers::get_mut`] refuses on `outstanding`. *Reading* a
573    /// buffer only races a kernel that is writing into it: a shared read
574    /// alongside the kernel's own read of an in-flight write is not a race at
575    /// all, and refusing it would deny a sound operation.
576    kernel_writes: AtomicUsize,
577    /// The length handed to `BuildIoRingRegisterBuffers`, in bytes.
578    registered_len: u32,
579}
580
581/// Which way the kernel touches a registered buffer for the life of an
582/// operation.
583///
584/// Named from the kernel's point of view rather than the operation's, because
585/// that is the direction the data-race question actually turns on, and
586/// "a read means the kernel writes" is exactly the inversion that makes this
587/// easy to get backwards.
588#[derive(Clone, Copy, PartialEq, Eq)]
589enum KernelAccess {
590    /// A read: the kernel writes into the buffer.
591    WritesBuffer,
592    /// A write: the kernel reads out of the buffer.
593    ReadsBuffer,
594}
595
596/// A marker a [`Token`] owns while a registered-buffer-indexed read or write
597/// is outstanding (M5.2, M5.3).
598///
599/// Decrements [`RegisteredBuffers`]'s count **for the one buffer the
600/// operation addressed** only when actually claimed: dropping an unclaimed
601/// token forgets this like any other value a `Token` holds, which correctly
602/// leaves the registration believing that use is still outstanding, since
603/// nothing proved otherwise.
604///
605/// The count is per buffer rather than per registration (M13.2) so that
606/// [`RegisteredBuffers::get_mut`] can hand out a `&mut` to a *quiet* buffer
607/// while operations are still in flight against its neighbours -- which is
608/// what an arena a caller refills has to be able to do.
609pub struct RegisteredUse {
610    state: Arc<[BufferState]>,
611    index: usize,
612    access: KernelAccess,
613}
614
615impl Drop for RegisteredUse {
616    fn drop(&mut self) {
617        if let Some(state) = self.state.get(self.index) {
618            if self.access == KernelAccess::WritesBuffer {
619                state.kernel_writes.fetch_sub(1, Ordering::SeqCst);
620            }
621            state.outstanding.fetch_sub(1, Ordering::SeqCst);
622        }
623    }
624}
625
626/// Buffers registered with a ring so later reads and writes can address them
627/// by index instead of handing over a fresh owned buffer each call (M5.2).
628///
629/// Win32's `IoRing` has no unregister call at all: once registered, an
630/// index is valid for the ring's remaining life. This type's own job is
631/// narrower than reproducing that -- refusing to let its *backing memory*
632/// be freed while this crate still has an operation outstanding against it
633/// (M5.3) -- not proving an index will never be touched again by some
634/// later, differently-built SQE. That wider hazard belongs to
635/// [`crate::IoRing::push_raw`]'s existing `unsafe` contract, the same as any
636/// raw use of an index this crate handed out.
637pub struct RegisteredBuffers<B: IoBufMut> {
638    buffers: ManuallyDrop<Vec<B>>,
639    base_index: u32,
640    /// One entry per registered buffer, indexed by position within this
641    /// registration.
642    state: Arc<[BufferState]>,
643    ring_id: RingId,
644}
645
646impl<B: IoBufMut> RegisteredBuffers<B> {
647    /// How many buffers this registration holds.
648    #[must_use]
649    pub fn len(&self) -> u32 {
650        u32::try_from(self.buffers.len()).unwrap_or(u32::MAX)
651    }
652
653    /// Whether this registration holds no buffers.
654    #[must_use]
655    pub fn is_empty(&self) -> bool {
656        self.buffers.is_empty()
657    }
658
659    /// Borrow the `i`-th buffer's registered bytes, for reading a completed
660    /// read's or write's bytes without going through the ring at all.
661    ///
662    /// # Why it can refuse
663    ///
664    /// Because otherwise it is a data race with the kernel, reachable from
665    /// entirely safe code. [`Batch::read_registered`] takes
666    /// `&RegisteredBuffers` and returns a [`Token`] holding only a
667    /// [`RegisteredUse`] -- no borrow of this registration outlives the push.
668    /// So without the check below, this sequence compiles with no `unsafe`
669    /// anywhere and reads memory the kernel is concurrently writing:
670    ///
671    /// ```ignore
672    /// batch.read_registered(&file, &arena, span, 0, PushOptions::new())?;
673    /// batch.submit()?;                 // kernel now filling that buffer
674    /// let bytes = arena.get(span.buffer_index)?;
675    /// ```
676    ///
677    /// The reverse order is the one the check alone could not stop, and is why
678    /// this takes `&mut self` -- see below:
679    ///
680    /// ```ignore
681    /// let bytes = arena.get(0)?;       // quiet here, so the check passes
682    /// batch.read_registered(&file, &arena, span, 0, PushOptions::new())?;
683    /// batch.submit()?;                 // kernel now filling that same buffer
684    /// let stale = bytes[0];            // ... read through the live borrow
685    /// ```
686    ///
687    /// # Why this takes `&mut self` to hand back a shared slice
688    ///
689    /// The refusal above is a check at one instant, but the slice it returns
690    /// lives as long as the borrow. Taking `&self` made those two different
691    /// lengths of time, and the gap was reachable: `read_registered` takes the
692    /// registration by *shared* reference, so a caller could take the borrow
693    /// while the buffer was quiet, then push and submit a read into that same
694    /// buffer and keep reading through the still-live slice. Measured, not
695    /// argued -- a probe watched the bytes change under a live `&[u8]` while a
696    /// fresh `get` at that same instant correctly refused.
697    ///
698    /// `&mut self` closes it by making the *borrow* conflict with starting the
699    /// operation, not merely its creation: `read_registered(&arena, ..)` cannot
700    /// be called while this slice is alive. That costs nothing real, because
701    /// **no caller needs to read a buffer during the window it is refused**.
702    /// While a read is in flight the bytes are indeterminate -- partially
703    /// written, in arbitrary order -- and only become meaningful once the
704    /// completion is observed. Earlier or later is always available.
705    ///
706    /// It does not restrict an arena. A [`Token`] holds a [`RegisteredUse`],
707    /// not a borrow of this registration, so the shared borrow ends when the
708    /// push returns; a caller may still read or fill *quiet* buffers while
709    /// operations are outstanding against their neighbours, which is what makes
710    /// a registered arena usable at all.
711    ///
712    /// # Why it refuses less than [`get_mut`] does
713    ///
714    /// `get_mut` refuses while *any* operation is outstanding, because
715    /// mutating races the kernel whichever way the kernel is touching the
716    /// buffer. Reading is narrower: it races only a kernel that is **writing
717    /// into** the buffer, so this refuses only while a *read* is outstanding --
718    /// the bytes of a write in flight are stable, and the kernel is reading
719    /// them too.
720    ///
721    /// A count returns to zero only when a [`Token`] is claimed against a real
722    /// popped [`Completion`], so "not outstanding" means the operation was
723    /// *observed* to finish, never merely assumed to have.
724    ///
725    /// # Why this hands back `&[u8]` and not `&B`
726    ///
727    /// For the same reason [`get_mut`] does: the slice is exactly the length
728    /// handed to `BuildIoRingRegisterBuffers`, which is what the kernel knows
729    /// about. `B`'s own `bytes_len()` is a live value that may since have
730    /// diverged from it, so answering from the registered length keeps this in
731    /// step with the registration rather than with the buffer.
732    ///
733    /// [`get_mut`]: RegisteredBuffers::get_mut
734    ///
735    /// # Holding the borrow across a push is refused at compile time
736    ///
737    /// This is the regression guard for the hazard described above. Taking the
738    /// borrow while the buffer is quiet and *then* submitting a read into it
739    /// must not compile:
740    ///
741    /// ```compile_fail
742    /// # use windows_ioring_sys::{Batch, IoRing, PushOptions, RegisteredBuffers, RegisteredSpan, SharedFile};
743    /// # fn hazard<B: windows_ioring_sys::IoBufMut>(
744    /// #     ring: &mut IoRing,
745    /// #     arena: &mut RegisteredBuffers<B>,
746    /// #     file: &SharedFile,
747    /// #     span: RegisteredSpan,
748    /// # ) -> std::io::Result<u8> {
749    /// let bytes: &[u8] = arena.get(0)?;      // quiet here, so the check passes
750    /// let mut batch = Batch::new(ring);
751    /// let _token = batch.read_registered(file, arena, span, 0, PushOptions::new())?;
752    /// batch.submit()?;                       // kernel now filling that buffer
753    /// Ok(bytes[0])                           // ... read through the live borrow
754    /// # }
755    /// ```
756    ///
757    /// Reading a *quiet* buffer while an operation is outstanding against a
758    /// neighbour is still allowed, because a [`Token`] borrows nothing from the
759    /// registration -- that is the arena pattern, and it must keep working:
760    ///
761    /// ```no_run
762    /// # use windows_ioring_sys::{Batch, IoRing, PushOptions, RegisteredBuffers, RegisteredSpan, SharedFile};
763    /// # fn arena_still_works<B: windows_ioring_sys::IoBufMut>(
764    /// #     ring: &mut IoRing,
765    /// #     arena: &mut RegisteredBuffers<B>,
766    /// #     file: &SharedFile,
767    /// #     span: RegisteredSpan,
768    /// # ) -> std::io::Result<()> {
769    /// let mut batch = Batch::new(ring);
770    /// let token = batch.read_registered(file, arena, span, 0, PushOptions::new())?;
771    /// batch.submit()?;
772    /// let _neighbour = arena.get(1)?;        // a different, quiet buffer
773    /// # let _ = token;
774    /// # Ok(())
775    /// # }
776    /// ```
777    ///
778    /// # Errors
779    ///
780    /// [`io::ErrorKind::InvalidInput`] if `i` is out of range for this
781    /// registration, or [`io::ErrorKind::WouldBlock`] if a read is still
782    /// outstanding into that buffer -- pop its completion and claim its token
783    /// first.
784    pub fn get(&mut self, i: u32) -> io::Result<&[u8]> {
785        let index = usize::try_from(i).map_err(|_| {
786            io::Error::new(
787                io::ErrorKind::InvalidInput,
788                "buffer index does not fit usize",
789            )
790        })?;
791        let Some(state) = self.state.get(index) else {
792            return Err(io::Error::new(
793                io::ErrorKind::InvalidInput,
794                format!("buffer index {i} is out of range for this registration"),
795            ));
796        };
797        let kernel_writes = state.kernel_writes.load(Ordering::SeqCst);
798        if kernel_writes > 0 {
799            return Err(io::Error::new(
800                io::ErrorKind::WouldBlock,
801                format!(
802                    "buffer {i} still has {kernel_writes} read(s) outstanding into it; the kernel \
803                     may be writing through it. Pop the completion and claim its token before \
804                     reading this buffer."
805                ),
806            ));
807        }
808        let len = state.registered_len as usize;
809        let buffer = self
810            .buffers
811            .get(index)
812            .expect("state and buffers are built together and have equal length");
813        // SAFETY: `stable_ptr` is `IoBuf`'s promised stable address for this
814        // buffer, and `registered_len` was recorded from that same buffer at
815        // registration, so those bytes are allocated and initialized. `&mut
816        // self` excludes concurrent mutation through `get_mut` *and* -- because
817        // the returned slice borrows it for its whole life -- excludes pushing
818        // a new operation against this registration while the slice is alive,
819        // since `read_registered` needs `&RegisteredBuffers`. So the
820        // zero-`kernel_writes` check above cannot be outrun: no read into this
821        // buffer can start before the borrow ends.
822        Ok(unsafe { std::slice::from_raw_parts(buffer.stable_ptr(), len) })
823    }
824
825    /// Borrow the `i`-th buffer's registered bytes **mutably**, to fill them
826    /// before an operation reads out of them (M13.2).
827    ///
828    /// This is what makes a registered arena usable for writes a caller
829    /// composes itself -- a log record, a serialized message -- rather than
830    /// only for bytes the kernel put there. Without it, [`get`] can show a
831    /// caller what a read landed but nothing can put anything into a buffer
832    /// after it is registered.
833    ///
834    /// # Why this hands back `&mut [u8]` and not `&mut B`
835    ///
836    /// Because `&mut B` would be **unsound**, and quietly so. Win32's
837    /// `IoRing` has no unregister call: the address and length handed to
838    /// `BuildIoRingRegisterBuffers` stay live for the ring's remaining life.
839    /// A `&mut Vec<u8>` lets safe code assign a whole new vector, `reserve`,
840    /// or `resize` -- each of which frees or moves the very allocation the
841    /// kernel still holds the address of. No `unsafe` is involved, and it
842    /// happens at a moment when nothing is outstanding, so no runtime check
843    /// would catch it. [`IoBuf`]'s contract requires that address to stay put
844    /// for as long as this crate holds the value, and a registration holds it
845    /// forever.
846    ///
847    /// A byte slice of exactly the registered length grants the one power a
848    /// caller actually needs -- writing the bytes -- and none of the powers
849    /// that would break the registration.
850    ///
851    /// # Why it can refuse
852    ///
853    /// `&mut self` is not on its own enough either. It excludes concurrent
854    /// `&self` borrows, but an operation already in flight holds no borrow at
855    /// all -- [`Batch::write_registered`] takes `&RegisteredBuffers` for the
856    /// length of the call, and the [`Token`] it returns keeps only a
857    /// [`RegisteredUse`]. So the compiler would happily allow mutation of a
858    /// buffer the kernel is reading through right now, which is a data race
859    /// with the kernel.
860    ///
861    /// The runtime half of the check is therefore the per-buffer outstanding
862    /// count: this refuses while any operation is still outstanding against
863    /// *this* buffer. Neighbouring buffers being in flight is fine, which is
864    /// the point of counting per buffer -- an arena a caller refills has to
865    /// be able to prepare buffer 5 while buffer 2 is still being written.
866    ///
867    /// A count returns to zero only when a [`Token`] is claimed against a
868    /// real popped [`Completion`], so "not outstanding" means the operation
869    /// was *observed* to finish, never merely assumed to have.
870    ///
871    /// [`get`]: RegisteredBuffers::get
872    ///
873    /// # Errors
874    ///
875    /// [`io::ErrorKind::InvalidInput`] if `i` is out of range for this
876    /// registration, or [`io::ErrorKind::WouldBlock`] if an operation is
877    /// still outstanding against that buffer -- pop its completion and claim
878    /// its token first.
879    pub fn get_mut(&mut self, i: u32) -> io::Result<&mut [u8]> {
880        let index = usize::try_from(i).map_err(|_| {
881            io::Error::new(
882                io::ErrorKind::InvalidInput,
883                "buffer index does not fit usize",
884            )
885        })?;
886        let Some(state) = self.state.get(index) else {
887            return Err(io::Error::new(
888                io::ErrorKind::InvalidInput,
889                format!("buffer index {i} is out of range for this registration"),
890            ));
891        };
892        let outstanding = state.outstanding.load(Ordering::SeqCst);
893        if outstanding > 0 {
894            return Err(io::Error::new(
895                io::ErrorKind::WouldBlock,
896                format!(
897                    "buffer {i} still has {outstanding} operation(s) outstanding; the kernel may \
898                     be reading or writing through it. Pop the completion and claim its token \
899                     before filling this buffer."
900                ),
901            ));
902        }
903        let len = state.registered_len as usize;
904        let buffer = self
905            .buffers
906            .get_mut(index)
907            .expect("state and buffers are built together and have equal length");
908        // SAFETY: `stable_mut_ptr` is `IoBufMut`'s promised stable, uniquely
909        // owned address for this buffer, and `registered_len` was recorded
910        // from that same buffer at registration, so those bytes are allocated
911        // and initialized. `&mut self` plus the zero-outstanding check above
912        // establish that nothing else -- kernel included -- is reading or
913        // writing them for this borrow's life.
914        Ok(unsafe { std::slice::from_raw_parts_mut(buffer.stable_mut_ptr(), len) })
915    }
916
917    /// How many operations are still outstanding against the `i`-th buffer,
918    /// or `None` if `i` is out of range.
919    ///
920    /// Lets a caller pick a quiet buffer without provoking an error from
921    /// [`RegisteredBuffers::get_mut`], which is the normal shape for an arena
922    /// cycling through its slots.
923    #[must_use]
924    pub fn outstanding(&self, i: u32) -> Option<usize> {
925        self.state
926            .get(usize::try_from(i).ok()?)
927            .map(|state| state.outstanding.load(Ordering::SeqCst))
928    }
929
930    /// Record that one more operation is outstanding against `span`'s buffer,
931    /// and produce the marker whose drop records it finished.
932    ///
933    /// Call only after [`RegisteredBuffers::checked_span`] has validated
934    /// `span`, which is what makes the index below in range.
935    fn begin_use(&self, span: RegisteredSpan, access: KernelAccess) -> RegisteredUse {
936        let index = span.buffer_index as usize;
937        if let Some(state) = self.state.get(index) {
938            // The direction-specific counter first, so a concurrent `get` can
939            // never observe `outstanding` raised without also seeing the
940            // reason it was raised.
941            if access == KernelAccess::WritesBuffer {
942                state.kernel_writes.fetch_add(1, Ordering::SeqCst);
943            }
944            state.outstanding.fetch_add(1, Ordering::SeqCst);
945        }
946        RegisteredUse {
947            state: Arc::clone(&self.state),
948            index,
949            access,
950        }
951    }
952
953    /// The registered index of the `i`-th buffer, or an
954    /// [`io::ErrorKind::InvalidInput`] error if out of range.
955    fn checked_index(&self, i: u32) -> io::Result<u32> {
956        if i < self.len() {
957            Ok(self.base_index + i)
958        } else {
959            Err(io::Error::new(
960                io::ErrorKind::InvalidInput,
961                format!("buffer index {i} is out of range for this registration"),
962            ))
963        }
964    }
965
966    /// As [`RegisteredBuffers::checked_index`], plus checking that
967    /// `span.offset .. span.offset + span.len` actually fits inside that
968    /// buffer -- validating only the index and handing an unchecked range to
969    /// the kernel would let it read or write outside the registered
970    /// allocation.
971    ///
972    /// Checked against the length recorded **at registration**, never against
973    /// the buffer's current `bytes_len()`. Those are the same number today,
974    /// because nothing this type exposes can resize a registered buffer, but
975    /// the kernel's view is the registered one and that is what a span has to
976    /// fit inside. Validating against a live length would mean a buffer that
977    /// somehow grew could address memory the kernel does not know about.
978    fn checked_span(&self, span: RegisteredSpan) -> io::Result<u32> {
979        let index = self.checked_index(span.buffer_index)?;
980        let registered_len = self
981            .state
982            .get(span.buffer_index as usize)
983            .expect("checked_index just validated this index")
984            .registered_len;
985        let fits = u64::from(span.offset)
986            .checked_add(u64::from(span.len))
987            .is_some_and(|end| end <= u64::from(registered_len));
988        if !fits {
989            return Err(io::Error::new(
990                io::ErrorKind::InvalidInput,
991                format!(
992                    "span offset {} + len {} does not fit inside buffer {} \
993                     ({registered_len} registered bytes)",
994                    span.offset, span.len, span.buffer_index
995                ),
996            ));
997        }
998        Ok(index)
999    }
1000}
1001
1002impl<B: IoBufMut> Drop for RegisteredBuffers<B> {
1003    fn drop(&mut self) {
1004        if self
1005            .state
1006            .iter()
1007            .any(|state| state.outstanding.load(Ordering::SeqCst) > 0)
1008        {
1009            // Refused, not silently permitted (M5.3): freeing now would
1010            // leave an outstanding `IORING_BUFFER_REF` pointing at freed
1011            // memory. Loud in debug builds; in release, leaking is the safe
1012            // failure mode -- the same choice `Token` already makes
1013            // ("leak is safe, use-after-free is not"), so `buffers` is
1014            // simply never reclaimed rather than freed out from under a
1015            // still-outstanding op. Any one buffer still in use holds the
1016            // whole registration, because they are freed together.
1017            debug_assert!(
1018                false,
1019                "RegisteredBuffers dropped while an operation still references it"
1020            );
1021            return;
1022        }
1023        // SAFETY: every per-buffer count is zero, so no operation still
1024        // references these buffers, and this is the only place `buffers` is
1025        // ever dropped.
1026        unsafe { ManuallyDrop::drop(&mut self.buffers) };
1027    }
1028}
1029
1030/// Which bytes of one [`RegisteredBuffers`] entry an op reads or writes
1031/// (M5.2): bundled into one value so `read_registered`/`write_registered`
1032/// stay under a sane argument count.
1033#[derive(Clone, Copy, Debug)]
1034pub struct RegisteredSpan {
1035    /// Which buffer within the registration, per [`RegisteredBuffers::get`].
1036    pub buffer_index: u32,
1037    /// The starting byte offset within that buffer.
1038    pub offset: u32,
1039    /// How many bytes to read or write.
1040    pub len: u32,
1041}
1042
1043/// A [`Batch::register_buffers`] push not yet matched to its completion.
1044///
1045/// `buffers` is `ManuallyDrop` and this type's own `Drop` is deliberately
1046/// empty, mirroring [`Token`] (PR #20 review response): the registration is
1047/// already queued via `BuildIoRingRegisterBuffers` the instant
1048/// [`Batch::register_buffers`] returns, before any completion is observed,
1049/// so a caller that drops this without ever matching a completion has no
1050/// proof the kernel is done deciding whether to retain these addresses.
1051/// Freeing them here anyway would risk handing memory the kernel still
1052/// references back to the allocator; leaking is the safe failure mode, the
1053/// same choice `Token` and [`RegisteredBuffers`] both already make.
1054pub struct PendingBufferRegistration<B: IoBufMut> {
1055    user_data: usize,
1056    base_index: u32,
1057    buffers: ManuallyDrop<Vec<B>>,
1058    /// The lengths handed to `BuildIoRingRegisterBuffers`, captured there
1059    /// rather than recomputed here, so what a span is later validated against
1060    /// is exactly what the kernel was told.
1061    registered_lens: Vec<u32>,
1062    ring_id: RingId,
1063}
1064
1065impl<B: IoBufMut> PendingBufferRegistration<B> {
1066    /// This push's `UserData` identity, to match against a popped
1067    /// [`Completion`].
1068    #[must_use]
1069    pub fn user_data(&self) -> usize {
1070        self.user_data
1071    }
1072
1073    /// Turn this pending registration into usable [`RegisteredBuffers`] once
1074    /// `completion` names it, or hand it back unchanged if `completion`
1075    /// names a different operation.
1076    ///
1077    /// # Errors
1078    ///
1079    /// The inner `Result` is `Err` if the registration itself failed; the
1080    /// buffers are dropped normally in that case, exactly as if they had
1081    /// never been registered -- a matched completion, success or failure, is
1082    /// exactly the proof this type's `Drop` is waiting for.
1083    pub fn claim_if(
1084        mut self,
1085        completion: &Completion,
1086    ) -> Result<io::Result<RegisteredBuffers<B>>, Self> {
1087        if completion.user_data() != self.user_data || completion.ring_id() != self.ring_id {
1088            return Err(self);
1089        }
1090        // SAFETY: `completion` names this exact registration, so the kernel
1091        // has already decided whether to retain these addresses -- the
1092        // condition this type's `Drop` would otherwise wait forever for.
1093        let buffers = unsafe { ManuallyDrop::take(&mut self.buffers) };
1094        let base_index = self.base_index;
1095        let ring_id = self.ring_id;
1096        let registered_lens = std::mem::take(&mut self.registered_lens);
1097        Ok(completion.result().map(move |_| {
1098            let state: Arc<[BufferState]> = registered_lens
1099                .into_iter()
1100                .map(|registered_len| BufferState {
1101                    outstanding: AtomicUsize::new(0),
1102                    kernel_writes: AtomicUsize::new(0),
1103                    registered_len,
1104                })
1105                .collect();
1106            debug_assert_eq!(
1107                state.len(),
1108                buffers.len(),
1109                "one state entry per registered buffer"
1110            );
1111            RegisteredBuffers {
1112                buffers: ManuallyDrop::new(buffers),
1113                base_index,
1114                state,
1115                ring_id,
1116            }
1117        }))
1118    }
1119}
1120
1121impl<B: IoBufMut> Drop for PendingBufferRegistration<B> {
1122    fn drop(&mut self) {
1123        // Deliberately empty; see this type's own doc comment.
1124    }
1125}
1126
1127impl<B: IoBufMut> std::fmt::Debug for PendingBufferRegistration<B> {
1128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1129        // Not derived: deriving would require `B: Debug`, which a caller's
1130        // buffer type need not satisfy.
1131        f.debug_struct("PendingBufferRegistration")
1132            .field("user_data", &self.user_data)
1133            .field("base_index", &self.base_index)
1134            .finish_non_exhaustive()
1135    }
1136}
1137
1138/// A scoped, exclusive submission window over one [`IoRing`] (D-5).
1139///
1140/// Holding `&mut IoRing` for its lifetime is what turns Win32's own
1141/// "you must serialize submission" footnote into a compiler-enforced
1142/// guarantee: two batches over the same ring cannot coexist. Every push
1143/// queues its SQE the instant it succeeds -- there is no rewind -- so
1144/// `Batch` submits on [`Drop`] rather than leaving queued SQEs for some
1145/// later, unrelated submit to discover (D-5).
1146pub struct Batch<'ring> {
1147    ring: &'ring mut IoRing,
1148    submitted: bool,
1149}
1150
1151impl<'ring> Batch<'ring> {
1152    /// Open a batch over `ring`.
1153    pub fn new(ring: &'ring mut IoRing) -> Self {
1154        Self {
1155            ring,
1156            submitted: false,
1157        }
1158    }
1159
1160    fn require(&self, op: Op) -> io::Result<()> {
1161        if self.ring.supports(op) {
1162            Ok(())
1163        } else {
1164            Err(io::Error::new(
1165                io::ErrorKind::Unsupported,
1166                format!("this ring does not support {op:?}"),
1167            ))
1168        }
1169    }
1170
1171    /// Queue a read of `buffer.bytes_len()` bytes from `file` at `offset`.
1172    ///
1173    /// Prefer [`Batch::read`] unless `file` needs to address a raw
1174    /// `FileRef` directly, without `SharedFile`'s `Arc` bookkeeping.
1175    ///
1176    /// # Safety
1177    ///
1178    /// If `file` is [`FileRef::Raw`], the handle must be valid, opened with
1179    /// read access, and must remain valid -- not closed, not reused for a
1180    /// different object -- until this operation's completion is observed
1181    /// (via a popped [`Completion`] or [`Token::claim_if`]) or until the
1182    /// ring runs down (M8, PR #20 review response). A [`FileRef::Registered`]
1183    /// target needs none of this.
1184    ///
1185    /// # Errors
1186    ///
1187    /// [`io::ErrorKind::Unsupported`] if the ring was not probed as
1188    /// supporting [`Op::Read`]; [`io::ErrorKind::InvalidInput`] if the
1189    /// buffer is longer than `u32::MAX`; an [`crate::IoRingError`] wrapping
1190    /// `IORING_E_SUBMISSION_QUEUE_FULL` if the queue has no room (M3.3, not
1191    /// auto-flushed -- see [`Batch`]'s own docs); or any other error from
1192    /// `BuildIoRingReadFile`. On any error the buffer is dropped normally,
1193    /// not leaked or handed back.
1194    pub unsafe fn read_raw<B: IoBufMut>(
1195        &mut self,
1196        file: impl Into<FileRef>,
1197        mut buffer: B,
1198        offset: u64,
1199        options: PushOptions,
1200    ) -> io::Result<Token<B>> {
1201        self.require(Op::Read)?;
1202        let len = checked_len(buffer.bytes_len())?;
1203        let address = buffer.stable_mut_ptr().cast::<c_void>();
1204        let target = handle_ref(file.into(), self.ring.ring_id())?;
1205        let token = Token::new(self.ring, buffer)?;
1206        let user_data = token.id();
1207        // SAFETY: `self.ring`'s handle is live; `address` is `IoBufMut`'s
1208        // promised stable, exclusively-owned pointer, valid for `len` bytes
1209        // until `token` is claimed; `file` is the caller's to keep alive,
1210        // forwarded from this function's own contract.
1211        let hr = unsafe {
1212            BuildIoRingReadFile(
1213                self.ring.raw_handle(),
1214                target,
1215                raw_buffer_ref(address),
1216                len,
1217                offset,
1218                user_data,
1219                options.sqe_flags(),
1220            )
1221        };
1222        self.finish_push(hr, token)
1223    }
1224
1225    /// As [`Batch::read_raw`], but safe: `file` is a [`SharedFile`] rather
1226    /// than a bare [`FileRef`], so the pushed operation keeps its own clone
1227    /// of the handle alive regardless of what the caller does with its
1228    /// copy. The returned token yields `(buffer, file)` once claimed.
1229    ///
1230    /// # Errors
1231    ///
1232    /// As [`Batch::read_raw`], plus [`io::ErrorKind::InvalidInput`] if `file`
1233    /// is a [`RegisteredFile`] from a different ring.
1234    pub fn read<B: IoBufMut, F: FileTarget>(
1235        &mut self,
1236        file: &F,
1237        mut buffer: B,
1238        offset: u64,
1239        options: PushOptions,
1240    ) -> io::Result<Token<(B, F::Guard)>> {
1241        self.require(Op::Read)?;
1242        let len = checked_len(buffer.bytes_len())?;
1243        let address = buffer.stable_mut_ptr().cast::<c_void>();
1244        let target = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1245        let token = Token::new(self.ring, (buffer, file.guard()))?;
1246        let user_data = token.id();
1247        // SAFETY: `self.ring`'s handle is live; `address` is `IoBufMut`'s
1248        // promised stable, exclusively-owned pointer, valid for `len` bytes
1249        // until `token` is claimed; `target` stays valid at least that long
1250        // too, because `token` holds `file`'s guard -- a clone of the `Arc`
1251        // for a `SharedFile`, and nothing needing to be kept alive at all for
1252        // a `RegisteredFile`, whose index names the ring's own table.
1253        let hr = unsafe {
1254            BuildIoRingReadFile(
1255                self.ring.raw_handle(),
1256                target,
1257                raw_buffer_ref(address),
1258                len,
1259                offset,
1260                user_data,
1261                options.sqe_flags(),
1262            )
1263        };
1264        self.finish_push(hr, token)
1265    }
1266
1267    /// Queue a write of `buffer.bytes_len()` bytes to `file` at `offset`.
1268    ///
1269    /// `caching` is `FILE_WRITE_FLAGS`; see [`WriteCaching`], and note that
1270    /// write-through is a cache directive rather than a durability guarantee.
1271    /// Durability comes from [`Batch::flush`] with
1272    /// [`FlushCoverage::CoversPrecedingOperations`], never from a write flag.
1273    ///
1274    /// Prefer [`Batch::write`] unless `file` needs to address a raw
1275    /// `FileRef` directly, without `SharedFile`'s `Arc` bookkeeping.
1276    ///
1277    /// # Safety
1278    ///
1279    /// As [`Batch::read_raw`]'s, for a write-access handle.
1280    ///
1281    /// # Errors
1282    ///
1283    /// As [`Batch::read_raw`], plus any error from `BuildIoRingWriteFile`.
1284    pub unsafe fn write_raw<B: IoBuf>(
1285        &mut self,
1286        file: impl Into<FileRef>,
1287        buffer: B,
1288        offset: u64,
1289        options: PushOptions,
1290        caching: WriteCaching,
1291    ) -> io::Result<Token<B>> {
1292        self.require(Op::Write)?;
1293        let len = checked_len(buffer.bytes_len())?;
1294        let address = buffer.stable_ptr().cast_mut().cast::<c_void>();
1295        let target = handle_ref(file.into(), self.ring.ring_id())?;
1296        let token = Token::new(self.ring, buffer)?;
1297        let user_data = token.id();
1298        // SAFETY: `address` is `IoBuf`'s promised stable pointer, valid for
1299        // `len` bytes until `token` is claimed; the kernel only reads
1300        // through it for a write, so the cast away from `const` does not
1301        // authorize mutation. `file` is the caller's to keep alive.
1302        let hr = unsafe {
1303            BuildIoRingWriteFile(
1304                self.ring.raw_handle(),
1305                target,
1306                raw_buffer_ref(address),
1307                len,
1308                offset,
1309                caching.raw(),
1310                user_data,
1311                options.sqe_flags(),
1312            )
1313        };
1314        self.finish_push(hr, token)
1315    }
1316
1317    /// As [`Batch::write_raw`], but safe: `file` is a [`SharedFile`] rather
1318    /// than a bare [`FileRef`]. The returned token yields `(buffer, file)`
1319    /// once claimed.
1320    ///
1321    /// # Errors
1322    ///
1323    /// As [`Batch::write_raw`], plus [`io::ErrorKind::InvalidInput`] if `file`
1324    /// is a [`RegisteredFile`] from a different ring.
1325    pub fn write<B: IoBuf, F: FileTarget>(
1326        &mut self,
1327        file: &F,
1328        buffer: B,
1329        offset: u64,
1330        options: PushOptions,
1331        caching: WriteCaching,
1332    ) -> io::Result<Token<(B, F::Guard)>> {
1333        self.require(Op::Write)?;
1334        let len = checked_len(buffer.bytes_len())?;
1335        let address = buffer.stable_ptr().cast_mut().cast::<c_void>();
1336        let target = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1337        let token = Token::new(self.ring, (buffer, file.guard()))?;
1338        let user_data = token.id();
1339        // SAFETY: as `write_raw`'s; `target` stays valid at least as long as
1340        // `token`'s hold on `file`'s guard does (see `Batch::read`).
1341        let hr = unsafe {
1342            BuildIoRingWriteFile(
1343                self.ring.raw_handle(),
1344                target,
1345                raw_buffer_ref(address),
1346                len,
1347                offset,
1348                caching.raw(),
1349                user_data,
1350                options.sqe_flags(),
1351            )
1352        };
1353        self.finish_push(hr, token)
1354    }
1355
1356    /// Refuse a [`RegisteredBuffers`] that did not come from this batch's own
1357    /// ring (PR #20 review response): its index space is only meaningful
1358    /// against the ring that registered it, and a different ring may have an
1359    /// entirely unrelated (or already-freed) buffer at the same index.
1360    fn check_registration_ring<B: IoBufMut>(
1361        &self,
1362        registration: &RegisteredBuffers<B>,
1363    ) -> io::Result<()> {
1364        if registration.ring_id == self.ring.ring_id() {
1365            Ok(())
1366        } else {
1367            Err(io::Error::new(
1368                io::ErrorKind::InvalidInput,
1369                "this RegisteredBuffers was registered on a different IoRing",
1370            ))
1371        }
1372    }
1373
1374    /// Reclaim `token`'s value and release its reservation if `hr` failed,
1375    /// or hand `token` back unchanged on success -- the shared tail of every
1376    /// push in this module.
1377    fn finish_push<T: Send + 'static>(
1378        &mut self,
1379        hr: windows_sys::core::HRESULT,
1380        token: Token<T>,
1381    ) -> io::Result<Token<T>> {
1382        match check(hr) {
1383            Ok(()) => Ok(token),
1384            Err(error) => {
1385                // The SQE was never queued: reclaim and drop the value
1386                // normally instead of leaking it (this crate's own code
1387                // knows the op never reached the kernel, so an unconditional
1388                // `claim` is sound here -- unlike `claim_if`, which requires
1389                // a real popped `Completion`), and release the reservation
1390                // so it does not count against rundown.
1391                let _ = token.claim();
1392                self.ring.cancel_reservation();
1393                Err(error)
1394            }
1395        }
1396    }
1397
1398    /// Queue a flush of `file`'s buffered data.
1399    ///
1400    /// `coverage` decides whether this flush covers the operations queued
1401    /// before it. It is required rather than defaulted because there is no
1402    /// safe default -- see [`FlushCoverage`] and the measured contract below.
1403    ///
1404    /// `mode` is `FILE_FLUSH_MODE`; [`FlushMode::Default`] is the durability
1405    /// barrier. Note that [`FlushMode::NoSync`] issues no device sync and so
1406    /// makes nothing durable, whatever `coverage` says.
1407    ///
1408    /// There is no buffer, so this returns the raw `UserData` identity
1409    /// rather than a [`Token`]: nothing owns a buffer for a completion to
1410    /// hand back. Prefer [`Batch::flush`] unless `file` needs to address a
1411    /// raw `FileRef` directly.
1412    ///
1413    /// # A flush is the ring's only durability primitive
1414    ///
1415    /// **The ring has no FUA.** `BuildIoRingWriteFile`'s entire flag set is
1416    /// `{FILE_WRITE_FLAGS_NONE, FILE_WRITE_FLAGS_WRITE_THROUGH}`, and
1417    /// write-through is a cache-bypass directive to the OS rather than a
1418    /// device-level durability guarantee -- whether it becomes a Force Unit
1419    /// Access bit depends on the driver, the volume, and whether the device's
1420    /// write cache is enabled (see [`WriteCaching`]). So this operation is the
1421    /// only way the ring makes anything durable, and only with
1422    /// [`FlushCoverage::CoversPrecedingOperations`] and a syncing
1423    /// [`FlushMode`].
1424    ///
1425    /// # The measured contract (D-23 in `DESIGN-NOTES.md`)
1426    ///
1427    /// **An unflagged flush does not cover preceding writes.** It is an
1428    /// ordinary operation competing with them, and it frequently wins: a
1429    /// flush pushed after a batch of writes with no barrier was observed
1430    /// completing while 17, and on another run 23, of 32 of those writes were
1431    /// still outstanding. A caller that reads such a completion as "the
1432    /// writes before it are now durable" has lost data it believes it has
1433    /// committed, and nothing reports the loss until power fails.
1434    ///
1435    /// Which *direction* the reordering shows in is device-dependent, and a
1436    /// machine where the flush happens to land last anyway proves nothing:
1437    /// that is incidental behavior of one device stack, not a guarantee. Only
1438    /// the barrier makes it one.
1439    ///
1440    /// Durability on this ring is therefore a property of an **epoch**, never
1441    /// of an individual write, because there is no per-write primitive to
1442    /// make it one: stream the writes unflagged, close the epoch with one
1443    /// covering flush, and wait on that flush rather than on the writes.
1444    /// "Durability on the ring" in `DESIGN-NOTES.md` has the full
1445    /// construction, and the three ways to pay for the barrier's ring-wide
1446    /// stall.
1447    ///
1448    /// # Safety
1449    ///
1450    /// As [`Batch::read_raw`]'s, for a [`FileRef::Raw`] target.
1451    ///
1452    /// # Errors
1453    ///
1454    /// [`io::ErrorKind::Unsupported`] if the ring was not probed as
1455    /// supporting [`Op::Flush`]; an [`crate::IoRingError`] wrapping
1456    /// `IORING_E_SUBMISSION_QUEUE_FULL` if the queue has no room; or any
1457    /// other error from `BuildIoRingFlushFile`.
1458    pub unsafe fn flush_raw(
1459        &mut self,
1460        file: impl Into<FileRef>,
1461        coverage: FlushCoverage,
1462        mode: FlushMode,
1463    ) -> io::Result<usize> {
1464        self.require(Op::Flush)?;
1465        let target = handle_ref(file.into(), self.ring.ring_id())?;
1466        let user_data = self.ring.reserve_user_data()?;
1467        // SAFETY: `self.ring`'s handle is live; `file` is the caller's to
1468        // keep alive, forwarded from this function's own contract; there is
1469        // no buffer.
1470        let hr = unsafe {
1471            BuildIoRingFlushFile(
1472                self.ring.raw_handle(),
1473                target,
1474                mode.raw(),
1475                user_data,
1476                coverage.sqe_flags(),
1477            )
1478        };
1479        if let Err(error) = check(hr) {
1480            self.ring.cancel_reservation();
1481            return Err(error);
1482        }
1483        Ok(user_data)
1484    }
1485
1486    /// As [`Batch::flush_raw`], but safe: `file` is a [`FileTarget`] -- a
1487    /// [`SharedFile`] or a [`RegisteredFile`] -- and the returned [`Token`]
1488    /// (rather than a bare `UserData`) holds its guard until this
1489    /// operation's completion is observed.
1490    ///
1491    /// `coverage` is required for the reason [`FlushCoverage`] documents: an
1492    /// unflagged flush does not cover preceding writes (D-23), so there is no
1493    /// default spelling of this call that is safe to inherit. `mode` selects
1494    /// how much is flushed and whether the device is synced at all; see
1495    /// [`FlushMode`].
1496    ///
1497    /// # Errors
1498    ///
1499    /// As [`Batch::flush_raw`], plus [`io::ErrorKind::InvalidInput`] if
1500    /// `file` is a [`RegisteredFile`] from a different ring.
1501    pub fn flush<F: FileTarget>(
1502        &mut self,
1503        file: &F,
1504        coverage: FlushCoverage,
1505        mode: FlushMode,
1506    ) -> io::Result<Token<F::Guard>> {
1507        self.require(Op::Flush)?;
1508        let target = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1509        let token = Token::new(self.ring, file.guard())?;
1510        let user_data = token.id();
1511        // SAFETY: `target` stays valid at least as long as `token`'s hold on
1512        // `file`'s guard does (see `Batch::read`); there is no buffer.
1513        let hr = unsafe {
1514            BuildIoRingFlushFile(
1515                self.ring.raw_handle(),
1516                target,
1517                mode.raw(),
1518                user_data,
1519                coverage.sqe_flags(),
1520            )
1521        };
1522        self.finish_push(hr, token)
1523    }
1524
1525    /// Queue cancellation of the operation identified by `target` (the
1526    /// `usize` a prior push returned), against `file`.
1527    ///
1528    /// A cancel is itself an operation: it completes on its own `UserData`,
1529    /// returned here, independently of whether `target` was actually
1530    /// outstanding. Cancelling a target that has already completed -- or
1531    /// was never outstanding -- reports `ERROR_NOT_FOUND` through *this*
1532    /// completion rather than failing to build (M3.6). Prefer
1533    /// [`Batch::cancel`] unless `file` needs to address a raw `FileRef`
1534    /// directly.
1535    ///
1536    /// # Safety
1537    ///
1538    /// As [`Batch::read_raw`]'s, for a [`FileRef::Raw`] target.
1539    ///
1540    /// # Errors
1541    ///
1542    /// [`io::ErrorKind::Unsupported`] if the ring was not probed as
1543    /// supporting [`Op::Cancel`]; an [`crate::IoRingError`] wrapping
1544    /// `IORING_E_SUBMISSION_QUEUE_FULL` if the queue has no room; or any
1545    /// other error from `BuildIoRingCancelRequest`.
1546    pub unsafe fn cancel_raw(
1547        &mut self,
1548        file: impl Into<FileRef>,
1549        target: usize,
1550    ) -> io::Result<usize> {
1551        self.require(Op::Cancel)?;
1552        let handle = handle_ref(file.into(), self.ring.ring_id())?;
1553        let user_data = self.ring.reserve_user_data()?;
1554        // SAFETY: `self.ring`'s handle is live; `file` is the caller's to
1555        // keep alive, forwarded from this function's own contract;
1556        // `BuildIoRingCancelRequest` takes no SQE-flags parameter.
1557        let hr =
1558            unsafe { BuildIoRingCancelRequest(self.ring.raw_handle(), handle, target, user_data) };
1559        if let Err(error) = check(hr) {
1560            self.ring.cancel_reservation();
1561            return Err(error);
1562        }
1563        Ok(user_data)
1564    }
1565
1566    /// As [`Batch::cancel_raw`], but safe: `file` is a [`SharedFile`], and
1567    /// the returned [`Token`] keeps `file`'s clone alive until this
1568    /// operation's completion is observed.
1569    ///
1570    /// A cancel is a request, not a guarantee (M10.2): `target` may complete
1571    /// normally regardless, and this push produces its *own* completion in
1572    /// addition to the target's, so a cancelled operation yields two. A
1573    /// result of `ERROR_NOT_FOUND` on this push's own completion means
1574    /// `target` was no longer outstanding -- a normal race, not a caller
1575    /// error.
1576    ///
1577    /// # Errors
1578    ///
1579    /// As [`Batch::cancel_raw`].
1580    pub fn cancel<F: FileTarget>(
1581        &mut self,
1582        file: &F,
1583        target: usize,
1584    ) -> io::Result<Token<F::Guard>> {
1585        self.require(Op::Cancel)?;
1586        let handle = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1587        let token = Token::new(self.ring, file.guard())?;
1588        let user_data = token.id();
1589        // SAFETY: `handle` stays valid at least as long as `token`'s hold on
1590        // `file`'s guard does (see `Batch::read`);
1591        // `BuildIoRingCancelRequest` takes no SQE-flags parameter.
1592        let hr =
1593            unsafe { BuildIoRingCancelRequest(self.ring.raw_handle(), handle, target, user_data) };
1594        self.finish_push(hr, token)
1595    }
1596
1597    /// Queue registration of `handles` as a ring's file-handle table (M5.1).
1598    ///
1599    /// `BuildIoRingRegisterFileHandles` *replaces* the ring's entire
1600    /// file-handle table rather than appending to it (Win32 docs: "If a
1601    /// previous registration exists, this replaces the previous
1602    /// registration completely"), which would silently invalidate every
1603    /// [`RegisteredFile`] index a prior registration handed out. Rather than
1604    /// track and resubmit that whole prior table transparently, this method
1605    /// refuses a second registration outright: a ring accepts at most one
1606    /// file-handle registration *that assigned an index* in its lifetime.
1607    ///
1608    /// Two consequences of that rule being enforced against
1609    /// [`crate::IoRing::registered_file_count`] rather than a flag (M10.1):
1610    /// a zero-length `handles` does not spend the ring's one registration,
1611    /// since it hands out no index for a later replacement to invalidate;
1612    /// and the count advances when this call *queues*, not when its
1613    /// completion succeeds (D-14), so a registration whose completion
1614    /// reports failure has still spent it. There is no retry -- a consumer
1615    /// whose registration fails must build the registration on a new ring.
1616    ///
1617    /// `handles` only needs to stay valid for this call, unlike a data
1618    /// buffer referenced through an `IORING_HANDLE_REF`/`IORING_BUFFER_REF`:
1619    /// `BuildIoRingRegisterFileHandles` has no such ref, it takes the array
1620    /// directly and reads it synchronously -- confirmed by measurement, not
1621    /// assumed (D-32). The handles themselves must still stay open for as
1622    /// long as the registration is used -- this crate does not take
1623    /// ownership of them, only of their assigned indices' bookkeeping.
1624    ///
1625    /// Do **not** generalize this to [`Batch::register_buffers`]:
1626    /// `BuildIoRingRegisterBuffers` reads its array when the op *runs*, and
1627    /// assuming otherwise was a live use-after-free in 0.1.2.
1628    ///
1629    /// # Safety
1630    ///
1631    /// Every handle in `handles` must be valid, and must remain valid for
1632    /// as long as the resulting registration is used -- for the ring's
1633    /// remaining life, since Win32 has no unregister call (M8, PR #20
1634    /// review response). There is no safe counterpart: a single-push
1635    /// `Token` cannot express a lifetime spanning arbitrarily many later
1636    /// reads and writes against every registered index, unlike a `Token`
1637    /// tied to one push's own completion.
1638    ///
1639    /// # Errors
1640    ///
1641    /// [`io::ErrorKind::AlreadyExists`] if this ring already has a
1642    /// file-handle registration (see above); [`io::ErrorKind::Unsupported`]
1643    /// if the ring was not probed as
1644    /// supporting [`Op::RegisterFiles`](crate::Op::RegisterFiles);
1645    /// [`io::ErrorKind::InvalidInput`] if `handles` has more than
1646    /// `u32::MAX` entries; an [`crate::IoRingError`] wrapping
1647    /// `IORING_E_SUBMISSION_QUEUE_FULL` if the queue has no room; or any
1648    /// other error from `BuildIoRingRegisterFileHandles`.
1649    pub unsafe fn register_files(
1650        &mut self,
1651        handles: &[HANDLE],
1652    ) -> io::Result<PendingFileRegistration> {
1653        self.require(Op::RegisterFiles)?;
1654        if self.ring.registered_file_count() > 0 {
1655            return Err(io::Error::new(
1656                io::ErrorKind::AlreadyExists,
1657                "this ring already has a file-handle registration; BuildIoRingRegisterFileHandles \
1658                 replaces the whole table, so a second call would invalidate every RegisteredFile \
1659                 index already handed out",
1660            ));
1661        }
1662        let count = checked_len(handles.len())?;
1663        let base_index = self.ring.registered_file_count();
1664        let user_data = self.ring.reserve_user_data()?;
1665        // SAFETY: `self.ring`'s handle is live; `handles` is read
1666        // synchronously for the duration of this call only -- confirmed by
1667        // measurement (D-32), not inherited from the sibling registration,
1668        // which behaves the opposite way.
1669        let hr = unsafe {
1670            BuildIoRingRegisterFileHandles(
1671                self.ring.raw_handle(),
1672                count,
1673                handles.as_ptr(),
1674                user_data,
1675            )
1676        };
1677        if let Err(error) = check(hr) {
1678            self.ring.cancel_reservation();
1679            return Err(error);
1680        }
1681        self.ring.reserve_registered_files(count);
1682        Ok(PendingFileRegistration {
1683            user_data,
1684            base_index,
1685            count,
1686            ring_id: self.ring.ring_id(),
1687        })
1688    }
1689
1690    /// Queue registration of `buffers` as a ring's registered-buffer table
1691    /// (M5.2).
1692    ///
1693    /// As [`Batch::register_files`]: `BuildIoRingRegisterBuffers` replaces
1694    /// the ring's entire buffer table rather than appending to it, so this
1695    /// method refuses a second registration outright -- a ring accepts at
1696    /// most one buffer registration *that assigned an index* in its
1697    /// lifetime, with the same zero-length and failed-registration
1698    /// consequences [`Batch::register_files`] spells out (M10.1).
1699    ///
1700    /// Unlike [`Batch::register_files`], **two** things must outlive this
1701    /// call, and the asymmetry is measured rather than assumed (D-32):
1702    ///
1703    /// - the *bytes each entry points at* -- the registration case `IoBuf`'s
1704    ///   contract was extended to cover (D-11), so `buffers` is taken by
1705    ///   value and kept inside the returned [`RegisteredBuffers`] once
1706    ///   claimed;
1707    /// - the `IORING_BUFFER_INFO` array itself. `BuildIoRingRegisterBuffers`
1708    ///   does **not** read it synchronously the way
1709    ///   `BuildIoRingRegisterFileHandles` reads its `handles` array; the
1710    ///   kernel reads it when the registration op runs, during a later
1711    ///   `SubmitIoRing`. This crate builds that array and hands it to the
1712    ///   ring, which holds it for its remaining life, so a caller has
1713    ///   nothing to do -- but the distinction is why
1714    ///   [`crate::IoRing::push_raw`] callers building this op themselves must
1715    ///   not pass a temporary.
1716    ///
1717    /// # Errors
1718    ///
1719    /// As [`Batch::register_files`], for [`Op::RegisterBuffers`](crate::Op::RegisterBuffers)
1720    /// and `BuildIoRingRegisterBuffers`.
1721    pub fn register_buffers<B: IoBufMut>(
1722        &mut self,
1723        mut buffers: Vec<B>,
1724    ) -> io::Result<PendingBufferRegistration<B>> {
1725        self.require(Op::RegisterBuffers)?;
1726        if self.ring.registered_buffer_count() > 0 {
1727            return Err(io::Error::new(
1728                io::ErrorKind::AlreadyExists,
1729                "this ring already has a buffer registration; BuildIoRingRegisterBuffers \
1730                 replaces the whole table, so a second call would invalidate every buffer \
1731                 index already handed out",
1732            ));
1733        }
1734        let count = checked_len(buffers.len())?;
1735        let base_index = self.ring.registered_buffer_count();
1736        let mut infos = Vec::with_capacity(buffers.len());
1737        let mut registered_lens = Vec::with_capacity(buffers.len());
1738        for buffer in &mut buffers {
1739            let length = checked_len(buffer.bytes_len())?;
1740            registered_lens.push(length);
1741            infos.push(IORING_BUFFER_INFO {
1742                Address: buffer.stable_mut_ptr().cast::<c_void>(),
1743                Length: length,
1744            });
1745        }
1746        let user_data = self.ring.reserve_user_data()?;
1747        // The array must outlive this call: the kernel reads it when the
1748        // registration op runs, during a later `SubmitIoRing`, not here
1749        // (D-32, measured). Hand it to the ring, which holds it for its
1750        // remaining life, and build the SQE from *that* pointer rather than
1751        // from the local `Vec` about to go out of scope.
1752        let infos_ptr = self.ring.hold_registered_buffer_infos(infos);
1753        // SAFETY: `self.ring`'s handle is live; `infos_ptr` addresses the
1754        // array the ring now owns, which outlives every submit that could run
1755        // this SQE; each `Address` points into `buffers`, which the caller
1756        // keeps alive via the returned `PendingBufferRegistration` and, once
1757        // claimed, `RegisteredBuffers`.
1758        let hr = unsafe {
1759            BuildIoRingRegisterBuffers(self.ring.raw_handle(), count, infos_ptr, user_data)
1760        };
1761        if let Err(error) = check(hr) {
1762            self.ring.cancel_reservation();
1763            return Err(error);
1764        }
1765        self.ring.reserve_registered_buffers(count);
1766        Ok(PendingBufferRegistration {
1767            user_data,
1768            base_index,
1769            buffers: ManuallyDrop::new(buffers),
1770            registered_lens,
1771            ring_id: self.ring.ring_id(),
1772        })
1773    }
1774
1775    /// Queue a read of `span.len` bytes from `file` at `file_offset`, into
1776    /// `span`'s byte offset of `registration`'s buffer at `span.buffer_index`,
1777    /// instead of handing over a fresh owned buffer (M5.2).
1778    ///
1779    /// The returned [`Token`] must be claimed once its completion is
1780    /// observed, exactly like [`Batch::read`]'s -- but claiming it recovers
1781    /// no buffer, only releases this use against `registration`'s own drop
1782    /// check (M5.3). Read the transferred bytes back from `registration`
1783    /// itself afterward, for example via a caller-side accessor into the
1784    /// buffer it was constructed from. Prefer [`Batch::read_registered`]
1785    /// unless `file` needs to address a raw `FileRef` directly.
1786    ///
1787    /// # Safety
1788    ///
1789    /// As [`Batch::read_raw`]'s.
1790    ///
1791    /// # Errors
1792    ///
1793    /// As [`Batch::read_raw`], plus [`io::ErrorKind::InvalidInput`] if
1794    /// `span.buffer_index` is out of range for `registration`.
1795    pub unsafe fn read_registered_raw<B: IoBufMut>(
1796        &mut self,
1797        file: impl Into<FileRef>,
1798        registration: &RegisteredBuffers<B>,
1799        span: RegisteredSpan,
1800        file_offset: u64,
1801        options: PushOptions,
1802    ) -> io::Result<Token<RegisteredUse>> {
1803        self.require(Op::Read)?;
1804        self.check_registration_ring(registration)?;
1805        let target = handle_ref(file.into(), self.ring.ring_id())?;
1806        let index = registration.checked_span(span)?;
1807        let token = Token::new(
1808            self.ring,
1809            registration.begin_use(span, KernelAccess::WritesBuffer),
1810        )?;
1811        let user_data = token.id();
1812        // SAFETY: `self.ring`'s handle is live; `index` was just checked
1813        // against `registration`, whose buffer stays put until it drops;
1814        // `file` is the caller's to keep alive, forwarded from this
1815        // function's own contract.
1816        let hr = unsafe {
1817            BuildIoRingReadFile(
1818                self.ring.raw_handle(),
1819                target,
1820                registered_buffer_ref(index, span.offset),
1821                span.len,
1822                file_offset,
1823                user_data,
1824                options.sqe_flags(),
1825            )
1826        };
1827        self.finish_push(hr, token)
1828    }
1829
1830    /// As [`Batch::read_registered_raw`], but safe: `file` is a
1831    /// [`FileTarget`] rather than a bare [`FileRef`]. The returned token
1832    /// yields `(RegisteredUse, guard)` once claimed.
1833    ///
1834    /// Passing a [`RegisteredFile`] here is the fully-registered form --
1835    /// registered file *and* registered buffer, neither costing a handle
1836    /// lookup nor a buffer pin per operation -- which before M10.4 the safe
1837    /// API could not express at all.
1838    ///
1839    /// # Errors
1840    ///
1841    /// As [`Batch::read_registered_raw`], plus
1842    /// [`io::ErrorKind::InvalidInput`] if `file` is a [`RegisteredFile`] from
1843    /// a different ring.
1844    pub fn read_registered<B: IoBufMut, F: FileTarget>(
1845        &mut self,
1846        file: &F,
1847        registration: &RegisteredBuffers<B>,
1848        span: RegisteredSpan,
1849        file_offset: u64,
1850        options: PushOptions,
1851    ) -> io::Result<Token<(RegisteredUse, F::Guard)>> {
1852        self.require(Op::Read)?;
1853        self.check_registration_ring(registration)?;
1854        let target = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1855        let index = registration.checked_span(span)?;
1856        let token = Token::new(
1857            self.ring,
1858            (
1859                registration.begin_use(span, KernelAccess::WritesBuffer),
1860                file.guard(),
1861            ),
1862        )?;
1863        let user_data = token.id();
1864        // SAFETY: `index` was just checked against `registration`, whose
1865        // buffer stays put until it drops; `target` stays valid at least as
1866        // long as `token`'s hold on `file`'s guard does (see `Batch::read`).
1867        let hr = unsafe {
1868            BuildIoRingReadFile(
1869                self.ring.raw_handle(),
1870                target,
1871                registered_buffer_ref(index, span.offset),
1872                span.len,
1873                file_offset,
1874                user_data,
1875                options.sqe_flags(),
1876            )
1877        };
1878        self.finish_push(hr, token)
1879    }
1880
1881    /// Queue a write of `span.len` bytes to `file` at `file_offset`, from
1882    /// `span`'s byte offset of `registration`'s buffer at `span.buffer_index`
1883    /// (M5.2).
1884    ///
1885    /// As [`Batch::read_registered_raw`], but for `BuildIoRingWriteFile`.
1886    /// Prefer [`Batch::write_registered`] unless `file` needs to address a
1887    /// raw `FileRef` directly.
1888    ///
1889    /// # Safety
1890    ///
1891    /// As [`Batch::read_registered_raw`]'s.
1892    ///
1893    /// # Errors
1894    ///
1895    /// As [`Batch::read_registered_raw`].
1896    pub unsafe fn write_registered_raw<B: IoBufMut>(
1897        &mut self,
1898        file: impl Into<FileRef>,
1899        registration: &RegisteredBuffers<B>,
1900        span: RegisteredSpan,
1901        file_offset: u64,
1902        options: PushOptions,
1903        caching: WriteCaching,
1904    ) -> io::Result<Token<RegisteredUse>> {
1905        self.require(Op::Write)?;
1906        self.check_registration_ring(registration)?;
1907        let target = handle_ref(file.into(), self.ring.ring_id())?;
1908        let index = registration.checked_span(span)?;
1909        let token = Token::new(
1910            self.ring,
1911            registration.begin_use(span, KernelAccess::ReadsBuffer),
1912        )?;
1913        let user_data = token.id();
1914        // SAFETY: as `read_registered_raw`; the kernel only reads through
1915        // this reference for a write.
1916        let hr = unsafe {
1917            BuildIoRingWriteFile(
1918                self.ring.raw_handle(),
1919                target,
1920                registered_buffer_ref(index, span.offset),
1921                span.len,
1922                file_offset,
1923                caching.raw(),
1924                user_data,
1925                options.sqe_flags(),
1926            )
1927        };
1928        self.finish_push(hr, token)
1929    }
1930
1931    /// As [`Batch::write_registered_raw`], but safe: `file` is a
1932    /// [`FileTarget`] rather than a bare [`FileRef`]. The returned token
1933    /// yields `(RegisteredUse, guard)` once claimed.
1934    ///
1935    /// Passing a [`RegisteredFile`] here is the fully-registered form, as
1936    /// for [`Batch::read_registered`].
1937    ///
1938    /// # Errors
1939    ///
1940    /// As [`Batch::write_registered_raw`], plus
1941    /// [`io::ErrorKind::InvalidInput`] if `file` is a [`RegisteredFile`] from
1942    /// a different ring.
1943    pub fn write_registered<B: IoBufMut, F: FileTarget>(
1944        &mut self,
1945        file: &F,
1946        registration: &RegisteredBuffers<B>,
1947        span: RegisteredSpan,
1948        file_offset: u64,
1949        options: PushOptions,
1950        caching: WriteCaching,
1951    ) -> io::Result<Token<(RegisteredUse, F::Guard)>> {
1952        self.require(Op::Write)?;
1953        self.check_registration_ring(registration)?;
1954        let target = handle_ref(file.as_file_ref(), self.ring.ring_id())?;
1955        let index = registration.checked_span(span)?;
1956        let token = Token::new(
1957            self.ring,
1958            (
1959                registration.begin_use(span, KernelAccess::ReadsBuffer),
1960                file.guard(),
1961            ),
1962        )?;
1963        let user_data = token.id();
1964        // SAFETY: as `read_registered`'s; the kernel only reads through
1965        // this reference for a write.
1966        let hr = unsafe {
1967            BuildIoRingWriteFile(
1968                self.ring.raw_handle(),
1969                target,
1970                registered_buffer_ref(index, span.offset),
1971                span.len,
1972                file_offset,
1973                caching.raw(),
1974                user_data,
1975                options.sqe_flags(),
1976            )
1977        };
1978        self.finish_push(hr, token)
1979    }
1980
1981    /// Submit everything queued so far, returning the number of entries the
1982    /// kernel accepted.
1983    ///
1984    /// That count is entries *submitted*, never entries completed (M10.2):
1985    /// draining is [`crate::IoRing::try_pop`]'s job and stays the caller's.
1986    ///
1987    /// # Errors
1988    ///
1989    /// Returns any error from `SubmitIoRing`.
1990    pub fn submit(self) -> io::Result<u32> {
1991        self.submit_and_wait(0, 0)
1992    }
1993
1994    /// Submit everything queued so far, then block until at least
1995    /// `wait_operations` of them have completed or `timeout_ms` elapses
1996    /// (D-3's Model B primitive: the fused submit-and-wait *is* the event
1997    /// loop for a pinned-thread consumer).
1998    ///
1999    /// Returning does not mean `wait_operations` completions are poppable:
2000    /// the timeout can expire first, and the returned count is entries
2001    /// submitted rather than completed (M10.2). Drain with
2002    /// [`crate::IoRing::try_pop`] and count for yourself.
2003    ///
2004    /// # Errors
2005    ///
2006    /// Returns any error from `SubmitIoRing`.
2007    pub fn submit_and_wait(mut self, wait_operations: u32, timeout_ms: u32) -> io::Result<u32> {
2008        self.do_submit(wait_operations, timeout_ms)
2009    }
2010
2011    fn do_submit(&mut self, wait_operations: u32, timeout_ms: u32) -> io::Result<u32> {
2012        let mut submitted = 0_u32;
2013        // SAFETY: `self.ring`'s handle is live.
2014        let hr = unsafe {
2015            SubmitIoRing(
2016                self.ring.raw_handle(),
2017                wait_operations,
2018                timeout_ms,
2019                &raw mut submitted,
2020            )
2021        };
2022        // Marked attempted *before* propagating a failure (PR #20 review
2023        // response): `submit_and_wait` takes `self` by value, so this call
2024        // failing still runs `Drop` once its caller's `self` goes out of
2025        // scope. Setting this first is what stops `Drop` from silently
2026        // retrying an explicit submit that already ran -- which could
2027        // succeed on the retry, submitting operations the caller's `Err`
2028        // never told them about.
2029        self.submitted = true;
2030        check(hr)?;
2031        Ok(submitted)
2032    }
2033}
2034
2035impl Drop for Batch<'_> {
2036    fn drop(&mut self) {
2037        if !self.submitted {
2038            // Best-effort: Drop cannot propagate an error, and a batch that
2039            // queued nothing submits zero entries harmlessly (D-5).
2040            let _ = self.do_submit(0, 0);
2041        }
2042    }
2043}
2044
2045#[cfg(test)]
2046mod tests;