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;