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;