diff --git a/CHANGELOG.md b/CHANGELOG.md index ea25f8f..b4f0e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +- Added `SharedStaticCell` + ## 2.1.1 - 2025-06-22 - Soundness fix: ConstStaticCell should only be Send/Sync if T: Send. (#19, #20) diff --git a/README.md b/README.md index 7486c6d..3a22560 100644 --- a/README.md +++ b/README.md @@ -33,12 +33,12 @@ assert_eq!(*x, 42); - If you can use `alloc`, you can use `Box::leak()`. - If you're OK with `unsafe`, you can use `static mut THING: MaybeUninit`. -- If you need just `&'static T` (instead of `&'static mut T`), there's [`OnceCell`](https://doc.rust-lang.org/stable/core/cell/struct.OnceCell.html) (not thread-safe though) or [`OnceLock`](https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html) (thread-safe, but requires `std`). +- If you need just `&'static T` (instead of `&'static mut T`), there's [`SharedStaticCell`](https://docs.rs/static-cell/latest/static_cell/struct.SharedStaticCell.html) (which you must initialize before the first access), [`OnceCell`](https://doc.rust-lang.org/stable/core/cell/struct.OnceCell.html) (not thread-safe though) or [`OnceLock`](https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html) (thread-safe, but requires `std`). ## Interoperability This crate uses [`portable-atomic`](https://crates.io/crates/portable-atomic), so on targets without native -atomics you must import a crate that provides a [`critical-section`](https://github.com/rust-embedded/critical-section) +atomics you must import a crate that provides a [`critical-section`](https://github.com/rust-embedded/critical-section) implementation. See the `critical-section` README for details. ## Minimum Supported Rust Version (MSRV) diff --git a/src/lib.rs b/src/lib.rs index 69da548..dedc618 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ #![doc = include_str!("../README.md")] #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(feature = "nightly", feature(type_alias_impl_trait))] // for tests +#![allow(clippy::new_without_default)] use core::cell::UnsafeCell; use core::mem::MaybeUninit; @@ -121,6 +122,7 @@ impl StaticCell { /// Using this method directly is not recommended, but it can be used to construct `T` in-place directly /// in a guaranteed fashion. #[inline] + #[allow(clippy::mut_from_ref)] pub fn try_uninit(&'static self) -> Option<&'static mut MaybeUninit> { if self .used @@ -209,6 +211,152 @@ impl ConstStaticCell { } } +// --- + +/// Statically allocated, initialized at runtime cell, that allows shared access. +/// +/// It has two states: "empty" and "full". It is created "empty", and obtaining a reference +/// to the contents permanently changes it to "full". This allows that reference to be valid +/// forever. +/// +/// If your value can be initialized as a `const` value, consider using a plain old static variable. +#[derive(Debug)] +pub struct SharedStaticCell { + state: AtomicState, + val: UnsafeCell>, +} + +// Prefer pointer-width atomic operations, as narrower ones may be slower. + +#[cfg(all(target_pointer_width = "32", target_has_atomic = "32"))] +type AtomicState = portable_atomic::AtomicU32; +#[cfg(not(all(target_pointer_width = "32", target_has_atomic = "32")))] +type AtomicState = portable_atomic::AtomicU8; + +#[cfg(all(target_pointer_width = "32", target_has_atomic = "32"))] +type StateBits = u32; +#[cfg(not(all(target_pointer_width = "32", target_has_atomic = "32")))] +type StateBits = u8; + +unsafe impl Sync for SharedStaticCell {} +unsafe impl Send for SharedStaticCell {} + +impl SharedStaticCell { + const STATE_EMPTY: StateBits = 0; + const STATE_INITIALIZING: StateBits = 1; + const STATE_FULL: StateBits = 2; + + /// Create a new, empty `SharedStaticCell`. + /// + /// It can be initialized at runtime with [`SharedStaticCell::init()`] or similar methods. + #[inline] + pub const fn new() -> Self { + Self { + state: AtomicState::new(Self::STATE_EMPTY), + val: UnsafeCell::new(MaybeUninit::uninit()), + } + } + + /// Initialize the `SharedStaticCell` with a value, returning a shared reference to it. + /// + /// Using this method, the compiler usually constructs `val` in the stack and then moves + /// it into the `SharedStaticCell`. If `T` is big, this is likely to cause stack overflows. + /// Considering using [`SharedStaticCell::init_with`] instead, which will construct it in-place inside the `SharedStaticCell`. + /// + /// # Panics + /// + /// Panics if this `SharedStaticCell` is already full. + #[inline] + pub fn init(&'static self, val: T) -> &'static T { + self.init_with(|| val) + } + + /// Initialize the `SharedStaticCell` with the closure's return value, returning a shared reference to it. + /// + /// The advantage over [`SharedStaticCell::init`] is that this method allows the closure to construct + /// the `T` value in-place directly inside the `SharedStaticCell`, saving stack space. + /// + /// # Panics + /// + /// Panics if this `SharedStaticCell` is already full. + #[inline] + pub fn init_with(&'static self, val: impl FnOnce() -> T) -> &'static T { + self.try_init_with(val) + .expect("SharedStaticCell is already full, it cannot be initialized twice.") + } + + /// Try initializing the `SharedStaticCell` with a value, returning a shared reference to it. + /// + /// Using this method, the compiler usually constructs `val` in the stack and then moves + /// it into the `SharedStaticCell`. If `T` is big, this is likely to cause stack overflows. + /// Considering using [`SharedStaticCell::try_init_with`] instead, which will construct it in-place inside the `SharedStaticCell`. + /// + /// Will only return a Some(&'static T) when the `SharedStaticCell` was not yet initialized. + #[inline] + pub fn try_init(&'static self, val: T) -> Option<&'static T> { + self.try_init_with(|| val) + } + + /// Try initializing the `SharedStaticCell` with the closure's return value, returning a shared reference to it. + /// + /// The advantage over [`SharedStaticCell::try_init`] is that this method allows the closure to construct + /// the `T` value in-place directly inside the `SharedStaticCell`, saving stack space. + /// + /// Will only return a Some(&'static T) when the `SharedStaticCell` was not yet initialized. + #[inline] + pub fn try_init_with(&'static self, val: impl FnOnce() -> T) -> Option<&'static T> { + self.try_uninit().map(|mu| { + let val_ref = &*mu.write(val()); + self.state.store(Self::STATE_FULL, Ordering::Release); + val_ref + }) + } + + // Note that the state needs to be written to `STATE_FULL` by the caller before returning the reference, otherwise `get` will never succeed. + #[allow(clippy::mut_from_ref)] + fn try_uninit(&'static self) -> Option<&'static mut MaybeUninit> { + if self + .state + .compare_exchange( + Self::STATE_EMPTY, + Self::STATE_INITIALIZING, + Ordering::Acquire, + Ordering::Relaxed, + ) + .is_ok() + { + // SAFETY: We just checked that the value is not yet taken and marked it as taken. + let val = unsafe { &mut *self.val.get() }; + Some(val) + } else { + None + } + } + + /// Retrieves a shared reference to the data. + /// + /// # Panics + /// + /// This function will panic if the `SharedStaticCell` is not initialized. + #[inline] + pub fn get(&'static self) -> &'static T { + self.try_get().expect("SharedStaticCell is not initialized") + } + + /// Attempts to retrieve a shared reference to the data. + /// + /// Returns `Some(&'static T)` if the `SharedStaticCell` is initialized, otherwise `None`. + #[inline] + pub fn try_get(&'static self) -> Option<&'static T> { + if self.state.load(Ordering::Acquire) == Self::STATE_FULL { + // SAFETY: We just checked that the value is initialized. + Some(unsafe { (&*self.val.get()).assume_init_ref() }) + } else { + None + } + } +} + /// Convert a `T` to a `&'static mut T`. /// /// The macro declares a `static StaticCell` and then initializes it when run, returning the `&'static mut`. @@ -251,7 +399,7 @@ macro_rules! make_static { #[cfg(test)] mod tests { - use crate::StaticCell; + use crate::{SharedStaticCell, StaticCell}; #[test] fn test_static_cell() { @@ -260,6 +408,30 @@ mod tests { assert_eq!(*val, 42); } + #[test] + fn test_shared_static_cell() { + static CELL: SharedStaticCell = SharedStaticCell::new(); + + let should_be_none = CELL.try_get(); + assert!(should_be_none.is_none()); + + let val: &'static u32 = CELL.init(42u32); + assert_eq!(*val, 42); + + let should_be_some = CELL.try_get(); + assert_eq!(should_be_some, Some(&42)); + + let should_not_panic = CELL.get(); + assert_eq!(*should_not_panic, 42); + } + + #[test] + #[should_panic] + fn test_shared_static_cell_panics_if_accessed_when_empty() { + static CELL: SharedStaticCell = SharedStaticCell::new(); + CELL.get(); + } + #[cfg(feature = "nightly")] #[test] fn test_make_static() {