surelock 0.1.0

Deadlock-free locks for Rust with compile time guarantees, incremental locks, and atomic lock sets.
Documentation
//! Factory for distributing [`KeyVoucher`]s
//! across execution contexts.
//!
//! A [`Locksmith`] is a program-wide singleton created once (typically
//! during system init). It issues
//! [`KeyVoucher`]s that can be sent to
//! other threads or cores. Each voucher is redeemed for a
//! [`KeyHandle`](crate::key_handle::KeyHandle) on the target context.
//!
//! `Locksmith` is `!Send` and `!Sync` -- it stays on the creating
//! thread/core. Only the vouchers travel.
//!
//! Only one `Locksmith` can exist at a time (enforced via a global
//! `AtomicBool`). Creating a second returns `None`.
//!
//! # Examples
//!
//! ```rust
//! use surelock::locksmith::Locksmith;
//!
//! let smith = Locksmith::new(4).unwrap(); // 4-core system
//!
//! let _v0 = smith.issue().unwrap();
//! let _v1 = smith.issue().unwrap();
//! let _v2 = smith.issue().unwrap();
//! let _v3 = smith.issue().unwrap();
//! assert!(smith.issue().is_none()); // limit reached
//! ```

use core::marker::PhantomData;

use crate::atomic::{AtomicBool, AtomicUsize, Ordering};

use crate::key_voucher::KeyVoucher;

/// Global singleton flag. Only one `Locksmith` can exist at a time.
static LOCKSMITH_EXISTS: AtomicBool = AtomicBool::new(false);

/// Factory for distributing lock scope capabilities across threads
/// or cores.
///
/// Program-wide singleton -- only one `Locksmith` can exist at a
/// time. Created via [`new`](Locksmith::new) (with a limit) or
/// [`unlimited`](Locksmith::unlimited) (no limit). Issues
/// [`KeyVoucher`]s that can be sent
/// to target threads/cores and redeemed for
/// [`KeyHandle`](crate::key_handle::KeyHandle)s.
///
/// `!Send`, `!Sync` -- stays on the creating thread/core.
/// Only the vouchers travel.
///
/// # Caveats
///
/// The singleton invariant relies on a global `AtomicBool`. On Unix,
/// `fork()` duplicates the parent's memory -- including the flag --
/// into the child process. The child inherits `LOCKSMITH_EXISTS =
/// true` but has no `Locksmith` value, so it can never create one
/// and the flag is never cleared. Avoid `fork()` after creating a
/// `Locksmith`, or use `exec` immediately (the standard `fork`/`exec`
/// pattern resets the address space).
pub struct Locksmith {
    issued: AtomicUsize,
    limit: Option<usize>,
    _not_send: PhantomData<*const ()>, // !Send, !Sync
}

impl Locksmith {
    /// Try to create a new `Locksmith` with the given voucher limit.
    ///
    /// Returns `None` if a `Locksmith` already exists. Only one can
    /// exist at a time (enforced via a global `AtomicBool`).
    /// When the `Locksmith` is dropped, the slot is released.
    #[must_use]
    pub fn new(limit: usize) -> Option<Self> {
        LOCKSMITH_EXISTS
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .ok()
            .map(|_| Self {
                issued: AtomicUsize::new(0),
                limit: Some(limit),
                _not_send: PhantomData,
            })
    }

    /// Try to create a `Locksmith` with no voucher limit.
    ///
    /// Returns `None` if a `Locksmith` already exists.
    /// `issue()` will always succeed (no limit check).
    #[must_use]
    pub fn unlimited() -> Option<Self> {
        LOCKSMITH_EXISTS
            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
            .ok()
            .map(|_| Self {
                issued: AtomicUsize::new(0),
                limit: None,
                _not_send: PhantomData,
            })
    }

    /// Create a `Locksmith` with the given limit, panicking if one
    /// already exists.
    ///
    /// # Panics
    ///
    /// Panics if a `Locksmith` already exists.
    #[must_use]
    #[allow(clippy::expect_used)]
    pub fn create(limit: usize) -> Self {
        Self::new(limit).expect("surelock: a Locksmith already exists")
    }

    /// Create an unlimited `Locksmith`, panicking if one already
    /// exists.
    ///
    /// # Panics
    ///
    /// Panics if a `Locksmith` already exists.
    #[must_use]
    #[allow(clippy::expect_used)]
    pub fn create_unlimited() -> Self {
        Self::unlimited().expect("surelock: a Locksmith already exists")
    }

    /// Issue a [`KeyVoucher`].
    ///
    /// Returns `None` if the limit has been reached. If the
    /// `Locksmith` is unlimited, always returns `Some`.
    ///
    /// The voucher is `Send` and can be transferred to another
    /// thread or core.
    ///
    /// The counter only increments -- slots are not reclaimed when
    /// vouchers or key handles are dropped. For embedded systems with
    /// static core counts, issue exactly N vouchers at init.
    #[must_use]
    pub fn issue(&self) -> Option<KeyVoucher> {
        match self.limit {
            None => {
                self.issued.fetch_add(1, Ordering::Relaxed);
                Some(KeyVoucher::new_internal())
            }
            Some(limit) => self
                .issued
                .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
                    if n < limit { Some(n + 1) } else { None }
                })
                .ok()
                .map(|_| KeyVoucher::new_internal()),
        }
    }

    /// Number of vouchers issued so far.
    #[must_use]
    pub fn issued(&self) -> usize {
        self.issued.load(Ordering::Relaxed)
    }

    /// The configured limit, or `None` if unlimited.
    #[must_use]
    pub const fn limit(&self) -> Option<usize> {
        self.limit
    }

    /// Remaining vouchers that can be issued, or `None` if unlimited.
    #[must_use]
    pub fn remaining(&self) -> Option<usize> {
        self.limit.map(|l| l.saturating_sub(self.issued()))
    }
}

impl Drop for Locksmith {
    fn drop(&mut self) {
        LOCKSMITH_EXISTS.store(false, Ordering::Release);
    }
}

impl core::fmt::Debug for Locksmith {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        f.debug_struct("Locksmith")
            .field("issued", &self.issued())
            .field("limit", &self.limit)
            .finish()
    }
}