Skip to main content

windows_ioring_sys/
error.rs

1// Copyright (c) 2026 Mike Grier
2//! IoRing errors: `HRESULT`, not the `GetLastError` convention.
3
4use std::fmt;
5use std::io;
6
7use windows_sys::Win32::Foundation::{
8    IORING_E_COMPLETION_QUEUE_TOO_BIG, IORING_E_COMPLETION_QUEUE_TOO_FULL, IORING_E_CORRUPT,
9    IORING_E_REQUIRED_FLAG_NOT_SUPPORTED, IORING_E_SUBMISSION_QUEUE_FULL,
10    IORING_E_SUBMISSION_QUEUE_TOO_BIG, IORING_E_SUBMIT_IN_PROGRESS, IORING_E_VERSION_NOT_SUPPORTED,
11};
12use windows_sys::core::HRESULT;
13
14mod sealed {
15    pub trait Sealed {}
16    impl Sealed for std::io::Error {}
17}
18
19/// A named `IORING_E_*` condition (M10.5).
20///
21/// The `HRESULT` -> name mapping is defined here once and everywhere else asks
22/// (`IoRingError::name` is derived from this, not a second copy of it), so a
23/// new condition cannot be added to one and forgotten in the other.
24///
25/// `#[non_exhaustive]`: the kernel's error set can grow, and a consumer must
26/// not be able to write an exhaustive `match` that a new variant would break
27/// -- the same reasoning as [`crate::Op`]'s (D-7).
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29#[non_exhaustive]
30pub enum RingCondition {
31    /// `IORING_E_REQUIRED_FLAG_NOT_SUPPORTED`.
32    RequiredFlagNotSupported,
33    /// `IORING_E_VERSION_NOT_SUPPORTED`.
34    VersionNotSupported,
35    /// `IORING_E_SUBMISSION_QUEUE_FULL`: the submission queue has no room.
36    /// Recoverable -- submit what is queued, drain completions, retry.
37    SubmissionQueueFull,
38    /// `IORING_E_SUBMISSION_QUEUE_TOO_BIG`.
39    SubmissionQueueTooBig,
40    /// `IORING_E_COMPLETION_QUEUE_TOO_BIG`.
41    CompletionQueueTooBig,
42    /// `IORING_E_CORRUPT`: the ring's shared state is inconsistent. Fatal.
43    Corrupt,
44    /// `IORING_E_SUBMIT_IN_PROGRESS`: another submit is already running on
45    /// this ring.
46    SubmitInProgress,
47    /// `IORING_E_COMPLETION_QUEUE_TOO_FULL`: the completion queue has no room
48    /// for more results. Recoverable -- drain completions.
49    CompletionQueueTooFull,
50}
51
52impl RingCondition {
53    /// The condition `code` names, or `None` if this crate does not name it.
54    #[must_use]
55    pub fn from_hresult(code: HRESULT) -> Option<Self> {
56        match code {
57            IORING_E_REQUIRED_FLAG_NOT_SUPPORTED => Some(Self::RequiredFlagNotSupported),
58            IORING_E_VERSION_NOT_SUPPORTED => Some(Self::VersionNotSupported),
59            IORING_E_SUBMISSION_QUEUE_FULL => Some(Self::SubmissionQueueFull),
60            IORING_E_SUBMISSION_QUEUE_TOO_BIG => Some(Self::SubmissionQueueTooBig),
61            IORING_E_COMPLETION_QUEUE_TOO_BIG => Some(Self::CompletionQueueTooBig),
62            IORING_E_CORRUPT => Some(Self::Corrupt),
63            IORING_E_SUBMIT_IN_PROGRESS => Some(Self::SubmitInProgress),
64            IORING_E_COMPLETION_QUEUE_TOO_FULL => Some(Self::CompletionQueueTooFull),
65            _ => None,
66        }
67    }
68
69    /// The `IORING_E_*` constant's spelling.
70    #[must_use]
71    pub fn name(self) -> &'static str {
72        match self {
73            Self::RequiredFlagNotSupported => "IORING_E_REQUIRED_FLAG_NOT_SUPPORTED",
74            Self::VersionNotSupported => "IORING_E_VERSION_NOT_SUPPORTED",
75            Self::SubmissionQueueFull => "IORING_E_SUBMISSION_QUEUE_FULL",
76            Self::SubmissionQueueTooBig => "IORING_E_SUBMISSION_QUEUE_TOO_BIG",
77            Self::CompletionQueueTooBig => "IORING_E_COMPLETION_QUEUE_TOO_BIG",
78            Self::Corrupt => "IORING_E_CORRUPT",
79            Self::SubmitInProgress => "IORING_E_SUBMIT_IN_PROGRESS",
80            Self::CompletionQueueTooFull => "IORING_E_COMPLETION_QUEUE_TOO_FULL",
81        }
82    }
83
84    /// The raw `HRESULT` this condition is reported as.
85    #[must_use]
86    pub fn code(self) -> HRESULT {
87        match self {
88            Self::RequiredFlagNotSupported => IORING_E_REQUIRED_FLAG_NOT_SUPPORTED,
89            Self::VersionNotSupported => IORING_E_VERSION_NOT_SUPPORTED,
90            Self::SubmissionQueueFull => IORING_E_SUBMISSION_QUEUE_FULL,
91            Self::SubmissionQueueTooBig => IORING_E_SUBMISSION_QUEUE_TOO_BIG,
92            Self::CompletionQueueTooBig => IORING_E_COMPLETION_QUEUE_TOO_BIG,
93            Self::Corrupt => IORING_E_CORRUPT,
94            Self::SubmitInProgress => IORING_E_SUBMIT_IN_PROGRESS,
95            Self::CompletionQueueTooFull => IORING_E_COMPLETION_QUEUE_TOO_FULL,
96        }
97    }
98}
99
100/// An error from an `IoRing` API call.
101///
102/// Every `IoRing` entry point reports failure as an `HRESULT`, not through
103/// `GetLastError` the way most of Win32 does, so this crate cannot reuse
104/// `io::Error::last_os_error` the way `windows-overlapped-io-sys` does.
105///
106/// # Matching on one of these
107///
108/// A kernel-reported failure always reaches a caller as an [`io::Error`] whose
109/// [`kind`](io::Error::kind) is [`io::ErrorKind::Other`], because there is no
110/// faithful `ErrorKind` for most `IORING_E_*` conditions (M10.2, D-30). The
111/// `HRESULT` is not lost -- it is this value, behind a downcast -- but
112/// `kind()` will not find it. `kind()` discriminates only this crate's *own*
113/// rejections, which use `Unsupported`, `InvalidInput`, and `AlreadyExists`.
114///
115/// So a consumer branching on a ring condition -- most often
116/// `IORING_E_SUBMISSION_QUEUE_FULL`, the backpressure signal every push's
117/// docs name -- asks through [`IoRingErrorExt`] (M10.5), which is the
118/// downcast plus the comparison named once rather than hand-rolled:
119///
120/// ```
121/// use windows_ioring_sys::IoRingErrorExt;
122///
123/// fn classify(error: &std::io::Error) -> &'static str {
124///     if error.is_submission_queue_full() {
125///         "backpressure: submit, drain, retry"
126///     } else if let Some(condition) = error.ring_condition() {
127///         condition.name()
128///     } else {
129///         "not a ring condition"
130///     }
131/// }
132/// ```
133#[derive(Clone, Copy, PartialEq, Eq)]
134pub struct IoRingError {
135    code: HRESULT,
136}
137
138impl IoRingError {
139    pub(crate) fn new(code: HRESULT) -> Self {
140        Self { code }
141    }
142
143    /// The raw `HRESULT`.
144    #[must_use]
145    pub fn code(&self) -> HRESULT {
146        self.code
147    }
148
149    /// The named `IORING_E_*` constant this value matches, if any.
150    ///
151    /// Derived from [`IoRingError::condition`] rather than matching the
152    /// `HRESULT` a second time, so the two can never disagree about which
153    /// codes are named (M10.5).
154    #[must_use]
155    pub fn name(&self) -> Option<&'static str> {
156        self.condition().map(RingCondition::name)
157    }
158
159    /// The named condition this error reports, or `None` for an `HRESULT`
160    /// this crate does not name (M10.5).
161    ///
162    /// `match` on this when a consumer needs to distinguish several
163    /// conditions; use a predicate like
164    /// [`IoRingError::is_submission_queue_full`] for a single branch.
165    #[must_use]
166    pub fn condition(&self) -> Option<RingCondition> {
167        RingCondition::from_hresult(self.code)
168    }
169
170    /// Whether this is `IORING_E_SUBMISSION_QUEUE_FULL`: the backpressure
171    /// signal every push's documentation names (M3.3).
172    ///
173    /// Recoverable, and the one condition a normal submission loop is
174    /// expected to branch on: submit what is already queued, drain
175    /// completions, and retry the push.
176    #[must_use]
177    pub fn is_submission_queue_full(&self) -> bool {
178        self.condition() == Some(RingCondition::SubmissionQueueFull)
179    }
180
181    /// Whether this is `IORING_E_COMPLETION_QUEUE_TOO_FULL`: the completion
182    /// queue has no room for further results, so completions must be drained
183    /// with [`crate::IoRing::try_pop`].
184    #[must_use]
185    pub fn is_completion_queue_too_full(&self) -> bool {
186        self.condition() == Some(RingCondition::CompletionQueueTooFull)
187    }
188
189    /// Whether this is `IORING_E_SUBMIT_IN_PROGRESS`: another submit is
190    /// already running on this ring.
191    #[must_use]
192    pub fn is_submit_in_progress(&self) -> bool {
193        self.condition() == Some(RingCondition::SubmitInProgress)
194    }
195}
196
197/// Ask an [`io::Error`] this crate produced about the ring condition inside
198/// it (M10.5, D-30).
199///
200/// Exists because [`io::Error::kind`] cannot answer: every kernel-reported
201/// failure is `io::ErrorKind::Other`, since there is no faithful `ErrorKind`
202/// for most `IORING_E_*` conditions and inventing one would trade an honest
203/// `Other` for a lossy guess. The `HRESULT` survives behind a downcast, and
204/// this trait is that downcast plus the comparison, named once here instead
205/// of hand-rolled at each call site.
206///
207/// ```no_run
208/// use windows_ioring_sys::{Batch, IoRing, IoRingErrorExt, PushOptions, SharedHandle};
209///
210/// # fn demo(ring: &mut IoRing<Vec<u8>>, file: &SharedHandle) -> std::io::Result<()> {
211/// let mut batch = Batch::new(ring);
212/// match batch.read_owned(file, vec![0_u8; 4096], (), 0, PushOptions::new()) {
213///     Ok(_id) => {}
214///     // Backpressure, not a failure: submit and drain, then retry.
215///     Err(refused) if refused.error.is_submission_queue_full() => {
216///         batch.submit()?;
217///         return Ok(());
218///     }
219///     Err(refused) => return Err(refused.into()),
220/// }
221/// # Ok(())
222/// # }
223/// ```
224///
225/// **Sealed**: implemented for [`io::Error`] and nothing else.
226pub trait IoRingErrorExt: sealed::Sealed {
227    /// The [`IoRingError`] this error wraps, if it came from this crate's
228    /// report of a native `HRESULT`.
229    ///
230    /// `None` for this crate's *own* rejections (`Unsupported`,
231    /// `InvalidInput`, `AlreadyExists`), which carry a meaningful
232    /// [`io::Error::kind`] instead and never wrap an `HRESULT`.
233    fn as_ioring_error(&self) -> Option<&IoRingError>;
234
235    /// The named condition this error reports, if any.
236    fn ring_condition(&self) -> Option<RingCondition>;
237
238    /// As [`IoRingError::is_submission_queue_full`].
239    fn is_submission_queue_full(&self) -> bool;
240
241    /// As [`IoRingError::is_completion_queue_too_full`].
242    fn is_completion_queue_too_full(&self) -> bool;
243
244    /// As [`IoRingError::is_submit_in_progress`].
245    fn is_submit_in_progress(&self) -> bool;
246}
247
248impl IoRingErrorExt for io::Error {
249    fn as_ioring_error(&self) -> Option<&IoRingError> {
250        self.get_ref()?.downcast_ref::<IoRingError>()
251    }
252
253    fn ring_condition(&self) -> Option<RingCondition> {
254        self.as_ioring_error()?.condition()
255    }
256
257    fn is_submission_queue_full(&self) -> bool {
258        self.as_ioring_error()
259            .is_some_and(IoRingError::is_submission_queue_full)
260    }
261
262    fn is_completion_queue_too_full(&self) -> bool {
263        self.as_ioring_error()
264            .is_some_and(IoRingError::is_completion_queue_too_full)
265    }
266
267    fn is_submit_in_progress(&self) -> bool {
268        self.as_ioring_error()
269            .is_some_and(IoRingError::is_submit_in_progress)
270    }
271}
272
273impl fmt::Debug for IoRingError {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        f.debug_struct("IoRingError")
276            .field("code", &format_args!("0x{:08X}", self.code as u32))
277            .finish()
278    }
279}
280
281impl fmt::Display for IoRingError {
282    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283        match self.name() {
284            Some(name) => write!(f, "{name} (HRESULT 0x{:08X})", self.code as u32),
285            None => write!(f, "HRESULT 0x{:08X}", self.code as u32),
286        }
287    }
288}
289
290impl std::error::Error for IoRingError {}
291
292/// A push the ring refused, carrying everything the push was given (M30.1,
293/// [D-80](../DESIGN-NOTES.md#d-80)).
294///
295/// Every owned push returns this on refusal: the [`io::Error`] that explains
296/// it, the payload the push took (`None` for one that takes no payload, such
297/// as a flush, a cancel or a registered-span push), and the sidecar `extra`.
298/// [`Batch::register_buffers`](crate::Batch::register_buffers) returns one too,
299/// with the whole `Vec` of buffers as its payload and `()` as its sidecar.
300/// A refused push queued nothing, so nothing will ever complete to hand these
301/// back; this is the only way back.
302///
303/// The commonest refusal is `IORING_E_SUBMISSION_QUEUE_FULL`. No caller can
304/// rule it out beforehand, because building the operation is the test, and it
305/// is exactly the case where the caller wants the same buffer for a retry:
306///
307/// ```no_run
308/// use windows_ioring_sys::{Batch, IoRing, IoRingErrorExt, PushOptions, SharedHandle};
309///
310/// # fn demo(ring: &mut IoRing<Vec<u8>>, file: &SharedHandle) -> std::io::Result<()> {
311/// let mut batch = Batch::new(ring);
312/// if let Err(refused) = batch.read_owned(file, vec![0_u8; 4096], (), 0, PushOptions::new()) {
313///     if !refused.error.is_submission_queue_full() {
314///         return Err(refused.into());
315///     }
316///     // Backpressure, not a failure: submit what is queued, then retry with
317///     // the buffer the refusal handed back.
318///     batch.submit()?;
319///     let buffer = refused.payload.expect("a read takes a payload");
320///     let mut batch = Batch::new(ring);
321///     batch.read_owned(file, buffer, (), 0, PushOptions::new())?;
322/// }
323/// # Ok(())
324/// # }
325/// ```
326///
327/// A caller that wants only the error converts with `?` or `.into()`, which
328/// keeps the error unchanged (so [`IoRingErrorExt`] still finds the
329/// `HRESULT`) and drops the payload and sidecar there, by the caller's
330/// choice rather than the ring's.
331///
332/// The ring-owned parts of an operation are not returned: a file guard and a
333/// registered-buffer use belong to the ring, never to the caller, and are
334/// released.
335///
336/// `Debug` and `Display` are written by hand so they place no bound on `T`
337/// or `X`. A consumer's buffer type need not be `Debug` for `.expect(..)` to
338/// compile.
339#[non_exhaustive]
340pub struct PushRefused<T, X> {
341    /// Why the push was refused.
342    pub error: io::Error,
343    /// The payload the push took, or `None` if it took none.
344    pub payload: Option<T>,
345    /// The sidecar the push took.
346    pub extra: X,
347}
348
349impl<T, X> PushRefused<T, X> {
350    pub(crate) fn new(error: io::Error, payload: Option<T>, extra: X) -> Self {
351        Self {
352            error,
353            payload,
354            extra,
355        }
356    }
357
358    /// Discard the payload and sidecar, keeping only the error.
359    #[must_use]
360    pub fn into_error(self) -> io::Error {
361        self.error
362    }
363}
364
365impl<T, X> From<PushRefused<T, X>> for io::Error {
366    fn from(refused: PushRefused<T, X>) -> Self {
367        refused.error
368    }
369}
370
371impl<T, X> fmt::Debug for PushRefused<T, X> {
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        f.debug_struct("PushRefused")
374            .field("error", &self.error)
375            .field("payload", &self.payload.as_ref().map(|_| ".."))
376            .field("extra", &format_args!(".."))
377            .finish()
378    }
379}
380
381impl<T, X> fmt::Display for PushRefused<T, X> {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        write!(f, "push refused: {}", self.error)
384    }
385}
386
387impl<T, X> std::error::Error for PushRefused<T, X> {
388    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
389        Some(&self.error)
390    }
391}
392
393/// Convert a native call's `HRESULT` into `Ok(())` or a wrapped
394/// [`IoRingError`], following the `FAILED(hr)` convention (a negative
395/// `HRESULT` is a failure; `S_OK` and `S_FALSE` are both non-negative
396/// successes).
397pub(crate) fn check(hr: HRESULT) -> io::Result<()> {
398    if hr < 0 {
399        Err(io::Error::other(IoRingError::new(hr)))
400    } else {
401        Ok(())
402    }
403}
404
405#[cfg(test)]
406mod tests;