once_cell_no_std 0.2.0

Sync single assignment cells for `no_std`
Documentation

once_cell_no_std

The once_cell_no_std crate provides a no_std OnceCell type that implements Sync and can be used in statics. It does not use spinlocks or any other form of blocking. Instead, concurrent initialization is reported as an explicit ConcurrentInitialization error that the caller can handle as it likes.

The only thing this crate requires from the target is atomic compare-and-swap on u8. This covers most no_std targets, but not all of them: CAS-less targets such as thumbv6m-none-eabi (Cortex-M0/M0+), AVR, or RISC-V without the A extension are not supported and fail to compile.

OnceCell might store arbitrary non-Copy types, can be assigned to at most once and provide direct access to the stored contents. In a nutshell, the API looks like this:

use once_cell_no_std::OnceCell;

// `new` is a `const fn`, so a cell can live in a `static`
static CELL: OnceCell<u32> = OnceCell::new();
assert_eq!(CELL.get(), None);

// `set` takes `&self`, so it works on a non-mutable `static`
CELL.set(92).unwrap();
assert_eq!(CELL.get(), Some(&92));

// the cell keeps its first value, later updates are rejected. The value that was not written is
// handed back to the caller instead of being dropped.
assert_eq!(CELL.set(1).unwrap_err().into_rejected_value(), 1);

Initializing on first use

get_or_insert combines the two steps above: it writes the value if the cell is still empty, and either way hands out a reference to whatever ends up stored. get_or_init does the same for a value that has to be computed by a closure.

use once_cell_no_std::OnceCell;

static CELL: OnceCell<u32> = OnceCell::new();

// `get_or_insert` returns an `Err` on concurrent initialization by another caller. This crate
// never blocks/spins, so you have to decide yourself how to handle this (e.g. panic or retry in
// a loop).
let stored = match CELL.get_or_insert(92) {
    Ok(insertion) => insertion.stored(),
    Err(_) => panic!("concurrent initialization"),
};
assert_eq!(stored, &92);

// the cell still keeps its first value, so a later one is handed back
let insertion = CELL.get_or_insert(1).unwrap();
assert_eq!(insertion.into_rejected_value(), Some(1));
assert_eq!(insertion.stored(), &92); // still the previous value

// the closure only runs if the cell is still empty
assert_eq!(CELL.get_or_init(|| unreachable!()), Ok(&92));

Waiting for a concurrent initialization

Since this crate never blocks, waiting is something the caller opts into rather than something that happens by default. Every method that can run into a concurrent initialization reports it as an explicit error, so a caller that wants to wait can simply retry:

use once_cell_no_std::{OnceCell, error::ConcurrentInitialization};

/// Returns the value of the cell, initializing it with `init` if it is still empty.
///
/// Spins until the value is available if another caller is initializing the cell.
fn get_or_spin<T>(cell: &OnceCell<T>, mut init: impl FnMut() -> T) -> &T {
    loop {
        // `&mut init` is passed instead of `init` so that it survives for the next attempt
        match cell.get_or_init(&mut init) {
            Ok(value) => return value,
            // another caller is initializing the cell: retry in a busy loop
            Err(ConcurrentInitialization) => core::hint::spin_loop(),
        }
    }
}

let cell = OnceCell::new();
assert_eq!(get_or_spin(&cell, || "Hello, World!"), &"Hello, World!");
// the init function is not called for an already initialized cell
assert_eq!(get_or_spin(&cell, || unreachable!()), &"Hello, World!");

This is the lazy initialization that spin::Once and the lazy_static crate (with its spin_no_std feature) provide for no_std environments. The advantage of building it yourself is that the strategy stays yours, and spinning is not always the right one.

A spinning caller is hard to tell apart from one doing useful work, so a scheduler may keep running it in place of the very caller it is waiting for. Spinlocks Considered Harmful makes the case in full. Waiting for an interrupt, yielding to the scheduler, or reporting the error upwards may suit your system better. This crate enables you to switch to one of those (later) without changing your cell type.

Related crates

This crate was forked from the great once_cell crate. The original once_cell crate provides two flavors of OnceCell types: unsync::OnceCell and sync::OnceCell. The following table compares the types against once_cell_no_std::OnceCell:

OnceCell (this crate) once_cell::sync::OnceCell once_cell::unsync::OnceCell
implements Sync yes yes no
concurrent initialization leads to ConcurrentInitialization error returned thread blocked cannot happen
no_std supported yes partially (requires critical-section implementation) yes

Parts of once_cell API are included into std/core as of Rust 1.70.0. The following table compares once_cell_no_std::OnceCell against the core::cell::OnceCell and std::sync::OnceLock types:

OnceCell (this crate) std::sync::OnceLock core::cell::OnceCell
implements Sync yes yes no
concurrent initialization leads to ConcurrentInitialization error returned thread blocked cannot happen
no_std supported yes no yes

For more related crates, check out the README of once_cell.