Skip to main content

windows_threadpool_sys/
io.rs

1// Copyright (c) 2026 Mike Grier
2//! Thread-pool I/O (`TP_IO`): a completion backend over the shared overlapped
3//! submission seam.
4//!
5//! [`ThreadpoolIo`] is the third completion backend for the overlapped model
6//! defined by `windows-overlapped-io-sys`. It reuses that crate's endpoint
7//! ownership and pinned [`Operation`] storage unchanged, and adds the two
8//! concerns that only the thread pool has:
9//!
10//! - **Balanced accounting.** `StartThreadpoolIo` must precede every overlapped
11//!   operation, and every start must be balanced exactly once -- by the I/O
12//!   callback when a completion will be delivered, or by `CancelThreadpoolIo`
13//!   when the submission failed immediately or completed synchronously on a
14//!   handle in `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` mode.
15//! - **Callback-driven reclamation.** The pool, not the caller, dequeues
16//!   completions. Each callback reclaims its operation's storage: typed through
17//!   [`IoCompletion::claim`] when the payload type is known, or generically
18//!   through the seam's `reclaim_overlapped` when the callback lets the
19//!   completion drop.
20//!
21//! The pool's internal completion port is system-managed and is never exposed:
22//! this backend neither posts to it nor dequeues from it.
23
24use std::cell::Cell;
25use std::fmt;
26use std::io;
27use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle};
28use std::ptr;
29use std::sync::Arc;
30
31use windows_overlapped_io_sys::{
32    EndpointHandle, EndpointObserver, Issued, Operation, OperationId, OperationRegistry,
33    OperationState, Submitted, UnassociatedEndpoint, reclaim_overlapped,
34};
35use windows_sys::Win32::Foundation::{FALSE, HANDLE, NO_ERROR};
36use windows_sys::Win32::System::IO::{CancelIoEx, OVERLAPPED};
37use windows_sys::Win32::System::Threading::{
38    CancelThreadpoolIo, CloseThreadpoolIo, CreateThreadpoolIo, PTP_CALLBACK_INSTANCE, PTP_IO,
39    StartThreadpoolIo, WaitForThreadpoolIoCallbacks,
40};
41
42use crate::callback_env::CallbackEnviron;
43
44/// Heap-allocated callback state kept alive for the lifetime of the `TP_IO`
45/// object, and freed only after [`ThreadpoolIo::drop`] has drained every
46/// operation and waited for every executing callback.
47struct IoContext {
48    live: Arc<OperationRegistry>,
49    /// This pool's entry in the self-heal registry.
50    ///
51    /// Held for its `Drop`, not read. The claim keeps the entry alive while
52    /// this object exists, which is what makes the entry's pre-created repair
53    /// work object available -- and bound to the pool, deferring its free --
54    /// if a wait on the same pool is later cancelled. Only a wait reads a
55    /// registration, because only a wait reaches the removal primitive that
56    /// owes a repair.
57    #[allow(dead_code)]
58    registration: crate::heal::Registration,
59    callback: Box<dyn Fn(&IoCompletion) + Send + Sync + 'static>,
60}
61
62/// Trampoline from the raw `PTP_WIN32_IO_CALLBACK` ABI into the boxed closure.
63///
64/// SAFETY: `context` must point to a live [`IoContext`] for the entire duration
65/// of every callback invocation, and `overlapped` must be the identity of an
66/// operation submitted through [`ThreadpoolIo::submit`] whose storage has not
67/// been reclaimed. [`ThreadpoolIo`]'s `Drop` ordering guarantees both.
68unsafe extern "system" fn io_trampoline(
69    _instance: PTP_CALLBACK_INSTANCE,
70    context: *mut core::ffi::c_void,
71    overlapped: *mut core::ffi::c_void,
72    io_result: u32,
73    bytes_transferred: usize,
74    _io: PTP_IO,
75) {
76    // SAFETY: context is a valid *mut IoContext for the full callback duration (see Drop).
77    let ctx = unsafe { &*(context as *const IoContext) };
78    crate::trace_record!("io", "trampoline-entered", overlapped as usize, io_result);
79    // Stamped before the callback, not after: a dispatch that is still running
80    // is evidence the pool is live, and a long callback must not look like
81    // silence to the self-heal.
82
83    let overlapped = overlapped.cast::<OVERLAPPED>();
84
85    // Deregister before running any user code, and take the identity from the
86    // same lookup. This balances the `StartThreadpoolIo` for this operation.
87    //
88    // It must happen here rather than after the callback: the kernel is finished
89    // with the operation by the time its callback is entered, and the callback
90    // may take ownership of the storage with `IoCompletion::claim` and drop it
91    // immediately, so the address can become available for reuse at any point
92    // from here on. The invariant is that an address is **never registered while
93    // it is available for reuse** -- otherwise a concurrent submission handed
94    // that address would collide with the entry still sitting in the registry.
95    let id = ctx.live.remove(overlapped);
96
97    // Not contained: the callback contract requires that it not unwind, and a
98    // callback that breaks it aborts here rather than being silently forgiven.
99    let completion = IoCompletion {
100        overlapped,
101        id,
102        io_result,
103        bytes_transferred,
104        claimed: Cell::new(false),
105    };
106    (ctx.callback)(&completion);
107    crate::trace_record!("io", "trampoline-left", overlapped as usize, io_result);
108}
109
110/// An owned thread-pool I/O object bound to one overlapped endpoint.
111///
112/// Creating a `ThreadpoolIo` consumes an [`UnassociatedEndpoint`], which is the
113/// same one-time, consuming association the other backends use: `CreateThreadpoolIo`
114/// binds the handle to the pool's internal completion port, and no second
115/// association is possible afterward.
116///
117/// Every [`ThreadpoolIo::submit`] is paired with exactly one balancing action, so
118/// [`ThreadpoolIo::outstanding`] is always the number of operations whose storage
119/// the kernel or pool still owns. `Drop` never frees that storage early: it
120/// cancels what is outstanding, waits for the resulting callbacks, waits for any
121/// callback still executing, and only then releases the object, the handle, and
122/// the callback context.
123///
124/// # Examples
125///
126/// Read a file with one overlapped operation. Submission is `unsafe` because
127/// only the caller can guarantee the native call it issues matches the operation
128/// it was handed; everything after that is safe.
129///
130/// ```
131/// use std::io;
132/// use std::os::windows::io::AsRawHandle;
133/// use std::ptr;
134/// use std::sync::mpsc;
135/// use windows_overlapped_io_sys::{Issued, Operation, Submitted, UnassociatedEndpoint};
136/// use windows_sys::Win32::Foundation::ERROR_IO_PENDING;
137/// use windows_sys::Win32::Storage::FileSystem::ReadFile;
138/// use windows_threadpool_sys::io::{IoCompletion, ThreadpoolIo};
139///
140/// let path = std::env::temp_dir().join(format!("wtps-doc-{}.tmp", std::process::id()));
141/// std::fs::write(&path, b"overlapped hello")?;
142///
143/// let endpoint = UnassociatedEndpoint::open(&path, true, false, 0)?;
144/// let (tx, rx) = mpsc::channel();
145/// let sender = std::sync::Mutex::new(tx);
146///
147/// let tp = ThreadpoolIo::new(endpoint, move |completion: &IoCompletion| {
148///     // SAFETY: this object only ever carries `Operation<()>`, submitted
149///     // below, and each completion is claimed exactly once.
150///     let _operation = unsafe { completion.claim::<()>() };
151///     let _ = sender.lock().expect("send").send(completion.bytes_transferred());
152/// }, None)?;
153///
154/// let mut buffer = [0_u8; 64];
155/// let buf_ptr = buffer.as_mut_ptr();
156/// let buf_len = buffer.len() as u32;
157///
158/// let mut operation = Operation::new(());
159/// operation.set_offset(0);
160///
161/// // SAFETY: issues exactly one overlapped ReadFile into `buffer`, which stays
162/// // alive until the completion is received below. The handle is not in
163/// // skip-on-success mode, so both synchronous success and ERROR_IO_PENDING
164/// // deliver a completion callback.
165/// let submitted = unsafe {
166///     tp.submit(operation, |handle, overlapped| {
167///         let ok = ReadFile(handle.as_raw_handle(), buf_ptr, buf_len, ptr::null_mut(), overlapped);
168///         if ok != 0 {
169///             return Ok(Issued::Pending);
170///         }
171///         let error = io::Error::last_os_error();
172///         if error.raw_os_error() == Some(ERROR_IO_PENDING as i32) {
173///             return Ok(Issued::Pending);
174///         }
175///         Err(error)
176///     })
177/// };
178/// assert!(matches!(submitted, Submitted::Pending(_)));
179///
180/// let read = rx.recv().expect("a completion");
181/// tp.run_down();
182/// assert_eq!(&buffer[..read], b"overlapped hello");
183///
184/// drop(tp);
185/// let _ = std::fs::remove_file(&path);
186/// # Ok::<(), std::io::Error>(())
187/// ```
188pub struct ThreadpoolIo {
189    tp_io: PTP_IO,
190    // Shared with any observers of the endpoint, which may keep the handle open after this drops.
191    handle: EndpointHandle,
192    // Kept alive as a raw pointer until Drop has drained and waited.
193    context: *mut IoContext,
194    live: Arc<OperationRegistry>,
195}
196
197// SAFETY: PTP_IO is a cross-thread pool object and EndpointHandle is Send + Sync.
198// `context` points to an IoContext whose callback is Fn + Send + Sync and whose
199// registry is an Arc<OperationRegistry>; it is only read (never reassigned)
200// until Drop frees it after all callbacks have finished.
201unsafe impl Send for ThreadpoolIo {}
202unsafe impl Sync for ThreadpoolIo {}
203
204impl ThreadpoolIo {
205    /// Bind an overlapped endpoint to the thread pool, invoking `callback` for
206    /// every operation completion the pool delivers.
207    ///
208    /// Pass `Some(env)` to select a private pool or callback priority; `None`
209    /// uses the process-default pool with default priority.
210    ///
211    /// Do **not** point `env` at a cleanup group. A `TP_IO` object must not be
212    /// closed while an overlapped operation is outstanding, because the kernel
213    /// still owns that operation's storage, and a group's bulk release has no
214    /// way to establish that -- which is why
215    /// [`CleanupGroup`](crate::cleanup_group::CleanupGroup) has no `create_io`.
216    /// A group would also close this object while its own `Drop` still expects
217    /// to, closing it twice. Let `Drop` run it down instead: it cancels, drains,
218    /// and only then closes.
219    ///
220    /// The callback runs on a shared, process-managed pool thread. It must
221    /// restore any thread state it changes, must not terminate its thread, and
222    /// must not block waiting on this object's rundown. It must not panic: a
223    /// panic unwinds to the `extern "system"` trampoline and aborts the process.
224    ///
225    /// # Errors
226    ///
227    /// Returns the error from `CreateThreadpoolIo`, most commonly when the
228    /// handle was not opened for overlapped I/O.
229    pub fn new<F>(
230        endpoint: UnassociatedEndpoint,
231        callback: F,
232        env: Option<&mut CallbackEnviron>,
233    ) -> io::Result<Self>
234    where
235        F: Fn(&IoCompletion) + Send + Sync + 'static,
236    {
237        let handle = endpoint.into_endpoint_handle();
238        let live = Arc::new(OperationRegistry::new());
239        // Read before `env` is consumed below, and registered before the object
240        // exists: the entry must be able to repair this pool from the moment
241        // anything of ours can dispatch on it.
242        let registration = crate::heal::register(crate::heal::key_of(env.as_deref()));
243        let context = Box::into_raw(Box::new(IoContext {
244            live: Arc::clone(&live),
245            registration,
246            callback: Box::new(callback),
247        }));
248
249        let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
250
251        let tp_io = crate::trace_call!("CreateThreadpoolIo", handle.as_raw_handle() as usize, 0, {
252            // SAFETY: the handle is a live overlapped endpoint that no other backend
253            // has associated, context is a valid heap pointer that outlives every
254            // callback, and env_ptr is valid (or null) for the duration of this call.
255            unsafe {
256                CreateThreadpoolIo(
257                    handle.as_raw_handle(),
258                    Some(io_trampoline),
259                    context.cast(),
260                    env_ptr.cast_const(),
261                )
262            }
263        });
264
265        if tp_io == 0 {
266            let error = io::Error::last_os_error();
267            // SAFETY: the pool never saw context; reclaim it immediately.
268            unsafe { drop(Box::from_raw(context)) };
269            return Err(error);
270        }
271
272        crate::trace_record!("io", "created", tp_io, handle.as_raw_handle() as usize);
273        Ok(Self {
274            tp_io,
275            handle,
276            context,
277            live,
278        })
279    }
280
281    /// Borrow the underlying handle for issuing native operations.
282    #[must_use]
283    pub fn handle(&self) -> BorrowedHandle<'_> {
284        self.handle.as_handle()
285    }
286
287    /// A new observer of this object's handle, which keeps the handle open
288    /// after this object drops.
289    ///
290    /// An observer is not an endpoint, so it cannot be bound to a second
291    /// `TP_IO` object:
292    ///
293    /// ```compile_fail,E0308
294    /// # use windows_threadpool_sys::io::ThreadpoolIo;
295    /// fn rebind(tp: &ThreadpoolIo) -> std::io::Result<ThreadpoolIo> {
296    ///     ThreadpoolIo::new(tp.observer(), |_| {}, None)
297    /// }
298    /// ```
299    ///
300    /// What that rejects is the observer, not the call: an endpoint compiles.
301    ///
302    /// ```
303    /// # use windows_threadpool_sys::io::ThreadpoolIo;
304    /// # use windows_overlapped_io_sys::UnassociatedEndpoint;
305    /// fn bind(endpoint: UnassociatedEndpoint) -> std::io::Result<ThreadpoolIo> {
306    ///     ThreadpoolIo::new(endpoint, |_| {}, None)
307    /// }
308    /// ```
309    #[must_use]
310    pub fn observer(&self) -> EndpointObserver {
311        self.handle.observer()
312    }
313
314    /// The number of submitted operations whose completion callback has not yet
315    /// started.
316    ///
317    /// A `TP_IO` callback deregisters its operation on entry -- before it can
318    /// claim or drop the storage -- so this counts operations still awaiting
319    /// their callback, not live allocations: it can read zero while a final
320    /// callback is still running and its operation's storage is still alive. Use
321    /// [`run_down`](Self::run_down) to wait for callbacks to finish. It is
322    /// equivalently the number of `StartThreadpoolIo` calls not yet balanced by a
323    /// callback start or a `CancelThreadpoolIo`.
324    #[must_use]
325    pub fn outstanding(&self) -> usize {
326        self.live.len()
327    }
328
329    /// Submit an owned operation on this endpoint.
330    ///
331    /// `StartThreadpoolIo` is issued before `issue` runs, as the SDK requires,
332    /// and is balanced exactly once: by the I/O callback on the
333    /// [`Issued::Pending`] path, or by `CancelThreadpoolIo` here on the
334    /// synchronous-completion and immediate-failure paths.
335    ///
336    /// `issue` performs the single native overlapped call using the endpoint's
337    /// handle and the operation's stable `OVERLAPPED` pointer, and classifies the
338    /// outcome as an [`Issued`]: [`Issued::Pending`] when the pool will deliver a
339    /// completion callback, or [`Issued::Completed`] when the call finished
340    /// synchronously and no callback will arrive -- the outcome a handle in
341    /// `FILE_SKIP_COMPLETION_PORT_ON_SUCCESS` mode reports on synchronous
342    /// success. It returns `Err` for an immediate failure that yields no
343    /// callback.
344    ///
345    /// On the pending path the operation's storage is transferred out and is
346    /// recovered later inside the callback with [`IoCompletion::claim`]. On the
347    /// synchronous and failure paths the operation is returned intact through
348    /// [`Submitted`] so its storage can be reused or inspected.
349    ///
350    /// # Panics
351    ///
352    /// Panics if this object already has a live operation registered at the new
353    /// operation's storage address. That cannot happen through ordinary use --
354    /// `operation` owns freshly boxed storage -- and indicates a defect in this
355    /// crate's own bookkeeping rather than in the calling code. See
356    /// `OperationRegistry::insert` in `windows-overlapped-io-sys` for the
357    /// invariant involved.
358    ///
359    /// # Safety
360    ///
361    /// `issue` must start exactly one overlapped operation using the provided
362    /// `OVERLAPPED` pointer and no other storage, and must classify the outcome
363    /// correctly: [`Issued::Pending`] only when a completion callback will be
364    /// delivered for this object, [`Issued::Completed`] only when the operation
365    /// is already complete and no callback will arrive, and `Err` only when the
366    /// submission failed and no callback will arrive. Misclassifying either
367    /// unbalances the pool's accounting or frees storage the kernel still owns.
368    ///
369    /// `issue` must not unwind. `StartThreadpoolIo` is issued before it runs and
370    /// is balanced only on the paths below; a panic out of `issue` skips that
371    /// balancing while leaving the start pending, and -- because a panic before
372    /// starting the I/O is indistinguishable from one after -- rundown could then
373    /// wait forever for a callback that will never arrive. A closure that might
374    /// panic must catch it and return `Err` instead.
375    ///
376    /// `P: 'static` because submitting leaks the operation's storage, to be
377    /// freed later through a thunk carrying no lifetime -- see
378    /// [`Operation::into_overlapped`].
379    pub unsafe fn submit<P, F>(&self, operation: Operation<P>, issue: F) -> Submitted<P>
380    where
381        P: Send + 'static,
382        F: FnOnce(BorrowedHandle<'_>, *mut OVERLAPPED) -> io::Result<Issued>,
383    {
384        // Transfer the operation's storage out; the kernel owns it until it is
385        // reclaimed. `into_overlapped` arms the type-erased reclaim thunk.
386        let overlapped = operation.into_overlapped();
387        // Stamp this submission with a fresh generation, so the identity names
388        // this operation and not whatever later operation may reuse the address.
389        let id = OperationId::mint(overlapped);
390
391        // Register before starting so a callback can never race ahead of the
392        // accounting; the registry's length is the unbalanced-start count.
393        self.live.insert(id);
394        // SAFETY: tp_io is valid for the lifetime of self. This start is balanced
395        // exactly once on every path below.
396        crate::trace_call!("StartThreadpoolIo", self.tp_io, overlapped as usize, {
397            // SAFETY: tp_io is valid for the lifetime of self. This start is balanced
398            // exactly once on every path below.
399            unsafe { StartThreadpoolIo(self.tp_io) };
400        });
401        crate::trace_record!("io", "started", self.tp_io, overlapped as usize);
402
403        match issue(self.handle(), overlapped) {
404            Ok(Issued::Pending) => Submitted::Pending(id),
405            Ok(Issued::Completed { bytes_transferred }) => {
406                // SAFETY: no callback will arrive, so the start must be balanced
407                // here or the pool would wait for a completion that never comes.
408                crate::trace_call!("CancelThreadpoolIo", self.tp_io, overlapped as usize, {
409                    // SAFETY: no callback will arrive, so the start must be balanced
410                    // here or the pool would wait for a completion that never comes.
411                    unsafe { CancelThreadpoolIo(self.tp_io) };
412                });
413                // Paired with `started`: this operation will never reach the
414                // trampoline, so without this record its `started` would look
415                // like a dispatch that never happened.
416                crate::trace_record!("io", "start-cancelled", self.tp_io, overlapped as usize);
417                self.live.remove(overlapped);
418                // SAFETY: the operation completed synchronously and no callback
419                // will arrive, so the kernel is done with the storage; reclaim
420                // the box we just leaked, exactly once.
421                let mut operation = unsafe { Operation::<P>::from_overlapped(overlapped) };
422                operation.set_state(OperationState::Completed);
423                Submitted::Completed {
424                    operation,
425                    bytes_transferred,
426                }
427            }
428            Err(error) => {
429                // SAFETY: the submission failed and no callback will arrive, so
430                // the start must be balanced here.
431                crate::trace_call!("CancelThreadpoolIo", self.tp_io, overlapped as usize, {
432                    // SAFETY: the submission failed and no callback will arrive, so
433                    // the start must be balanced here.
434                    unsafe { CancelThreadpoolIo(self.tp_io) };
435                });
436                crate::trace_record!("io", "start-cancelled", self.tp_io, overlapped as usize);
437                self.live.remove(overlapped);
438                // SAFETY: no callback will arrive, so reclaim the operation we
439                // just leaked, exactly once.
440                let mut operation = unsafe { Operation::<P>::from_overlapped(overlapped) };
441                operation.set_state(OperationState::Idle);
442                Submitted::Failed { operation, error }
443            }
444        }
445    }
446
447    /// Request cancellation of a single outstanding operation.
448    ///
449    /// Cancellation is only a request: the operation still completes, typically
450    /// with `ERROR_OPERATION_ABORTED`, and that completion callback remains the
451    /// point at which its storage is reclaimed.
452    ///
453    /// The identity is checked against this object's live operations first. An
454    /// identity whose operation has already completed is rejected with
455    /// [`io::ErrorKind::NotFound`] and no native call is made, even if another
456    /// operation has since been given the same storage address. Cancelling
457    /// therefore races safely against completion: the worst outcome of a late
458    /// cancel is this error, never the cancellation of an unrelated operation.
459    ///
460    /// # Errors
461    ///
462    /// Returns [`io::ErrorKind::NotFound`] if `id` no longer names a live
463    /// operation, or the error from `CancelIoEx` if the native request fails.
464    pub fn cancel(&self, id: OperationId) -> io::Result<()> {
465        // The liveness check and the native call happen under one registry
466        // guard; splitting them would let the address be recycled in between.
467        self.live.cancel_if_live(id, || {
468            // SAFETY: cancelling by a valid handle and an OVERLAPPED identity
469            // the registry has confirmed still names a live operation, and which
470            // cannot be reclaimed and reissued while the guard is held.
471            let ok = crate::trace_call!(
472                "CancelIoEx",
473                self.raw_handle() as usize,
474                id.as_ptr() as usize,
475                {
476                    // SAFETY: the handle is owned by self and the OVERLAPPED is live.
477                    unsafe { CancelIoEx(self.raw_handle(), id.as_ptr()) }
478                }
479            );
480            if ok == 0 {
481                return Err(io::Error::last_os_error());
482            }
483            Ok(())
484        })
485    }
486
487    /// Request cancellation of every outstanding operation on this endpoint.
488    ///
489    /// # Errors
490    ///
491    /// Returns the error from `CancelIoEx`, which reports `ERROR_NOT_FOUND` when
492    /// nothing was outstanding.
493    pub fn cancel_all(&self) -> io::Result<()> {
494        // SAFETY: a null OVERLAPPED cancels all operations on the handle.
495        let ok = crate::trace_call!("CancelIoEx(all)", self.raw_handle() as usize, 0, {
496            // SAFETY: the handle is owned by self; a null OVERLAPPED cancels all.
497            unsafe { CancelIoEx(self.raw_handle(), ptr::null()) }
498        });
499        if ok == 0 {
500            return Err(io::Error::last_os_error());
501        }
502        Ok(())
503    }
504
505    /// Block until every outstanding operation has completed and every
506    /// completion callback has finished running.
507    ///
508    /// Every outstanding operation must already be cancelled or otherwise
509    /// destined to complete -- which [`ThreadpoolIo::cancel_all`] guarantees --
510    /// or this waits indefinitely.
511    ///
512    /// Two things are waited for, because an operation is deregistered when its
513    /// callback is *entered* rather than when that callback returns: first that
514    /// no operation is outstanding, then that no callback is still executing.
515    /// Together they mean a caller can read whatever its callbacks recorded as
516    /// soon as this returns.
517    ///
518    /// Must not be called from inside this object's own callback, which would
519    /// wait on the callback's own completion.
520    pub fn run_down(&self) {
521        crate::trace_record!("io", "rundown-begin", self.tp_io, self.outstanding());
522        self.live.wait_until_empty();
523        // Deregistration happens at callback entry, so an empty registry does
524        // not by itself mean the callbacks have finished.
525        self.wait();
526        crate::trace_record!("io", "rundown-ended", self.tp_io);
527    }
528
529    /// Block until no I/O callback for this object is executing.
530    ///
531    /// This waits for callbacks that have already started; it does not cancel
532    /// pending ones. There is deliberately no cancelling variant: cancelling a
533    /// pending I/O callback would neither cancel the underlying operation nor
534    /// make its `OVERLAPPED` storage safe to free, so the only sound way to stop
535    /// outstanding I/O is [`ThreadpoolIo::cancel_all`] followed by
536    /// [`ThreadpoolIo::run_down`].
537    pub fn wait(&self) {
538        // SAFETY: tp_io is valid for the lifetime of self; FALSE never cancels
539        // pending callbacks, so no operation's storage is orphaned.
540        crate::trace_call!("WaitForThreadpoolIoCallbacks", self.tp_io, 0, {
541            // SAFETY: tp_io is valid for the lifetime of self; FALSE never cancels
542            // pending callbacks, so no operation storage is orphaned.
543            unsafe { WaitForThreadpoolIoCallbacks(self.tp_io, FALSE) };
544        });
545    }
546
547    fn raw_handle(&self) -> HANDLE {
548        self.handle.as_raw_handle()
549    }
550}
551
552impl fmt::Debug for ThreadpoolIo {
553    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
554        f.debug_struct("ThreadpoolIo")
555            .field("outstanding", &self.outstanding())
556            .finish_non_exhaustive()
557    }
558}
559
560impl Drop for ThreadpoolIo {
561    fn drop(&mut self) {
562        let count = self.outstanding();
563        crate::trace_record!("io", "drop-begin", self.tp_io, count);
564        if count > 0 {
565            // A blocking Drop signals that rundown was skipped. Report it from
566            // this single site, then make the block terminate: because only
567            // this object submits through the handle (its observers cannot),
568            // cancelling guarantees every outstanding operation delivers its
569            // callback.
570            //
571            // The report is a trace event rather than the `eprintln!` this
572            // shipped with, per [A developer-facing report is a trace
573            // event](../../../DESIGN-NOTES.md#reports-are-trace-events). The
574            // outstanding count rides in the second numeric slot, so the only
575            // thing the conversion cost is the English. This type needs no
576            // `CloseObligation`: `outstanding()` answers the same question from
577            // live state, which is why the other four types carry a flag and
578            // this one does not.
579            crate::trace_record!(
580                "io",
581                crate::obligation::DROP_OBLIGATION_OWED,
582                self.tp_io,
583                count
584            );
585            let _ = self.cancel_all();
586            self.live.wait_until_empty();
587        }
588
589        // The count reaches zero when the last callback is *entered*, not when
590        // it returns, so a callback frame may still be running here. Both calls
591        // below must happen before the context is freed and before this object
592        // releases its hold on the handle.
593        //
594        // SAFETY: tp_io is valid and no operation is outstanding, so waiting
595        // without cancelling cannot orphan any storage, and closing the object
596        // is legal once its callbacks have finished.
597        crate::trace_call!("WaitForThreadpoolIoCallbacks", self.tp_io, 0, {
598            // SAFETY: tp_io is valid and nothing is outstanding, so waiting without
599            // cancelling cannot orphan any storage.
600            unsafe { WaitForThreadpoolIoCallbacks(self.tp_io, FALSE) };
601        });
602        crate::trace_call!("CloseThreadpoolIo", self.tp_io, 0, {
603            // SAFETY: closing the object is legal once its callbacks have finished.
604            unsafe { CloseThreadpoolIo(self.tp_io) };
605        });
606        crate::trace_record!("io", "drop-drained", self.tp_io);
607
608        // SAFETY: the TP_IO object is closed and every callback has finished, so
609        // nothing can reach the context again; free it exactly once. This
610        // object's hold on `handle` is released after this, when its field is
611        // dropped; the handle closes then unless an observer still holds it.
612        unsafe { drop(Box::from_raw(self.context)) };
613        // The last record this object emits, and it carries the handle because
614        // nothing after it can: releasing the handle is the field drop that runs
615        // the instant this body returns, with no code of ours in between.
616        crate::trace_record!("io", "drop-closed", self.tp_io, self.raw_handle() as usize);
617        // Last, after the rundown, the close and the context free: a panic
618        // unwinds, so anything after it would be skipped; the endpoint handle
619        // is still released, because field drops run even when `Drop::drop`
620        // unwinds.
621        //
622        // `count` was read at the top, before the cancel and the wait emptied
623        // the registry -- reading it here would find zero on exactly the objects
624        // that owed a rundown.
625        crate::obligation::fail_fast_if_owed(count > 0, "ThreadpoolIo", "run_down");
626    }
627}
628
629/// One operation completion delivered to a [`ThreadpoolIo`] callback.
630///
631/// The completion borrows the operation's storage for the duration of the
632/// callback. Recover the owned operation with [`IoCompletion::claim`] when the
633/// payload type is known; otherwise the storage is reclaimed generically when
634/// the callback returns, which is what lets one object carry operations of mixed
635/// payload types.
636pub struct IoCompletion {
637    overlapped: *mut OVERLAPPED,
638    /// The identity of the completing operation, read whole from the registry
639    /// while it was still registered. `None` only if the entry was already gone,
640    /// which a correctly-operating backend does not produce.
641    id: Option<OperationId>,
642    io_result: u32,
643    bytes_transferred: usize,
644    claimed: Cell<bool>,
645}
646
647impl fmt::Debug for IoCompletion {
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        f.debug_struct("IoCompletion")
650            .field("overlapped", &self.overlapped)
651            .field("id", &self.id)
652            .field("io_result", &self.io_result)
653            .field("bytes_transferred", &self.bytes_transferred)
654            .finish_non_exhaustive()
655    }
656}
657
658impl IoCompletion {
659    /// The number of bytes transferred by the operation.
660    #[must_use]
661    pub fn bytes_transferred(&self) -> usize {
662        self.bytes_transferred
663    }
664
665    /// The raw Win32 result the pool reported, `NO_ERROR` on success.
666    #[must_use]
667    pub fn io_result(&self) -> u32 {
668        self.io_result
669    }
670
671    /// The failure of the completed operation, if it did not succeed.
672    ///
673    /// A cancelled operation completes here as `ERROR_OPERATION_ABORTED`.
674    #[must_use]
675    pub fn error(&self) -> Option<io::Error> {
676        if self.io_result == NO_ERROR {
677            return None;
678        }
679        Some(io::Error::from_raw_os_error(self.io_result as i32))
680    }
681
682    /// The identity of the completed operation, as returned by
683    /// [`ThreadpoolIo::submit`].
684    ///
685    /// It compares equal to the [`OperationId`] that submission returned, so a
686    /// caller holding submission-time identities can match a completion against
687    /// them directly.
688    #[must_use]
689    pub fn id(&self) -> Option<OperationId> {
690        self.id
691    }
692
693    /// The `OVERLAPPED` pointer identifying the completed operation.
694    #[must_use]
695    pub fn overlapped_ptr(&self) -> *mut OVERLAPPED {
696        self.overlapped
697    }
698
699    /// Recover the owned operation whose completion this is.
700    ///
701    /// The operation is returned in [`OperationState::Completed`] whether or not
702    /// it succeeded, matching the raw IOCP backend; read [`IoCompletion::error`]
703    /// for the outcome.
704    ///
705    /// # Safety
706    ///
707    /// This completion must be for an `Operation<P>` of this exact type
708    /// submitted through [`ThreadpoolIo::submit`], and it must be claimed at
709    /// most once.
710    pub unsafe fn claim<P>(&self) -> Operation<P> {
711        // Mark claimed so this completion's own drop will not also reclaim it.
712        self.claimed.set(true);
713        // SAFETY: by this function's contract, the identity is a matching leaked
714        // Operation<P>, reclaimed exactly once here.
715        let mut operation = unsafe { Operation::<P>::from_overlapped(self.overlapped) };
716        operation.set_state(OperationState::Completed);
717        operation
718    }
719}
720
721impl Drop for IoCompletion {
722    fn drop(&mut self) {
723        // A claimed completion handed ownership to the callback.
724        if self.claimed.get() || self.overlapped.is_null() {
725            return;
726        }
727        // SAFETY: the callback arrived, so the kernel and pool are done with the
728        // storage; the operation's armed reclaim thunk frees the box exactly once.
729        unsafe { reclaim_overlapped(self.overlapped) };
730    }
731}
732
733#[cfg(test)]
734mod tests;