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 thread;
use MemoryPriority;
use ;
// 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 = capture?
.with_declared;
let applied = spawn
.join
.expect?;
assert_eq!;
// 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!;
# Ok::
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 Captured;
let omitted: = NotCaptured;
let asked_and_empty: = Absent;
// Both yield nothing, which is what `Option` would collapse them to...
assert_eq!;
assert_eq!;
// ...but only one of them is a decision, and that stays recoverable.
assert!;
assert!;
An invalid error-mode bit is not representable
use 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 = from_bits
.expect_err;
assert_eq!;
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
SetThreadTokenplus 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.