# once_type_map
A thread-safe, insert-once type map that hands out plain `&T` references, not
lock guards.
You can think of it as a `OnceLock` per type, for types you don't know when you
write the owning struct. Library authors can embed one `OnceTypeMap` in a
long-lived object (a client, a session, an app context). Downstream crates can
then attach their own state to that object, such as caches, adapters, or
implementers of extra traits, without the original struct knowing about them.
```rust
use once_type_map::OnceTypeMap;
struct Client {
extensions: OnceTypeMap,
}
impl Client {
pub fn extensions(&self) -> &OnceTypeMap {
&self.extensions
}
}
// Somewhere in a downstream crate:
struct MyCache { /* ... */ }
fn cache(client: &Client) -> &MyCache {
client.extensions().get_or_init(|| MyCache { /* expensive setup */ })
}
```
[`examples/extensions.rs`](examples/extensions.rs) shows the full pattern. A
library defines the extensible `Client`, and two independent extension modules
add methods to it through extension traits. The application imports those
traits and uses them, including from several threads:
```sh
cargo run --example extensions
```
[`examples/async_extensions.rs`](examples/async_extensions.rs) shows the
same idea when the extension's state has to be created asynchronously, for
example by a login call. It stores a `tokio::sync::OnceCell` in the map, so
concurrent tasks wait for a single login without blocking the executor:
```sh
cargo run --example async_extensions
```
## Why references instead of guards?
Typical type maps (`http::Extensions`, `anymap`, `RwLock<HashMap<TypeId, Box<dyn Any>>>`)
either need `&mut self` to insert or return a guard that borrows the lock. Both
make it hard for a wrapper type to expose `fn foo(&self) -> &Foo`.
`OnceTypeMap` returns a `&T` that lives as long as the borrow of the map. This
is sound because of three rules:
1. Each type's value lives in its own heap-allocated `OnceLock<T>` slot. When
the internal hash map grows, it moves the pointer to the slot, not the slot.
2. Through `&self`, a slot can only be created and filled once.
3. Slots are only mutated, removed or dropped through `&mut self` or by
dropping the map. The borrow checker allows that only when no `&T` handed
out by the map is still alive.
The same reasoning is why `OnceLock` itself can offer `get_mut` and `take`.
## API
| `get::<T>()` | `&self` | `Some(&T)` if set |
| `get_or_init(f)` | `&self` | Returns the value, running `f` first if it is unset |
| `insert(value)` | `&self` | `Ok(&T)` if it was unset, otherwise gives the value back in `Err` |
| `contains::<T>()` | `&self` | Whether a value is set |
| `len()` / `is_empty()` | `&self` | Number of types whose value is set |
| `get_mut::<T>()` | `&mut self` | `Some(&mut T)` if set |
| `remove::<T>()` | `&mut self` | Takes the value out; the type can be set again |
| `clear()` | `&mut self` | Removes all values and frees all slots |
## Initialization semantics
- `get_or_init` runs the initializer outside the map's lock. A slow
initializer for one type doesn't block other types, and an initializer may
use the map for other types, as long as initializers don't depend on each
other in a cycle (see below).
- When many threads race to initialize the same type, only one initializer
runs. The other threads block until the value is ready. Once an initializer
returns, no other initializer for that type runs until the value is removed.
- If the initializer panics, the value stays unset and the next call runs its
own initializer. So an initializer can run more than once over the life of
the map: after a panic, and after `remove` or `clear`.
### Deadlocks
Neither of these cases panics or returns an error. They hang:
- Calling `get_or_init::<T>` or `insert::<T>` from inside the initializer for
the same `T`. This is a reentrant `OnceLock` initialization. The standard
library leaves the outcome unspecified, and today it deadlocks. `insert`
doesn't return `Err` here.
- Initializers that depend on each other in a cycle. If `A`'s initializer
uses `B` and `B`'s uses `A`, a single thread hits the reentrant case above.
With two threads, one starting with `A` and the other with `B`, each blocks
forever waiting for the other's slot.
### Empty slots
A type's slot is created the first time `get_or_init` or `insert` is called
for it. If the initializer panics, the empty slot stays in the map. Nothing
called through `&self` removes it, though a later successful initialization
reuses it. This costs at most one small allocation per distinct type, but
`len()` and `is_empty()` scan empty slots along with full ones. `remove::<T>()`
and `clear()` free empty slots too.
## Caveats
- One value per type. Use newtypes to store several values of the same
underlying type.
- Values must be `Send + Sync + 'static`.
- Initializers are synchronous and can't fail. See the next section.
- A lookup takes a shared read lock. Because the returned reference isn't tied
to a guard, hot paths can look a value up once and keep the `&T`.
- The minimum supported Rust version is 1.86.
## Fallible and async initialization
std's `OnceLock::get_or_try_init` isn't stable yet, so the map has no
`get_or_try_init`. What to store instead depends on what a failure means:
- **The failure is final** (a malformed setting, say). Store the `Result`
itself with `get_or_init`. Every caller sees the same outcome, and setup
runs once.
- **The failure is temporary** (a server that isn't up yet). Store a cell
that supports a fallible initializer, such as `once_cell::sync::OnceCell`:
```rust
map.get_or_init(|| DbCell(OnceCell::new()))
.0
.get_or_try_init(|| connect())
```
A failed attempt stores nothing, so the next call tries again, and only one
attempt runs at a time.
- **Setup is async.** Store an async cell such as `tokio::sync::OnceCell`. The
map only creates the empty cell, and the cell's async `get_or_init` (or
`get_or_try_init`, if setup can also fail) does the work without blocking the
executor. Don't run slow or blocking setup in the
map's own `get_or_init` from async code, because callers waiting on it block
their thread.
[`examples/fallible_extensions.rs`](examples/fallible_extensions.rs) shows the
first two cases side by side:
```sh
cargo run --example fallible_extensions
```
Once std stabilizes `OnceLock::get_or_try_init`, the map will gain a matching
method.
## Checking the unsafe code
All `unsafe` code goes through one private `unsafe fn`, `detach`. It extends a
slot reference from the lock guard's lifetime to the borrow of the map. There
are three `unsafe` blocks: the one in the body of `detach`, and one at each of
its two call sites (`slot` and `create_slot`). Each call site has to meet
`detach`'s precondition: the slot must be owned by the map. Any new call to
`detach` needs the same review. The test suite includes a test that keeps a
reference alive while the map grows. Run it under
[Miri](https://github.com/rust-lang/miri) with both aliasing models:
```sh
cargo +nightly miri test
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test
```
## License
Apache-2.0