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;