Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

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.

Awesome Lists containing this project

README

        

# tokenlock

[docs.rs](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-cell

License: MIT/Apache-2.0