windows-ioring-sys 0.7.1

Memory-safe Rust over the Windows IoRing submission/completion ring, with the ring holding each in-flight buffer until its completion.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
// Copyright (c) 2026 Mike Grier
//! IoRing errors: `HRESULT`, not the `GetLastError` convention.

use std::fmt;
use std::io;

use windows_sys::Win32::Foundation::{
    IORING_E_COMPLETION_QUEUE_TOO_BIG, IORING_E_COMPLETION_QUEUE_TOO_FULL, IORING_E_CORRUPT,
    IORING_E_REQUIRED_FLAG_NOT_SUPPORTED, IORING_E_SUBMISSION_QUEUE_FULL,
    IORING_E_SUBMISSION_QUEUE_TOO_BIG, IORING_E_SUBMIT_IN_PROGRESS, IORING_E_VERSION_NOT_SUPPORTED,
};
use windows_sys::core::HRESULT;

mod sealed {
    pub trait Sealed {}
    impl Sealed for std::io::Error {}
}

/// A named `IORING_E_*` condition (M10.5).
///
/// The `HRESULT` -> name mapping is defined here once and everywhere else asks
/// (`IoRingError::name` is derived from this, not a second copy of it), so a
/// new condition cannot be added to one and forgotten in the other.
///
/// `#[non_exhaustive]`: the kernel's error set can grow, and a consumer must
/// not be able to write an exhaustive `match` that a new variant would break
/// -- the same reasoning as [`crate::Op`]'s (D-7).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RingCondition {
    /// `IORING_E_REQUIRED_FLAG_NOT_SUPPORTED`.
    RequiredFlagNotSupported,
    /// `IORING_E_VERSION_NOT_SUPPORTED`.
    VersionNotSupported,
    /// `IORING_E_SUBMISSION_QUEUE_FULL`: the submission queue has no room.
    /// Recoverable -- submit what is queued, drain completions, retry.
    SubmissionQueueFull,
    /// `IORING_E_SUBMISSION_QUEUE_TOO_BIG`.
    SubmissionQueueTooBig,
    /// `IORING_E_COMPLETION_QUEUE_TOO_BIG`.
    CompletionQueueTooBig,
    /// `IORING_E_CORRUPT`: the ring's shared state is inconsistent. Fatal.
    Corrupt,
    /// `IORING_E_SUBMIT_IN_PROGRESS`: another submit is already running on
    /// this ring.
    SubmitInProgress,
    /// `IORING_E_COMPLETION_QUEUE_TOO_FULL`: the completion queue has no room
    /// for more results. Recoverable -- drain completions.
    CompletionQueueTooFull,
}

impl RingCondition {
    /// The condition `code` names, or `None` if this crate does not name it.
    #[must_use]
    pub fn from_hresult(code: HRESULT) -> Option<Self> {
        match code {
            IORING_E_REQUIRED_FLAG_NOT_SUPPORTED => Some(Self::RequiredFlagNotSupported),
            IORING_E_VERSION_NOT_SUPPORTED => Some(Self::VersionNotSupported),
            IORING_E_SUBMISSION_QUEUE_FULL => Some(Self::SubmissionQueueFull),
            IORING_E_SUBMISSION_QUEUE_TOO_BIG => Some(Self::SubmissionQueueTooBig),
            IORING_E_COMPLETION_QUEUE_TOO_BIG => Some(Self::CompletionQueueTooBig),
            IORING_E_CORRUPT => Some(Self::Corrupt),
            IORING_E_SUBMIT_IN_PROGRESS => Some(Self::SubmitInProgress),
            IORING_E_COMPLETION_QUEUE_TOO_FULL => Some(Self::CompletionQueueTooFull),
            _ => None,
        }
    }

    /// The `IORING_E_*` constant's spelling.
    #[must_use]
    pub fn name(self) -> &'static str {
        match self {
            Self::RequiredFlagNotSupported => "IORING_E_REQUIRED_FLAG_NOT_SUPPORTED",
            Self::VersionNotSupported => "IORING_E_VERSION_NOT_SUPPORTED",
            Self::SubmissionQueueFull => "IORING_E_SUBMISSION_QUEUE_FULL",
            Self::SubmissionQueueTooBig => "IORING_E_SUBMISSION_QUEUE_TOO_BIG",
            Self::CompletionQueueTooBig => "IORING_E_COMPLETION_QUEUE_TOO_BIG",
            Self::Corrupt => "IORING_E_CORRUPT",
            Self::SubmitInProgress => "IORING_E_SUBMIT_IN_PROGRESS",
            Self::CompletionQueueTooFull => "IORING_E_COMPLETION_QUEUE_TOO_FULL",
        }
    }

    /// The raw `HRESULT` this condition is reported as.
    #[must_use]
    pub fn code(self) -> HRESULT {
        match self {
            Self::RequiredFlagNotSupported => IORING_E_REQUIRED_FLAG_NOT_SUPPORTED,
            Self::VersionNotSupported => IORING_E_VERSION_NOT_SUPPORTED,
            Self::SubmissionQueueFull => IORING_E_SUBMISSION_QUEUE_FULL,
            Self::SubmissionQueueTooBig => IORING_E_SUBMISSION_QUEUE_TOO_BIG,
            Self::CompletionQueueTooBig => IORING_E_COMPLETION_QUEUE_TOO_BIG,
            Self::Corrupt => IORING_E_CORRUPT,
            Self::SubmitInProgress => IORING_E_SUBMIT_IN_PROGRESS,
            Self::CompletionQueueTooFull => IORING_E_COMPLETION_QUEUE_TOO_FULL,
        }
    }
}

/// An error from an `IoRing` API call.
///
/// Every `IoRing` entry point reports failure as an `HRESULT`, not through
/// `GetLastError` the way most of Win32 does, so this crate cannot reuse
/// `io::Error::last_os_error` the way `windows-overlapped-io-sys` does.
///
/// # Matching on one of these
///
/// A kernel-reported failure always reaches a caller as an [`io::Error`] whose
/// [`kind`](io::Error::kind) is [`io::ErrorKind::Other`], because there is no
/// faithful `ErrorKind` for most `IORING_E_*` conditions (M10.2, D-30). The
/// `HRESULT` is not lost -- it is this value, behind a downcast -- but
/// `kind()` will not find it. `kind()` discriminates only this crate's *own*
/// rejections, which use `Unsupported`, `InvalidInput`, and `AlreadyExists`.
///
/// So a consumer branching on a ring condition -- most often
/// `IORING_E_SUBMISSION_QUEUE_FULL`, the backpressure signal every push's
/// docs name -- asks through [`IoRingErrorExt`] (M10.5), which is the
/// downcast plus the comparison named once rather than hand-rolled:
///
/// ```
/// use windows_ioring_sys::IoRingErrorExt;
///
/// fn classify(error: &std::io::Error) -> &'static str {
///     if error.is_submission_queue_full() {
///         "backpressure: submit, drain, retry"
///     } else if let Some(condition) = error.ring_condition() {
///         condition.name()
///     } else {
///         "not a ring condition"
///     }
/// }
/// ```
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct IoRingError {
    code: HRESULT,
}

impl IoRingError {
    pub(crate) fn new(code: HRESULT) -> Self {
        Self { code }
    }

    /// The raw `HRESULT`.
    #[must_use]
    pub fn code(&self) -> HRESULT {
        self.code
    }

    /// The named `IORING_E_*` constant this value matches, if any.
    ///
    /// Derived from [`IoRingError::condition`] rather than matching the
    /// `HRESULT` a second time, so the two can never disagree about which
    /// codes are named (M10.5).
    #[must_use]
    pub fn name(&self) -> Option<&'static str> {
        self.condition().map(RingCondition::name)
    }

    /// The named condition this error reports, or `None` for an `HRESULT`
    /// this crate does not name (M10.5).
    ///
    /// `match` on this when a consumer needs to distinguish several
    /// conditions; use a predicate like
    /// [`IoRingError::is_submission_queue_full`] for a single branch.
    #[must_use]
    pub fn condition(&self) -> Option<RingCondition> {
        RingCondition::from_hresult(self.code)
    }

    /// Whether this is `IORING_E_SUBMISSION_QUEUE_FULL`: the backpressure
    /// signal every push's documentation names (M3.3).
    ///
    /// Recoverable, and the one condition a normal submission loop is
    /// expected to branch on: submit what is already queued, drain
    /// completions, and retry the push.
    #[must_use]
    pub fn is_submission_queue_full(&self) -> bool {
        self.condition() == Some(RingCondition::SubmissionQueueFull)
    }

    /// Whether this is `IORING_E_COMPLETION_QUEUE_TOO_FULL`: the completion
    /// queue has no room for further results, so completions must be drained
    /// with [`crate::IoRing::try_pop`].
    #[must_use]
    pub fn is_completion_queue_too_full(&self) -> bool {
        self.condition() == Some(RingCondition::CompletionQueueTooFull)
    }

    /// Whether this is `IORING_E_SUBMIT_IN_PROGRESS`: another submit is
    /// already running on this ring.
    #[must_use]
    pub fn is_submit_in_progress(&self) -> bool {
        self.condition() == Some(RingCondition::SubmitInProgress)
    }
}

/// Ask an [`io::Error`] this crate produced about the ring condition inside
/// it (M10.5, D-30).
///
/// Exists because [`io::Error::kind`] cannot answer: every kernel-reported
/// failure is `io::ErrorKind::Other`, since there is no faithful `ErrorKind`
/// for most `IORING_E_*` conditions and inventing one would trade an honest
/// `Other` for a lossy guess. The `HRESULT` survives behind a downcast, and
/// this trait is that downcast plus the comparison, named once here instead
/// of hand-rolled at each call site.
///
/// ```no_run
/// use windows_ioring_sys::{Batch, IoRing, IoRingErrorExt, PushOptions, SharedHandle};
///
/// # fn demo(ring: &mut IoRing<Vec<u8>>, file: &SharedHandle) -> std::io::Result<()> {
/// let mut batch = Batch::new(ring);
/// match batch.read_owned(file, vec![0_u8; 4096], (), 0, PushOptions::new()) {
///     Ok(_id) => {}
///     // Backpressure, not a failure: submit and drain, then retry.
///     Err(refused) if refused.error.is_submission_queue_full() => {
///         batch.submit()?;
///         return Ok(());
///     }
///     Err(refused) => return Err(refused.into()),
/// }
/// # Ok(())
/// # }
/// ```
///
/// **Sealed**: implemented for [`io::Error`] and nothing else.
pub trait IoRingErrorExt: sealed::Sealed {
    /// The [`IoRingError`] this error wraps, if it came from this crate's
    /// report of a native `HRESULT`.
    ///
    /// `None` for this crate's *own* rejections (`Unsupported`,
    /// `InvalidInput`, `AlreadyExists`), which carry a meaningful
    /// [`io::Error::kind`] instead and never wrap an `HRESULT`.
    fn as_ioring_error(&self) -> Option<&IoRingError>;

    /// The named condition this error reports, if any.
    fn ring_condition(&self) -> Option<RingCondition>;

    /// As [`IoRingError::is_submission_queue_full`].
    fn is_submission_queue_full(&self) -> bool;

    /// As [`IoRingError::is_completion_queue_too_full`].
    fn is_completion_queue_too_full(&self) -> bool;

    /// As [`IoRingError::is_submit_in_progress`].
    fn is_submit_in_progress(&self) -> bool;
}

impl IoRingErrorExt for io::Error {
    fn as_ioring_error(&self) -> Option<&IoRingError> {
        self.get_ref()?.downcast_ref::<IoRingError>()
    }

    fn ring_condition(&self) -> Option<RingCondition> {
        self.as_ioring_error()?.condition()
    }

    fn is_submission_queue_full(&self) -> bool {
        self.as_ioring_error()
            .is_some_and(IoRingError::is_submission_queue_full)
    }

    fn is_completion_queue_too_full(&self) -> bool {
        self.as_ioring_error()
            .is_some_and(IoRingError::is_completion_queue_too_full)
    }

    fn is_submit_in_progress(&self) -> bool {
        self.as_ioring_error()
            .is_some_and(IoRingError::is_submit_in_progress)
    }
}

impl fmt::Debug for IoRingError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IoRingError")
            .field("code", &format_args!("0x{:08X}", self.code as u32))
            .finish()
    }
}

impl fmt::Display for IoRingError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.name() {
            Some(name) => write!(f, "{name} (HRESULT 0x{:08X})", self.code as u32),
            None => write!(f, "HRESULT 0x{:08X}", self.code as u32),
        }
    }
}

impl std::error::Error for IoRingError {}

/// A push the ring refused, carrying everything the push was given (M30.1,
/// [D-80](../DESIGN-NOTES.md#d-80)).
///
/// Every owned push returns this on refusal: the [`io::Error`] that explains
/// it, the payload the push took (`None` for one that takes no payload, such
/// as a flush, a cancel or a registered-span push), and the sidecar `extra`.
/// [`Batch::register_buffers`](crate::Batch::register_buffers) returns one too,
/// with the whole `Vec` of buffers as its payload and `()` as its sidecar.
/// A refused push queued nothing, so nothing will ever complete to hand these
/// back; this is the only way back.
///
/// The commonest refusal is `IORING_E_SUBMISSION_QUEUE_FULL`. No caller can
/// rule it out beforehand, because building the operation is the test, and it
/// is exactly the case where the caller wants the same buffer for a retry:
///
/// ```no_run
/// use windows_ioring_sys::{Batch, IoRing, IoRingErrorExt, PushOptions, SharedHandle};
///
/// # fn demo(ring: &mut IoRing<Vec<u8>>, file: &SharedHandle) -> std::io::Result<()> {
/// let mut batch = Batch::new(ring);
/// if let Err(refused) = batch.read_owned(file, vec![0_u8; 4096], (), 0, PushOptions::new()) {
///     if !refused.error.is_submission_queue_full() {
///         return Err(refused.into());
///     }
///     // Backpressure, not a failure: submit what is queued, then retry with
///     // the buffer the refusal handed back.
///     batch.submit()?;
///     let buffer = refused.payload.expect("a read takes a payload");
///     let mut batch = Batch::new(ring);
///     batch.read_owned(file, buffer, (), 0, PushOptions::new())?;
/// }
/// # Ok(())
/// # }
/// ```
///
/// A caller that wants only the error converts with `?` or `.into()`, which
/// keeps the error unchanged (so [`IoRingErrorExt`] still finds the
/// `HRESULT`) and drops the payload and sidecar there, by the caller's
/// choice rather than the ring's.
///
/// The ring-owned parts of an operation are not returned: a file guard and a
/// registered-buffer use belong to the ring, never to the caller, and are
/// released.
///
/// `Debug` and `Display` are written by hand so they place no bound on `T`
/// or `X`. A consumer's buffer type need not be `Debug` for `.expect(..)` to
/// compile.
#[non_exhaustive]
pub struct PushRefused<T, X> {
    /// Why the push was refused.
    pub error: io::Error,
    /// The payload the push took, or `None` if it took none.
    pub payload: Option<T>,
    /// The sidecar the push took.
    pub extra: X,
}

impl<T, X> PushRefused<T, X> {
    pub(crate) fn new(error: io::Error, payload: Option<T>, extra: X) -> Self {
        Self {
            error,
            payload,
            extra,
        }
    }

    /// Discard the payload and sidecar, keeping only the error.
    #[must_use]
    pub fn into_error(self) -> io::Error {
        self.error
    }
}

impl<T, X> From<PushRefused<T, X>> for io::Error {
    fn from(refused: PushRefused<T, X>) -> Self {
        refused.error
    }
}

impl<T, X> fmt::Debug for PushRefused<T, X> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("PushRefused")
            .field("error", &self.error)
            .field("payload", &self.payload.as_ref().map(|_| ".."))
            .field("extra", &format_args!(".."))
            .finish()
    }
}

impl<T, X> fmt::Display for PushRefused<T, X> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "push refused: {}", self.error)
    }
}

impl<T, X> std::error::Error for PushRefused<T, X> {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(&self.error)
    }
}

/// Convert a native call's `HRESULT` into `Ok(())` or a wrapped
/// [`IoRingError`], following the `FAILED(hr)` convention (a negative
/// `HRESULT` is a failure; `S_OK` and `S_FALSE` are both non-negative
/// successes).
pub(crate) fn check(hr: HRESULT) -> io::Result<()> {
    if hr < 0 {
        Err(io::Error::other(IoRingError::new(hr)))
    } else {
        Ok(())
    }
}

#[cfg(test)]
mod tests;