Skip to main content

windows_threadpool_sys/
work.rs

1// Copyright (c) 2026 Mike Grier
2//! Thread-pool work objects: `CreateThreadpoolWork` / `SubmitThreadpoolWork` /
3//! `WaitForThreadpoolWorkCallbacks` / `CloseThreadpoolWork`.
4
5use core::ffi::c_void;
6use std::io;
7use std::mem::ManuallyDrop;
8use std::ptr;
9
10use windows_sys::Win32::Foundation::{FALSE, TRUE};
11use windows_sys::Win32::System::Threading::{
12    CloseThreadpoolWork, CreateThreadpoolWork, PTP_CALLBACK_INSTANCE, PTP_WORK,
13    SubmitThreadpoolWork, WaitForThreadpoolWorkCallbacks,
14};
15
16use crate::callback_env::CallbackEnviron;
17
18/// Heap-allocated callback state kept alive for the lifetime of the work object.
19struct WorkContext {
20    f: Box<dyn Fn() + Send + Sync + 'static>,
21    /// Whether a submission is outstanding that the caller has not waited for.
22    ///
23    /// Unlike the wait and the one-shot timer, a dispatch does not settle this:
24    /// `submit` may be called any number of times, so a trampoline entry would
25    /// have to decrement a count rather than clear a flag. The count is not
26    /// worth keeping, because a caller who never called `wait` could not have
27    /// known the work had finished -- leaving the drain to `Drop` is what they
28    /// did regardless of how the race turned out.
29    obligation: crate::obligation::CloseObligation,
30    /// This pool's entry in the self-heal registry.
31    ///
32    /// Held for its `Drop`, not read. The claim keeps the entry alive while
33    /// this object exists, which is what makes the entry's pre-created repair
34    /// work object available -- and bound to the pool, deferring its free --
35    /// if a wait on the same pool is later cancelled. Only a wait reads a
36    /// registration, because only a wait reaches the removal primitive that
37    /// owes a repair.
38    #[allow(dead_code)]
39    registration: crate::heal::Registration,
40}
41
42/// Trampoline from the raw Windows callback ABI into the boxed closure.
43///
44/// SAFETY: `context` must point to a live `WorkContext` for the entire duration
45/// of every callback invocation — guaranteed by `ThreadpoolWork`'s Drop ordering.
46unsafe extern "system" fn work_trampoline(
47    _instance: PTP_CALLBACK_INSTANCE,
48    context: *mut core::ffi::c_void,
49    _work: PTP_WORK,
50) {
51    // SAFETY: context is a valid *mut WorkContext for the full callback duration (see Drop).
52    let ctx = unsafe { &*(context as *const WorkContext) };
53    crate::trace_record!("work", "trampoline-entered", _work);
54    // Stamped before the callback, not after: a dispatch that is still running
55    // is evidence the pool is live, and a long callback must not look like
56    // silence to the self-heal.
57    // Not contained: the callback contract requires that it not unwind, and a
58    // callback that breaks it aborts here rather than being silently forgiven.
59    (ctx.f)();
60    crate::trace_record!("work", "trampoline-left", _work);
61}
62
63/// An owned thread-pool work object.
64///
65/// Each call to [`ThreadpoolWork::submit`] queues one invocation of the callback
66/// on the process thread pool. Multiple invocations may execute concurrently.
67///
68/// [`Drop`] calls `WaitForThreadpoolWorkCallbacks` (allowing in-flight callbacks
69/// to complete) before releasing the callback context, so the captured closure
70/// remains valid for the full lifetime of every callback execution.
71///
72/// # Examples
73///
74/// ```
75/// use std::sync::Arc;
76/// use std::sync::atomic::{AtomicUsize, Ordering};
77/// use windows_threadpool_sys::work::ThreadpoolWork;
78///
79/// let total = Arc::new(AtomicUsize::new(0));
80/// let counter = Arc::clone(&total);
81///
82/// let work = ThreadpoolWork::new(move || {
83///     counter.fetch_add(1, Ordering::SeqCst);
84/// }, None)?;
85///
86/// // Each submission queues one independent invocation; they may run
87/// // concurrently, so the callback must tolerate that.
88/// for _ in 0..8 {
89///     work.submit();
90/// }
91/// work.wait();
92///
93/// assert_eq!(total.load(Ordering::SeqCst), 8);
94/// # Ok::<(), std::io::Error>(())
95/// ```
96pub struct ThreadpoolWork {
97    handle: PTP_WORK,
98    // Kept alive as a raw pointer until Drop has drained all callbacks.
99    ctx: *mut WorkContext,
100}
101
102// SAFETY: PTP_WORK is a cross-thread handle; WorkContext contains Fn + Send + Sync.
103unsafe impl Send for ThreadpoolWork {}
104unsafe impl Sync for ThreadpoolWork {}
105
106impl ThreadpoolWork {
107    /// Creates a new work object that invokes `callback` each time it is submitted.
108    ///
109    /// Pass `Some(env)` to associate a non-default callback environment; `None`
110    /// uses the process-default pool with default priority.
111    pub fn new<F>(callback: F, env: Option<&mut CallbackEnviron>) -> io::Result<Self>
112    where
113        F: Fn() + Send + Sync + 'static,
114    {
115        // Read before `env` is consumed below, and registered before the object
116        // exists: the entry must be able to repair this pool from the moment
117        // anything of ours can dispatch on it.
118        let registration = crate::heal::register(crate::heal::key_of(env.as_deref()));
119        let ctx = Box::into_raw(Box::new(WorkContext {
120            f: Box::new(callback),
121            obligation: crate::obligation::CloseObligation::new(),
122            registration,
123        }));
124
125        let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
126
127        // SAFETY: ctx is a valid heap pointer; env_ptr is valid (or null) for this call.
128        let handle = crate::trace_call!("CreateThreadpoolWork", 0, 0, {
129            // SAFETY: ctx is a valid heap pointer; env_ptr is valid (or null) for this call.
130            unsafe { CreateThreadpoolWork(Some(work_trampoline), ctx.cast(), env_ptr.cast_const()) }
131        });
132
133        if handle == 0 {
134            // Read before the free, as the other three constructors do. `ctx`
135            // owns a `Registration`, whose drop can reach `PoolEntry::drop` and
136            // from there `WaitForThreadpoolWorkCallbacks` and
137            // `CloseThreadpoolWork`. Whether either of those disturbs the
138            // thread's last error on success is not documented, so this is not
139            // a demonstrated clobber -- but the ordering that cannot be wrong
140            // costs a line, and this was the only one of the four doing it the
141            // other way round.
142            let error = io::Error::last_os_error();
143            // SAFETY: the pool never saw ctx; reclaim it immediately.
144            unsafe { drop(Box::from_raw(ctx)) };
145            return Err(error);
146        }
147
148        crate::trace_record!("work", "created", handle);
149        Ok(Self { handle, ctx })
150    }
151
152    /// Queues one invocation of the callback on the thread pool.
153    ///
154    /// May be called repeatedly; each call queues an independent invocation.
155    /// Multiple queued invocations may execute concurrently.
156    pub fn submit(&self) {
157        // SAFETY: the context outlives every callback and is freed only by Drop,
158        // which cannot run while this borrow of self is alive.
159        unsafe { &*self.ctx }.obligation.record_live_before(|| {
160            crate::trace_call!("SubmitThreadpoolWork", self.handle, 0, {
161                // SAFETY: handle is valid for the lifetime of self.
162                unsafe { SubmitThreadpoolWork(self.handle) };
163            });
164            // The submit is the start of the interval a stalled dispatch is
165            // measured over; without it, a `trampoline-entered` has nothing to be
166            // late relative to.
167            crate::trace_record!("work", "submitted", self.handle);
168        });
169    }
170
171    /// Blocks until all queued and in-progress invocations have completed.
172    ///
173    /// This type has no separately-named synchronous close: this *is* the drain
174    /// that [`Drop`] would otherwise perform, so calling it discharges the
175    /// obligation `Drop` reports.
176    pub fn wait(&self) {
177        crate::trace_call!("WaitForThreadpoolWorkCallbacks", self.handle, 0, {
178            // SAFETY: handle is valid for the lifetime of self.
179            unsafe { WaitForThreadpoolWorkCallbacks(self.handle, FALSE) };
180        });
181        // SAFETY: the context outlives every callback and is freed only by Drop,
182        // which cannot run while this borrow of self is alive.
183        unsafe { &*self.ctx }.obligation.record_settled();
184    }
185
186    /// Stop accepting work and block until none is queued or executing.
187    ///
188    /// The drain every type in this crate offers under this name, so a caller
189    /// tearing down a mixed set of objects can reach for one method.
190    ///
191    /// There is nothing to *stop* on a work object -- a submission cannot be
192    /// withdrawn, only waited for -- so this is exactly [`wait`](Self::wait),
193    /// which is why that method is documented as this type's drain. The name
194    /// exists because a caller should not have to know which of this crate's
195    /// types has something to stop.
196    pub fn stop_and_drain(&self) {
197        self.wait();
198    }
199
200    /// Cancels callbacks that have not yet started, then waits for any
201    /// currently-executing invocations to finish.
202    ///
203    /// Settles the drain obligation, exactly as [`wait`](Self::wait) does and
204    /// for the same reason: on return nothing is queued and nothing is
205    /// executing, which is what the obligation tracks. It used not to, so a
206    /// caller who submitted, cancelled, and dropped was told at drop that it
207    /// still owed a drain -- and under `fail-fast` that report is a panic, on a
208    /// caller that had done nothing wrong.
209    ///
210    /// **Sound here, and deliberately not done on the wait or the timer.**
211    /// Those two can be re-armed from inside their own callbacks
212    /// ([`crate::wait::WaitActivation::rearm`],
213    /// [`crate::timer::TimerFiring::rearm_after`]), so a callback running
214    /// during the cancel can leave the object live again before this returns --
215    /// which is why their `stop_and_drain` suppresses re-arms before draining
216    /// and only then settles. A work callback is a bare `Fn()` with no handle
217    /// to its object, so nothing can re-queue it except an explicit
218    /// [`submit`](Self::submit), and that arms the obligation again itself.
219    pub fn cancel_pending(&self) {
220        crate::trace_call!("WaitForThreadpoolWorkCallbacks(cancel)", self.handle, 1, {
221            // SAFETY: handle is valid for the lifetime of self.
222            unsafe { WaitForThreadpoolWorkCallbacks(self.handle, TRUE) };
223        });
224        // SAFETY: the context outlives every callback and is freed only by Drop,
225        // which cannot run while this borrow of self is alive.
226        unsafe { &*self.ctx }.obligation.record_settled();
227    }
228
229    /// Give up ownership, returning the raw object and its callback context.
230    ///
231    /// Used only by [`crate::cleanup_group::CleanupGroup`], which takes over
232    /// both: a group member is released by `CloseThreadpoolCleanupGroupMembers`
233    /// and must not close itself, so this suppresses this type's `Drop`.
234    pub(crate) fn into_parts(self) -> (PTP_WORK, *mut c_void) {
235        let this = ManuallyDrop::new(self);
236        (this.handle, this.ctx.cast())
237    }
238
239    /// Free a context returned by [`ThreadpoolWork::into_parts`].
240    ///
241    /// # Safety
242    ///
243    /// `context` must come from `into_parts` on this type, its object must
244    /// already have been released, and it must be freed exactly once.
245    pub(crate) unsafe fn drop_context(context: *mut c_void) {
246        // SAFETY: forwarded from this function's own contract.
247        drop(unsafe { Box::from_raw(context.cast::<WorkContext>()) });
248    }
249
250    /// Whether `Drop` would report an undischarged drain obligation right now.
251    ///
252    /// Exists so the obligation's wiring can be asserted without depending on
253    /// the trace, whose filter is fixed before `main` and so cannot be narrowed
254    /// from inside a test.
255    #[cfg(test)]
256    pub(crate) fn obligation_owed(&self) -> bool {
257        // SAFETY: the context outlives every callback and is freed only by Drop.
258        unsafe { &*self.ctx }.obligation.is_owed()
259    }
260}
261
262impl Drop for ThreadpoolWork {
263    fn drop(&mut self) {
264        crate::trace_record!("work", "drop-begin", self.handle);
265        // Read before the drain, and emitted before it: the record marks the
266        // start of the blocking interval it is reporting, so a reader sees what
267        // the following gap is for rather than learning it afterwards.
268        // SAFETY: the context is still live; it is freed at the end of this body.
269        // Captured, not re-read later: the context is freed before the
270        // fail-fast, so this must be a value rather than a borrow.
271        let owed = unsafe { &*self.ctx }.obligation.is_owed();
272        if owed {
273            crate::trace_record!("work", crate::obligation::DROP_OBLIGATION_OWED, self.handle);
274        }
275        crate::trace_call!("WaitForThreadpoolWorkCallbacks", self.handle, 0, {
276            // Let all in-flight callbacks run to completion before freeing the context.
277            // SAFETY: handle is valid until it is closed just below.
278            unsafe { WaitForThreadpoolWorkCallbacks(self.handle, FALSE) };
279        });
280        crate::trace_record!("work", "drop-drained", self.handle);
281        crate::trace_call!("CloseThreadpoolWork", self.handle, 0, {
282            // SAFETY: no callback remains, so the object can be closed once.
283            unsafe { CloseThreadpoolWork(self.handle) };
284        });
285        // SAFETY: nothing can reach the context again; free it exactly once.
286        unsafe { drop(Box::from_raw(self.ctx)) };
287        crate::trace_record!("work", "drop-closed", self.handle);
288        // Last, after the drain, the close and the context free: a panic
289        // unwinds, so anything after it would be skipped.
290        crate::obligation::fail_fast_if_owed(owed, "ThreadpoolWork", "stop_and_drain");
291    }
292}
293
294#[cfg(test)]
295mod tests;