Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/yvt/tokenlock
Provides cell types that decouple permissions from data.
https://github.com/yvt/tokenlock
memory-safety multithreading rust
Last synced: about 2 months ago
JSON representation
Provides cell types that decouple permissions from data.
- Host: GitHub
- URL: https://github.com/yvt/tokenlock
- Owner: yvt
- License: apache-2.0
- Created: 2017-10-19T14:59:27.000Z (about 7 years ago)
- Default Branch: main
- Last Pushed: 2023-06-27T16:58:45.000Z (over 1 year ago)
- Last Synced: 2024-08-09T16:18:57.607Z (5 months ago)
- Topics: memory-safety, multithreading, rust
- Language: Rust
- Homepage: https://crates.io/crates/tokenlock
- Size: 88.9 KB
- Stars: 8
- Watchers: 2
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE-APACHE
Awesome Lists containing this project
README
# tokenlock
[](https://docs.rs/tokenlock/)
This crate provides a cell type, `TokenLock`, which can only be borrowed
by presenting the correct unforgeable token, thus decoupling permissions
from data.## Examples
### Basics
```rust
// Create a token
let mut token = IcToken::new();// Create a keyhole by `token.id()` and use this to create a `TokenLock`.
let lock: IcTokenLock = TokenLock::new(token.id(), 1);
assert_eq!(*lock.read(&token), 1);// Unlock the `TokenLock` using the matching token
let mut guard = lock.write(&mut token);
assert_eq!(*guard, 1);
*guard = 2;
```Only the matching `Token`'s owner can access its contents. `Token`
cannot be cloned:```rust
let lock = Arc::new(TokenLock::new(token.id(), 1));let lock_1 = Arc::clone(&lock);
std::thread::spawn(move || {
let lock_1 = lock_1;
let mut token_1 = token;// I have `Token` so I can get a mutable reference to the contents
lock_1.write(&mut token_1);
});// can't access the contents; I no longer have `Token`
// lock.write(&mut token);
```### Zero-sized tokens
Some token types, such as `BrandedToken` and `SingletonToken`, rely
solely on type safety and compile-time checks to guarantee uniqueness and
don't use runtime data for identification. As such, the keyholes for such
tokens can be default-constructed. `TokenLock::wrap` lets you construct a
`TokenLock` with a default-constructed keyhole.
On the other hand, creating such tokens usually has specific requirements.
See the following example that uses `with_branded_token`:```rust
with_branded_token(|mut token| {
// The lifetime of `token: BrandedToken<'brand>` is bound to
// this closure.// lock: BrandedTokenLock<'brand, i32>
let lock = BrandedTokenLock::wrap(42);lock.set(&mut token, 56);
assert_eq!(lock.get(&token), 56);
});
```### Lifetimes
The lifetime of the returned reference is limited by both of the `TokenLock`
and `Token`.```rust
let mut token = IcToken::new();
let lock = TokenLock::new(token.id(), 1);
let guard = lock.write(&mut token);
drop(lock); // compile error: `guard` cannot outlive `TokenLock`
drop(guard);
``````rust
drop(token); // compile error: `guard` cannot outlive `Token`
drop(guard);
```It also prevents from forming a reference to the contained value when
there already is a mutable reference to it:```rust
let write_guard = lock.write(&mut token);
let read_guard = lock.read(&token); // compile error
drop(write_guard);
```While allowing multiple immutable references:
```rust
let read_guard1 = lock.read(&token);
let read_guard2 = lock.read(&token);
```### Use case: Linked lists
An operating system kernel often needs to store the global state in a global
variable. Linked lists are a common data structure used in a kernel, but
Rust's ownership does not allow forming `'static` references into values
protected by a mutex. Common work-arounds, such as smart pointers and index
references, take a heavy toll on a small microcontroller with a single-issue
in-order pipeline and no hardware multiplier.```rust
struct Process {
prev: Option<& /* what lifetime? */ Process>,
next: Option<& /* what lifetime? */ Process>,
state: u8,
/* ... */
}
struct SystemState {
first_process: Option<& /* what lifetime? */ Process>,
process_pool: [Process; 64],
}
static STATE: Mutex = todo!();
````tokenlock` makes the `'static` reference approach possible by detaching the
lock granularity from the protected data's granularity.```rust
use tokenlock::*;
use std::cell::Cell;
struct Tag;
impl_singleton_token_factory!(Tag);type KLock = UnsyncSingletonTokenLock;
type KLockToken = UnsyncSingletonToken;
type KLockTokenId = SingletonTokenId;struct Process {
prev: KLock>,
next: KLock>,
state: KLock,
/* ... */
}
struct SystemState {
first_process: KLock>,
process_pool: [Process; 1],
}
static STATE: SystemState = SystemState {
first_process: KLock::new(KLockTokenId::new(), None),
process_pool: [
Process {
prev: KLock::new(KLockTokenId::new(), None),
next: KLock::new(KLockTokenId::new(), None),
state: KLock::new(KLockTokenId::new(), 0),
}
],
};
```## Cell types
The `TokenLock` type family is comprised of the following types:
| | `Sync` tokens | `!Sync` tokens² |
| ---------- | ---------------- | ---------------------- |
| Unpinned | `TokenLock` | `UnsyncTokenLock` |
| Pinned¹ | `PinTokenLock` | `UnsyncPinTokenLock` |¹That is, these types respect `T` being `!Unpin` and prevent the
exposure of `&mut T` through `&Self` or `Pin<&mut Self>`.²`Unsync*TokenLock` require that tokens are `!Sync` (not sharable
across threads). In exchange, such cells can be `Sync` even if the contained
data is not `Sync`, just like `std::sync::Mutex`.## Token types
This crate provides the following types implementing `Token`.
(**`std` only**) `IcToken` uses a global counter (with thread-local pools)
to generate unique 128-bit tokens.(**`alloc` only**) `RcToken` and `ArcToken` ensure their uniqueness by
reference-counted memory allocations.`SingletonToken` is a singleton token, meaning only one of such
instance can exist at any point of time during the program's execution.
`impl_singleton_token_factory!` instantiates a `static` flag to indicate
`SingletonToken`'s liveness and allows you to construct it safely by
`SingletonToken::new`. Alternatively, you can use
`SingletonToken::new_unchecked`, but this is unsafe if misused.`BrandedToken<'brand>` implements an extension of [`GhostCell`][1]. It's
created by `with_branded_token` or `with_branded_token_async`, which
makes the created token available only within the provided closure or the
created `Future`. This token incurs no runtime cost.[1]: http://plv.mpi-sws.org/rustbelt/ghostcell/
| Token ID (keyhole) | Token (key) |
| ------------------------------ | --------------------------------- |
| `IcTokenId` | `IcToken` + `u128` comparison |
| `RcTokenId` | `RcToken` + `usize` comparison |
| `ArcTokenId` | `ArcToken` + `usize` comparison |
| `SingletonTokenId` | `SingletonToken` |
| `BrandedTokenId<'brand>` | `BrandedToken<'brand>` |## `!Sync` tokens
`UnsyncTokenLock` is similar to `TokenLock` but designed for non-`Sync`
tokens and has relaxed requirements on the inner type for thread safety.
Specifically, it can be `Sync` even if the inner type is not `Sync`. This
allows for storing non-`Sync` cells such as `Cell` and reading and
writing them using shared references (all of which must be on the same
thread because the token is `!Sync`) to the token.```rust
use std::cell::Cell;
let mut token = ArcToken::new();
let lock = Arc::new(UnsyncTokenLock::new(token.id(), Cell::new(1)));let lock_1 = Arc::clone(&lock);
std::thread::spawn(move || {
// "Lock" the token to the current thread using
// `ArcToken::borrow_as_unsync`
let token = token.borrow_as_unsync();// Shared references can alias
let (token_1, token_2) = (&token, &token);lock_1.read(token_1).set(2);
lock_1.read(token_2).set(4);
});
````!Sync` tokens, of course, cannot be shared between threads:
```rust
let mut token = ArcToken::new();
let token = token.borrow_as_unsync();
let (token_1, token_2) = (&token, &token);// compile error: `&ArcTokenUnsyncRef` is not `Send` because
// `ArcTokenUnsyncRef` is not `Sync`
std::thread::spawn(move || {
let _ = token_2;
});let _ = token_1;
```## Cargo Features
- **`std`** enables the items that depend on `std` or `alloc`.
- **`alloc`** enables the items that depend on `alloc`.
- **`unstable`** enables experimental items that are not subject to the
semver guarantees.
- **`const-default_1`** enables the implementation of `ConstDefault` from
[`const-default ^1`][].[`const-default ^1`]: https://crates.io/crates/const-default/1.0.0
## Related Work
- [`ghost-cell`][1] is the official implementation of [`GhostCell`][2] and
has been formally proven to be sound. It provides an equivalent of
`BrandedTokenLock` with a simpler, more focused interface.- `SCell` from [`singleton-cell`][3] is a more generalized version of
`GhostCell` and accepts any singleton token types, and thus it's more
closer to our `TokenLock`. It provides equivalents of our
`BrandedToken` and `SingletonToken` out-of-box. It trades away
non-ZST token types for an advantage: `SCell` can be transposed
to `[SCell]`. It uses the [`singleton-trait`][5] crate (which did
not exist when `tokenlock::SingletonToken` was added) to mark singleton
token types.- [`qcell`][4] provides multiple cell types with different check
mechanisms. `QCell` uses a 32-bit integer as a token identifier, `TCell`
and `TLCell` use a marker type, and `LCell` uses lifetime branding.- `TokenCell` from [`token-cell`][6] is related to our `SingletonToken`,
but like `SCell` (but differing slightly), it supports transposition
from `&TokenCell` to `&[TokenCell]`. It uses a
custom trait to mark singleton token types.[1]: https://crates.io/crates/ghost-cell
[2]: http://plv.mpi-sws.org/rustbelt/ghostcell/
[3]: https://crates.io/crates/singleton-cell
[4]: https://crates.io/crates/qcell
[5]: https://crates.io/crates/singleton-trait
[6]: https://crates.io/crates/token-cellLicense: MIT/Apache-2.0