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}
22
23/// Trampoline from the raw Windows callback ABI into the boxed closure.
24///
25/// SAFETY: `context` must point to a live `WorkContext` for the entire duration
26/// of every callback invocation — guaranteed by `ThreadpoolWork`'s Drop ordering.
27unsafe extern "system" fn work_trampoline(
28 _instance: PTP_CALLBACK_INSTANCE,
29 context: *mut core::ffi::c_void,
30 _work: PTP_WORK,
31) {
32 // SAFETY: context is a valid *mut WorkContext for the full callback duration (see Drop).
33 let ctx = unsafe { &*(context as *const WorkContext) };
34 // Not contained: the callback contract requires that it not unwind, and a
35 // callback that breaks it aborts here rather than being silently forgiven.
36 (ctx.f)();
37}
38
39/// An owned thread-pool work object.
40///
41/// Each call to [`ThreadpoolWork::submit`] queues one invocation of the callback
42/// on the process thread pool. Multiple invocations may execute concurrently.
43///
44/// [`Drop`] calls `WaitForThreadpoolWorkCallbacks` (allowing in-flight callbacks
45/// to complete) before releasing the callback context, so the captured closure
46/// remains valid for the full lifetime of every callback execution.
47///
48/// # Examples
49///
50/// ```
51/// use std::sync::Arc;
52/// use std::sync::atomic::{AtomicUsize, Ordering};
53/// use windows_threadpool_sys::work::ThreadpoolWork;
54///
55/// let total = Arc::new(AtomicUsize::new(0));
56/// let counter = Arc::clone(&total);
57///
58/// let work = ThreadpoolWork::new(move || {
59/// counter.fetch_add(1, Ordering::SeqCst);
60/// }, None)?;
61///
62/// // Each submission queues one independent invocation; they may run
63/// // concurrently, so the callback must tolerate that.
64/// for _ in 0..8 {
65/// work.submit();
66/// }
67/// work.wait();
68///
69/// assert_eq!(total.load(Ordering::SeqCst), 8);
70/// # Ok::<(), std::io::Error>(())
71/// ```
72pub struct ThreadpoolWork {
73 handle: PTP_WORK,
74 // Kept alive as a raw pointer until Drop has drained all callbacks.
75 ctx: *mut WorkContext,
76}
77
78// SAFETY: PTP_WORK is a cross-thread handle; WorkContext contains Fn + Send + Sync.
79unsafe impl Send for ThreadpoolWork {}
80unsafe impl Sync for ThreadpoolWork {}
81
82impl ThreadpoolWork {
83 /// Creates a new work object that invokes `callback` each time it is submitted.
84 ///
85 /// Pass `Some(env)` to associate a non-default callback environment; `None`
86 /// uses the process-default pool with default priority.
87 pub fn new<F>(callback: F, env: Option<&mut CallbackEnviron>) -> io::Result<Self>
88 where
89 F: Fn() + Send + Sync + 'static,
90 {
91 let ctx = Box::into_raw(Box::new(WorkContext {
92 f: Box::new(callback),
93 }));
94
95 let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
96
97 // SAFETY: ctx is a valid heap pointer; env_ptr is valid (or null) for this call.
98 let handle = unsafe {
99 CreateThreadpoolWork(Some(work_trampoline), ctx.cast(), env_ptr.cast_const())
100 };
101
102 if handle == 0 {
103 // SAFETY: the pool never saw ctx; reclaim it immediately.
104 unsafe { drop(Box::from_raw(ctx)) };
105 return Err(io::Error::last_os_error());
106 }
107
108 Ok(Self { handle, ctx })
109 }
110
111 /// Queues one invocation of the callback on the thread pool.
112 ///
113 /// May be called repeatedly; each call queues an independent invocation.
114 /// Multiple queued invocations may execute concurrently.
115 pub fn submit(&self) {
116 // SAFETY: handle is valid for the lifetime of self.
117 unsafe { SubmitThreadpoolWork(self.handle) };
118 }
119
120 /// Blocks until all queued and in-progress invocations have completed.
121 pub fn wait(&self) {
122 // SAFETY: handle is valid for the lifetime of self.
123 unsafe { WaitForThreadpoolWorkCallbacks(self.handle, FALSE) };
124 }
125
126 /// Cancels callbacks that have not yet started, then waits for any
127 /// currently-executing invocations to finish.
128 pub fn cancel_pending(&self) {
129 // SAFETY: handle is valid for the lifetime of self.
130 unsafe { WaitForThreadpoolWorkCallbacks(self.handle, TRUE) };
131 }
132
133 /// Give up ownership, returning the raw object and its callback context.
134 ///
135 /// Used only by [`crate::cleanup_group::CleanupGroup`], which takes over
136 /// both: a group member is released by `CloseThreadpoolCleanupGroupMembers`
137 /// and must not close itself, so this suppresses this type's `Drop`.
138 pub(crate) fn into_parts(self) -> (PTP_WORK, *mut c_void) {
139 let this = ManuallyDrop::new(self);
140 (this.handle, this.ctx.cast())
141 }
142
143 /// Free a context returned by [`ThreadpoolWork::into_parts`].
144 ///
145 /// # Safety
146 ///
147 /// `context` must come from `into_parts` on this type, its object must
148 /// already have been released, and it must be freed exactly once.
149 pub(crate) unsafe fn drop_context(context: *mut c_void) {
150 // SAFETY: forwarded from this function's own contract.
151 drop(unsafe { Box::from_raw(context.cast::<WorkContext>()) });
152 }
153}
154
155impl Drop for ThreadpoolWork {
156 fn drop(&mut self) {
157 unsafe {
158 // Let all in-flight callbacks run to completion before freeing the context.
159 WaitForThreadpoolWorkCallbacks(self.handle, FALSE);
160 CloseThreadpoolWork(self.handle);
161 drop(Box::from_raw(self.ctx));
162 }
163 }
164}
165
166#[cfg(test)]
167mod tests;