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