windows_threadpool_sys/wait.rs
1// Copyright (c) 2026 Mike Grier
2//! Thread-pool waits: `CreateThreadpoolWait` / `SetThreadpoolWait` /
3//! `WaitForThreadpoolWaitCallbacks` / `CloseThreadpoolWait`.
4//!
5//! A wait object watches one waitable handle and queues its callback when the
6//! handle is signalled or the wait times out. Two SDK contracts shape this API:
7//!
8//! - **The handle must stay valid while a wait is pending.** [`ThreadpoolWait`]
9//! therefore *owns* its handle rather than borrowing one, so it cannot be
10//! closed underneath a pending wait. Use [`ThreadpoolWait::handle`] to
11//! inspect it; to signal an event the wait owns, build the wait from a
12//! `win-sync-sys` [`Event`] and keep a clone of it.
13//! - **A wait fires at most once per arming.** The SDK requires the wait to be
14//! rearmed explicitly for each activation, so the callback receives a
15//! [`WaitActivation`] carrying [`WaitActivation::rearm`]. A callback that does
16//! not rearm simply stops watching.
17//!
18//! Mutex handles are not supported by the thread pool and must not be passed to
19//! [`ThreadpoolWait::new`].
20
21use std::io;
22use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, OwnedHandle};
23use std::ptr;
24use std::sync::atomic::{AtomicIsize, Ordering};
25use std::time::Duration;
26
27use crate::rearm::RearmSuppression;
28
29use win_sync_sys::{Event, ResetMode};
30use windows_sys::Win32::Foundation::{FALSE, FILETIME, HANDLE, TRUE, WAIT_TIMEOUT};
31use windows_sys::Win32::System::Threading::{
32 CloseThreadpoolWait, CreateThreadpoolWait, PTP_CALLBACK_INSTANCE, PTP_WAIT, SetThreadpoolWait,
33 WaitForThreadpoolWaitCallbacks,
34};
35use windows_sys::core::BOOL;
36
37use crate::callback_env::CallbackEnviron;
38
39/// Wait results the pool reports. Changing either value is a breaking change.
40mod wait_status {
41 /// `WAIT_OBJECT_0`: the handle was signalled.
42 pub const SIGNALLED: u32 = 0;
43}
44
45/// 100-nanosecond intervals per second, for building a relative `FILETIME`.
46/// Changing this value is a breaking change.
47const FILETIME_TICKS_PER_SECOND: u64 = 10_000_000;
48/// Nanoseconds per 100-nanosecond interval. Changing it is a breaking change.
49const FILETIME_NANOS_PER_TICK: u32 = 100;
50
51/// Build the negative tick count that means "relative timeout".
52fn relative_filetime(timeout: Duration) -> FILETIME {
53 let ticks = timeout
54 .as_secs()
55 .saturating_mul(FILETIME_TICKS_PER_SECOND)
56 .saturating_add(u64::from(timeout.subsec_nanos() / FILETIME_NANOS_PER_TICK));
57 let ticks = i64::try_from(ticks).unwrap_or(i64::MAX);
58 let bits = (-ticks) as u64;
59 FILETIME {
60 dwLowDateTime: bits as u32,
61 dwHighDateTime: (bits >> 32) as u32,
62 }
63}
64
65/// A Win32 routine that closes a wait target.
66///
67/// This is the shape Win32 close routines already have, so one can be passed
68/// directly with no shim: `CloseHandle` and `FindCloseChangeNotification` both
69/// match it. The return value is ignored -- there is nothing a destructor could
70/// usefully do with a close failure.
71pub type WaitCloseFn = unsafe extern "system" fn(HANDLE) -> BOOL;
72
73/// Owns a handle that is closed with a routine other than `CloseHandle`.
74///
75/// The `Drop` lives here rather than on [`WaitTarget`] so that the enum itself
76/// has no destructor and can be taken apart by an ordinary `match`.
77pub(crate) struct CustomClose {
78 raw: HANDLE,
79 close: WaitCloseFn,
80}
81
82impl Drop for CustomClose {
83 fn drop(&mut self) {
84 // Bracketed like a Win32 call because that is what it usually is: the
85 // routine is caller-supplied and the case this exists for,
86 // `FindCloseChangeNotification`, is a kernel close. It runs while the
87 // owning wait is being torn down, so a slow one shows up as a stalled
88 // teardown with no other explanation.
89 crate::trace_call!("WaitCloseFn", self.raw as usize, self.close as usize, {
90 // SAFETY: the handle was vouched for by `assume_waitable_with` and is
91 // still open -- every owner drains the wait before dropping this, so the
92 // pool is no longer watching it. This runs exactly once, because `Drop`
93 // does.
94 unsafe { (self.close)(self.raw) };
95 });
96 }
97}
98
99/// Owns a wait target and closes it with the routine that target requires.
100///
101/// Most handles are closed with `CloseHandle`, which is what a std
102/// [`OwnedHandle`] does, so that stays the default. Some are not: a
103/// `FindFirstChangeNotification` handle must be closed with
104/// `FindCloseChangeNotification`, and closing it the usual way is wrong. The
105/// second variant carries the caller's routine so those targets can be owned by
106/// the pool on the same terms as any other.
107pub(crate) enum WaitTarget {
108 /// The default: closed by [`OwnedHandle`]'s own drop, with `CloseHandle`.
109 Owned(OwnedHandle),
110 /// Closed with the caller-supplied routine.
111 Custom(CustomClose),
112}
113
114// SAFETY: a handle is an opaque OS-owned value, not a pointer into this
115// process, and both variants only ever read it or hand it to a thread-safe Win32
116// call. This restores what the plain `OwnedHandle` field had automatically.
117unsafe impl Send for WaitTarget {}
118unsafe impl Sync for WaitTarget {}
119
120impl WaitTarget {
121 /// The raw handle, for arming the wait and for the callback context.
122 pub(crate) fn raw(&self) -> HANDLE {
123 match self {
124 WaitTarget::Owned(handle) => handle.as_raw_handle(),
125 WaitTarget::Custom(custom) => custom.raw,
126 }
127 }
128
129 /// Borrow the handle, for signalling or inspecting it.
130 pub(crate) fn borrow(&self) -> BorrowedHandle<'_> {
131 match self {
132 WaitTarget::Owned(handle) => handle.as_handle(),
133 // SAFETY: the handle stays open for as long as `self` owns it, and
134 // the returned borrow cannot outlive that.
135 WaitTarget::Custom(custom) => unsafe { BorrowedHandle::borrow_raw(custom.raw) },
136 }
137 }
138}
139
140impl std::fmt::Debug for WaitTarget {
141 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 // The close routine is a code pointer with no useful rendering.
143 f.debug_struct("WaitTarget")
144 .field("raw", &self.raw())
145 .field(
146 "close",
147 &match self {
148 WaitTarget::Owned(_) => "CloseHandle",
149 WaitTarget::Custom(_) => "custom",
150 },
151 )
152 .finish()
153 }
154}
155
156/// A handle the thread pool is able to wait on.
157///
158/// The pool does not support every waitable object: a mutex handle in
159/// particular produces undefined behaviour rather than an error. Requiring this
160/// type instead of a bare [`OwnedHandle`] moves that precondition from prose
161/// into the type system, so a safe caller cannot reach the undefined case.
162///
163/// Construct one safely from a `win-sync-sys` [`Event`] (`From<Event>`) or with
164/// [`WaitableHandle::event`], or vouch for a handle
165/// obtained elsewhere with the narrow [`WaitableHandle::assume_waitable`] seam --
166/// or [`WaitableHandle::assume_waitable_with`] when the handle needs a close
167/// routine other than `CloseHandle`.
168/// This mirrors `UnassociatedEndpoint` in `windows-overlapped-io-sys`, which
169/// pairs a safe `open` with an `assume_overlapped` escape hatch for the same
170/// reason.
171#[derive(Debug)]
172pub struct WaitableHandle {
173 target: WaitTarget,
174}
175
176impl WaitableHandle {
177 /// Create an event and wrap it as a waitable handle.
178 ///
179 /// An event is always a supported wait target, so this needs no `unsafe`.
180 /// A `manual_reset` event stays signalled until it is reset; an auto-reset
181 /// event returns to unsignalled as soon as one wait is satisfied, which
182 /// makes it the usual choice for handing off work one activation at a time.
183 ///
184 /// The event is a `win-sync-sys` [`Event`], so this crate creates none of
185 /// its own. To be able to set the event once the wait owns it, create the
186 /// [`Event`] directly, keep a [`try_clone`](Event::try_clone), and convert
187 /// the other with `From<Event>`.
188 ///
189 /// # Errors
190 ///
191 /// Returns the error from [`Event::new`].
192 pub fn event(manual_reset: bool, initially_signalled: bool) -> io::Result<Self> {
193 let reset = if manual_reset {
194 ResetMode::Manual
195 } else {
196 ResetMode::Auto
197 };
198 Ok(Event::new(reset, initially_signalled)?.into())
199 }
200
201 /// Wrap a handle whose wait support the caller vouches for.
202 ///
203 /// This is the extensibility seam for wait targets this crate cannot create
204 /// itself -- semaphores, waitable timers, processes, threads, console input,
205 /// change notifications, and so on.
206 ///
207 /// # Safety
208 ///
209 /// The caller guarantees that:
210 ///
211 /// - the handle is a waitable object the thread pool supports, and in
212 /// particular is **not a mutex**, which the SDK does not support and which
213 /// yields undefined behaviour rather than an error; and
214 /// - ownership transfers exclusively into the returned value, so nothing
215 /// else closes the handle while a wait on it is pending.
216 #[must_use]
217 pub unsafe fn assume_waitable(handle: OwnedHandle) -> Self {
218 Self {
219 target: WaitTarget::Owned(handle),
220 }
221 }
222
223 /// Wrap a handle that must be closed with a routine other than
224 /// `CloseHandle`.
225 ///
226 /// Some waitable objects have their own destructor: a
227 /// `FindFirstChangeNotification` handle is closed with
228 /// `FindCloseChangeNotification`, and handing it to
229 /// [`assume_waitable`](Self::assume_waitable) --
230 /// which takes a std [`OwnedHandle`] and therefore closes it with
231 /// `CloseHandle` -- would be wrong. `close` is invoked exactly once, and
232 /// only after the wait has been drained, whether the object is torn down by
233 /// [`ThreadpoolWait`]'s own drop or by a
234 /// [`CleanupGroup`](crate::cleanup_group::CleanupGroup) release.
235 ///
236 /// `close` has the shape Win32 close routines already have, so it can be
237 /// passed directly with no wrapper.
238 ///
239 /// # Safety
240 ///
241 /// The caller guarantees that:
242 ///
243 /// - the handle is a waitable object the thread pool supports, and in
244 /// particular is **not a mutex**, which the SDK does not support and which
245 /// yields undefined behaviour rather than an error;
246 /// - ownership transfers exclusively into the returned value, so nothing
247 /// else closes the handle while a wait on it is pending; and
248 /// - `close` is the correct destructor for this handle and is safe to call
249 /// once on it after the pool has stopped watching it.
250 #[must_use]
251 pub unsafe fn assume_waitable_with(handle: HANDLE, close: WaitCloseFn) -> Self {
252 Self {
253 target: WaitTarget::Custom(CustomClose { raw: handle, close }),
254 }
255 }
256
257 /// Borrow the underlying handle, for signalling or inspecting it.
258 #[must_use]
259 pub fn handle(&self) -> BorrowedHandle<'_> {
260 self.target.borrow()
261 }
262
263 /// Consume the wrapper and recover the owned handle.
264 ///
265 /// # Errors
266 ///
267 /// Returns the wrapper back unchanged when it carries a custom close
268 /// routine (see [`assume_waitable_with`](Self::assume_waitable_with)): an
269 /// [`OwnedHandle`] closes what it holds with `CloseHandle`, which is
270 /// precisely the wrong destructor for such a target, so there is no correct
271 /// value to hand back. The handle is neither closed nor leaked -- ownership
272 /// simply stays where it was.
273 pub fn into_handle(self) -> Result<OwnedHandle, Self> {
274 match self.target {
275 WaitTarget::Owned(handle) => Ok(handle),
276 target @ WaitTarget::Custom(_) => Err(Self { target }),
277 }
278 }
279
280 /// Consume the wrapper and recover the owner, whichever kind it is.
281 pub(crate) fn into_target(self) -> WaitTarget {
282 self.target
283 }
284}
285
286/// An event is always a target the pool supports, so this needs no `unsafe`.
287///
288/// An [`Event`] is guaranteed to be an event rather than a mutex -- adopting a
289/// handle as one is `unsafe` in `win-sync-sys` for exactly this reason -- and
290/// to carry the access a wait needs.
291impl From<Event> for WaitableHandle {
292 fn from(event: Event) -> Self {
293 Self {
294 target: WaitTarget::Owned(event.into()),
295 }
296 }
297}
298
299/// Why a wait callback ran.
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum WaitResult {
302 /// The watched handle became signalled.
303 Signalled,
304 /// The timeout given when the wait was armed elapsed first.
305 TimedOut,
306 /// The pool reported a result this crate does not model. The raw value is
307 /// preserved so a caller can inspect it rather than having it discarded.
308 Other(u32),
309}
310
311impl WaitResult {
312 fn from_raw(value: u32) -> Self {
313 match value {
314 wait_status::SIGNALLED => Self::Signalled,
315 WAIT_TIMEOUT => Self::TimedOut,
316 other => Self::Other(other),
317 }
318 }
319}
320
321/// Heap-allocated callback state kept alive for the lifetime of the wait object.
322///
323/// `wait` is filled in after `CreateThreadpoolWait` returns, because rearming
324/// from inside a callback needs the object the callback belongs to.
325struct WaitContext {
326 wait: AtomicIsize,
327 handle: HANDLE,
328 /// Stops a callback re-arming this wait once a teardown has begun.
329 ///
330 /// Arming takes this lock and does nothing while the count is non-zero, so
331 /// a callback that re-arms cannot start watching again after a disarm from
332 /// outside. Its two users here are [`ThreadpoolWait::stop_and_drain`],
333 /// which raises and lowers it, and `Drop`, which raises it permanently --
334 /// the asymmetry [`RearmSuppression`] is a count for. The lock is only ever
335 /// held across the native `SetThreadpoolWait` call.
336 rearm: RearmSuppression,
337 /// Whether the object is armed with nothing having drained it.
338 ///
339 /// A dispatch settles it because `SetThreadpoolWait` arms for exactly one
340 /// activation: once the callback has entered, the object is no longer
341 /// watching, so there is nothing for `Drop` to wait on and nothing to
342 /// report. A callback that re-arms sets it again, which is why this is
343 /// cleared at trampoline entry rather than on the way out.
344 obligation: crate::obligation::CloseObligation,
345 /// This pool's entry in the self-heal registry.
346 ///
347 /// **The one registration that is read rather than only held.** A wait is
348 /// the only object that reaches the removal primitive a cancellation owes a
349 /// repair for, so `try_cancel_pending`, `recover_repair` and `owe_repair`
350 /// all go through this field; the other four context types hold theirs
351 /// purely for its `Drop`.
352 ///
353 /// On the context rather than on [`ThreadpoolWait`] so it survives
354 /// [`into_parts`](ThreadpoolWait::into_parts) into a cleanup-group member,
355 /// which is how the group reaches it after the object itself is gone.
356 registration: crate::heal::Registration,
357 callback: Box<dyn Fn(&WaitActivation<'_>) + Send + Sync + 'static>,
358}
359
360impl WaitContext {
361 /// Lock the suppression count, recovering from a panicking holder.
362 fn suppression(&self) -> std::sync::MutexGuard<'_, u32> {
363 self.rearm.lock()
364 }
365
366 /// Start suppressing re-arming, and disarm under the same acquisition.
367 ///
368 /// Doing both under one lock is what makes the pair atomic against a
369 /// callback: a re-arm either lands entirely before this, or is suppressed by
370 /// it. The lock is released before any drain.
371 fn suppress_and_disarm(&self) {
372 self.rearm.suppress_and(|suppressed| {
373 let wait = self.wait.load(Ordering::Acquire);
374 crate::trace_record!("wait", "suppress-and-disarm", wait, suppressed);
375 if wait == 0 {
376 return;
377 }
378 // SAFETY: `wait` is this object's live PTP_WAIT, published before any
379 // callback could run and valid until Drop closes it.
380 unsafe { disarm_raw(wait) };
381 });
382 }
383
384 /// Stop suppressing re-arming.
385 fn release_suppression(&self) {
386 self.rearm.release();
387 }
388}
389
390// SAFETY: `handle` is a raw handle owned by the ThreadpoolWait that outlives
391// this context; it is only passed back to SetThreadpoolWait, never closed here.
392unsafe impl Send for WaitContext {}
393unsafe impl Sync for WaitContext {}
394
395/// One activation of a [`ThreadpoolWait`], handed to its callback.
396///
397/// The wait is not armed when the callback runs. Call [`WaitActivation::rearm`]
398/// to watch the handle again; doing nothing leaves the wait idle.
399pub struct WaitActivation<'ctx> {
400 result: WaitResult,
401 ctx: &'ctx WaitContext,
402}
403
404impl std::fmt::Debug for WaitActivation<'_> {
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 // The context holds the boxed callback and the raw wait object, neither
407 // of which is meaningful to a reader; the result is the whole story.
408 f.debug_struct("WaitActivation")
409 .field("result", &self.result)
410 .finish_non_exhaustive()
411 }
412}
413
414impl WaitActivation<'_> {
415 /// Why this callback ran.
416 #[must_use]
417 pub fn result(&self) -> WaitResult {
418 self.result
419 }
420
421 /// Whether the watched handle was signalled.
422 #[must_use]
423 pub fn is_signalled(&self) -> bool {
424 self.result == WaitResult::Signalled
425 }
426
427 /// Borrow the handle this activation was for.
428 ///
429 /// The wait owns the handle and outlives every callback, so it is open for
430 /// the duration of this borrow. This is what makes the documented way out of
431 /// the overlap hazard on [`rearm`](Self::rearm) reachable: a callback
432 /// watching a manual-reset event can reset it before re-arming, so the next
433 /// activation waits for a fresh signal instead of starting immediately
434 /// alongside this one.
435 #[must_use]
436 pub fn handle(&self) -> BorrowedHandle<'_> {
437 // SAFETY: the wait owns this handle and cannot be dropped while a
438 // callback is running, so it is open for at least this borrow.
439 unsafe { BorrowedHandle::borrow_raw(self.ctx.handle) }
440 }
441
442 /// Arm the wait again, so the next signal or timeout activates it.
443 ///
444 /// `timeout` of `None` waits indefinitely. This is the mechanism the SDK
445 /// requires for repeated waits: an activation consumes the arming, so a
446 /// callback that wants to keep watching must rearm from inside itself.
447 ///
448 /// # This can overlap the callback with itself
449 ///
450 /// Re-arming takes effect immediately, and the pool activates as soon as the
451 /// handle is signalled. If the handle is *still signalled* when this is
452 /// called -- which is the normal state of a manual-reset event -- the next
453 /// activation is queued at once and can begin before the current callback
454 /// returns. Re-arming early in a long callback therefore runs it
455 /// concurrently with itself, repeatedly: a 20ms callback that re-armed at
456 /// its start was measured entering 7529 times in 400ms, 5110 of those
457 /// overlapping an earlier entry.
458 ///
459 /// This is not the guarantee [`TimerFiring::rearm_after`] gives. A one-shot
460 /// timer's re-arm is deferred until the callback returns, precisely so
461 /// firings stay sequential; a wait's re-arm is not, because the SDK requires
462 /// the wait to be re-armed for the handle's *current* signal state to be
463 /// observed.
464 ///
465 /// Either reset the event before re-arming, so the next activation waits
466 /// for a fresh signal -- through a clone of the `win-sync-sys` [`Event`]
467 /// the wait was built from, which the callback keeps:
468 ///
469 /// ```no_run
470 /// # fn example(activation: &windows_threadpool_sys::wait::WaitActivation<'_>, event: &win_sync_sys::Event) {
471 /// // `event` is the callback's clone of the event this wait watches.
472 /// event
473 /// .reset()
474 /// .expect("an event created here has the access to reset it");
475 /// activation.rearm(None);
476 /// # }
477 /// ```
478 ///
479 /// or accept the concurrency and make everything the callback touches
480 /// tolerate it. An auto-reset event does not have this problem, because the
481 /// wait consumes the signal.
482 ///
483 /// [`TimerFiring::rearm_after`]: crate::timer::TimerFiring::rearm_after
484 ///
485 /// # Re-arm before the next signal
486 ///
487 /// The ordering rule described on [`ThreadpoolWait::arm`] applies to
488 /// every arming, not just the first -- `SetThreadpoolWait` says the event
489 /// must be re-registered "before signaling it each time". A producer that
490 /// signals in the window after an activation consumed the arming but
491 /// before this call re-establishes it may not get a callback for that
492 /// signal.
493 ///
494 /// Re-arming *before* draining closes that window, at the cost of
495 /// callbacks that find nothing to do; draining first and re-arming after
496 /// leaves it open. A caller who cannot order the two can drain, re-arm,
497 /// then drain again, so that anything which landed in the window is
498 /// picked up by the second pass rather than waited for.
499 ///
500 /// # Teardown
501 ///
502 /// Re-arming after the object has begun tearing down does nothing, so a
503 /// callback racing [`ThreadpoolWait`]'s `Drop` cannot leave the object armed
504 /// behind it.
505 pub fn rearm(&self, timeout: Option<Duration>) {
506 let _ = self.rearm_reporting(timeout);
507 }
508
509 /// [`rearm`](Self::rearm), reporting whether the arming actually happened.
510 ///
511 /// Returns `false` when the request was suppressed because the object is
512 /// tearing down. The public entry point discards this, because a caller
513 /// cannot act on it: by the time it could look, the object is gone. Tests
514 /// use it to observe the suppression directly, which is otherwise only
515 /// visible as the absence of undefined behaviour.
516 pub(crate) fn rearm_reporting(&self, timeout: Option<Duration>) -> bool {
517 // Recorded *before* the lock, not after: the acquisition can block on a
518 // concurrent `suppress_and_disarm`, and a re-arm parked on that mutex is
519 // one of the things a silent interval in the trace could be. Entry and
520 // exit straddling the lock is what tells the two apart.
521 let wait = self.ctx.wait.load(Ordering::Acquire);
522 crate::trace_record!("wait", "rearm-entered", wait);
523 // Taken before arming and held across it, so this either happens before
524 // a suppressing caller raises the count or is suppressed by it -- never
525 // in between.
526 let suppressed = self.ctx.suppression();
527 if *suppressed > 0 {
528 crate::trace_record!("wait", "rearm-suppressed", wait, *suppressed);
529 return false;
530 }
531 debug_assert_ne!(
532 wait, 0,
533 "the wait object must be published before callbacks"
534 );
535 // Recorded before the arming is published, not after: by here the
536 // suppression check has already returned, so the arming is certain to
537 // happen, and a dispatch it produces can settle the obligation on
538 // another thread before this one gets any further. See
539 // `CloseObligation::record_live_before`.
540 self.ctx.obligation.record_live_before(|| {
541 // SAFETY: `wait` is this object's live PTP_WAIT, published before any
542 // callback could run, and `handle` is owned by that object so it is
543 // still open. The timeout, if any, is a live stack value for the call.
544 unsafe { arm_raw(wait, self.ctx.handle, timeout) };
545 });
546 drop(suppressed);
547 crate::trace_record!("wait", "rearm-left", wait, self.ctx.handle as usize);
548 true
549 }
550}
551
552/// Stop a raw wait object.
553///
554/// SAFETY: `wait` must be a live `PTP_WAIT`.
555pub(crate) unsafe fn disarm_raw(wait: PTP_WAIT) {
556 crate::trace_call!("SetThreadpoolWait(disarm)", wait, 0, {
557 // SAFETY: forwarded; a null handle is the documented way to cancel a wait.
558 unsafe { SetThreadpoolWait(wait, ptr::null_mut(), ptr::null()) };
559 });
560 crate::trace_record!("wait", "disarmed", wait);
561}
562
563/// Arm a raw wait object against a borrowed target.
564///
565/// SAFETY: `wait` must be a live `PTP_WAIT` and `target` must stay open until
566/// the wait is disarmed or the object released.
567pub(crate) unsafe fn arm_member(wait: PTP_WAIT, target: &WaitTarget, timeout: Option<Duration>) {
568 // SAFETY: forwarded from this function's own contract.
569 unsafe { arm_raw(wait, target.raw(), timeout) };
570}
571
572/// Arm or disarm a wait object.
573///
574/// SAFETY: `wait` must be a live `PTP_WAIT` and `handle` a live waitable handle
575/// (or null to disarm).
576unsafe fn arm_raw(wait: PTP_WAIT, handle: HANDLE, timeout: Option<Duration>) {
577 crate::trace_call!("SetThreadpoolWait", wait, handle as usize, {
578 match timeout {
579 Some(timeout) => {
580 let filetime = relative_filetime(timeout);
581 // SAFETY: forwarded from this function's contract; `filetime` is
582 // read only for the duration of the call.
583 unsafe { SetThreadpoolWait(wait, handle, &filetime) };
584 }
585 // SAFETY: forwarded; a null timeout means "wait indefinitely".
586 None => unsafe { SetThreadpoolWait(wait, handle, ptr::null()) },
587 }
588 });
589 crate::trace_record!("wait", "armed", wait, handle as usize);
590}
591
592/// Trampoline from the raw `PTP_WAIT_CALLBACK` ABI into the boxed closure.
593///
594/// SAFETY: `context` must point to a live [`WaitContext`] for the entire
595/// duration of every callback invocation, which [`ThreadpoolWait`]'s `Drop`
596/// ordering guarantees.
597unsafe extern "system" fn wait_trampoline(
598 _instance: PTP_CALLBACK_INSTANCE,
599 context: *mut core::ffi::c_void,
600 _wait: PTP_WAIT,
601 wait_result: u32,
602) {
603 crate::trace_record!("wait", "trampoline-entered", _wait, wait_result);
604 // SAFETY: context is a valid *mut WaitContext for the full callback duration.
605 let ctx = unsafe { &*(context as *const WaitContext) };
606 // This activation consumed the arming: the pool is no longer watching, so
607 // nothing is owed unless the callback below arms it again.
608 ctx.obligation.record_settled();
609 // Stamped before the callback, not after: a dispatch that is still running
610 // is evidence the pool is live, and a long callback must not look like
611 // silence to the self-heal.
612 let activation = WaitActivation {
613 result: WaitResult::from_raw(wait_result),
614 ctx,
615 };
616 // Not contained: see the callback contract in the crate docs.
617 (ctx.callback)(&activation);
618 // Paired with the record above. The pair is what makes a silent interval
619 // readable: an entry with no exit says a callback is still inside the
620 // closure, which is a different finding from no entry at all.
621 crate::trace_record!("wait", "trampoline-left", _wait, wait_result);
622}
623
624/// An owned thread-pool wait object bound to one waitable handle.
625///
626/// The object owns the handle, so the handle cannot be closed while a wait is
627/// pending. A newly created wait is idle; arm it with [`ThreadpoolWait::arm`],
628/// and rearm from inside the callback with [`WaitActivation::rearm`].
629///
630/// [`Drop`] disarms before draining callbacks, then closes the object and only
631/// afterwards releases the callback context and the handle.
632///
633/// Unlike [`ThreadpoolTimer`](crate::timer::ThreadpoolTimer), **the callback can
634/// run concurrently with itself**: a wait's re-arm takes effect immediately, so
635/// re-arming while the handle is still signalled queues the next activation
636/// before the current callback returns. See [`WaitActivation::rearm`] for the
637/// measurements and the two ways to avoid it.
638///
639/// # Examples
640///
641/// Watch an event once. The wait takes ownership of the handle it is given, so
642/// keep a clone of the [`Event`] -- a second handle to the same event -- to
643/// signal it through:
644///
645/// ```
646/// use std::sync::mpsc;
647/// use win_sync_sys::{Event, ResetMode};
648/// use windows_threadpool_sys::wait::{ThreadpoolWait, WaitResult};
649///
650/// let event = Event::new(ResetMode::Manual, false)?;
651/// let signal = event.try_clone()?;
652///
653/// let (tx, rx) = mpsc::channel();
654/// let sender = std::sync::Mutex::new(tx);
655/// let wait = ThreadpoolWait::new(event.into(), move |activation| {
656/// let _ = sender.lock().expect("send").send(activation.result());
657/// }, None)?;
658///
659/// wait.arm(None);
660/// signal.set()?;
661///
662/// assert_eq!(rx.recv().expect("activation"), WaitResult::Signalled);
663/// # Ok::<(), std::io::Error>(())
664/// ```
665///
666/// Keep watching across activations by rearming from inside the callback, which
667/// is what the SDK requires -- an activation consumes the arming:
668///
669/// ```
670/// # use std::sync::Arc;
671/// # use std::sync::atomic::{AtomicUsize, Ordering};
672/// use win_sync_sys::{Event, ResetMode};
673/// use windows_threadpool_sys::wait::ThreadpoolWait;
674///
675/// let event = Event::new(ResetMode::Auto, false)?;
676/// let signal = event.try_clone()?;
677///
678/// let seen = Arc::new(AtomicUsize::new(0));
679/// let counter = Arc::clone(&seen);
680/// let wait = ThreadpoolWait::new(event.into(), move |activation| {
681/// counter.fetch_add(1, Ordering::SeqCst);
682/// activation.rearm(None);
683/// }, None)?;
684///
685/// wait.arm(None);
686/// for _ in 0..3 {
687/// signal.set()?;
688/// std::thread::sleep(std::time::Duration::from_millis(5));
689/// }
690///
691/// // Disarms and drains in one step, discharging the drain this wait owes
692/// // rather than leaving the blocking teardown to `Drop`.
693/// wait.stop_and_drain();
694/// assert!(seen.load(Ordering::SeqCst) >= 1);
695/// # Ok::<(), std::io::Error>(())
696/// ```
697pub struct ThreadpoolWait {
698 wait: PTP_WAIT,
699 target: WaitTarget,
700 // Kept alive as a raw pointer until Drop has disarmed and drained.
701 context: *mut WaitContext,
702}
703
704// SAFETY: PTP_WAIT is a cross-thread pool object, WaitTarget is Send + Sync,
705// and the context is Send + Sync; the pointer is only read until Drop frees it
706// after all callbacks have finished.
707unsafe impl Send for ThreadpoolWait {}
708unsafe impl Sync for ThreadpoolWait {}
709
710impl ThreadpoolWait {
711 /// Create an idle wait watching `handle`.
712 ///
713 /// The object takes ownership of the handle and closes it on drop, which is
714 /// what guarantees the handle outlives any pending wait.
715 ///
716 /// Pass `Some(env)` to select a private pool or callback priority; `None`
717 /// uses the process-default pool with default priority.
718 ///
719 /// The callback runs on a shared, process-managed pool thread, must restore
720 /// any thread state it changes, and must not terminate its thread. It must
721 /// not panic: a panic unwinds to the `extern "system"` trampoline and aborts
722 /// the process.
723 ///
724 /// Taking a [`WaitableHandle`] rather than a bare handle is what keeps this
725 /// constructor safe: the thread pool does not support every waitable object,
726 /// and a mutex handle in particular is undefined rather than an error.
727 ///
728 /// # Errors
729 ///
730 /// Returns the error from `CreateThreadpoolWait`.
731 pub fn new<F>(
732 handle: WaitableHandle,
733 callback: F,
734 env: Option<&mut CallbackEnviron<'_>>,
735 ) -> io::Result<Self>
736 where
737 F: Fn(&WaitActivation<'_>) + Send + Sync + 'static,
738 {
739 let target = handle.into_target();
740 // Read before `env` is consumed below, and registered before the object
741 // exists: the entry must be able to repair this pool from the moment
742 // anything of ours can dispatch on it.
743 let registration = crate::heal::register(crate::heal::key_of(env.as_deref()));
744 let context = Box::into_raw(Box::new(WaitContext {
745 wait: AtomicIsize::new(0),
746 handle: target.raw(),
747 rearm: RearmSuppression::new(),
748 obligation: crate::obligation::CloseObligation::new(),
749 registration,
750 callback: Box::new(callback),
751 }));
752 let env_ptr = env.map_or(ptr::null_mut(), |e| e.as_mut_ptr());
753
754 // SAFETY: context is a valid heap pointer that outlives every callback,
755 // and env_ptr is valid (or null) for the duration of this call.
756 let wait = crate::trace_call!("CreateThreadpoolWait", 0, 0, {
757 // SAFETY: context is a valid heap pointer that outlives every callback,
758 // and env_ptr is valid (or null) for the duration of this call.
759 unsafe {
760 CreateThreadpoolWait(Some(wait_trampoline), context.cast(), env_ptr.cast_const())
761 }
762 });
763
764 if wait == 0 {
765 let error = io::Error::last_os_error();
766 // SAFETY: the pool never saw context; reclaim it immediately.
767 unsafe { drop(Box::from_raw(context)) };
768 return Err(error);
769 }
770
771 // Publish the object before any callback can run. No wait is armed yet,
772 // so no callback can observe the unpublished value.
773 // SAFETY: context is live and exclusively ours until the first arming.
774 unsafe { (*context).wait.store(wait, Ordering::Release) };
775 crate::trace_record!("wait", "created", wait, target.raw() as usize);
776
777 Ok(Self {
778 wait,
779 target,
780 context,
781 })
782 }
783
784 /// Borrow the watched handle, for signalling or inspecting it.
785 #[must_use]
786 pub fn handle(&self) -> BorrowedHandle<'_> {
787 self.target.borrow()
788 }
789
790 /// Arm the wait, so the next signal or timeout runs the callback once.
791 ///
792 /// `timeout` of `None` waits indefinitely. Arming replaces any previous
793 /// arming rather than adding to it, and an activation consumes the arming --
794 /// rearm from inside the callback with [`WaitActivation::rearm`] to keep
795 /// watching.
796 ///
797 /// # Arm before you signal
798 ///
799 /// `SetThreadpoolWait` documents that "you must re-register the event with
800 /// the wait object before signaling it each time to trigger the wait
801 /// callback". Signal a handle that is not currently armed -- including in
802 /// the window between constructing a [`ThreadpoolWait`] and this call --
803 /// and the callback is not guaranteed to run for that signal.
804 ///
805 /// An auto-reset event makes a dropped signal permanent rather than merely
806 /// late, because the signal is consumed and there is nothing left for a
807 /// subsequent arming to observe. If the handle is only ever signalled once,
808 /// as a wakeup for state that is already present, that lost signal is the
809 /// last one the waiter will ever get.
810 ///
811 /// Every example in this module arms first for that reason; so does every
812 /// caller in this workspace. A caller who cannot control the ordering
813 /// should watch a manual-reset event kept in agreement with the state it
814 /// reports, which is level-triggered and so has no signal to lose.
815 pub fn arm(&self, timeout: Option<Duration>) {
816 // SAFETY: the context outlives every callback and is freed only by Drop,
817 // which cannot run while this borrow of self is alive.
818 unsafe { &*self.context }.obligation.record_live_before(|| {
819 // SAFETY: `wait` is valid for the lifetime of self, and the handle is
820 // owned by self so it is still open.
821 unsafe { arm_raw(self.wait, self.target.raw(), timeout) };
822 });
823 }
824
825 /// Stop watching.
826 ///
827 /// New activations stop being queued, but a callback already queued still
828 /// runs; use `try_cancel_pending`, or
829 /// [`try_cancel_pending_no_heal_tracking`](Self::try_cancel_pending_no_heal_tracking)
830 /// in a build without the `self-heal` feature, to drop those as well.
831 pub fn disarm(&self) {
832 crate::trace_call!("SetThreadpoolWait(disarm)", self.wait, 0, {
833 // SAFETY: `wait` is valid for the lifetime of self; a null handle is the
834 // documented way to cancel a pending wait.
835 unsafe { SetThreadpoolWait(self.wait, ptr::null_mut(), ptr::null()) };
836 });
837 crate::trace_record!("wait", "disarmed", self.wait);
838 }
839
840 /// Let every queued callback run, and block until none is executing.
841 ///
842 /// This does **not** leave a self-re-arming wait idle: a callback running
843 /// during this call can [`rearm`](WaitActivation::rearm) before it returns,
844 /// so the object is watching again when this returns. Use
845 /// [`stop_and_drain`](Self::stop_and_drain) to reach quiescence.
846 pub fn wait(&self) {
847 crate::trace_call!("WaitForThreadpoolWaitCallbacks", self.wait, 0, {
848 // SAFETY: `wait` is valid for the lifetime of self.
849 unsafe { WaitForThreadpoolWaitCallbacks(self.wait, FALSE) };
850 });
851 }
852
853 /// Drop callbacks that have not started, then wait for any executing one.
854 ///
855 /// Like [`wait`](Self::wait), this does not by itself leave a self-re-arming
856 /// wait idle: it does not suppress the re-arm of a callback that is already
857 /// running. Use [`stop_and_drain`](Self::stop_and_drain) when the wait must
858 /// actually be quiescent afterwards.
859 ///
860 /// # This brings a process-wide hazard forward; it does not create it
861 ///
862 /// Cancelling asks the kernel to remove a completion packet that may already
863 /// have been delivered to the pool's completion port
864 /// (`IopCancelWaitCompletionPacket` with `RemoveSignaledPacket` set).
865 /// Measured on this workspace's reproducer, a removal that lands a few
866 /// microseconds after the packet was queued, on a port whose factory has no
867 /// threads yet, can **sever the notification from that port to the worker
868 /// factory**.
869 ///
870 /// The scope of that is one pool -- but it is the whole of that pool, not
871 /// this wait and not waits as a kind. Measured during such a stall: already
872 /// armed waits, a freshly armed and signalled wait, a fresh timer, and a
873 /// completed overlapped read all fail to dispatch. A second pool in the same
874 /// process is unaffected. Since the pool normally affected is the **default**
875 /// one, the practical reach is every component in the process that did not
876 /// explicitly create its own -- including code that has nothing to do with
877 /// the caller.
878 ///
879 /// Submitting a work item recovers it, because that reaches the factory by a
880 /// route the broken notification is not on. Nothing else observed does.
881 ///
882 /// **But this method is not the only way to reach that, and on the measured
883 /// evidence it is not even the usual one.** `CloseThreadpoolWait` performs
884 /// the same removal, through the same kernel routine with the same flag,
885 /// whenever it finds a packet still outstanding -- so a teardown that never
886 /// calls this method reaches the hazard anyway. In the reproducer, dropping
887 /// the cancel entirely changed nothing measurable: 22 failures in 20004
888 /// without it against 16 with it.
889 ///
890 /// What this method does is perform the removal *earlier*, which removes any
891 /// chance for the callback to run first.
892 ///
893 /// The fix is therefore not to avoid this method but to leave nothing to
894 /// remove: [`wait`](Self::wait) and [`stop_and_drain`](Self::stop_and_drain)
895 /// let the queued callback run, which clears the association, after which the
896 /// close has nothing to take. Draining cannot reach the removal primitive on
897 /// any path. Prefer them -- but note that doing so is what makes the
898 /// *subsequent close* safe, not merely this call.
899 /// This crate repairs the pool afterwards, which is what makes this safe to
900 /// offer -- with one exception, stated because a guarantee with an unstated
901 /// hole is worse than one that names it.
902 ///
903 /// If this object's pool could not be registered for repair when the object
904 /// was created, which happens only when creating the repair work item
905 /// failed, this call registers it **here** rather than giving up. Only when
906 /// that allocation fails too is the cancellation performed with nothing to
907 /// repair it, and that case records `cancel-untracked` on the `heal` target
908 /// -- naming the pool -- and panics under the `fail-fast` feature. See
909 /// [README-FEATURE-self-heal.md](https://docs.rs/crate/windows-threadpool-sys/latest/source/README-FEATURE-self-heal.md).
910 ///
911 /// # Availability
912 ///
913 /// Requires the `self-heal` feature, which is on by default. Without it this
914 /// method does not exist and a call to it fails to compile, naming
915 /// [`try_cancel_pending_no_heal_tracking`](Self::try_cancel_pending_no_heal_tracking)
916 /// as what to reach for instead. That is the designed behaviour, not an
917 /// oversight: a guarantee you were relying on has been removed, and a
918 /// compile error is the only way you find that out.
919 #[cfg(feature = "self-heal")]
920 pub fn try_cancel_pending(&self) {
921 // SAFETY: the obligation this transfers is discharged here, by marking
922 // the pool so the self-heal repairs it.
923 unsafe { self.try_cancel_pending_no_heal_tracking() };
924 // SAFETY: the context outlives every callback and is freed only by Drop,
925 // which cannot run while this borrow of self is alive.
926 let tracked = unsafe { &*self.context }.registration.owe_repair();
927 // Last in the function, so the panic this may raise cannot skip the
928 // marking above -- the same placement rule the drop fail-fast follows.
929 crate::obligation::fail_fast_if_untracked(tracked, "ThreadpoolWait");
930 }
931
932 /// `try_cancel_pending` without the repair.
933 ///
934 /// Not a link, deliberately: the method it would name does not exist in a
935 /// build with `self-heal` off, and this one does, so the link would dangle
936 /// in exactly the configuration this method exists for.
937 ///
938 /// Always present, including in builds with `self-heal` off, which is the
939 /// point: it is the method that still exists when the gated one does not,
940 /// and its signature says what the caller takes on.
941 ///
942 /// # Safety
943 ///
944 /// The caller must ensure the pool is repaired. Submitting any work item to
945 /// it does so; so does knowing the pool is kept live by something else.
946 /// Leaving it unrepaired can stop the pool dispatching -- which, for the
947 /// process-default pool, reaches every component in the process, including
948 /// code with no connection to this call.
949 ///
950 /// This is not a memory-safety obligation, and the keyword is not claiming
951 /// one. It is here because the obligation is statable and dischargeable by
952 /// the caller, which is what `unsafe` marks; see
953 /// [the self-heal decision](https://docs.rs/crate/windows-threadpool-sys/latest/source/DESIGN-NOTES.md).
954 pub unsafe fn try_cancel_pending_no_heal_tracking(&self) {
955 crate::trace_call!("WaitForThreadpoolWaitCallbacks(cancel)", self.wait, 1, {
956 // SAFETY: `wait` is valid for the lifetime of self. A cancelled wait
957 // callback owns no storage, so dropping queued callbacks orphans nothing.
958 unsafe { WaitForThreadpoolWaitCallbacks(self.wait, TRUE) };
959 });
960 }
961
962 /// Take a repair claim on this wait's pool while the object is still live.
963 ///
964 /// For [`crate::cleanup_group::CleanupGroup`], which must do this *before*
965 /// its native release: a member whose pool was never registered holds
966 /// nothing that keeps that pool alive, and the release frees the last bound
967 /// object. The claim this returns owns a repair work object bound to the
968 /// pool, so holding it across the release keeps the pool alive for the
969 /// marking pass that follows.
970 ///
971 /// # Safety
972 ///
973 /// `context` must come from [`into_parts`](Self::into_parts) on this type
974 /// and name a still-live object whose context the caller has not yet freed.
975 pub(crate) unsafe fn recover_repair(
976 context: *mut core::ffi::c_void,
977 ) -> Option<crate::heal::Registration> {
978 // SAFETY: forwarded; the context outlives the member until the group
979 // frees it, and this only touches that object's registration.
980 let ctx = unsafe { &*context.cast::<WaitContext>() };
981 Some(ctx.registration.reclaim())
982 }
983
984 /// Mark this wait's pool as owing a repair, retrying the registration.
985 ///
986 /// Gated because its only caller is `WaitMember::try_cancel_pending`, which
987 /// the feature gates too. It was reachable in every configuration until
988 /// `M-T11.4` removed the `OwnedResource` hook that named it: the group's
989 /// marking pass now goes through the claims, which is what the retry here
990 /// must not be reached from.
991 ///
992 /// Reports whether the pool ended up tracked. A caller that accumulates
993 /// this across several members must act on it only after every context is
994 /// freed -- never mid-loop, where an unwind would skip those frees.
995 ///
996 /// # Safety
997 ///
998 /// `context` must come from [`into_parts`](Self::into_parts) on this type
999 /// and name a still-live object whose context the caller has not yet freed.
1000 #[cfg(feature = "self-heal")]
1001 #[must_use]
1002 pub(crate) unsafe fn owe_repair(context: *mut core::ffi::c_void) -> bool {
1003 // SAFETY: forwarded; the context outlives the member until the group
1004 // frees it, and this only touches that object's registration.
1005 let ctx = unsafe { &*context.cast::<WaitContext>() };
1006 ctx.registration.owe_repair()
1007 }
1008
1009 /// Stop watching and block until the wait is idle, leaving it reusable.
1010 ///
1011 /// This exists because neither [`disarm`](Self::disarm) nor
1012 /// `try_cancel_pending` can stop a self-re-arming wait on
1013 /// its own: a callback already running can call [`WaitActivation::rearm`]
1014 /// after a disarm from outside has taken effect. This suppresses re-arming
1015 /// for the duration of the call, using the same mechanism `Drop` uses, and
1016 /// lifts the suppression before returning so the wait can be armed again.
1017 ///
1018 /// # What this guarantees
1019 ///
1020 /// On return, provided no other thread arms the wait during the call:
1021 ///
1022 /// - no callback is queued or executing, and
1023 /// - the object is not watching -- a re-arm requested by a callback that ran
1024 /// during the call is discarded rather than deferred.
1025 ///
1026 /// **A queued callback runs; it is not discarded.** That is the difference
1027 /// between this and `try_cancel_pending`, and it is why
1028 /// this can block for as long as the callback takes. See
1029 /// [the teardown-drains decision](../../../DESIGN-NOTES.md#teardown-drains)
1030 /// for why finishing the work is preferred to abandoning it.
1031 ///
1032 /// # What it does not
1033 ///
1034 /// **A concurrent [`arm`](Self::arm) from another thread is not excluded.**
1035 /// `ThreadpoolWait` is `Sync` and `arm` takes `&self`, so it does not pass
1036 /// through the suppression this uses, and nothing in this crate orders such
1037 /// a call against this one. A caller needing the wait to be provably idle
1038 /// must ensure nothing else arms it for the duration, by owning it
1039 /// exclusively or serializing access to it.
1040 ///
1041 /// Calling this from inside the wait's own callback would deadlock, because
1042 /// it waits for that callback to finish.
1043 pub fn stop_and_drain(&self) {
1044 // SAFETY: the context outlives every callback and is freed only by Drop,
1045 // which cannot run while this borrow of self is alive.
1046 unsafe { Self::stop_and_drain_parts(self.context.cast(), self.wait) };
1047 }
1048
1049 /// [`stop_and_drain`](Self::stop_and_drain) against a detached context.
1050 ///
1051 /// One body rather than two, so this type and the cleanup-group member that
1052 /// wraps the same object cannot drift apart: the suppression discipline here
1053 /// is what makes the drain mean anything, and a second copy of it is a
1054 /// second place for a later change to reach one and miss the other.
1055 ///
1056 /// # Safety
1057 ///
1058 /// `context` must come from [`into_parts`](Self::into_parts) on this type
1059 /// and name a still-live object whose context the caller has not yet freed,
1060 /// and `wait` must be that object.
1061 pub(crate) unsafe fn stop_and_drain_parts(context: *mut core::ffi::c_void, wait: PTP_WAIT) {
1062 // SAFETY: forwarded from this function's own contract.
1063 let ctx = unsafe { &*context.cast::<WaitContext>() };
1064 ctx.suppress_and_disarm();
1065 // Drained with the lock released: a callback blocked on it would
1066 // otherwise never finish, and this would never return.
1067 crate::trace_call!("WaitForThreadpoolWaitCallbacks", wait, 0, {
1068 // SAFETY: `wait` is live, and waiting without cancelling cannot
1069 // orphan any storage.
1070 unsafe { WaitForThreadpoolWaitCallbacks(wait, FALSE) };
1071 });
1072 // Settled after the drain, not before: a callback running during it may
1073 // have asked to re-arm, and the suppression discards that request, so
1074 // the object really is idle here. Releasing the suppression below lets a
1075 // later `arm` make it live again, which sets this afresh.
1076 ctx.obligation.record_settled();
1077 ctx.release_suppression();
1078 }
1079
1080 /// Give up ownership, returning the raw object, its callback context, and
1081 /// the watched target.
1082 ///
1083 /// Used only by [`crate::cleanup_group::CleanupGroup`], which takes over all
1084 /// three. The target must go with them: the pool may still be watching it
1085 /// until the group releases the member, so it cannot be closed when the
1086 /// borrowing member goes out of scope.
1087 pub(crate) fn into_parts(self) -> (PTP_WAIT, *mut core::ffi::c_void, WaitTarget) {
1088 let this = std::mem::ManuallyDrop::new(self);
1089 // SAFETY: `this` is never dropped, so moving the target out cannot be
1090 // observed by a later drop of the original value.
1091 let target = unsafe { ptr::read(&this.target) };
1092 (this.wait, this.context.cast(), target)
1093 }
1094
1095 /// Free a context returned by [`ThreadpoolWait::into_parts`].
1096 ///
1097 /// # Safety
1098 ///
1099 /// `context` must come from `into_parts` on this type, its object must
1100 /// already have been released, and it must be freed exactly once.
1101 pub(crate) unsafe fn drop_context(context: *mut core::ffi::c_void) {
1102 // SAFETY: forwarded from this function's own contract.
1103 drop(unsafe { Box::from_raw(context.cast::<WaitContext>()) });
1104 }
1105
1106 /// Whether `Drop` would report an undischarged drain obligation right now.
1107 ///
1108 /// Exists so the obligation's wiring can be asserted without depending on
1109 /// the trace, whose filter is fixed before `main` and so cannot be narrowed
1110 /// from inside a test.
1111 #[cfg(test)]
1112 pub(crate) fn obligation_owed(&self) -> bool {
1113 // SAFETY: the context outlives every callback and is freed only by Drop.
1114 unsafe { &*self.context }.obligation.is_owed()
1115 }
1116
1117 /// Suppress this member's re-arm and disarm it, before a
1118 /// [`crate::cleanup_group::CleanupGroup`] bulk-releases its members.
1119 ///
1120 /// `CloseThreadpoolCleanupGroupMembers` waits for executing callbacks but
1121 /// does not stop one from re-arming: a callback still running can call
1122 /// [`WaitActivation::rearm`] after the bulk release has begun, which would
1123 /// re-arm an object being torn down and then free its context under a freshly
1124 /// queued callback. Raising the suppression before the release closes that
1125 /// door, exactly as this type's own `Drop` does; the suppression is never
1126 /// lifted because the member is being destroyed.
1127 ///
1128 /// # Safety
1129 ///
1130 /// `context` must come from [`into_parts`](Self::into_parts) on this type
1131 /// and name a still-live object whose context the caller has not yet freed.
1132 pub(crate) unsafe fn prepare_shutdown(context: *mut core::ffi::c_void) {
1133 // SAFETY: forwarded; the context outlives the member until the group
1134 // frees it, and `suppress_and_disarm` only touches this object.
1135 let ctx = unsafe { &*context.cast::<WaitContext>() };
1136 ctx.suppress_and_disarm();
1137 }
1138}
1139
1140impl Drop for ThreadpoolWait {
1141 fn drop(&mut self) {
1142 // Close the door on re-arming before disarming, and do both under the
1143 // same lock. Disarming alone is not enough: a callback already running
1144 // could re-arm afterwards, the drain below could then return with the
1145 // object armed, and the close and context free would race a freshly
1146 // queued callback.
1147 // SAFETY: the context outlives every callback; Drop frees it below,
1148 // after the drain.
1149 let ctx = unsafe { &*self.context };
1150 // Raised and never released: unlike `stop_and_drain`, there is no
1151 // afterwards for this object.
1152 crate::trace_record!("wait", "drop-begin", self.wait, self.target.raw() as usize);
1153 // Read before the disarm and drain, and emitted before them: the record
1154 // marks the start of the blocking interval it reports, so a reader sees
1155 // what the following gap is for rather than learning it afterwards.
1156 // Captured, not re-read later: a callback dispatched during the drain
1157 // below settles the obligation, so asking afterwards would find nothing
1158 // owed on exactly the objects that owed something. The context is also
1159 // freed before the fail-fast, so this must be a value rather than a
1160 // borrow.
1161 let owed = ctx.obligation.is_owed();
1162 if owed {
1163 crate::trace_record!("wait", crate::obligation::DROP_OBLIGATION_OWED, self.wait);
1164 }
1165 ctx.suppress_and_disarm();
1166 // The lock is released before draining: a callback blocked on it would
1167 // otherwise never finish, and this wait would never return.
1168 self.wait();
1169 crate::trace_record!("wait", "drop-drained", self.wait);
1170
1171 // SAFETY: no callback can be queued or executing, so the object can be
1172 // closed and the context freed exactly once. `target` is dropped after
1173 // this, when its field is dropped, so the handle outlives the wait
1174 // object and its close routine -- `CloseHandle` or a custom one -- runs
1175 // only once the pool has stopped watching it.
1176 crate::trace_call!("CloseThreadpoolWait", self.wait, 0, {
1177 unsafe { CloseThreadpoolWait(self.wait) };
1178 });
1179 // SAFETY: as above; the pool can no longer reach the context.
1180 unsafe {
1181 drop(Box::from_raw(self.context));
1182 }
1183 // The last record this object emits, and it carries the target handle
1184 // because nothing after it can: the close of that handle is the field
1185 // drop that runs the instant this body returns, with no code of ours in
1186 // between. So the interval from `drop-drained` to the handle's close
1187 // holds exactly `CloseThreadpoolWait`, the context free, and that field
1188 // drop -- a reader who sees a gap there is looking at one of those three
1189 // and not at something unrecorded.
1190 crate::trace_record!("wait", "drop-closed", self.wait, self.target.raw() as usize);
1191 // Last, after the drain, the close and the context free. A panic
1192 // unwinds, so anything after it would be skipped; the target handle
1193 // still closes, because field drops run even when `Drop::drop` unwinds.
1194 crate::obligation::fail_fast_if_owed(owed, "ThreadpoolWait", "stop_and_drain");
1195 }
1196}
1197
1198#[cfg(test)]
1199mod tests;