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}
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;