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, SharedFile};
209///
210/// # fn demo(ring: &mut IoRing, file: &SharedFile) -> std::io::Result<()> {
211/// let mut batch = Batch::new(ring);
212/// match batch.read(file, vec![0_u8; 4096], 0, PushOptions::new()) {
213///     Ok(_token) => {}
214///     // Backpressure, not a failure: submit and drain, then retry.
215///     Err(error) if error.is_submission_queue_full() => {
216///         batch.submit()?;
217///         return Ok(());
218///     }
219///     Err(error) => return Err(error),
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/// Convert a native call's `HRESULT` into `Ok(())` or a wrapped
293/// [`IoRingError`], following the `FAILED(hr)` convention (a negative
294/// `HRESULT` is a failure; `S_OK` and `S_FALSE` are both non-negative
295/// successes).
296pub(crate) fn check(hr: HRESULT) -> io::Result<()> {
297    if hr < 0 {
298        Err(io::Error::other(IoRingError::new(hr)))
299    } else {
300        Ok(())
301    }
302}
303
304#[cfg(test)]
305mod tests;