windows-thread-ambient-sys 0.2.0

Capture a Windows thread's ambient state and apply it on another thread.
Documentation

windows-thread-ambient-sys

Capture a Windows thread's ambient state and apply it on another thread.

Windows only. Every public item is behind cfg(windows); the crate builds to an empty shell on other platforms.

Why

Some Windows behaviour is not a parameter of the call you make. It is ambient state hanging off the calling thread:

  • an impersonation token decides whose rights an open is checked against, and even which drive letters resolve;
  • the thread error mode decides whether a hard device error raises a modal dialog;
  • WOW64 filesystem redirection decides which of two directories a 32-bit process actually reaches.

None of it travels with work handed to another thread. A thread-pool worker inherits none of it: measured, OpenThreadToken on a worker returns ERROR_NO_TOKEN while the submitting thread genuinely held a token, and the worker's error mode is 0 -- so an absent removable drive can put a modal dialog on process-shared infrastructure.

Scope

The crate carries thread-scoped ambient state that changes what a Win32 call does. It does not carry call parameters, does not open files, and does not know what any particular Windows operation is.

It also holds no policy. Every aspect is offered for capture and for explicit declaration. A consumer running on shared threads will want to force the dialog-suppressing error-mode bits; a consumer with a private thread is entitled to the opposite choice, and does not have to fight this layer to make it.

Two sets, because the aspects do not relate to the caller the same way

Aspects that can be read off the calling thread are captured, and the caller chooses which to collect. Aspects that cannot be read -- WOW64 redirection has no getter at all, and I/O priority has no documented one -- are declared instead: the caller states the value it wants installed. A declared aspect has nothing to collect, so it is not part of any capture set, and leaving it unspecified means the target thread's own value is untouched.

Examples

Carry a caller's context onto a worker

use std::thread;

use windows_thread_ambient_sys::declared::MemoryPriority;
use windows_thread_ambient_sys::{AmbientState, CaptureSet, Declared};

// Captured on the submitting thread, where a failure is still the caller's to
// see rather than arriving later from a worker. Declared aspects are stated
// rather than read; unspecified ones leave the worker's own values alone.
let state = AmbientState::capture(CaptureSet::DEFAULT)?
    .with_declared(Declared::none().with_memory_priority(MemoryPriority::Low));

let applied = thread::spawn(move || {
    // Applied outermost-first and released in exact reverse, with
    // impersonation innermost because its window is the narrowest.
    state.with_applied(|| "ran as the submitter")
})
.join()
.expect("the worker did not panic")?;

assert_eq!(*applied.value(), "ran as the submitter");

// A restore failure for the reported aspects -- the error mode, the declared
// aspects, the transaction -- does not discard the value; it is reported
// alongside it, so a caller can retire a contaminated thread without losing the
// result. Impersonation is deliberately not among them: its restore is
// fail-fast and panics, as "One capture, many workers" below explains.
assert!(applied.restore().is_clean());
# Ok::<(), Box<dyn std::error::Error>>(())

To override the error mode rather than transplant it -- forcing the dialog-suppressing bits on a shared worker -- leave CaptureSet::ERROR_MODE out of the capture set and wrap the call in your own ThreadErrorMode::apply guard, which then sits outermost.

Not captured is not the same as captured and absent

use windows_thread_ambient_sys::Captured;

let omitted: Captured<u32> = Captured::NotCaptured;
let asked_and_empty: Captured<u32> = Captured::Absent;

// Both yield nothing, which is what `Option` would collapse them to...
assert_eq!(omitted.present(), None);
assert_eq!(asked_and_empty.present(), None);

// ...but only one of them is a decision, and that stays recoverable.
assert!(!omitted.was_captured());
assert!(asked_and_empty.was_captured());

An invalid error-mode bit is not representable

use windows_thread_ambient_sys::ThreadErrorMode;

// 0x0004 is SEM_NOALIGNMENTFAULTEXCEPT. Measured, Windows rejects it per
// thread *and* an invalid bit fails the whole call -- so a caller combining it
// with valid bits would install none of them. The type refuses it instead.
let refused = ThreadErrorMode::from_bits(0x0001 | 0x0004)
    .expect_err("the alignment bit is not settable per thread");
assert_eq!(refused.bits(), 0x0004);

What is carried

Aspect How it relates to the caller Notes
Impersonation captured Consumed from windows-impersonation-token-sys; its fail-fast restore is inherited unchanged
Thread error mode captured and declarable The only aspect in both sets, so a consumer may transplant it or impose its own
TxF transaction captured Outside the default set: deprecated, and a captured transaction can be committed or rolled back beneath the worker
WOW64 redirection declared Has no getter at all, so there is nothing to capture
Memory priority declared Readable, but remoting a caller's priority without being asked is a policy choice
Background mode declared Moves CPU, I/O and memory priority together, which the name says out loud

One capture, many workers

AmbientState is Send and Sync, so a single capture can be shared through an Arc and applied concurrently on any number of workers -- the shape a traversal or scan engine has, capturing once at submission and running it on every worker for the length of the job. Sharing is also the cheap option: capture duplicates a kernel token object, so re-capturing per unit of work re-pays for a snapshot you already hold.

Two things a consumer of that shape should know before adopting it:

  • Granularity is yours to choose and costs something. Applying once around a batch and applying per operation are both expressible; each application is a SetThreadToken plus a call per other aspect in play. Prefer the widest window the aspects allow -- but the narrowest is sometimes right for a non-performance reason, as in the enumeration crate, which impersonates only around its directory open because later queries use the handle and need no token.
  • A restore failure for impersonation is fail-fast. It panics, and a panic inside a thread-pool callback aborts the process. On sixty-four impersonated workers that is not one failed operation, it is the whole process. This is the intended trade -- a shared worker returned to a pool under an unknown identity is a process-wide security failure -- but it should be a decision, not a discovery.

Status

The aspects and the composite are complete: capture, declaration, ordered application, exact-reverse release, and the restore report. Milestones M22 and M23 of CHECKLIST-thread-ambient.md are finished; the design decisions behind them are in DESIGN-NOTES.md. Not yet released to crates.io.

The examples above are compiled as doctests, so a contract change breaks the build rather than leaving the README teaching the old answer. The composite is additionally proved on a real Windows thread-pool worker in tests/thread_pool.rs -- including the negative that motivates the crate, that an aspect which was not captured does not arrive on the worker.