#token #cell #data-access #lock #borrow #ghost #borrowing

no-std tokenlock

Provides cell types that decouple permissions from data

16 releases

0.3.8 Jun 12, 2022
0.3.7 Dec 19, 2021
0.3.5 Sep 4, 2021
0.3.4 Jan 31, 2021
0.1.5 Oct 27, 2017

#351 in Rust patterns

40 downloads per month
Used in 10 crates (2 directly)

MIT/Apache

100KB
1K SLoC

tokenlock

docs.rs

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

// Create a token
let mut token = IcToken::new();

// Create a keyhole by `token.id()` and use this to create a `TokenLock`.
let lock: IcTokenLock<i32> = 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:

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:

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.

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);
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:

let write_guard = lock.write(&mut token);
let read_guard = lock.read(&token); // compile error
drop(write_guard);

While allowing multiple immutable references:

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.

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<SystemState> = todo!();

tokenlock makes the 'static reference approach possible by detaching the lock granularity from the protected data's granularity.

use tokenlock::*;
use std::cell::Cell;
struct Tag;
impl_singleton_token_factory!(Tag);

type KLock<T> = UnsyncSingletonTokenLock<T, Tag>;
type KLockToken = UnsyncSingletonToken<Tag>;
type KLockTokenId = SingletonTokenId<Tag>;

struct Process {
    prev: KLock<Option<&'static Process>>,
    next: KLock<Option<&'static Process>>,
    state: KLock<u8>,
    /* ... */
}
struct SystemState {
    first_process: KLock<Option<&'static Process>>,
    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<Tag> 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. 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.

Token ID (keyhole) Token (key)
IcTokenId IcToken + u128 comparison
RcTokenId RcToken + usize comparison
ArcTokenId ArcToken + usize comparison
SingletonTokenId<Tag> SingletonToken<Tag>
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.

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:

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.
  • ghost-cell is the official implementation of GhostCell and has been formally proven to be sound. It provides an equivalent of BrandedTokenLock with a simpler, more focused interface.

  • SCell from singleton-cell 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<Key, [T]> can be transposed to [SCell<Key, T>]. It uses the singleton-trait crate (which did not exist when tokenlock::SingletonToken was added) to mark singleton token types.

  • qcell 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 is related to our SingletonToken, but like SCell (but differing slightly), it supports transposition from &TokenCell<Token, &[T]> to &[TokenCell<Token, T>]. It uses a custom trait to mark singleton token types.

License: MIT/Apache-2.0

Dependencies