diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0f6f09956..7189a00b2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -43,6 +43,7 @@ Please see each crate's change log below:
- [`templated_uri_macros`](./crates/templated_uri_macros/CHANGELOG.md)
- [`templated_uri_macros_impl`](./crates/templated_uri_macros_impl/CHANGELOG.md)
- [`thread_aware`](./crates/thread_aware/CHANGELOG.md)
+- [`thread_aware_core`](./crates/thread_aware_core/CHANGELOG.md)
- [`thread_aware_macros`](./crates/thread_aware_macros/CHANGELOG.md)
- [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/CHANGELOG.md)
- [`tick`](./crates/tick/CHANGELOG.md)
diff --git a/Cargo.lock b/Cargo.lock
index 694a37d85..998fd851f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -141,7 +141,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -152,7 +152,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -1499,7 +1499,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -3066,7 +3066,7 @@ version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -3374,7 +3374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4264,7 +4264,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4320,7 +4320,7 @@ dependencies = [
"security-framework",
"security-framework-sys",
"webpki-root-certs",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4676,7 +4676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
- "windows-sys 0.61.2",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -4803,7 +4803,7 @@ dependencies = [
"getrandom 0.4.3",
"once_cell",
"rustix",
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
@@ -4928,6 +4928,13 @@ dependencies = [
"thread_aware",
]
+[[package]]
+name = "thread_aware_core"
+version = "0.1.0"
+dependencies = [
+ "static_assertions",
+]
+
[[package]]
name = "thread_aware_macros"
version = "0.11.0"
@@ -5655,7 +5662,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [
- "windows-sys 0.61.2",
+ "windows-sys 0.52.0",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index 33f02cb4a..7f50a1174 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -231,6 +231,7 @@ templated_uri_macros_impl = { path = "crates/templated_uri_macros_impl", default
testing_aids = { path = "crates/testing_aids", default-features = false }
thiserror = { version = "2.0.17", default-features = false }
thread_aware = { path = "crates/thread_aware", default-features = false, version = "0.11.0" }
+thread_aware_core = { path = "crates/thread_aware_core", default-features = false, version = "0.1.0" }
thread_aware_macros = { path = "crates/thread_aware_macros", default-features = false, version = "0.11.0" }
thread_aware_macros_impl = { path = "crates/thread_aware_macros_impl", default-features = false, version = "0.11.0" }
thread_local = { version = "1.1.8", default-features = false }
diff --git a/README.md b/README.md
index 215727e71..6040fcf79 100644
--- a/README.md
+++ b/README.md
@@ -63,6 +63,7 @@ These are the primary crates built out of this repo:
- [`seatbelt_http`](./crates/seatbelt_http/README.md) - HTTP-specific extensions for the seatbelt crate.
- [`templated_uri`](./crates/templated_uri/README.md) - Standards-compliant URI handling with templating, safety validation, and data classification
- [`thread_aware`](./crates/thread_aware/README.md) - Facilities to support thread-isolated state.
+- [`thread_aware_core`](./crates/thread_aware_core/README.md) - Stable core traits and types for thread-aware state.
- [`tick`](./crates/tick/README.md) - Provides primitives to interact with and manipulate machine time.
- [`uniflight`](./crates/uniflight/README.md) - Coalesces duplicate async tasks into a single execution.
diff --git a/crates/thread_aware_core/CHANGELOG.md b/crates/thread_aware_core/CHANGELOG.md
new file mode 100644
index 000000000..825c32f0d
--- /dev/null
+++ b/crates/thread_aware_core/CHANGELOG.md
@@ -0,0 +1 @@
+# Changelog
diff --git a/crates/thread_aware_core/Cargo.toml b/crates/thread_aware_core/Cargo.toml
new file mode 100644
index 000000000..7bcfc6194
--- /dev/null
+++ b/crates/thread_aware_core/Cargo.toml
@@ -0,0 +1,37 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+[package]
+name = "thread_aware_core"
+description = "Stable core traits and types for thread-aware state."
+version = "0.1.0"
+readme = "README.md"
+keywords = ["oxidizer", "thread", "aware"]
+categories = ["data-structures", "no-std"]
+
+edition = { workspace = true }
+rust-version = { workspace = true }
+authors = { workspace = true }
+license = { workspace = true }
+homepage = { workspace = true }
+include = { workspace = true }
+repository = "https://github.com/microsoft/oxidizer/tree/main/crates/thread_aware_core"
+
+[package.metadata.docs.rs]
+all-features = true
+
+[features]
+default = ["std"]
+std = []
+
+[dev-dependencies]
+static_assertions = { workspace = true }
+
+[[example]]
+name = "relocation"
+required-features = ["std"]
+
+# >>> anvil-managed: anvil-lints
+[lints]
+workspace = true
+# <<< anvil-managed: anvil-lints
diff --git a/crates/thread_aware_core/README.md b/crates/thread_aware_core/README.md
new file mode 100644
index 000000000..154bcbaf0
--- /dev/null
+++ b/crates/thread_aware_core/README.md
@@ -0,0 +1,254 @@
+
+

+
+# Thread Aware Core
+
+[](https://crates.io/crates/thread_aware_core)
+[](https://docs.rs/thread_aware_core)
+[](https://crates.io/crates/thread_aware_core)
+[](https://github.com/microsoft/oxidizer/actions/workflows/main.yml)
+[](https://codecov.io/gh/microsoft/oxidizer)
+[](https://github.com/microsoft/oxidizer/blob/main/LICENSE)
+

+
+
+
+Support for values that adapt when a runtime moves them to another thread.
+
+This crate contains the small API shared by thread-aware libraries:
+
+* [`ThreadAware`][__link0] notifies a value that it has moved.
+* [`Thread`][__link1] records where it now runs: which runtime, which OS thread, and which memory is
+ closest to it.
+
+[`Thread`][__link2] is a coordinate, not a handle: a runtime builds one to describe where a value
+is running, and it owns no operating-system resource. It is unrelated to
+[`std::thread::Thread`][__link3], which is a handle to a live OS thread. Naming both in one module
+requires aliasing one of them.
+
+## The `thread_aware` family
+
+* **`thread_aware_core`** (this crate) — the vocabulary that two unrelated libraries must
+ agree on before either can relocate a value defined by the other. Deliberately small and
+ slow-moving, so naming [`ThreadAware`][__link4] or [`Thread`][__link5] in your own public API costs you
+ nothing later.
+* **[`thread_aware`][__link6]** — the utilities that make relocation convenient: a
+ [`#[derive(ThreadAware)]`][__link7] macro, wrappers for foreign types, a per-core
+ [`Arc`][__link8], containers and registries. Free to evolve, and not meant to appear in a
+ public API.
+
+Depend on this crate directly when all you need is the trait. It adds nothing to your
+dependency graph, and works without `std`: with default features turned off, [`Thread`][__link9]
+loses its thread id component and keeps [`Owner`][__link10] and [`NumaNode`][__link11].
+
+## Why relocation exists
+
+Thread-per-core and NUMA-aware runtimes are fast because each worker keeps to itself: it
+uses memory close to its own thread, talks to its own I/O driver, and does not
+synchronize with other workers. When a value moves to another worker, what used to be
+close by is now in the wrong place: a cache line shared between threads, memory in a
+distant region, a handle to another thread’s driver.
+
+[`ThreadAware`][__link12] lets that state repair itself. The runtime moves the value, then calls
+[`relocate`][__link13] to report where it now lives. Relocation has two
+sides, and most code sits on only one of them.
+
+## Library authors: implementing the trait
+
+**Library and application authors** implement [`ThreadAware`][__link14], usually through the
+[`#[derive(ThreadAware)]`][__link15] macro. They never call
+[`relocate`][__link16] and never construct a [`Thread`][__link17]; the runtime does
+both and then invokes the implementation. It is a callback, like [`Drop::drop`][__link18].
+
+The derive lives in [`thread_aware`][__link19], so a library that wants it depends on that crate.
+Only the trait and [`Thread`][__link20] cross the public boundary, and both come from here, so the
+dependency stays an implementation detail:
+
+```rust
+// A build dependency, not part of what this library promises.
+use thread_aware::ThreadAware;
+
+/// A codec whose scratch buffer should follow the memory it is used from.
+#[derive(ThreadAware)]
+pub struct Encoder {
+ scratch: Scratch,
+ dictionary: Dictionary,
+}
+```
+
+The derive writes the forwarding implementation, calling `relocate` on `scratch` and
+`dictionary` in turn. Because a composed type forwards to its fields, one call at the top
+reaches everything below it. Callers of `Encoder` never name [`thread_aware`][__link21].
+
+## Runtime authors: driving relocation
+
+**Runtime authors** construct a [`Thread`][__link22] per worker and call
+[`relocate`][__link23] after moving a value, passing where it came from and
+where it now runs. The example below plays the part of the runtime so the order is
+visible.
+
+```rust
+use std::thread;
+
+use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware};
+
+// What a library author writes.
+struct Worker {
+ thread: Option,
+}
+
+impl ThreadAware for Worker {
+ fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) {
+ self.thread = Some(destination.id());
+ }
+}
+
+// What the runtime does.
+let here = thread::current().id();
+let there = thread::spawn(|| thread::current().id()).join().unwrap();
+
+let owner = Owner::new(2);
+let first = Thread::new(owner.clone(), here, NumaNode::new(0));
+let second = Thread::new(owner, there, NumaNode::new(1));
+
+let mut worker = Worker { thread: None };
+
+worker.relocate(None, &first); // first placement, no previous `Thread`
+worker.relocate(Some(&first), &second); // moved to another thread
+
+assert_eq!(worker.thread, Some(there));
+```
+
+## Performance, not correctness
+
+Relocation is an optimization, not a guarantee. A value must remain correct if the call
+never comes, comes twice, or reports the same source and destination. Missing calls are
+normal: a value can reach another thread through `std::thread::spawn`, a channel, or a
+runtime that knows nothing about this trait. That may make things slower, but it must
+never cause a panic, a deadlock, or a wrong answer.
+
+Nor is it a hot path. Expect roughly one relocation per object graph per job or request,
+after which the value is used normally. Avoiding synchronization matters more than saving a
+few cycles.
+
+## What the ids mean
+
+* **Thread id** — a `std::thread::ThreadId`, unique among the threads alive at once, so
+ state keyed on it is never shared by accident, not even between two runtimes in one
+ process.
+* **[`NumaNode`][__link24]** — the memory closest to that thread. Unlike the thread id it is
+ *shared*: every thread near the same memory reports the same node, which is what suits
+ it to state shared within a region but not across the machine. That holds only while
+ every runtime numbers the regions identically. Nothing checks it, and runtimes that
+ disagree make shared state wrong rather than merely slow.
+* **[`Owner`][__link25]** — the runtime a [`Thread`][__link26] belongs to. Every new owner is unique, so two
+ live runtimes never share one. It lets a value detect that it has crossed into a
+ different runtime and release anything the previous one owned.
+
+An implementation reads only the ids its state depends on. A per-thread cache or a handle
+to a thread-local driver keys on the thread id; a buffer pool keys on [`NumaNode`][__link27] and
+survives a move to another thread near the same memory; anything the runtime owns compares
+[`Owner`][__link28].
+
+The ids carry no meaning beyond identity: they need not start at zero or run
+consecutively, and the [`Thread`][__link29]s in use cannot be enumerated. State keyed on any of them
+belongs in a map rather than an array indexed by it. [`Owner::min_threads`][__link30] is the one
+number on offer, and it is a floor to pre-size against, not a bound to index against.
+
+Without `std` there is no [`ThreadId`][__link31]: `Thread::new` and
+`Thread::id` are absent and only [`Owner`][__link32] and [`NumaNode`][__link33] remain. A `no_std` library can
+still implement [`ThreadAware`][__link34] and use whatever it is given; the runtime that drives
+relocation requires `std` regardless.
+
+## Relation to `Send`
+
+[`ThreadAware`][__link35] requires [`Send`][__link36], and in that order: a value is sent to another thread
+first, then told where it landed. [`Send`][__link37] is what makes the move safe, and
+[`ThreadAware`][__link38] adds nothing to it.
+
+## Provided implementations
+
+Types with nothing tied to a thread receive an empty implementation: primitives and their
+non-zero variants, the thread ids, `Duration`, strings, safe function pointers of up to
+twelve parameters, and, with the `std` feature, paths.
+
+Containers forward the call to what they hold: [`Option`][__link39], [`Result`][__link40], arrays, slices,
+`Vec`, `VecDeque`, `Box`, cells, tuples of up to twelve elements, and map values.
+
+References are not [`ThreadAware`][__link41]. Relocating through one would adapt something the value
+only borrows, and whoever owns it is relocated on its own account.
+
+Map keys are left alone, since altering one could change its hash or ordering and corrupt
+the map. Sets are not implemented at all for the same reason, so a `HashSet` or
+`BTreeSet` field is not [`ThreadAware`][__link42].
+
+`Cow` is omitted for now: relocating a borrowed one has to clone it into owned storage
+first, which is a surprising amount of work to hide behind a hint.
+
+`Arc` is also omitted: whether a shared allocation should stay shared across threads or
+be split per thread depends on what it holds. The per-core [`Arc`][__link43] in
+[`thread_aware`][__link44] covers the case where splitting is correct.
+
+## Features
+
+* **`std`** *(default)* - Adds [`Thread::new`][__link45] and [`Thread::id`][__link46], which need
+ [`ThreadId`][__link47], and implements [`ThreadAware`][__link48] for standard library
+ types such as `HashMap`, `Path` and `PathBuf`. Turn it off for `no_std`, which needs
+ only `alloc` and pointer-width atomics.
+
+
+
+
+This crate was developed as part of The Oxidizer Project. Browse this crate's source code.
+
+
+ [__cargo_doc2readme_dependencies_info]: ggGmYW0CYXZlMC43LjJhdIQb11VxC_uAPOQbtUn4Wx2-BfAbid3Nt1Y27Pobprn8Z6FjFy9hYvRhcoQbHZWlC1ydpTAb-CfpCfELSs4bQbTDXMAtkA8bpQ3Y3OBUQHFhZIGCcXRocmVhZF9hd2FyZV9jb3JlZTAuMS4w
+ [__link0]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link1]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link10]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner
+ [__link11]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode
+ [__link12]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link13]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate
+ [__link14]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link15]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html
+ [__link16]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate
+ [__link17]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link18]: https://doc.rust-lang.org/stable/std/?search=ops::Drop::drop
+ [__link19]: https://docs.rs/thread_aware
+ [__link2]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link20]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link21]: https://docs.rs/thread_aware
+ [__link22]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link23]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware::relocate
+ [__link24]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode
+ [__link25]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner
+ [__link26]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link27]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode
+ [__link28]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner
+ [__link29]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link3]: https://doc.rust-lang.org/stable/std/?search=thread::Thread
+ [__link30]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner::min_threads
+ [__link31]: https://doc.rust-lang.org/stable/std/?search=thread::ThreadId
+ [__link32]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Owner
+ [__link33]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=NumaNode
+ [__link34]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link35]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link36]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html
+ [__link37]: https://doc.rust-lang.org/stable/std/marker/trait.Send.html
+ [__link38]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link39]: https://doc.rust-lang.org/stable/std/option/enum.Option.html
+ [__link4]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link40]: https://doc.rust-lang.org/stable/std/result/struct.Result.html
+ [__link41]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link42]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link43]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html
+ [__link44]: https://docs.rs/thread_aware
+ [__link45]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread::new
+ [__link46]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread::id
+ [__link47]: https://doc.rust-lang.org/stable/std/?search=thread::ThreadId
+ [__link48]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=ThreadAware
+ [__link5]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
+ [__link6]: https://docs.rs/thread_aware
+ [__link7]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html
+ [__link8]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html
+ [__link9]: https://docs.rs/thread_aware_core/0.1.0/thread_aware_core/?search=Thread
diff --git a/crates/thread_aware_core/docs/DESIGN.md b/crates/thread_aware_core/docs/DESIGN.md
new file mode 100644
index 000000000..09ea58c06
--- /dev/null
+++ b/crates/thread_aware_core/docs/DESIGN.md
@@ -0,0 +1,175 @@
+# Thread Aware Core — Design
+
+## What this crate is
+
+`thread_aware_core` is the **stable vocabulary** for values that adapt when a
+runtime moves them between threads. It exists to be depended on permanently:
+crates are meant to name `ThreadAware` and `Thread` in their own public
+signatures, and this crate's job is to make sure that never becomes a liability.
+
+It exposes one trait, one identifier and its two component ids, and nothing
+else:
+
+- `ThreadAware` — the callback a value implements to be told it has moved.
+- `Thread` — where a value runs, built from `Owner`, a `std::thread::ThreadId`,
+ and `NumaNode`.
+
+Everything that makes relocation *convenient* — registries, containers,
+callbacks, derive macros, runtime integration — lives in the pre-1.0
+`thread_aware` crate. This crate carries only what two unrelated libraries must
+agree on in order to interoperate. Implementations of `ThreadAware` for
+`core`, `alloc` and `std` types ship here because they are part of that shared
+vocabulary; implementations for third-party types deliberately do not.
+
+## Why a separate crate
+
+A vocabulary trait is only useful if everyone names the same one. If a
+connection pool and a codec each define their own `ThreadAware`, a runtime that
+relocates a value containing both has two unrelated traits to satisfy and no way
+to treat them uniformly. The trait must come from a single crate they share.
+
+That makes this crate's compatibility a shared constraint rather than a private
+matter. Two libraries interoperate only when their public APIs resolve to the
+same `thread_aware_core` instance, so an incompatible release does not
+inconvenience one crate — it splits the ecosystem until every participant has
+upgraded. A crate in that position has to be *boring*: small, dependency-free,
+and stable far longer than the code around it. The MSRV is part of the same
+bargain, and is raised deliberately rather than incidentally.
+
+Splitting it out is what buys that. The volatile parts of `thread_aware` keep
+iterating pre-1.0 while the few items that appear in public signatures stabilise
+ahead of them.
+
+## Scope
+
+`Owner` describes a runtime; `NumaNode` describes hardware. Neither is
+validated, and their guarantees differ in ways implementations depend on. A
+thread id is meaningful only while that thread lives, an `Owner` only while its
+runtime does, and a `NumaNode` is shared by every thread near the same memory —
+including threads of a different runtime, but only while all of them number the
+nodes identically.
+
+`Owner` values are issued by runtime and integration code, never by ordinary
+data-structure authors. Every new owner is unique, so two runtimes alive at once
+cannot collide however carelessly they are set up. An owner also reports the
+smallest number of threads its runtime runs: a floor to pre-size against, which
+the runtime may exceed, and which is zero for one that spawns on demand.
+
+Implementing `ThreadAware` also commits a type to `Send`, since that is a
+supertrait. A type holding an `Rc` or another thread-bound handle cannot
+participate at all, even with an empty `relocate`.
+
+## Principles
+
+**Performance, not correctness.** `relocate` is advisory. A value must be fully
+correct if it is never called, called twice, or called with a `Thread` it has
+never seen. This is the most important property in the design: it is what lets a
+runtime call `relocate` opportunistically, lets an implementation give up and
+stay slow rather than fail, and lets the method be infallible. A trait whose
+correctness depended on being called would need error reporting, ordering
+guarantees, and a way to refuse a move — none of which this crate has.
+
+**Say only what interoperability requires.** Every item here is a permanent
+commitment, so the bar for adding one is whether two independent crates must
+agree on it. A helper that merely makes implementation pleasant does not
+qualify; it belongs in `thread_aware`, where it can still change.
+
+**Borrow from `std` rather than invent.** The thread coordinate is
+`std::thread::ThreadId`, not an id of our own: a bespoke type would be one more
+thing to convert to and from, and one more thing to keep stable forever. The
+cost is that `Thread::new` and `Thread::id` need the `std` feature. With
+default features off, a `no_std` crate can still implement `ThreadAware` and
+read `Owner` and `NumaNode` from the `Thread` values it is handed; it just cannot
+construct one, which only runtimes need to do. Such a build needs `alloc` and
+pointer-width atomics, the latter because `Owner` identities come from a
+process-wide counter.
+
+**No dependencies reach a consumer.** The manifest's only entry is a test-only
+dev-dependency, so adopting this crate cannot introduce a version conflict or
+pull anything unexpected into a build.
+
+**Describe, do not enforce.** Nothing checks that runtimes number NUMA nodes
+alike or that implementations avoid blocking. These are documented
+obligations; enforcing them would need runtime state and a coordination point,
+and this crate has neither.
+
+## Evolving without breaking changes
+
+The crate has to be able to describe hardware it does not model yet. Machines
+already subdivide a NUMA node into cache domains, and a `Thread` may one day need
+to describe a value pinned to memory but not to a processor. Both mean
+adding a coordinate, and the design makes that additive.
+
+**Ids are opaque, and take no `From`.** `NumaNode` exposes no accessor
+returning its integer, and both ids are built through inherent constructors
+rather than a `From` impl. That is a deliberate refusal, because a
+`From` on a permanent vocabulary type is a one-shot commitment. While
+one such impl exists it silently governs the width every unsuffixed literal
+infers; adding a second either breaks those call sites — `from(1)` falls back to
+`i32` and stops compiling — or, when the added impl happens to be `From`,
+*silently re-resolves them to the new impl* with no error at all. Replacing the
+impl instead breaks every caller passing a typed variable. None of these are
+reported by `cargo semver-checks`, so adding one would reach a release
+unchallenged.
+
+An inherent constructor avoids the trap entirely: `u32` already exceeds any node
+count real hardware reaches, and if a wider or fallible form is ever needed it
+arrives as a second, differently named constructor, which is purely additive.
+The *stored* width remains private and may grow silently — it is not part of the
+promise, and growing it is what would reserve values no existing constructor can
+reach.
+
+**`Thread` fields are private.** Coordinates are read through accessors, so the
+struct can gain a field without touching any existing reader, and the derived
+`Clone`, `Debug`, `PartialEq`, `Eq` and `Hash` pick it up automatically. No
+`#[non_exhaustive]` is needed: with no public fields there is nothing to
+destructure or update.
+
+What a new field *can* silently remove is an auto trait — a coordinate that is
+not `Send`, `Sync` or unwind-safe strips that property from `Thread` without any
+signature changing. Static assertions pin the auto traits the public types are
+expected to have, so this fails the build rather than reaching a release.
+
+**Every coordinate has a default variant.** This is the rule that makes the rest
+work: a new coordinate must have a value meaning *unknown, or not pinned* —
+something like a `NumaNode::UNPINNED` constant that no ordinary construction can
+produce. Existing constructors keep their exact signatures and fill the new
+coordinate with that default; callers that care about it opt in through a new
+constructor.
+
+Reserving such a value costs one representation state that no constructor hands
+out. Since `new` accepts the full `u32` range, a sentinel would come either from
+widening the private storage past it or from making the reserving constructor
+fallible, so that the reserved value can never be minted. A coordinate type
+introduced later can simply reserve one from the start.
+
+Behaviour is preserved as well as compilation. Every pre-existing construction
+path fills the new coordinate identically, so values built the old way remain
+equal to exactly the values they were equal to before; the new coordinate can
+only tell them apart once someone deliberately sets it.
+
+Adding a coordinate is therefore three additive steps:
+
+1. Add the private field and its accessor.
+2. Add the sentinel constant to the coordinate's type.
+3. Add a constructor that accepts it, leaving the existing one alone.
+
+**Trait additions stay defaulted and dyn-compatible.** `dyn ThreadAware` is part
+of the stable surface, so a default body alone is not enough. Any method added
+to `ThreadAware` must also avoid generic parameters, `impl Trait`, and `Self` by
+value, or carry `where Self: Sized` so the vtable ignores it. Otherwise every
+existing `impl` still compiles while every `Box` stops. An
+assertion pins dyn-compatibility for the same reason the auto traits are pinned.
+
+### What this does not permit
+
+The escape hatch is one-directional. Removing a coordinate, changing the
+signature of an existing constructor, adding a `From` impl, changing
+what an existing id *means*, or adding a method that breaks dyn-compatibility
+are all breaking, and no sentinel value makes them otherwise. Reserving the
+additive path for genuinely new information is what keeps it available.
+
+## Related documents
+
+For the exact list of items inside the stable boundary, see
+[STABILIZATION.md](./STABILIZATION.md).
diff --git a/crates/thread_aware_core/docs/STABILIZATION.md b/crates/thread_aware_core/docs/STABILIZATION.md
new file mode 100644
index 000000000..4f0eeec61
--- /dev/null
+++ b/crates/thread_aware_core/docs/STABILIZATION.md
@@ -0,0 +1,40 @@
+# Stabilization Notes
+
+These notes describe the stable boundary shared by the `thread_aware` crates.
+
+## Status
+
+`thread_aware_core` is a stand-alone crate. Nothing depends on it yet, and the
+`thread_aware` crate still ships its own `Affinity`-based API unchanged. The
+plan is to adopt the core crate later; until then the two evolve separately.
+
+## Stable boundary
+
+`thread_aware_core` 1.0 contains the API that downstream crates may expose:
+
+- `ThreadAware`
+- `Thread`, and its component ids `Owner` and `NumaNode`
+
+A `Thread` says where a value runs: which runtime owns it (`Owner`), which
+OS thread it is on, and which memory is closest to that thread (`NumaNode`). The
+thread component is `std::thread::ThreadId` rather than an id of our own, so it
+is not re-exported; callers take it from `std`.
+
+Nothing the crate depends on reaches a consumer build; its only manifest entry
+is a test-only dev-dependency. The `std` feature is enabled by default and adds
+implementations for standard-library types such as `HashMap`, `Path` and
+`PathBuf`. Turn it off for `no_std`, where the crate needs only `alloc` and
+pointer-width atomics; a
+`Thread` then loses its thread id component and cannot be constructed, leaving `Owner` and
+`NumaNode` readable so that a `no_std` library can still implement
+`ThreadAware`. Implementations for types from external crates are intentionally
+outside the stable boundary.
+
+## Unstable utilities
+
+Implementation helpers, containers, callbacks, registry APIs, derive support,
+and integration helpers remain in the pre-1.0 `thread_aware` crate. Stable
+downstream crates should not expose those types in their public APIs.
+
+This split allows the trait contract and its required `Thread` type to remain
+stable without prematurely stabilizing the larger utility surface.
diff --git a/crates/thread_aware_core/examples/relocation.rs b/crates/thread_aware_core/examples/relocation.rs
new file mode 100644
index 000000000..04a8bb60c
--- /dev/null
+++ b/crates/thread_aware_core/examples/relocation.rs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Creating `Thread` values and relocating a value between them.
+//!
+//! Run with `cargo run -p thread_aware_core --example relocation`.
+
+#![allow(missing_docs, reason = "Example code")]
+#![allow(clippy::print_stdout, reason = "Example code")]
+
+use std::thread;
+
+use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware};
+
+/// A sample value that remembers which thread it currently runs on.
+struct Worker {
+ thread: Option,
+}
+
+impl ThreadAware for Worker {
+ // `relocate` is limited to bounded local work, so this only records the destination.
+ // Reporting happens in `main`, outside the callback.
+ fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) {
+ self.thread = Some(destination.id());
+ }
+}
+
+fn main() {
+ // Manually build two `Thread` values, one per OS thread, on the same NUMA node.
+ let here = thread::current().id();
+ let there = thread::spawn(|| thread::current().id())
+ .join()
+ .expect("the spawned thread cannot panic");
+
+ let owner = Owner::new(2);
+ let first = Thread::new(owner.clone(), here, NumaNode::new(0));
+ let second = Thread::new(owner, there, NumaNode::new(0));
+
+ // Relocate a sample object between them.
+ let mut worker = Worker { thread: None };
+
+ worker.relocate(None, &first); // initial placement; no previous `Thread`
+ println!("placed on thread {:?}", worker.thread);
+
+ worker.relocate(Some(&first), &second); // migrate from the first `Thread` to the second
+ println!("relocated to thread {:?}", worker.thread);
+}
diff --git a/crates/thread_aware_core/favicon.ico b/crates/thread_aware_core/favicon.ico
new file mode 100644
index 000000000..53e517713
--- /dev/null
+++ b/crates/thread_aware_core/favicon.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:82222216219fb16db9695b0e01b605d092cdbd416e0c4c8095688b0bb07e70dd
+size 200138
diff --git a/crates/thread_aware_core/logo.png b/crates/thread_aware_core/logo.png
new file mode 100644
index 000000000..4058391e0
--- /dev/null
+++ b/crates/thread_aware_core/logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9d5770f820f40b5c656d676345569068ce7bfe747ddc46febd9b1a5eafd6103
+size 82978
diff --git a/crates/thread_aware_core/src/impls.rs b/crates/thread_aware_core/src/impls.rs
new file mode 100644
index 000000000..4cc7a3170
--- /dev/null
+++ b/crates/thread_aware_core/src/impls.rs
@@ -0,0 +1,632 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use alloc::boxed::Box;
+use alloc::collections::{BTreeMap, VecDeque};
+use alloc::string::String;
+use alloc::vec::Vec;
+use core::cell::{Cell, RefCell};
+use core::num::{
+ NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU128,
+ NonZeroUsize,
+};
+use core::time::Duration;
+#[cfg(any(test, feature = "std"))]
+use std::collections::HashMap;
+#[cfg(any(test, feature = "std"))]
+use std::path::{Path, PathBuf};
+
+use crate::{NumaNode, Owner, Thread, ThreadAware};
+
+/// Implements [`ThreadAware`] for a type that holds nothing bound to a thread, so a
+/// move leaves it valid as-is and `relocate` has nothing to do.
+macro_rules! impl_thread_aware {
+ ($t:ty) => {
+ impl ThreadAware for $t {
+ #[inline]
+ fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+ }
+ };
+}
+
+impl_thread_aware!(bool);
+impl_thread_aware!(u8);
+impl_thread_aware!(u16);
+impl_thread_aware!(u32);
+impl_thread_aware!(u64);
+impl_thread_aware!(u128);
+impl_thread_aware!(i8);
+impl_thread_aware!(i16);
+impl_thread_aware!(i32);
+impl_thread_aware!(i64);
+impl_thread_aware!(i128);
+impl_thread_aware!(usize);
+impl_thread_aware!(isize);
+impl_thread_aware!(f32);
+impl_thread_aware!(f64);
+impl_thread_aware!(char);
+
+impl_thread_aware!(NonZeroU8);
+impl_thread_aware!(NonZeroU16);
+impl_thread_aware!(NonZeroU32);
+impl_thread_aware!(NonZeroU64);
+impl_thread_aware!(NonZeroU128);
+impl_thread_aware!(NonZeroUsize);
+impl_thread_aware!(NonZeroI8);
+impl_thread_aware!(NonZeroI16);
+impl_thread_aware!(NonZeroI32);
+impl_thread_aware!(NonZeroI64);
+impl_thread_aware!(NonZeroI128);
+impl_thread_aware!(NonZeroIsize);
+
+impl_thread_aware!(String);
+#[cfg(any(test, feature = "std"))]
+impl_thread_aware!(PathBuf);
+impl_thread_aware!(Duration);
+#[cfg(any(test, feature = "std"))]
+impl_thread_aware!(Path);
+
+impl_thread_aware!(str);
+
+impl_thread_aware!(Owner);
+impl_thread_aware!(NumaNode);
+#[cfg(any(test, feature = "std"))]
+impl_thread_aware!(std::thread::ThreadId);
+impl_thread_aware!(Thread);
+
+// We need to implement `ThreadAware` for tuples ranging from 0 to 12 elements
+macro_rules! impl_thread_aware_tuple {
+ ($head:ident, $($tail:ident,)*) => {
+ impl<$head, $($tail),*> ThreadAware for ($head, $($tail),*)
+ where
+ $head: ThreadAware,
+ $($tail: ThreadAware),*
+ {
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ #[expect(non_snake_case, reason = "Macro-generated code uses uppercase identifiers for tuple elements")]
+ let ($head, $($tail),*) = self;
+ $head.relocate(source, destination);
+ $( $tail.relocate(source, destination); )*
+ }
+ }
+
+ // Recursively call the macro for the rest of the tuple
+ impl_thread_aware_tuple!($($tail,)*);
+ };
+
+ () => {
+ impl ThreadAware for () {
+ #[inline]
+ fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+ }
+ };
+}
+
+impl_thread_aware_tuple!(A, B, C, D, E, F, G, H, I, J, K, L,);
+
+macro_rules! impl_thread_aware_fn {
+ ($head:ident, $($tail:ident,)*) => {
+ impl ThreadAware for fn($head, $($tail),*) -> R {
+ fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+ }
+
+ // Recursively call the macro for the rest of the function parameters
+ impl_thread_aware_fn!($($tail,)*);
+ };
+ () => {
+ impl ThreadAware for fn() -> R {
+ fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+ }
+ }
+}
+
+impl_thread_aware_fn!(A, B, C, D, E, F, G, H, I, J, K, L,);
+
+impl ThreadAware for [T; N]
+where
+ T: ThreadAware,
+{
+ #[inline]
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ self.as_mut_slice().relocate(source, destination);
+ }
+}
+
+impl ThreadAware for [T]
+where
+ T: ThreadAware,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ for value in self.iter_mut() {
+ value.relocate(source, destination);
+ }
+ }
+}
+
+impl ThreadAware for Option
+where
+ T: ThreadAware,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ if let Some(value) = self {
+ value.relocate(source, destination);
+ }
+ }
+}
+
+impl ThreadAware for Result
+where
+ T: ThreadAware,
+ E: ThreadAware,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ match self {
+ Ok(value) => value.relocate(source, destination),
+ Err(err) => err.relocate(source, destination),
+ }
+ }
+}
+
+impl ThreadAware for Vec
+where
+ T: ThreadAware,
+{
+ #[inline]
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ self.as_mut_slice().relocate(source, destination);
+ }
+}
+
+impl ThreadAware for VecDeque
+where
+ T: ThreadAware,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ for value in self.iter_mut() {
+ value.relocate(source, destination);
+ }
+ }
+}
+
+impl ThreadAware for Box
+where
+ T: ThreadAware + ?Sized,
+{
+ #[inline]
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ (**self).relocate(source, destination);
+ }
+}
+
+impl ThreadAware for BTreeMap
+where
+ K: Send,
+ V: ThreadAware,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ for value in self.values_mut() {
+ value.relocate(source, destination);
+ }
+ }
+}
+
+/// Implements [`ThreadAware`] for a single-threaded interior-mutability wrapper.
+///
+/// `relocate` already holds `&mut self`, so the inner value is reachable through
+/// `get_mut` without a borrow flag or a `Cell` round-trip.
+macro_rules! impl_thread_aware_cell {
+ ($t:ident) => {
+ impl ThreadAware for $t
+ where
+ T: ThreadAware,
+ {
+ #[inline]
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ self.get_mut().relocate(source, destination);
+ }
+ }
+ };
+}
+
+impl_thread_aware_cell!(Cell);
+impl_thread_aware_cell!(RefCell);
+
+#[cfg(any(test, feature = "std"))]
+impl ThreadAware for HashMap
+where
+ K: Send,
+ V: ThreadAware,
+ S: Send,
+{
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+ for value in self.values_mut() {
+ value.relocate(source, destination);
+ }
+ }
+}
+
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[cfg(test)]
+mod tests {
+ use alloc::boxed::Box;
+ use alloc::collections::{BTreeMap, VecDeque};
+ use alloc::string::{String, ToString};
+ use alloc::vec;
+ use alloc::vec::Vec;
+ use core::cell::{Cell, RefCell};
+ use core::num::{
+ NonZero, NonZeroI8, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI128, NonZeroIsize, NonZeroU8, NonZeroU16, NonZeroU32, NonZeroU64,
+ NonZeroU128, NonZeroUsize,
+ };
+ use std::collections::HashMap;
+
+ use crate::{NumaNode, Owner, Thread, ThreadAware};
+
+ /// A type whose `relocate` visibly mutates state, so mutation tests catch
+ /// no-op replacements.
+ #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+ struct Tracker(bool);
+
+ impl ThreadAware for Tracker {
+ fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {
+ self.0 = true;
+ }
+ }
+
+ fn sample_threads() -> [Thread; 2] {
+ let owner = Owner::new(2);
+ let thread = std::thread::current().id();
+ [
+ Thread::new(owner.clone(), thread, NumaNode::new(0)),
+ Thread::new(owner, thread, NumaNode::new(1)),
+ ]
+ }
+
+ #[test]
+ fn all_nonzero_integer_types_are_thread_aware() {
+ fn assert_thread_aware() {}
+
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+ assert_thread_aware::();
+
+ assert_thread_aware::>();
+ }
+
+ #[test]
+ fn hashmap_relocates_values_and_tolerates_empty() {
+ let threads = sample_threads();
+ let source = Some(&threads[0]);
+ let destination = &threads[1];
+
+ let mut value: HashMap = HashMap::new();
+ value.insert(1, "one".to_string());
+ value.insert(2, "two".to_string());
+
+ value.relocate(source, destination);
+
+ assert_eq!(value.get(&1), Some(&"one".to_string()));
+ assert_eq!(value.get(&2), Some(&"two".to_string()));
+
+ let mut empty_value: HashMap = HashMap::new();
+ empty_value.relocate(source, destination);
+ assert!(empty_value.is_empty());
+ }
+
+ #[test]
+ fn tuples_of_every_supported_arity_relocate() {
+ let threads = sample_threads();
+ let source = Some(&threads[0]);
+ let destination = &threads[1];
+
+ // Test empty tuple
+ let mut empty_tuple = ();
+ empty_tuple.relocate(source, destination);
+
+ // Test single element tuple
+ let mut single = (42,);
+ single.relocate(source, destination);
+ assert_eq!(single, (42,));
+
+ // Test two element tuple
+ let mut two = (42, "hello".to_string());
+ two.relocate(source, destination);
+ assert_eq!(two, (42, "hello".to_string()));
+
+ // Test three element tuple with different types
+ let mut three = (1, "test".to_string(), 1.23);
+ three.relocate(source, destination);
+ assert_eq!(three, (1, "test".to_string(), 1.23));
+
+ // Test larger tuple (6 elements)
+ let mut six = (1, 2, 3, 4, 5, 6);
+ six.relocate(source, destination);
+ assert_eq!(six, (1, 2, 3, 4, 5, 6));
+
+ // Test tuple with nested Vec (complex type)
+ let mut nested = (vec![1, 2, 3], "data".to_string(), 100u64);
+ nested.relocate(source, destination);
+ assert_eq!(nested, (vec![1, 2, 3], "data".to_string(), 100u64));
+
+ // Test tuple with Option
+ let mut with_option = (Some(42), None::, "value".to_string());
+ with_option.relocate(source, destination);
+ assert_eq!(with_option, (Some(42), None::, "value".to_string()));
+
+ // Test large tuple (12 elements - maximum supported)
+ let mut twelve = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
+ twelve.relocate(source, destination);
+ assert_eq!(twelve, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12));
+ }
+
+ /// `impl_thread_aware_tuple!` recurses by peeling one element off the head, so the
+ /// 1-tuple is the last impl it generates before the `()` base case. That arm is
+ /// the one where a missing comma would silently change meaning: `let (A) = self`
+ /// is a parenthesized pattern binding the whole tuple, which sends the impl back
+ /// into itself instead of reaching the element. `Tracker` makes the visit
+ /// observable, so this pins the head, the tail and the 12-element maximum.
+ #[test]
+ fn every_tuple_element_is_relocated() {
+ let threads = sample_threads();
+ let source = Some(&threads[0]);
+ let destination = &threads[1];
+
+ let mut one = (Tracker(false),);
+ one.relocate(source, destination);
+ assert_eq!(one, (Tracker(true),));
+
+ let mut two = (Tracker(false), Tracker(false));
+ two.relocate(source, destination);
+ assert_eq!(two, (Tracker(true), Tracker(true)));
+
+ let mut twelve = (
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ Tracker(false),
+ );
+ twelve.relocate(source, destination);
+ assert_eq!(
+ twelve,
+ (
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ Tracker(true),
+ )
+ );
+ }
+
+ #[test]
+ fn function_pointers_stay_callable_after_relocate() {
+ // Helper functions for testing
+ fn no_args() -> i32 {
+ 42
+ }
+
+ fn one_arg(x: i32) -> i32 {
+ x * 2
+ }
+
+ fn two_args(x: i32, y: i32) -> i32 {
+ x + y
+ }
+
+ fn three_args(a: i32, b: i32, c: i32) -> i32 {
+ a + b + c
+ }
+
+ fn many_args(arg0: i32, arg1: i32, arg2: i32, arg3: i32, arg4: i32, arg5: i32) -> i32 {
+ arg0 + arg1 + arg2 + arg3 + arg4 + arg5
+ }
+
+ // Test with different return types
+ fn returns_string() -> String {
+ "hello".to_string()
+ }
+
+ fn returns_bool(x: i32) -> bool {
+ x > 0
+ }
+
+ let threads = sample_threads();
+ let source = Some(&threads[0]);
+ let destination = &threads[1];
+
+ // Test fn() -> R
+ let mut fn_ptr_no_args: fn() -> i32 = no_args;
+ fn_ptr_no_args.relocate(source, destination);
+ assert_eq!(fn_ptr_no_args(), 42);
+
+ // Test fn(A) -> R
+ let mut fn_ptr_one: fn(i32) -> i32 = one_arg;
+ fn_ptr_one.relocate(source, destination);
+ assert_eq!(fn_ptr_one(5), 10);
+
+ // Test fn(A, B) -> R
+ let mut fn_ptr_two: fn(i32, i32) -> i32 = two_args;
+ fn_ptr_two.relocate(source, destination);
+ assert_eq!(fn_ptr_two(3, 7), 10);
+
+ // Test fn(A, B, C) -> R
+ let mut fn_ptr_three: fn(i32, i32, i32) -> i32 = three_args;
+ fn_ptr_three.relocate(source, destination);
+ assert_eq!(fn_ptr_three(1, 2, 3), 6);
+
+ // Test with many arguments
+ let mut fn_ptr_many: fn(i32, i32, i32, i32, i32, i32) -> i32 = many_args;
+ fn_ptr_many.relocate(source, destination);
+ assert_eq!(fn_ptr_many(1, 2, 3, 4, 5, 6), 21);
+
+ let mut fn_string: fn() -> String = returns_string;
+ fn_string.relocate(source, destination);
+ assert_eq!(fn_string(), "hello".to_string());
+
+ let mut fn_bool: fn(i32) -> bool = returns_bool;
+ fn_bool.relocate(source, destination);
+ assert!(fn_bool(5));
+ assert!(!fn_bool(-3));
+ }
+
+ #[test]
+ fn result_relocates_both_variants() {
+ let threads = sample_threads();
+ let source = Some(&threads[0]);
+ let destination = &threads[1];
+
+ // Test Ok variant
+ let mut ok_value: Result = Ok("success".to_string());
+ ok_value.relocate(source, destination);
+ assert_eq!(ok_value, Ok("success".to_string()));
+
+ // Test Err variant
+ let mut err_value: Result = Err(42);
+ err_value.relocate(source, destination);
+ assert_eq!(err_value, Err(42));
+
+ // Test with complex types
+ let mut ok_vec: Result, String> = Ok(vec![1, 2, 3]);
+ ok_vec.relocate(source, destination);
+ assert_eq!(ok_vec, Ok(vec![1, 2, 3]));
+
+ let mut err_string: Result, String> = Err("error".to_string());
+ err_string.relocate(source, destination);
+ assert_eq!(err_string, Err("error".to_string()));
+ }
+
+ #[test]
+ fn option_some_forwards_relocate() {
+ let threads = sample_threads();
+ let mut val = Some(Tracker(false));
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert_eq!(val, Some(Tracker(true)));
+ }
+
+ #[test]
+ fn option_none_is_noop() {
+ let threads = sample_threads();
+ let mut val: Option = None;
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert_eq!(val, None);
+ }
+
+ #[test]
+ fn result_ok_forwards_relocate() {
+ let threads = sample_threads();
+ let mut val: Result = Ok(Tracker(false));
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert_eq!(val, Ok(Tracker(true)));
+ }
+
+ #[test]
+ fn result_err_forwards_relocate() {
+ let threads = sample_threads();
+ let mut val: Result = Err(Tracker(false));
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert_eq!(val, Err(Tracker(true)));
+ }
+
+ #[test]
+ fn vec_forwards_relocate_to_elements() {
+ let threads = sample_threads();
+ let mut val = vec![Tracker(false), Tracker(false)];
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert!(val.iter().all(|t| t.0), "all elements must be relocated");
+ }
+
+ #[test]
+ fn array_and_slice_forward_relocate_to_elements() {
+ let threads = sample_threads();
+ let mut val = [Tracker(false), Tracker(false)];
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert!(val.iter().all(|t| t.0), "all array elements must be relocated");
+
+ let slice: &mut [Tracker] = &mut val;
+ for value in slice.iter_mut() {
+ value.0 = false;
+ }
+ slice.relocate(Some(&threads[0]), &threads[1]);
+ assert!(slice.iter().all(|t| t.0), "all slice elements must be relocated");
+ }
+
+ #[test]
+ fn vec_deque_forwards_relocate_to_elements() {
+ let threads = sample_threads();
+ let mut val = VecDeque::from([Tracker(false), Tracker(false)]);
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert!(val.iter().all(|t| t.0), "all elements must be relocated");
+ }
+
+ #[test]
+ fn btree_map_relocates_values_without_mutating_keys() {
+ let threads = sample_threads();
+ let mut map = BTreeMap::new();
+ map.insert(Tracker(false), Tracker(false));
+ map.relocate(Some(&threads[0]), &threads[1]);
+
+ let (key, value) = map.first_key_value().unwrap();
+ assert!(!key.0, "key identity must remain stable");
+ assert!(value.0, "value must be relocated");
+ }
+
+ #[test]
+ fn cells_forward_relocate_to_inner_value() {
+ let threads = sample_threads();
+
+ let mut cell = Cell::new(Tracker(false));
+ cell.relocate(Some(&threads[0]), &threads[1]);
+ assert!(cell.into_inner().0);
+
+ let mut ref_cell = RefCell::new(Tracker(false));
+ ref_cell.relocate(Some(&threads[0]), &threads[1]);
+ assert!(ref_cell.into_inner().0);
+ }
+
+ #[test]
+ fn box_forwards_relocate() {
+ let threads = sample_threads();
+ let mut val: Box = Box::new(Tracker(false));
+ val.relocate(Some(&threads[0]), &threads[1]);
+ assert!(val.0, "must forward relocate to the inner value");
+ }
+
+ #[test]
+ fn hashmap_relocates_values_without_mutating_keys() {
+ use core::hash::BuildHasherDefault;
+ use std::hash::DefaultHasher;
+
+ let threads = sample_threads();
+ let mut map: HashMap> = HashMap::default();
+ map.insert(Tracker(false), Tracker(false));
+ map.relocate(Some(&threads[0]), &threads[1]);
+
+ let (key, value) = map.iter().next().unwrap();
+ assert!(!key.0, "key identity must remain stable");
+ assert!(value.0, "value must be relocated");
+ }
+}
diff --git a/crates/thread_aware_core/src/lib.rs b/crates/thread_aware_core/src/lib.rs
new file mode 100644
index 000000000..5b457be58
--- /dev/null
+++ b/crates/thread_aware_core/src/lib.rs
@@ -0,0 +1,214 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![no_std]
+#![cfg_attr(all(coverage_nightly, test), feature(coverage_attribute))]
+#![cfg_attr(docsrs, feature(doc_cfg))]
+#![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/thread_aware_core/logo.png")]
+#![doc(
+ html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/thread_aware_core/favicon.ico"
+)]
+
+//! Support for values that adapt when a runtime moves them to another thread.
+//!
+//! This crate contains the small API shared by thread-aware libraries:
+//!
+//! - [`ThreadAware`] notifies a value that it has moved.
+//! - [`Thread`] records where it now runs: which runtime, which OS thread, and which memory is
+//! closest to it.
+//!
+//! [`Thread`] is a coordinate, not a handle: a runtime builds one to describe where a value
+//! is running, and it owns no operating-system resource. It is unrelated to
+//! [`std::thread::Thread`], which is a handle to a live OS thread. Naming both in one module
+//! requires aliasing one of them.
+//!
+//! # The `thread_aware` family
+//!
+//! - **`thread_aware_core`** (this crate) — the vocabulary that two unrelated libraries must
+//! agree on before either can relocate a value defined by the other. Deliberately small and
+//! slow-moving, so naming [`ThreadAware`] or [`Thread`] in your own public API costs you
+//! nothing later.
+//! - **[`thread_aware`]** — the utilities that make relocation convenient: a
+//! [`#[derive(ThreadAware)]`][derive] macro, wrappers for foreign types, a per-core
+//! [`Arc`][arc], containers and registries. Free to evolve, and not meant to appear in a
+//! public API.
+//!
+//! [`thread_aware`]: https://docs.rs/thread_aware
+//! [derive]: https://docs.rs/thread_aware/latest/thread_aware/derive.ThreadAware.html
+//! [arc]: https://docs.rs/thread_aware/latest/thread_aware/struct.Arc.html
+//!
+//! Depend on this crate directly when all you need is the trait. It adds nothing to your
+//! dependency graph, and works without `std`: with default features turned off, [`Thread`]
+//! loses its thread id component and keeps [`Owner`] and [`NumaNode`].
+//!
+//! # Why relocation exists
+//!
+//! Thread-per-core and NUMA-aware runtimes are fast because each worker keeps to itself: it
+//! uses memory close to its own thread, talks to its own I/O driver, and does not
+//! synchronize with other workers. When a value moves to another worker, what used to be
+//! close by is now in the wrong place: a cache line shared between threads, memory in a
+//! distant region, a handle to another thread's driver.
+//!
+//! [`ThreadAware`] lets that state repair itself. The runtime moves the value, then calls
+//! [`relocate`](ThreadAware::relocate) to report where it now lives. Relocation has two
+//! sides, and most code sits on only one of them.
+//!
+//! # Library authors: implementing the trait
+//!
+//! **Library and application authors** implement [`ThreadAware`], usually through the
+//! [`#[derive(ThreadAware)]`][derive] macro. They never call
+//! [`relocate`](ThreadAware::relocate) and never construct a [`Thread`]; the runtime does
+//! both and then invokes the implementation. It is a callback, like [`Drop::drop`].
+//!
+//! The derive lives in [`thread_aware`], so a library that wants it depends on that crate.
+//! Only the trait and [`Thread`] cross the public boundary, and both come from here, so the
+//! dependency stays an implementation detail:
+//!
+//! ```ignore
+//! // A build dependency, not part of what this library promises.
+//! use thread_aware::ThreadAware;
+//!
+//! /// A codec whose scratch buffer should follow the memory it is used from.
+//! #[derive(ThreadAware)]
+//! pub struct Encoder {
+//! scratch: Scratch,
+//! dictionary: Dictionary,
+//! }
+//! ```
+//!
+//! The derive writes the forwarding implementation, calling `relocate` on `scratch` and
+//! `dictionary` in turn. Because a composed type forwards to its fields, one call at the top
+//! reaches everything below it. Callers of `Encoder` never name [`thread_aware`].
+//!
+//! # Runtime authors: driving relocation
+//!
+//! **Runtime authors** construct a [`Thread`] per worker and call
+//! [`relocate`](ThreadAware::relocate) after moving a value, passing where it came from and
+//! where it now runs. The example below plays the part of the runtime so the order is
+//! visible.
+//!
+//! ```
+//! # fn main() {
+//! # #[cfg(feature = "std")] {
+//! use std::thread;
+//!
+//! use thread_aware_core::{NumaNode, Owner, Thread, ThreadAware};
+//!
+//! // What a library author writes.
+//! struct Worker {
+//! thread: Option,
+//! }
+//!
+//! impl ThreadAware for Worker {
+//! fn relocate(&mut self, _source: Option<&Thread>, destination: &Thread) {
+//! self.thread = Some(destination.id());
+//! }
+//! }
+//!
+//! // What the runtime does.
+//! let here = thread::current().id();
+//! let there = thread::spawn(|| thread::current().id()).join().unwrap();
+//!
+//! let owner = Owner::new(2);
+//! let first = Thread::new(owner.clone(), here, NumaNode::new(0));
+//! let second = Thread::new(owner, there, NumaNode::new(1));
+//!
+//! let mut worker = Worker { thread: None };
+//!
+//! worker.relocate(None, &first); // first placement, no previous `Thread`
+//! worker.relocate(Some(&first), &second); // moved to another thread
+//!
+//! assert_eq!(worker.thread, Some(there));
+//! # }
+//! # }
+//! ```
+//!
+//! # Performance, not correctness
+//!
+//! Relocation is an optimization, not a guarantee. A value must remain correct if the call
+//! never comes, comes twice, or reports the same source and destination. Missing calls are
+//! normal: a value can reach another thread through `std::thread::spawn`, a channel, or a
+//! runtime that knows nothing about this trait. That may make things slower, but it must
+//! never cause a panic, a deadlock, or a wrong answer.
+//!
+//! Nor is it a hot path. Expect roughly one relocation per object graph per job or request,
+//! after which the value is used normally. Avoiding synchronization matters more than saving a
+//! few cycles.
+//!
+//! # What the ids mean
+//!
+//! - **Thread id** — a `std::thread::ThreadId`, unique among the threads alive at once, so
+//! state keyed on it is never shared by accident, not even between two runtimes in one
+//! process.
+//! - **[`NumaNode`]** — the memory closest to that thread. Unlike the thread id it is
+//! *shared*: every thread near the same memory reports the same node, which is what suits
+//! it to state shared within a region but not across the machine. That holds only while
+//! every runtime numbers the regions identically. Nothing checks it, and runtimes that
+//! disagree make shared state wrong rather than merely slow.
+//! - **[`Owner`]** — the runtime a [`Thread`] belongs to. Every new owner is unique, so two
+//! live runtimes never share one. It lets a value detect that it has crossed into a
+//! different runtime and release anything the previous one owned.
+//!
+//! An implementation reads only the ids its state depends on. A per-thread cache or a handle
+//! to a thread-local driver keys on the thread id; a buffer pool keys on [`NumaNode`] and
+//! survives a move to another thread near the same memory; anything the runtime owns compares
+//! [`Owner`].
+//!
+//! The ids carry no meaning beyond identity: they need not start at zero or run
+//! consecutively, and the [`Thread`]s in use cannot be enumerated. State keyed on any of them
+//! belongs in a map rather than an array indexed by it. [`Owner::min_threads`] is the one
+//! number on offer, and it is a floor to pre-size against, not a bound to index against.
+//!
+//! Without `std` there is no [`ThreadId`](std::thread::ThreadId): `Thread::new` and
+//! `Thread::id` are absent and only [`Owner`] and [`NumaNode`] remain. A `no_std` library can
+//! still implement [`ThreadAware`] and use whatever it is given; the runtime that drives
+//! relocation requires `std` regardless.
+//!
+//! # Relation to `Send`
+//!
+//! [`ThreadAware`] requires [`Send`], and in that order: a value is sent to another thread
+//! first, then told where it landed. [`Send`] is what makes the move safe, and
+//! [`ThreadAware`] adds nothing to it.
+//!
+//! # Provided implementations
+//!
+//! Types with nothing tied to a thread receive an empty implementation: primitives and their
+//! non-zero variants, the thread ids, `Duration`, strings, safe function pointers of up to
+//! twelve parameters, and, with the `std` feature, paths.
+//!
+//! Containers forward the call to what they hold: [`Option`], [`Result`], arrays, slices,
+//! `Vec`, `VecDeque`, `Box`, cells, tuples of up to twelve elements, and map values.
+//!
+//! References are not [`ThreadAware`]. Relocating through one would adapt something the value
+//! only borrows, and whoever owns it is relocated on its own account.
+//!
+//! Map keys are left alone, since altering one could change its hash or ordering and corrupt
+//! the map. Sets are not implemented at all for the same reason, so a `HashSet` or
+//! `BTreeSet` field is not [`ThreadAware`].
+//!
+//! `Cow` is omitted for now: relocating a borrowed one has to clone it into owned storage
+//! first, which is a surprising amount of work to hide behind a hint.
+//!
+//! `Arc` is also omitted: whether a shared allocation should stay shared across threads or
+//! be split per thread depends on what it holds. The per-core [`Arc`][arc] in
+//! [`thread_aware`] covers the case where splitting is correct.
+//!
+//! # Features
+//!
+//! - **`std`** *(default)* - Adds [`Thread::new`] and [`Thread::id`], which need
+//! [`ThreadId`](std::thread::ThreadId), and implements [`ThreadAware`] for standard library
+//! types such as `HashMap`, `Path` and `PathBuf`. Turn it off for `no_std`, which needs
+//! only `alloc` and pointer-width atomics.
+
+extern crate alloc;
+#[cfg(any(test, feature = "std"))]
+extern crate std;
+
+mod impls;
+mod thread;
+mod thread_aware;
+
+#[doc(inline)]
+pub use thread::{NumaNode, Owner, Thread};
+#[doc(inline)]
+pub use thread_aware::ThreadAware;
diff --git a/crates/thread_aware_core/src/thread.rs b/crates/thread_aware_core/src/thread.rs
new file mode 100644
index 000000000..3cc3ed388
--- /dev/null
+++ b/crates/thread_aware_core/src/thread.rs
@@ -0,0 +1,456 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! The [`Thread`] identifier and its component id types.
+//!
+//! A [`Thread`] records where a value runs: which runtime owns it, which OS thread it names,
+//! and which memory is closest to that OS thread. See
+//! [what the ids mean](crate#what-the-ids-mean) for the guarantees each id carries.
+
+use core::hash::{Hash, Hasher};
+use core::sync::atomic::{AtomicUsize, Ordering};
+#[cfg(any(test, feature = "std"))]
+use std::thread::ThreadId;
+
+/// Hands the next identity to each [`Owner`] created in this process.
+///
+/// Pointer-width so the counter does not wrap in practice; a wrap would hand out a live
+/// identity twice.
+static NEXT_OWNER: AtomicUsize = AtomicUsize::new(0);
+
+/// An identifier for the runtime that owns a [`Thread`].
+///
+/// Every new owner is unique, so two runtimes alive at the same time never share one. That
+/// is what lets a value notice it has crossed from one runtime into another and release
+/// anything the previous one owned.
+///
+/// An owner also reports the smallest number of threads its runtime runs, which a value can
+/// use to pre-size per-thread state before it has seen any of those threads. The runtime may
+/// run more; one that spawns threads entirely on demand promises none and reports `0`.
+///
+/// Two owners are the same runtime exactly when their identities match; the thread count
+/// plays no part in equality or hashing.
+///
+/// An owner names one live runtime, so it is `Clone` but not `Copy`, and
+/// [`Thread::owner`] lends it rather than handing out duplicates.
+///
+/// # Examples
+///
+/// ```
+/// use thread_aware_core::Owner;
+///
+/// let pool = Owner::new(4);
+/// assert_eq!(pool.min_threads(), 4);
+///
+/// let elastic = Owner::new(0);
+/// assert_eq!(elastic.min_threads(), 0);
+///
+/// // Every owner is distinct, however it was built.
+/// assert_ne!(pool, Owner::new(4));
+/// assert_ne!(elastic, Owner::new(0));
+/// ```
+#[derive(Clone, Debug)]
+pub struct Owner {
+ id: usize,
+ min_threads: usize,
+}
+
+impl PartialEq for Owner {
+ #[inline]
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ }
+}
+
+impl Eq for Owner {}
+
+impl Hash for Owner {
+ fn hash(&self, state: &mut H) {
+ self.id.hash(state);
+ }
+}
+
+impl Owner {
+ /// Creates an owner for a runtime that runs at least `min_threads` threads.
+ ///
+ /// The owner is unique: no other owner in this process compares equal to it.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use thread_aware_core::Owner;
+ ///
+ /// let owner = Owner::new(2);
+ ///
+ /// assert_eq!(owner.min_threads(), 2);
+ /// ```
+ #[must_use]
+ pub fn new(min_threads: usize) -> Self {
+ Self {
+ id: NEXT_OWNER.fetch_add(1, Ordering::Relaxed),
+ min_threads,
+ }
+ }
+
+ /// Returns the smallest number of threads the owning runtime runs.
+ ///
+ /// The runtime may run more, so this is a floor for pre-sizing rather than a bound to
+ /// index against. `0` means the runtime promises none and spawns on demand.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use thread_aware_core::Owner;
+ ///
+ /// assert_eq!(Owner::new(8).min_threads(), 8);
+ /// assert_eq!(Owner::new(0).min_threads(), 0);
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn min_threads(&self) -> usize {
+ self.min_threads
+ }
+}
+
+/// An identifier for the memory closest to a thread, usually a NUMA node.
+///
+/// On a large machine, memory is divided into regions and a thread reaches its own region
+/// fastest. Unlike the thread id, this identifier is shared: every thread near the same
+/// memory reports the same `NumaNode`, which is what makes it suitable for state that is
+/// shared within a region but not across the machine. Sharing between runtimes holds only
+/// while they all number the regions identically; see
+/// [what the ids mean](crate#what-the-ids-mean).
+///
+/// Nodes carry no meaning beyond identity, and the width is wide enough that no real
+/// machine can exhaust it.
+///
+/// # Examples
+///
+/// ```
+/// use thread_aware_core::NumaNode;
+///
+/// let node = NumaNode::new(0);
+///
+/// assert_eq!(node, NumaNode::new(0));
+/// assert_ne!(node, NumaNode::new(1));
+/// ```
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
+pub struct NumaNode(u32);
+
+impl NumaNode {
+ /// Creates a node identifier from the number the platform reports.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use thread_aware_core::NumaNode;
+ ///
+ /// assert_ne!(NumaNode::new(0), NumaNode::new(1));
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn new(node: u32) -> Self {
+ Self(node)
+ }
+}
+
+/// A record of where a value runs: its runtime, its OS thread, and its nearest memory.
+///
+/// A runtime constructs these, usually once per worker at startup, and passes them to
+/// [`ThreadAware::relocate`](crate::ThreadAware::relocate). An implementation reads whichever
+/// part it depends on; [what the ids mean](crate#what-the-ids-mean) describes which to
+/// choose.
+///
+/// # Relation to `std::thread::Thread`
+///
+/// This `Thread` is a coordinate, not a handle: it records where a value runs and owns no
+/// operating-system resource. [`std::thread::Thread`] is unrelated, and naming both in one
+/// module is `error[E0252]`; alias the standard handle if you need it.
+///
+/// `Thread` is cheap to clone but deliberately not `Copy`, and is passed by reference to
+/// [`relocate`](crate::ThreadAware::relocate), so cloning is rarely necessary.
+///
+/// Two values are equal only if all their ids match. Code concerned only with memory
+/// locality compares [`numa_node`](Self::numa_node) rather than whole `Thread`s, since
+/// threads sharing a NUMA node still have different thread ids.
+///
+/// # Without `std`
+///
+/// The thread id is `std::thread::ThreadId`, so without the `std` feature a `Thread` cannot
+/// be constructed and holds only [`owner`](Self::owner) and [`numa_node`](Self::numa_node).
+/// Equality and hashing then compare those two alone.
+///
+/// # Examples
+///
+/// ```
+/// # fn main() {
+/// # #[cfg(feature = "std")] {
+/// use std::thread;
+///
+/// use thread_aware_core::{NumaNode, Owner, Thread};
+///
+/// let here = thread::current().id();
+/// let owner = Owner::new(1);
+/// let mine = Thread::new(owner.clone(), here, NumaNode::new(1));
+///
+/// assert_eq!(mine.owner(), &owner);
+/// assert_eq!(mine.id(), here);
+///
+/// // The same OS thread under a different runtime is a different `Thread`...
+/// let elsewhere = Thread::new(Owner::new(1), here, NumaNode::new(1));
+/// assert_ne!(mine, elsewhere);
+///
+/// // ...but the thread id and its nearest memory are unchanged.
+/// assert_eq!(mine.numa_node(), elsewhere.numa_node());
+/// # }
+/// # }
+/// ```
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub struct Thread {
+ owner: Owner,
+ // `test` is part of the gate so the crate's own test build compiles this field without
+ // enumerating features (see docs/optional-deps-in-test-builds.md). The cost is that no
+ // unit test can observe the `no_std` shape: `cfg(test)` puts `id` back, so a test gated
+ // on `not(feature = "std")` still sees three fields and would assert the wrong thing.
+ // `tests/no_std_surface.rs` covers that shape from outside the test build instead.
+ #[cfg(any(test, feature = "std"))]
+ id: ThreadId,
+ numa_node: NumaNode,
+}
+
+impl Thread {
+ /// Creates a `Thread` from an owner, a thread id, and a NUMA node.
+ ///
+ /// Runtimes call this as they set up their workers. Tests may call it directly to
+ /// construct values without starting a runtime. A thread id is obtained from
+ /// [`thread::current`](std::thread::current).
+ ///
+ /// Requires the `std` feature.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use std::thread;
+ ///
+ /// use thread_aware_core::{NumaNode, Owner, Thread};
+ ///
+ /// let owner = Owner::new(1);
+ /// let mine = Thread::new(owner.clone(), thread::current().id(), NumaNode::new(0));
+ ///
+ /// assert_eq!(mine.owner(), &owner);
+ /// ```
+ #[cfg(any(test, feature = "std"))]
+ #[inline]
+ #[must_use]
+ pub const fn new(owner: Owner, id: ThreadId, numa_node: NumaNode) -> Self {
+ Self { owner, id, numa_node }
+ }
+
+ /// Returns the identifier of the runtime that owns this [`Thread`].
+ ///
+ /// Comparing owners detects that a value has moved between runtimes. Such a move remains
+ /// sound, but resources owned by the previous runtime usually cannot follow.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # fn main() {
+ /// # #[cfg(feature = "std")] {
+ /// use std::thread;
+ ///
+ /// use thread_aware_core::{NumaNode, Owner, Thread};
+ ///
+ /// let owner = Owner::new(7);
+ /// let mine = Thread::new(owner.clone(), thread::current().id(), NumaNode::new(0));
+ ///
+ /// assert_eq!(mine.owner(), &owner);
+ /// # }
+ /// # }
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn owner(&self) -> &Owner {
+ &self.owner
+ }
+
+ /// Returns the [`ThreadId`](std::thread::ThreadId) of the OS thread this `Thread` names.
+ ///
+ /// Distinct live OS threads have distinct ids, so this partitions state by OS thread
+ /// without keys colliding. Whether that state is contended depends on the storage around
+ /// it. A [`ThreadId`](std::thread::ThreadId) has no defined relationship to whatever
+ /// identifier the platform assigns.
+ ///
+ /// Requires the `std` feature.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use std::thread;
+ ///
+ /// use thread_aware_core::{NumaNode, Owner, Thread};
+ ///
+ /// let here = thread::current().id();
+ /// let mine = Thread::new(Owner::new(1), here, NumaNode::new(0));
+ ///
+ /// assert_eq!(mine.id(), here);
+ /// ```
+ #[cfg(any(test, feature = "std"))]
+ #[inline]
+ #[must_use]
+ pub const fn id(&self) -> ThreadId {
+ self.id
+ }
+
+ /// Returns the identifier of the memory nearest to this [`Thread`].
+ ///
+ /// This is the id to use when what matters is which memory is nearby rather than which
+ /// thread is running, since threads on the same node share that memory cheaply.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # fn main() {
+ /// # #[cfg(feature = "std")] {
+ /// use std::thread;
+ ///
+ /// use thread_aware_core::{NumaNode, Owner, Thread};
+ ///
+ /// let mine = Thread::new(Owner::new(1), thread::current().id(), NumaNode::new(3));
+ ///
+ /// assert_eq!(mine.numa_node(), NumaNode::new(3));
+ /// # }
+ /// # }
+ /// ```
+ #[inline]
+ #[must_use]
+ pub const fn numa_node(&self) -> NumaNode {
+ self.numa_node
+ }
+}
+
+#[cfg_attr(coverage_nightly, coverage(off))]
+#[cfg(test)]
+mod tests {
+ use core::panic::{RefUnwindSafe, UnwindSafe};
+ use std::hash::DefaultHasher;
+ use std::thread;
+
+ use static_assertions::{assert_impl_all, assert_not_impl_any};
+
+ use super::{Hash, Hasher, NumaNode, Owner, Thread};
+
+ assert_impl_all!(Owner: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe);
+ // An owner names one live runtime, so handing out silent duplicates is not something the
+ // type should make effortless; `Clone` keeps it deliberate.
+ assert_not_impl_any!(Owner: Copy);
+ assert_impl_all!(NumaNode: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe);
+ assert_impl_all!(Thread: Send, Sync, Unpin, UnwindSafe, RefUnwindSafe);
+
+ #[test]
+ fn exposes_components() {
+ let id = thread::current().id();
+ let owner = Owner::new(3);
+ let mine = Thread::new(owner.clone(), id, NumaNode::new(2));
+
+ assert_eq!(mine.owner(), &owner);
+ assert_eq!(mine.owner().min_threads(), 3, "the owner survives intact, count included");
+ assert_eq!(mine.id(), id);
+ assert_eq!(mine.numa_node(), NumaNode::new(2));
+ }
+
+ #[test]
+ fn different_owner_compares_unequal() {
+ let id = thread::current().id();
+ let numa_node = NumaNode::new(0);
+ let first = Thread::new(Owner::new(1), id, numa_node);
+ let second = Thread::new(Owner::new(1), id, numa_node);
+
+ assert_ne!(first, second);
+ }
+
+ #[test]
+ fn different_numa_compares_unequal() {
+ let id = thread::current().id();
+ // One owner, so the NUMA node is the only thing that differs.
+ let owner = Owner::new(1);
+ let first = Thread::new(owner.clone(), id, NumaNode::new(0));
+ let second = Thread::new(owner, id, NumaNode::new(1));
+
+ assert_ne!(first, second);
+ }
+
+ #[test]
+ fn different_thread_compares_unequal() {
+ let owner = Owner::new(0);
+ let numa_node = NumaNode::new(0);
+ let here = Thread::new(owner.clone(), thread::current().id(), numa_node);
+ let there = thread::spawn(move || Thread::new(owner, thread::current().id(), numa_node))
+ .join()
+ .unwrap();
+
+ assert_ne!(here, there);
+ }
+
+ #[test]
+ fn clone_preserves_components() {
+ let mine = Thread::new(Owner::new(4), thread::current().id(), NumaNode::new(9));
+ let cloned = mine.clone();
+
+ assert_eq!(cloned, mine);
+ assert_eq!(cloned.owner().min_threads(), mine.owner().min_threads());
+ assert_eq!(cloned.numa_node(), mine.numa_node());
+ }
+
+ #[test]
+ fn owner_identity_is_unique_per_construction() {
+ let first = Owner::new(4);
+ let second = Owner::new(4);
+
+ assert_ne!(first, second, "each owner takes its own identity");
+ assert_eq!(first, first, "an owner equals itself");
+ assert_eq!(first, first.clone(), "a clone keeps the same identity");
+ }
+
+ #[test]
+ fn owner_equality_ignores_the_thread_count() {
+ // The public API cannot produce two owners sharing an identity, but `Eq` and `Hash`
+ // must still agree if it ever could, so build the pair directly.
+ let one = Owner { id: 7, min_threads: 1 };
+ let other = Owner { id: 7, min_threads: 99 };
+
+ assert_eq!(one, other, "identity alone decides equality");
+ assert_eq!(hash_of(&one), hash_of(&other), "equal owners must hash equally");
+ }
+
+ #[test]
+ fn owner_hashes_by_identity() {
+ let one = Owner::new(4);
+ let other = Owner::new(4);
+
+ // Equal counts, so only the identity tells these apart. Hashing has to carry it, or
+ // every owner would land in one bucket.
+ assert_ne!(one, other);
+ assert_ne!(hash_of(&one), hash_of(&other), "distinct owners must hash apart");
+ }
+
+ #[test]
+ fn owner_reports_its_minimum() {
+ assert_eq!(Owner::new(8).min_threads(), 8);
+ assert_eq!(Owner::new(0).min_threads(), 0, "zero means the runtime promises none");
+ }
+
+ #[test]
+ fn owner_clone_preserves_the_thread_count() {
+ let owner = Owner::new(4);
+ let clone = owner.clone();
+
+ assert_eq!(clone, owner, "a clone keeps the identity");
+ assert_eq!(clone.min_threads(), 4, "a clone keeps the count, not just the identity");
+ }
+
+ fn hash_of(owner: &Owner) -> u64 {
+ let mut hasher = DefaultHasher::new();
+ owner.hash(&mut hasher);
+ hasher.finish()
+ }
+}
diff --git a/crates/thread_aware_core/src/thread_aware.rs b/crates/thread_aware_core/src/thread_aware.rs
new file mode 100644
index 000000000..673a50559
--- /dev/null
+++ b/crates/thread_aware_core/src/thread_aware.rs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! The [`ThreadAware`] trait.
+
+use crate::Thread;
+
+/// A type that adapts when it is moved to a different [`Thread`].
+///
+/// Implement this trait when part of a type depends on where it runs: memory near a
+/// particular node, a handle to a thread-local driver, a shard index, a cached thread id.
+/// [`relocate`](Self::relocate) brings that state back into line. Implementors do not call
+/// it themselves; a runtime does, as described in
+/// [driving relocation](crate#runtime-authors-driving-relocation).
+///
+/// Implement or derive it for any type that may end up inside a value a runtime relocates,
+/// including types that do nothing on relocation, since an empty implementation is what lets
+/// an enclosing type derive the trait. Do not implement it merely because a type is
+/// [`Send`], and never put anything correctness depends on inside
+/// [`relocate`](Self::relocate); it may affect performance only.
+///
+/// # Implementing
+///
+/// Implementations usually take one of four shapes.
+///
+/// 1. **Do nothing.** Nothing in the type depends on where it runs. An empty body is a
+/// complete implementation, and is what the primitive types do.
+///
+/// ```
+/// use thread_aware_core::{Thread, ThreadAware};
+///
+/// struct RequestId(u64);
+///
+/// impl ThreadAware for RequestId {
+/// fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+/// }
+/// ```
+/// 2. **Record the destination.** Store the new thread id, [`NumaNode`](crate::NumaNode) or
+/// [`Owner`](crate::Owner) for later use.
+/// 3. **Replace a resource.** Compare the id it depends on: [`NumaNode`](crate::NumaNode)
+/// for a buffer pool, the thread id for a driver handle, [`Owner`](crate::Owner) for
+/// anything the runtime owns. If it changed, release the old resource and acquire one for
+/// the new [`Thread`], moving out any real data it holds first.
+/// 4. **Forward to fields.** A type composed of other types calls `relocate` on each field.
+/// Prefer `#[derive(ThreadAware)]` to writing this by hand.
+///
+/// To *detach* is to release a resource and leave the field empty, so that the value can
+/// acquire a new one later or run without it. Such a field needs a type that can be empty,
+/// such as `Option`.
+///
+/// # Requirements
+///
+/// [`relocate`](Self::relocate) cannot fail and has no way to report an error, so every
+/// implementation must:
+///
+/// * **Preserve real data.** Anything that exists only for speed may be rebuilt: caches,
+/// pools, scratch buffers, handles. Nothing observable through the value may be lost or
+/// altered. A cache still holding writes that have not been flushed is real data, and must
+/// be moved rather than dropped.
+///
+/// * **Remain correct.** When the ideal adaptation is unavailable, being slower is preferred
+/// to being wrong, and keeping the old resource is acceptable. Whatever the value holds
+/// must keep working even if this method is never called, so a driver handle must remain
+/// usable from the new thread. Relocation brings it closer; it does not make it valid.
+///
+/// * **Neither panic nor block.** This runs while the runtime is placing work, so it
+/// performs no network or disk I/O, no waiting on another worker, and takes no contended
+/// lock. Release anything that would block here and re-acquire it on first use.
+///
+/// * **Tolerate repeated calls.** Relocating to the same [`Thread`], or with `source` equal to
+/// `destination`, is harmless, and should also be cheap: compare the relevant ids and
+/// return early when nothing has changed.
+///
+/// * **Tolerate no call at all.** The value remains correct either way.
+///
+/// * **Tolerate an unfamiliar [`Thread`].** A `destination` may name an OS thread the value has never
+/// seen and carry an [`Owner`](crate::Owner) belonging to another runtime, and this must
+/// remain sound. Release anything the previous runtime owned; state keyed on
+/// [`NumaNode`](crate::NumaNode) may remain valid, subject to the caveat in
+/// [what the ids mean](crate#what-the-ids-mean). Performance may suffer afterwards, though
+/// not permanently: once back on a [`Thread`] it can serve, the value re-acquires what it
+/// released.
+///
+/// A [`Thread`] may be cloned and retained, but the thread id it holds is meaningful only
+/// while that thread is alive, and an [`Owner`](crate::Owner) only while that runtime is.
+///
+/// Runtimes carry their own requirements. They call [`relocate`](Self::relocate) only after
+/// the value has actually moved, pass `None` when no previous [`Thread`] is known, build one
+/// [`Owner`](crate::Owner) per runtime, and never rely on the call for correctness. Nothing
+/// enforces any of this.
+///
+/// # Examples
+///
+/// A type that releases a scratch buffer when the nearest memory changes, so that the next
+/// use allocates near the new node. The buffer holds nothing between calls, so discarding it
+/// is safe, while the `name` field is real data and is left alone.
+///
+/// ```
+/// use thread_aware_core::{NumaNode, Thread, ThreadAware};
+///
+/// struct Encoder {
+/// name: String,
+/// numa_node: Option,
+/// /// Reused between calls purely to avoid re-allocating; empty outside a call.
+/// scratch: Vec,
+/// }
+///
+/// impl ThreadAware for Encoder {
+/// fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+/// // Only pay for this when the nearest memory actually changed.
+/// if source.map(Thread::numa_node) == Some(destination.numa_node()) {
+/// return;
+/// }
+///
+/// // Record the node and drop the buffer allocated near the old one. `Vec` cannot
+/// // choose a node itself, so placement comes from the allocator the application
+/// // installs; releasing here is what gives it the chance to allocate near
+/// // `numa_node` on the next use.
+/// self.numa_node = Some(destination.numa_node());
+/// self.scratch = Vec::new();
+/// }
+/// }
+/// ```
+///
+/// A type that forwards the call to its fields:
+///
+/// ```
+/// use thread_aware_core::{Thread, ThreadAware};
+///
+/// # struct Encoder;
+/// # impl ThreadAware for Encoder {
+/// # fn relocate(&mut self, _source: Option<&Thread>, _destination: &Thread) {}
+/// # }
+/// struct Session {
+/// id: u64,
+/// encoder: Encoder,
+/// }
+///
+/// impl ThreadAware for Session {
+/// fn relocate(&mut self, source: Option<&Thread>, destination: &Thread) {
+/// self.id.relocate(source, destination);
+/// self.encoder.relocate(source, destination);
+/// }
+/// }
+/// ```
+pub trait ThreadAware: Send {
+ /// Adapts this value to the [`Thread`] it now occupies.
+ ///
+ /// Implementors provide this method but do not normally call it. A runtime calls it
+ /// after moving the value.
+ ///
+ /// `destination` is where the value runs from now on. `source` is where it ran before,
+ /// or `None` when that is unknown, which is normal for a first placement or for a value
+ /// arriving from outside the runtime. `None` means the implementation can assume nothing
+ /// about the previous [`Thread`]; it does not indicate an error.
+ ///
+ /// This method cannot fail, must not panic, and is safe to call more than once,
+ /// including with `source` equal to `destination`. See the
+ /// [requirements](Self#requirements) for the rest, and the trait-level
+ /// [examples](Self#examples) for implementations.
+ fn relocate(&mut self, source: Option<&Thread>, destination: &Thread);
+}
+
+#[cfg(test)]
+mod tests {
+ use static_assertions::assert_obj_safe;
+
+ use super::ThreadAware;
+
+ // `dyn ThreadAware` is part of the stable surface, so anything added to the trait later
+ // has to keep it dyn-compatible.
+ assert_obj_safe!(ThreadAware);
+}
diff --git a/crates/thread_aware_core/tests/no_std_surface.rs b/crates/thread_aware_core/tests/no_std_surface.rs
new file mode 100644
index 000000000..a041e3f96
--- /dev/null
+++ b/crates/thread_aware_core/tests/no_std_surface.rs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Exercises the public surface from outside the crate's own test build.
+//!
+//! Unit tests in `src/` compile the library with `cfg(test)`, which switches on every
+//! `cfg(any(test, feature = "std"))` item whatever features are selected, so they always
+//! see the three-field `Thread`. This harness links the library as an ordinary dependency,
+//! so under `--no-default-features` the `no_std` shape is the one under test.
+
+use std::collections::hash_map::DefaultHasher;
+use std::hash::{Hash, Hasher};
+
+use static_assertions::assert_impl_all;
+use thread_aware_core::{NumaNode, Owner, Thread};
+
+assert_impl_all!(Thread: Clone, Eq, Send, Sync);
+assert_impl_all!(Owner: Clone, Eq, Send, Sync);
+assert_impl_all!(NumaNode: Copy, Eq, Send, Sync);
+
+fn hash_of(value: &T) -> u64
+where
+ T: Hash,
+{
+ let mut hasher = DefaultHasher::new();
+ value.hash(&mut hasher);
+ hasher.finish()
+}
+
+#[test]
+fn owners_keep_their_identity_and_count() {
+ let first = Owner::new(4);
+ let second = Owner::new(4);
+
+ assert_ne!(first, second, "each owner takes its own identity");
+ assert_eq!(first, first, "an owner equals itself");
+ assert_ne!(hash_of(&first), hash_of(&second), "distinct owners must hash apart");
+ assert_eq!(first.min_threads(), 4);
+}
+
+#[test]
+fn numa_nodes_compare_and_hash_by_value() {
+ assert_eq!(NumaNode::new(3), NumaNode::new(3));
+ assert_ne!(NumaNode::new(3), NumaNode::new(4));
+ assert_eq!(
+ hash_of(&NumaNode::new(3)),
+ hash_of(&NumaNode::new(3)),
+ "equal nodes must hash equally"
+ );
+}
+
+/// Without `std` the thread id is gone, so equality and hashing cover the owner and the
+/// NUMA node alone. `Thread` cannot be constructed here, which is exactly why that
+/// narrowing is unobservable in a real program, so pin the shape by size instead: anything
+/// wider than the two remaining fields means the id came back.
+#[cfg(not(feature = "std"))]
+#[test]
+fn without_std_a_thread_carries_no_thread_id() {
+ let align = align_of::();
+ let expected = (size_of::() + size_of::()).div_ceil(align) * align;
+
+ assert_eq!(size_of::(), expected, "a no_std `Thread` is its owner and its NUMA node");
+}
+
+#[cfg(feature = "std")]
+#[test]
+fn with_std_every_component_takes_part_in_equality() {
+ use std::thread;
+
+ let id = thread::current().id();
+ let owner = Owner::new(1);
+ let numa_node = NumaNode::new(0);
+ let mine = Thread::new(owner.clone(), id, numa_node);
+
+ assert_eq!(mine, Thread::new(owner.clone(), id, numa_node));
+ assert_eq!(hash_of(&mine), hash_of(&Thread::new(owner.clone(), id, numa_node)));
+
+ assert_ne!(mine, Thread::new(Owner::new(1), id, numa_node), "the owner counts");
+ assert_ne!(mine, Thread::new(owner.clone(), id, NumaNode::new(1)), "the NUMA node counts");
+
+ let elsewhere = thread::spawn(move || Thread::new(owner, thread::current().id(), numa_node))
+ .join()
+ .unwrap();
+
+ assert_ne!(mine, elsewhere, "the thread id counts");
+}