win_sync_sys/apc.rs
1// Copyright (c) 2026 Mike Grier
2//! Asynchronous procedure calls: `QueueUserAPC`.
3
4use std::io;
5use std::os::windows::io::{AsHandle, AsRawHandle, RawHandle};
6use std::ptr;
7
8use windows_sys::Win32::System::Threading::{
9 GetCurrentProcessId, GetCurrentThread, GetProcessIdOfThread, QueueUserAPC,
10};
11
12/// What is queued: a routine to run once, on the thread the APC is queued to.
13type Routine = Box<dyn FnOnce() + Send + 'static>;
14
15/// Queue `routine` to run on `thread` the next time it waits alertably: `QueueUserAPC`.
16///
17/// What ends an alertable wait ([`Alertable::Yes`](crate::Alertable::Yes)) is an APC, and this is
18/// how one is made. The routine runs **on `thread`**, inside whichever alertable wait or sleep it
19/// next makes, before that wait returns [`WaitOutcome::Alerted`](crate::WaitOutcome::Alerted); a
20/// thread that never waits alertably never runs it. APCs queued to a thread run in the order they
21/// were queued.
22///
23/// `thread` is any handle that lends itself, and must carry `THREAD_SET_CONTEXT` access, to queue,
24/// and `THREAD_QUERY_LIMITED_INFORMATION`, to ask which process it belongs to: a
25/// [`std::thread::JoinHandle`] carries both.
26///
27/// **It must be a thread of this process.** The routine is an address and a closure in this
28/// process, which mean nothing in another, where delivering it would run whatever is at that
29/// address. A thread of another process is refused, as [`io::ErrorKind::InvalidInput`], before
30/// anything is queued.
31///
32/// **A thread that has not finished starting runs it as it starts.** Windows delivers an APC queued
33/// to a new thread when the thread begins, before it reaches the Rust runtime that
34/// [`std::thread::spawn`] sets up around the closure it was given, so the routine runs outside that
35/// runtime: `std::thread::current` there aborts the process ("current thread handle already
36/// set"). Queue to a thread known to be running -- one that has announced itself -- unless the
37/// routine touches nothing of the runtime.
38///
39/// **The routine may never run.** If the thread ends first, or never waits alertably, it stays
40/// queued and is not dropped, so what it captured is leaked; no error says so, because the call
41/// succeeded. A routine that must not be lost needs a thread known to wait alertably.
42///
43/// **It runs in whatever context the thread is in** when it makes the wait -- possibly while the
44/// thread holds locks or is inside a callback of the caller's -- so it must not assume the thread
45/// is idle. A panic in it aborts the process, as a panic out of any `extern "system"` function does.
46///
47/// # Errors
48///
49/// Invalid handle for one that is not a thread, access denied for a thread handle without
50/// `THREAD_QUERY_LIMITED_INFORMATION` or `THREAD_SET_CONTEXT`, [`io::ErrorKind::InvalidInput`] for a
51/// thread of another process, and otherwise the error from `QueueUserAPC`. The routine is dropped,
52/// unrun, when the call fails.
53pub fn queue_user_apc(
54 thread: impl AsHandle,
55 routine: impl FnOnce() + Send + 'static,
56) -> io::Result<()> {
57 let thread = thread.as_handle().as_raw_handle();
58 require_this_process(thread)?;
59 queue(thread, Box::new(routine))
60}
61
62/// Refuse a thread that is not this process's: a routine's address is only meaningful here.
63fn require_this_process(thread: RawHandle) -> io::Result<()> {
64 // SAFETY: `thread` is open for the call, borrowed by the caller; a handle that is not a thread
65 // makes the call fail, reported below.
66 let owner = unsafe { GetProcessIdOfThread(thread) };
67 if owner == 0 {
68 return Err(io::Error::last_os_error());
69 }
70 // SAFETY: `GetCurrentProcessId` takes no arguments.
71 if owner != unsafe { GetCurrentProcessId() } {
72 return Err(io::Error::new(
73 io::ErrorKind::InvalidInput,
74 "the thread belongs to another process, where this process's routine means nothing",
75 ));
76 }
77 Ok(())
78}
79
80/// [`queue_user_apc`] to the calling thread.
81///
82/// The routine runs in the next alertable wait the thread makes, which is what makes the wait's
83/// [`Alerted`](crate::WaitOutcome::Alerted) ending reachable without a second thread.
84///
85/// # Errors
86///
87/// The error from `QueueUserAPC`, which for the calling thread arises only when the kernel cannot
88/// allocate the APC; no test reaches it.
89pub fn queue_user_apc_to_current_thread(routine: impl FnOnce() + Send + 'static) -> io::Result<()> {
90 // SAFETY: `GetCurrentThread` takes no arguments and returns a pseudo-handle for the calling
91 // thread, valid on that thread without being closed.
92 let thread = unsafe { GetCurrentThread() };
93 queue(thread, Box::new(routine))
94}
95
96fn queue(thread: RawHandle, routine: Routine) -> io::Result<()> {
97 // The routine is a fat pointer and the call carries one machine word, so it is boxed again.
98 let data = Box::into_raw(Box::new(routine));
99 // SAFETY: `run` is a valid APC routine, `thread` is open for the call (borrowed by the caller,
100 // or the pseudo-handle), and the word is a pointer that `run` takes back exactly once.
101 let queued = unsafe { QueueUserAPC(Some(run), thread, data.expose_provenance()) };
102 if queued == 0 {
103 let error = io::Error::last_os_error();
104 // SAFETY: the call failed, so nothing was queued and the pointer is still ours alone.
105 drop(unsafe { Box::from_raw(data) });
106 return Err(error);
107 }
108 Ok(())
109}
110
111/// The APC routine: takes back what `queue` boxed, and runs it.
112///
113/// # Safety
114///
115/// `data` must be the pointer `queue` gave to a successful `QueueUserAPC`, which the kernel
116/// delivers at most once.
117unsafe extern "system" fn run(data: usize) {
118 let data = ptr::with_exposed_provenance_mut::<Routine>(data);
119 // SAFETY: by the contract above, `data` is the live box `queue` made, taken back here once.
120 let routine = unsafe { Box::from_raw(data) };
121 (*routine)();
122}
123
124#[cfg(test)]
125mod tests;