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