Skip to main content

windows_threadpool_sys/
callback_env.rs

1// Copyright (c) 2026 Mike Grier
2//! SDK-equivalent helpers for [`TP_CALLBACK_ENVIRON_V3`].
3//!
4//! The Windows SDK's callback-environment functions are header-only inline
5//! helpers that `windows-sys` does not emit. This module provides Rust
6//! equivalents: a properly initialized [`CallbackEnviron`] wrapper and typed
7//! mutation methods matching `SetThreadpoolCallback*`.
8
9use core::mem;
10use std::marker::PhantomData;
11
12use windows_sys::Win32::System::Threading::{
13    PTP_CLEANUP_GROUP, PTP_CLEANUP_GROUP_CANCEL_CALLBACK, TP_CALLBACK_ENVIRON_V3,
14    TP_CALLBACK_ENVIRON_V3_0, TP_CALLBACK_PRIORITY, TP_CALLBACK_PRIORITY_HIGH,
15    TP_CALLBACK_PRIORITY_LOW, TP_CALLBACK_PRIORITY_NORMAL,
16};
17
18use crate::pool::ThreadpoolPool;
19
20/// Priority at which the thread pool schedules callbacks created with a
21/// [`CallbackEnviron`].
22///
23/// Win32 types the priority as `TP_CALLBACK_PRIORITY`, an open integer alias
24/// whose only defined values are the three below; a fourth constant,
25/// `TP_CALLBACK_PRIORITY_INVALID`, is a count sentinel rather than a priority.
26/// This crate owns its priority surface as a closed enum so safe code cannot
27/// hand an out-of-contract value to native object creation -- the very thing the
28/// bare alias would allow.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum CallbackPriority {
31    /// Highest scheduling priority (`TP_CALLBACK_PRIORITY_HIGH`).
32    High,
33    /// Default scheduling priority (`TP_CALLBACK_PRIORITY_NORMAL`); the value a
34    /// freshly initialized environment carries.
35    Normal,
36    /// Lowest scheduling priority (`TP_CALLBACK_PRIORITY_LOW`).
37    Low,
38}
39
40impl CallbackPriority {
41    /// The Win32 `TP_CALLBACK_PRIORITY` constant this priority maps to.
42    fn to_win32(self) -> TP_CALLBACK_PRIORITY {
43        match self {
44            Self::High => TP_CALLBACK_PRIORITY_HIGH,
45            Self::Normal => TP_CALLBACK_PRIORITY_NORMAL,
46            Self::Low => TP_CALLBACK_PRIORITY_LOW,
47        }
48    }
49}
50
51/// Bit positions within `TP_CALLBACK_ENVIRON_V3`'s flags word.
52///
53/// The SDK declares these as a bitfield on `TP_CALLBACK_ENVIRON_V3_0`, which
54/// `windows-sys` exposes only as the aliasing `Flags: u32`. The bit positions
55/// are therefore part of the ABI this crate depends on, and are named here
56/// rather than written inline at the point of use.
57///
58/// Changing any value is a breaking change: these describe the layout the
59/// operating system reads, not a private encoding.
60mod environ_flags {
61    /// `LongFunction`: the callback may run long, so the pool may add threads.
62    /// Set by `SetThreadpoolCallbackRunsLong`.
63    pub(super) const LONG_FUNCTION: u32 = 1 << 0;
64}
65
66/// The `TP_CALLBACK_ENVIRON_V3` structure version this crate initializes.
67///
68/// Like the flag bits above, this is an ABI identity the operating system reads,
69/// not a private encoding: it selects which layout the pool expects at the
70/// address it is handed. Changing it is a breaking change, and it must stay
71/// consistent with the `Size` field, which is taken from the V3 struct.
72const ENVIRON_VERSION: u32 = 3;
73
74/// Equivalent to `InitializeThreadpoolEnvironment` / `DestroyThreadpoolEnvironment`.
75///
76/// Wraps [`TP_CALLBACK_ENVIRON_V3`] with a guaranteed-valid initial state and a
77/// typed mutation surface. Construct with [`CallbackEnviron::new`] (or
78/// [`Default`]), mutate with the `set_*` methods, then pass to a thread-pool
79/// object creation function via [`CallbackEnviron::as_mut_ptr`].
80///
81/// `Drop` models `DestroyThreadpoolEnvironment`, which is currently a no-op in
82/// the SDK but marks the lifecycle boundary.
83///
84/// # Pool lifetime
85///
86/// An environment that names a [`ThreadpoolPool`] borrows it, so this sequence
87/// -- which would otherwise create an object from a dangling pool value -- does
88/// not compile:
89///
90/// ```compile_fail
91/// use windows_threadpool_sys::callback_env::CallbackEnviron;
92/// use windows_threadpool_sys::pool::ThreadpoolPool;
93///
94/// let mut env = CallbackEnviron::new();
95/// {
96///     let pool = ThreadpoolPool::new().expect("create pool");
97///     env.set_pool(&pool);
98/// } // `pool` is dropped here
99/// let _ptr = env.as_mut_ptr(); // error: `pool` does not live long enough
100/// ```
101pub struct CallbackEnviron<'pool> {
102    inner: TP_CALLBACK_ENVIRON_V3,
103    /// Ties the environment to the [`ThreadpoolPool`] it names.
104    ///
105    /// The environment stores only the pool's raw `PTP_POOL` value, which the
106    /// thread pool dereferences when an object is created from it. Without this
107    /// marker, safe code could set a pool, drop it, and then create an object
108    /// from the still-live environment with a dangling pool -- so the borrow has
109    /// to be real, not merely implied by the `&ThreadpoolPool` parameter.
110    pool: PhantomData<&'pool ThreadpoolPool>,
111}
112
113impl<'pool> CallbackEnviron<'pool> {
114    /// Returns a properly initialized callback environment.
115    ///
116    /// Equivalent to `InitializeThreadpoolEnvironment`: sets
117    /// `Version = ENVIRON_VERSION`,
118    /// `CallbackPriority = TP_CALLBACK_PRIORITY_NORMAL`, and
119    /// `Size = sizeof(TP_CALLBACK_ENVIRON_V3)`, with all other fields zeroed
120    /// or `None`.
121    pub fn new() -> Self {
122        Self {
123            inner: TP_CALLBACK_ENVIRON_V3 {
124                Version: ENVIRON_VERSION,
125                Pool: 0,
126                CleanupGroup: 0,
127                CleanupGroupCancelCallback: None,
128                RaceDll: core::ptr::null_mut(),
129                ActivationContext: 0,
130                FinalizationCallback: None,
131                u: TP_CALLBACK_ENVIRON_V3_0 { Flags: 0 },
132                CallbackPriority: TP_CALLBACK_PRIORITY_NORMAL,
133                Size: mem::size_of::<TP_CALLBACK_ENVIRON_V3>() as u32,
134            },
135            pool: PhantomData,
136        }
137    }
138
139    /// Equivalent to `SetThreadpoolCallbackPool`.
140    ///
141    /// Callbacks created with this environment run on `pool` instead of the
142    /// process-default pool.
143    ///
144    /// The environment genuinely borrows the pool for as long as it names it, so
145    /// the pool cannot be dropped while this environment is still usable. That
146    /// borrow is what makes the setter sound: until an object is created the pool
147    /// has no member keeping it alive, so `CloseThreadpool` on a freshly created
148    /// pool frees it immediately -- and an environment still naming it would then
149    /// hand that dangling value to `CreateThreadpool*`. The borrow forbids exactly
150    /// that.
151    ///
152    /// Objects created from the environment copy its contents rather than
153    /// retaining the borrow, but they do not need it: creating an object binds it
154    /// to the pool, and `CloseThreadpool` then releases the pool only *after*
155    /// every bound object is freed (its documented behaviour), so a live object
156    /// can never observe a freed pool however the two are dropped. Declaring the
157    /// pool before the objects it serves therefore controls only when teardown
158    /// blocks, not memory safety -- see [`ThreadpoolPool`].
159    ///
160    /// Use [`CallbackEnviron::clear_pool`] to go back to the default pool.
161    pub fn set_pool(&mut self, pool: &'pool ThreadpoolPool) {
162        self.inner.Pool = pool.as_raw();
163    }
164
165    /// Clear the pool selection, so objects created with this environment use
166    /// the process-default pool again.
167    ///
168    /// This only clears the selection. Any [`ThreadpoolPool`] the environment
169    /// named is borrowed, not owned, so it is neither dropped nor closed, and
170    /// the environment keeps its `'pool` lifetime -- the borrow is released when
171    /// the environment itself is dropped, not here.
172    pub fn clear_pool(&mut self) {
173        self.inner.Pool = 0;
174    }
175
176    /// Equivalent to `SetThreadpoolCallbackCleanupGroup`.
177    ///
178    /// Prefer [`CleanupGroup`], which creates its own members and upholds every
179    /// requirement below for you. This raw seam exists for handing the
180    /// environment to a cleanup group this crate does not own.
181    ///
182    /// # Safety
183    ///
184    /// This takes a raw `PTP_CLEANUP_GROUP`, so the caller must guarantee that:
185    ///
186    /// - `group` is a live cleanup group from `CreateThreadpoolCleanupGroup`,
187    ///   or `0` to clear the setting;
188    /// - it outlives every object created with this environment; and
189    /// - once `CloseThreadpoolCleanupGroupMembers` releases those objects, they
190    ///   are neither used nor closed again. This crate's individually-owned
191    ///   callback objects close themselves on drop, so putting one of those in a
192    ///   foreign cleanup group would close it twice. Use [`CleanupGroup`] to get
193    ///   members that are released by the group instead.
194    ///
195    /// [`CleanupGroup`]: crate::cleanup_group::CleanupGroup
196    pub unsafe fn set_cleanup_group(
197        &mut self,
198        group: PTP_CLEANUP_GROUP,
199        cancel_callback: PTP_CLEANUP_GROUP_CANCEL_CALLBACK,
200    ) {
201        self.inner.CleanupGroup = group;
202        self.inner.CleanupGroupCancelCallback = cancel_callback;
203    }
204
205    /// Equivalent to `SetThreadpoolCallbackPriority`.
206    ///
207    /// Takes the crate's closed [`CallbackPriority`] rather than the open Win32
208    /// `TP_CALLBACK_PRIORITY` alias, so only the three priorities the API
209    /// actually defines can reach native object creation.
210    pub fn set_priority(&mut self, priority: CallbackPriority) {
211        self.inner.CallbackPriority = priority.to_win32();
212    }
213
214    /// Equivalent to `SetThreadpoolCallbackRunsLong`.
215    ///
216    /// Hints to the thread pool that this callback may run for a long time,
217    /// allowing the pool to spawn additional threads.
218    pub fn set_runs_long(&mut self) {
219        // SAFETY: `Flags` and `s._bitfield` are the two halves of a union over the
220        // same u32, so writing through `Flags` sets the bitfield the SDK reads.
221        unsafe { self.inner.u.Flags |= environ_flags::LONG_FUNCTION }
222    }
223
224    /// Equivalent to `SetThreadpoolCallbackLibrary`.
225    ///
226    /// # Safety
227    ///
228    /// `dll` must be a valid `HMODULE` that remains loaded for the lifetime of
229    /// all thread-pool objects created with this environment.
230    pub unsafe fn set_library(&mut self, dll: *mut core::ffi::c_void) {
231        self.inner.RaceDll = dll;
232    }
233
234    /// Wrap an already-initialized environment structure.
235    ///
236    /// Used to copy an environment rather than mutate a caller's, so that
237    /// layering a setting on top -- as a cleanup group does when creating a
238    /// member -- cannot be observed through the original.
239    pub(crate) fn from_inner(inner: TP_CALLBACK_ENVIRON_V3) -> Self {
240        Self {
241            inner,
242            pool: PhantomData,
243        }
244    }
245
246    /// Returns a mutable pointer to the inner [`TP_CALLBACK_ENVIRON_V3`].
247    ///
248    /// For passing to thread-pool object creation functions.
249    pub fn as_mut_ptr(&mut self) -> *mut TP_CALLBACK_ENVIRON_V3 {
250        &raw mut self.inner
251    }
252
253    /// Returns a shared reference to the inner [`TP_CALLBACK_ENVIRON_V3`].
254    pub fn as_inner(&self) -> &TP_CALLBACK_ENVIRON_V3 {
255        &self.inner
256    }
257}
258
259impl Default for CallbackEnviron<'_> {
260    fn default() -> Self {
261        Self::new()
262    }
263}
264
265impl Drop for CallbackEnviron<'_> {
266    fn drop(&mut self) {
267        // DestroyThreadpoolEnvironment is a no-op in the current SDK; models the lifecycle boundary.
268    }
269}
270
271#[cfg(test)]
272mod tests;