diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index fc654a8..41fbcfd 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -29,7 +29,7 @@ steps: commands: - nix profile add nixpkgs#cachix - cachix use expede - - nix develop .#ci --command cargo check --package deathproof --no-default-features + - nix develop .#ci --command cargo check --package surelock --no-default-features depends_on: [] - name: wasm32-check @@ -41,7 +41,7 @@ steps: commands: - nix profile add nixpkgs#cachix - cachix use expede - - nix develop .#ci --command cargo check --package deathproof --no-default-features --target wasm32-unknown-unknown + - nix develop .#ci --command cargo check --package surelock --no-default-features --target wasm32-unknown-unknown depends_on: [] - name: thumbv6m-check @@ -53,7 +53,7 @@ steps: commands: - nix profile add nixpkgs#cachix - cachix use expede - - nix develop .#ci --command cargo check --package deathproof --no-default-features --target thumbv6m-none-eabi + - nix develop .#ci --command cargo check --package surelock --no-default-features --target thumbv6m-none-eabi depends_on: [] - name: msrv diff --git a/Cargo.lock b/Cargo.lock index 13b416d..8604757 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,12 +8,6 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" - [[package]] name = "bolero" version = "0.13.4" @@ -123,27 +117,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "deathproof" -version = "0.1.0" -dependencies = [ - "bolero", - "deathproof_macros", - "lock_api", - "parking_lot", - "spin", - "thiserror", -] - -[[package]] -name = "deathproof_macros" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "either" version = "1.15.0" @@ -217,29 +190,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -327,15 +277,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -348,17 +289,34 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "surelock" +version = "0.1.0" +dependencies = [ + "bolero", + "lock_api", + "spin", + "surelock_macros", + "thiserror", +] + +[[package]] +name = "surelock_macros" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "syn" @@ -423,12 +381,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "winnow" version = "0.5.40" diff --git a/Cargo.toml b/Cargo.toml index 509599c..babfa90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,32 @@ [workspace] resolver = "3" members = [ - "deathproof", - "deathproof_macros", + "surelock", + "surelock_macros", ] [workspace.package] +categories = ["concurrency", "no-std"] edition = "2024" +keywords = ["deadlock", "lock", "mutex", "no_std", "sync"] rust-version = "1.90" license = "MIT OR Apache-2.0" -repository = "https://codeberg.org/expede/deathproof" +repository = "https://codeberg.org/expede/surelock" authors = [ "Brooklyn Zelenka ", ] [workspace.dependencies] # Internal crates -deathproof = { version = "0.1.0", path = "deathproof", default-features = false } -deathproof_macros = { version = "0.1.0", path = "deathproof_macros" } +surelock = { version = "0.1.0", path = "surelock", default-features = false } +surelock_macros = { version = "0.1.0", path = "surelock_macros" } # External crates bolero = "0.13.4" lock_api = { version = "0.4", default-features = false } -parking_lot = { version = "0.12", default-features = false } proc-macro2 = "1" quote = "1" -spin = { version = "0.9", default-features = false } +spin = { version = "0.9", default-features = false, features = ["lock_api", "mutex", "spin_mutex"] } syn = { version = "2", features = ["full"] } thiserror = { version = "2.0", default-features = false } @@ -42,7 +43,7 @@ rust_2018_idioms = { level = "deny", priority = -1 } unreachable_pub = "deny" unused_extern_crates = "deny" -# unsafe_code is set per-crate: "forbid" in deathproof_macros, "allow" in deathproof +# unsafe_code is set per-crate: "forbid" in surelock_macros, "allow" in surelock # (workspace = true doesn't support per-crate overrides) [workspace.lints.clippy] diff --git a/README.md b/README.md index bbfffde..c8a33ca 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,51 @@ -# Deathproof +# Surelock > `no_std` deadlock-free locks for Rust -[![Lint](https://ci.codeberg.org/api/badges/expede/deathproof/status.svg)](https://ci.codeberg.org/repos/expede/deathproof) +[![Lint](https://ci.codeberg.org/api/badges/expede/surelock/status.svg)](https://ci.codeberg.org/repos/expede/surelock) [![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](LICENSE-MIT) -Deathproof prevents deadlocks by breaking the circular-wait +Surelock prevents deadlocks by breaking the circular-wait [Coffman condition](https://en.wikipedia.org/wiki/Deadlock#Coffman_conditions) -(1971). All lock acquisition goes through `LockSet`, which sorts locks -by a monotonic `LockId` assigned at creation time. A `LockScope` tracks -a high watermark to enforce ascending ID order across sequential -acquisitions. +(1971) via two complementary mechanisms: + +| Mechanism | Scope | Enforcement | Description | +|-----------|----------------|-----------------|----------------------------------------------------------------------------| +| `LockSet` | Within a level | By construction | Atomic acquisition of multiple locks, sorted by monotonic `LockId` | +| `Level`s | Across levels | Compile-time | `LockAfter` trait bounds on a consumed-and-re-emitted `LockKey` type param | + +Every lock call is infallible or doesn't compile. No `Result`, no +`Option`, no panic on any lock acquisition path. ## Features - Deadlock _prevention_ (not just detection) - `no_std + alloc` compatible -- Generic over lock backend (`Mutex` where `R: lock_api::RawMutex`) +- Generic over lock backend (`Mutex` where `R: lock_api::RawMutex`) - Dynamic lock sets (lock N items from a collection, determined at runtime) +- Compile-time level ordering via `lock_order!` macro and `LockAfter` trait - No spinlocks required -- pluggable backends (`spin`, `parking_lot`, etc.) - No `unsafe` in the public API - -## Three API Tiers - -| Tier | API | Wrong-order incremental | Can panic? | Can deadlock? | -|----------|--------------------------|-------------------------|:----------:|:-------------:| -| Simple | `scope.lock(&set)` | Panics with diagnostic | Yes | No | -| Fallible | `scope.try_lock(&set)` | Returns `Err` | No | No | -| GDP | `scope.lock_after(&set)` | Won't compile | No | No | - -The GDP tier uses [Ghosts of Departed Proofs](https://kataskeue.com/gdp.pdf)-style -phantom types combined with `LockAfter` trait bounds (declared via the -`lock_order!` macro) for compile-time cross-category ordering. +- Levels are opt-in -- `Mutex` defaults to `Base` level ## Crates -| Crate | Description | -|--------------------------------------------|---------------------------------------------------------------------------| -| [`deathproof`](./deathproof) | Core library: `Mutex`, `LockId`, `LockSet`, `LockScope`, `Lockable` | -| [`deathproof_macros`](./deathproof_macros) | Proc macros: `lock_order!` for declaring category orderings | +| Crate | Description | +|-------------------------------------------|-------------------------------------------------------------------------| +| [`surelock`](./surelock) | Core library: `Mutex`, `LockId`, `LockSet`, `LockKey`, `Lockable` | +| [`surelock_macros`](./surelock_macros) | Proc macros: `lock_order!` for declaring level orderings | ## Quick Start ```rust -use deathproof::{lock_scope, LockSet, Mutex}; +use surelock::{lock_scope, LockSet, Mutex}; use spin::Mutex as RawSpin; let counter: Mutex, u32> = Mutex::new(0); -lock_scope(|scope| { - let mut guard = scope.lock(&LockSet::new(&counter)); +lock_scope(|key| { + let set = LockSet::new(&counter); + let (mut guard, _key) = key.lock(&set); *guard += 1; }); ``` @@ -63,28 +59,39 @@ let accounts: Vec, u64>> = // Which accounts to lock is determined at runtime fn transfer(accounts: &[Mutex, u64>], from: usize, to: usize, amount: u64) { let set = LockSet::new((&accounts[from], &accounts[to])); - lock_scope(|scope| { - let (mut src, mut dst) = scope.lock(&set); + lock_scope(|key| { + let ((mut src, mut dst), _key) = key.lock(&set); *src -= amount; *dst += amount; }); } ``` -### GDP-Style Compile-Time Ordering +### Cross-Level Incremental Acquisition ```rust -use deathproof_macros::lock_order; +use surelock_macros::lock_order; -struct ConfigLock; -struct AccountLock; +struct Config; +impl Level for Config {} -lock_order! { ConfigLock => AccountLock } +struct Account; +impl Level for Account {} -proof_scope(|scope| { - let (cfg, scope) = scope.lock_after::(&config_set); - let (acct, scope) = scope.lock_after::(&acct_set); - // Reversing the order above would be a type error. +lock_order! { Config => Account } + +lock_scope(|key| { + // First: acquire config lock (Base -> Config level) + let (cfg, key) = key.lock(&config_set); + let target = cfg.target_account; + + // Second: acquire account lock (Config -> Account level, compile-time enforced) + let acct_set = LockSet::new(&accounts[target]); + let (mut acct, _key) = key.lock(&acct_set); + *acct += cfg.bonus; + + // Reversing the order above would be a type error: + // `Config` does not implement `LockAfter` }); ``` @@ -92,16 +99,16 @@ proof_scope(|scope| { The four Coffman conditions (all must hold simultaneously for deadlock): -| # | Condition | Meaning | -|---|------------------|-------------------------------------------------------| -| 1 | Mutual exclusion | At least one resource is held in a non-shareable mode | -| 2 | Hold and wait | A thread holds resource(s) while waiting for another | -| 3 | No preemption | Resources cannot be forcibly reclaimed from a holder | -| 4 | Circular wait | A cycle exists in the wait-for graph | +| # | Condition | Meaning | +|-----|------------------|-------------------------------------------------------| +| 1 | Mutual exclusion | At least one resource is held in a non-shareable mode | +| 2 | Hold and wait | A thread holds resource(s) while waiting for another | +| 3 | No preemption | Resources cannot be forcibly reclaimed from a holder | +| 4 | Circular wait | A cycle exists in the wait-for graph | -Deathproof breaks condition #4 (circular wait) via `LockSet` -ID-sorted acquisition and a monotonicity watermark in `LockScope` -that enforces ascending ID order across sequential acquisitions. +Surelock breaks condition #4 (circular wait). `LockSet` acquires +locks in `LockId` order within a level. `LockAfter` trait bounds +enforce strictly ascending cross-level acquisition at compile time. ## Development @@ -132,24 +139,26 @@ cargo test --workspace ## Inspiration -Deathproof is directly inspired by: +Surelock combines ideas from two existing libraries: -- Coffman, Elphick, and Shoshani's classic - ["System Deadlocks"](https://people.cs.umass.edu/~mcorner/courses/691J/papers/TS/coffman_deadlocks/coffman_deadlocks.pdf) - (1971) -- the four necessary and sufficient conditions for deadlock. - Break any one and deadlock is impossible. - [`happylock`](https://crates.io/crates/happylock) -- capability-token locking with `LockCollection` sorted acquisition and `Lockable` trait. - Deathproof borrows the core design (capability token, sorted lock sets, - `Lockable` trait with tuple impls) but replaces address-based ordering - with a stable monotonic `LockId` counter, removes the `std` requirement, - and adds GDP-style compile-time proofs. + Surelock replaces address-based ordering with a stable monotonic + `LockId` counter, removes the `std` requirement, and eliminates + `unsafe` from the public API. - [`lock_tree`](https://crates.io/crates/lock_tree) (Google Fuchsia) -- compile-time DAG of lock levels enforced via `LockAfter` trait bounds. - Deathproof adopts the `LockAfter` trait concept for cross-category - ordering but applies it to coarse-grained categories rather than - per-instance levels, avoiding the same-level limitation and O(N^2) - per-instance compile cost. + Surelock adds same-level multi-lock via `LockSet` (which `lock_tree` + cannot do) and makes levels opt-in with a `Base` default. + +Neither library alone covers both dynamic multi-lock and static ordering. +The combination -- with stable IDs and `no_std` throughout -- is +surelock's contribution. + +Grounded in Coffman, Elphick, and Shoshani's classic +["System Deadlocks"](https://people.cs.umass.edu/~mcorner/courses/691J/papers/TS/coffman_deadlocks/coffman_deadlocks.pdf) +(1971) -- the four necessary and sufficient conditions for deadlock. +Break any one and deadlock is impossible. ## License diff --git a/deathproof/src/lib.rs b/deathproof/src/lib.rs deleted file mode 100644 index 69e5642..0000000 --- a/deathproof/src/lib.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! # Deathproof: `no_std` Deadlock-Free Locks for Rust -//! -//! Deathproof prevents deadlocks by breaking the circular-wait Coffman -//! condition. All lock acquisition goes through [`LockSet`], which sorts -//! locks by a monotonic [`LockId`] assigned at creation time. A -//! [`LockScope`] tracks a high watermark to enforce ascending ID order -//! across sequential acquisitions within the same scope. -//! -//! # Three API Tiers -//! -//! | Tier | API | Wrong-order incremental | Can panic? | Can deadlock? | -//! |----------|--------------------------|-------------------------|:----------:|:-------------:| -//! | Simple | `scope.lock(&set)` | Panics with diagnostic | Yes | No | -//! | Fallible | `scope.try_lock(&set)` | Returns `Err` | No | No | -//! | GDP | `scope.lock_after(&set)` | Won't compile | No | No | -//! -//! # Quick Start -//! -//! ```rust,ignore -//! use deathproof::{lock_scope, LockSet, Mutex}; -//! use spin::Mutex as RawSpin; -//! -//! let counter: Mutex, u32> = Mutex::new(0); -//! -//! lock_scope(|scope| { -//! let mut guard = scope.lock(&LockSet::new(&counter)); -//! *guard += 1; -//! }); -//! ``` -//! -//! # Backend Agnostic -//! -//! `Mutex` is generic over any [`lock_api::RawMutex`] implementation. -//! Use `spin` for `no_std`, `parking_lot` for std, or any other backend. - -// Internal unsafe is required to implement lock/unlock around lock_api::RawMutex. -// All unsafe is confined to this crate; the public API is fully safe. -#![allow(unsafe_code)] -#![no_std] -// `alloc` will be needed once LockSet (requires Vec) is implemented. -// extern crate alloc; diff --git a/deathproof_macros/src/lib.rs b/deathproof_macros/src/lib.rs deleted file mode 100644 index 05007df..0000000 --- a/deathproof_macros/src/lib.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Proc macros for the [`deathproof`] deadlock-free locking library. -//! -//! Provides the [`lock_order!`] macro for declaring compile-time lock -//! category orderings. The macro generates [`LockAfter`] trait impls -//! (including transitive edges) from a declared chain, making cyclic -//! acquisition a type error. -//! -//! # Example -//! -//! ```rust,ignore -//! use deathproof_macros::lock_order; -//! -//! struct ConfigLock; -//! struct AccountLock; -//! struct TxnLock; -//! -//! lock_order! { ConfigLock => AccountLock => TxnLock } -//! // Generates: -//! // impl LockAfter for AccountLock {} -//! // impl LockAfter for TxnLock {} -//! // impl LockAfter for TxnLock {} // transitive -//! ``` - -#![forbid(unsafe_code)] - -use proc_macro::TokenStream; - -/// Declare a lock acquisition ordering for GDP-style proof-witnessed locking. -/// -/// Generates `LockAfter` trait implementations (including transitive -/// edges) from a chain of category types. For N categories, this -/// produces N(N-1)/2 implementations. -/// -/// # Syntax -/// -/// ```rust,ignore -/// lock_order! { A => B => C } -/// ``` -/// -/// Keep N small (5-10 categories). These are coarse-grained categories, -/// not per-lock-instance levels. -#[proc_macro] -pub fn lock_order(_input: TokenStream) -> TokenStream { - // TODO: implement lock_order! macro - TokenStream::new() -} diff --git a/flake.nix b/flake.nix index e22260d..2b1dca3 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "deathproof — no_std deadlock-free locks for Rust"; + description = "surelock — no_std deadlock-free locks for Rust"; inputs = { nixpkgs.url = "nixpkgs/nixos-25.11"; @@ -160,7 +160,7 @@ ]; in rec { devShells.default = pkgs.mkShell { - name = "deathproof_shell"; + name = "surelock_shell"; nativeBuildInputs = [ @@ -186,7 +186,7 @@ }; devShells.ci = pkgs.mkShell { - name = "deathproof_ci"; + name = "surelock_ci"; nativeBuildInputs = [ diff --git a/nix/commands.nix b/nix/commands.nix index 5c32c5d..d46c90a 100644 --- a/nix/commands.nix +++ b/nix/commands.nix @@ -28,16 +28,16 @@ in { "test:no_std" = cmd "Check no_std compatibility (wasm32, thumbv6m)" '' set -e - echo "===> Checking deathproof (no_std, no default features)..." - ${cargo} check --package deathproof --no-default-features -v + echo "===> Checking surelock (no_std, no default features)..." + ${cargo} check --package surelock --no-default-features -v echo "" - echo "===> Checking deathproof (wasm32-unknown-unknown)..." - ${cargo} check --package deathproof --no-default-features --target wasm32-unknown-unknown -v + echo "===> Checking surelock (wasm32-unknown-unknown)..." + ${cargo} check --package surelock --no-default-features --target wasm32-unknown-unknown -v echo "" - echo "===> Checking deathproof (thumbv6m-none-eabi)..." - ${cargo} check --package deathproof --no-default-features --target thumbv6m-none-eabi -v + echo "===> Checking surelock (thumbv6m-none-eabi)..." + ${cargo} check --package surelock --no-default-features --target thumbv6m-none-eabi -v echo "" echo "Done" @@ -60,7 +60,7 @@ in { set -e echo "========================================" - echo " Deathproof CI" + echo " Surelock CI" echo "========================================" echo "" @@ -114,22 +114,22 @@ in { set -e echo "========================================" - echo " Deathproof CI: no_std" + echo " Surelock CI: no_std" echo "========================================" echo "" - echo "===> [1/3] Checking deathproof (no_std, no default features)..." - ${cargo} check --package deathproof --no-default-features + echo "===> [1/3] Checking surelock (no_std, no default features)..." + ${cargo} check --package surelock --no-default-features echo "OK" echo "" - echo "===> [2/3] Checking deathproof (wasm32-unknown-unknown)..." - ${cargo} check --package deathproof --no-default-features --target wasm32-unknown-unknown + echo "===> [2/3] Checking surelock (wasm32-unknown-unknown)..." + ${cargo} check --package surelock --no-default-features --target wasm32-unknown-unknown echo "OK" echo "" - echo "===> [3/3] Checking deathproof (thumbv6m-none-eabi)..." - ${cargo} check --package deathproof --no-default-features --target thumbv6m-none-eabi + echo "===> [3/3] Checking surelock (thumbv6m-none-eabi)..." + ${cargo} check --package surelock --no-default-features --target thumbv6m-none-eabi echo "OK" echo "" @@ -142,7 +142,7 @@ in { set -e echo "========================================" - echo " Deathproof CI: all-features" + echo " Surelock CI: all-features" echo "========================================" echo "" @@ -170,7 +170,7 @@ in { set -e echo "========================================" - echo " Deathproof CI: Full Suite" + echo " Surelock CI: Full Suite" echo "========================================" echo "" diff --git a/deathproof/Cargo.toml b/surelock/Cargo.toml similarity index 69% rename from deathproof/Cargo.toml rename to surelock/Cargo.toml index ef24502..58be9f3 100644 --- a/deathproof/Cargo.toml +++ b/surelock/Cargo.toml @@ -1,28 +1,22 @@ [package] -name = "deathproof" +name = "surelock" version = "0.1.0" description = "no_std deadlock-free locks for Rust — break the circular-wait Coffman condition via LockId-sorted acquisition" readme = "../README.md" +categories.workspace = true edition.workspace = true +keywords.workspace = true license.workspace = true repository.workspace = true rust-version.workspace = true authors.workspace = true [dependencies] -deathproof_macros.workspace = true +surelock_macros.workspace = true lock_api.workspace = true thiserror.workspace = true -[dependencies.parking_lot] -workspace = true -optional = true - -[dependencies.spin] -workspace = true -optional = true - [dev-dependencies] bolero.workspace = true spin.workspace = true @@ -30,8 +24,7 @@ spin.workspace = true [features] default = [] std = [] -parking_lot = ["dep:parking_lot"] -spin = ["dep:spin"] +escape-hatch = [] [lints] workspace = true diff --git a/surelock/src/id.rs b/surelock/src/id.rs new file mode 100644 index 0000000..b030f13 --- /dev/null +++ b/surelock/src/id.rs @@ -0,0 +1,53 @@ +//! Monotonic lock identity. +//! +//! Every [`super::Mutex`] is assigned a unique [`LockId`] at creation +//! time via a global atomic counter. The total order on `LockId` values +//! is the foundation of [`super::LockSet`]'s deadlock prevention: locks +//! are always acquired in ascending `LockId` order. + +use core::sync::atomic::{AtomicU64, Ordering}; + +static NEXT_ID: AtomicU64 = AtomicU64::new(0); + +/// Unique, totally-ordered lock identifier. +/// +/// Assigned once at [`super::Mutex`] creation, immutable thereafter. +/// The ordering on `LockId` values determines the acquisition order +/// within a [`super::LockSet`]. +/// +/// `Relaxed` ordering on the counter is sufficient: we need uniqueness +/// and a total order on the _values_, not a happens-before relationship +/// between the `fetch_add` calls. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LockId(u64); + +impl LockId { + /// Allocate the next unique `LockId`. + pub(crate) fn next() -> Self { + Self(NEXT_ID.fetch_add(1, Ordering::Relaxed)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ids_are_unique() { + let a = LockId::next(); + let b = LockId::next(); + let c = LockId::next(); + assert_ne!(a, b); + assert_ne!(b, c); + assert_ne!(a, c); + } + + #[test] + fn ids_are_monotonically_increasing() { + let a = LockId::next(); + let b = LockId::next(); + let c = LockId::next(); + assert!(a < b); + assert!(b < c); + } +} diff --git a/surelock/src/key.rs b/surelock/src/key.rs new file mode 100644 index 0000000..0ef7711 --- /dev/null +++ b/surelock/src/key.rs @@ -0,0 +1,173 @@ +//! Scope token for ordered lock acquisition. +//! +//! [`LockKey`] tracks the current lock level as a type parameter. +//! It is consumed by [`LockKey::lock`] and re-emitted at the new level. +//! The key is `!Send + !Sync` and branded with an invariant lifetime +//! to prevent escape from the [`lock_scope`] closure. + +use core::marker::PhantomData; + +use crate::{ + level::{Bottom, Level, LockAfter}, + lockable::Lockable, + set::LockSet, +}; + +/// Scope token for ordered lock acquisition. +/// +/// Tracks the current lock level as a type parameter `Lvl`. Consumed +/// by [`lock`](LockKey::lock) and re-emitted at the new level. +/// +/// - `!Send + !Sync` — cannot cross thread boundaries. +/// - Branded lifetime `'scope` — cannot escape the [`lock_scope`] closure. +/// - `!Clone + !Copy` — prevents using the same key twice. +pub struct LockKey<'scope, Lvl: Level> { + // Invariant in 'scope: fn(&'scope ()) -> &'scope () makes 'scope + // neither covariant nor contravariant, preventing lifetime widening. + _brand: PhantomData &'scope ()>, + // *const () is !Send + !Sync. + _not_send: PhantomData<*const ()>, + _level: PhantomData, +} + +impl core::fmt::Debug for LockKey<'_, Lvl> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LockKey").finish_non_exhaustive() + } +} + +impl<'scope, Lvl: Level> LockKey<'scope, Lvl> { + /// Acquire all locks in the set. + /// + /// The set's level must implement [`LockAfter`] for the key's + /// current level. Consumes the key and returns guards plus a + /// new key at the set's level. + /// + /// This method is infallible — if the level relationship isn't + /// declared, the code doesn't compile. + pub fn lock<'set, L>(self, set: &'set LockSet) -> (L::Guards<'set>, LockKey<'scope, L::Lvl>) + where + L: Lockable, + L::Lvl: LockAfter, + { + // SAFETY: LockSet guarantees sorted_indices are valid and + // the locks are all from the Lockable. The LockAfter bound + // guarantees the ordering is safe. + unsafe { + set.lock_in_order(); + } + let guards = unsafe { set.make_guards() }; + + ( + guards, + LockKey { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + }, + ) + } + + /// Create a nested scope that inherits the current level. + /// + /// The inner key starts at the same level as the outer key, so + /// any subsequent [`lock`](LockKey::lock) calls must still be at + /// higher levels. The outer key is consumed for the duration of + /// the subscope. + /// + /// After the subscope returns, a key at the same level is returned + /// alongside the closure's return value. The inner key gets its + /// own branded lifetime, so its guards cannot escape the inner + /// closure. + pub fn subscope(self, f: F) -> (Ret, LockKey<'scope, Lvl>) + where + F: for<'inner> FnOnce(LockKey<'inner, Lvl>) -> Ret, + { + let result = f(LockKey { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + }); + + ( + result, + LockKey { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + }, + ) + } +} + +/// Entry point for all ordered lock acquisition. +/// +/// The `for<'scope>` bound makes `'scope` universally quantified: +/// the closure must work for _any_ `'scope`, so `Ret` cannot name +/// `'scope`. This prevents keys and guards from escaping the closure. +/// Same technique as [`std::thread::scope`]. +/// +/// On `std` (with the `std` feature enabled), a `thread_local!` flag +/// prevents nested `lock_scope` calls on the same thread. On `no_std`, +/// nested calls are a documented hazard — use +/// [`LockKey::subscope`] for safe nesting. +/// +/// # Panics +/// +/// On `std`: panics if a `lock_scope` is already active on the current +/// thread. Use [`LockKey::subscope`] for nesting instead. +/// +/// # Examples +/// +/// ```rust,ignore +/// use surelock::{lock_scope, LockSet, Mutex}; +/// use spin::Mutex as RawSpin; +/// +/// let counter: Mutex, u32> = Mutex::new(0); +/// +/// lock_scope(|key| { +/// let set = LockSet::new(&counter); +/// let (mut guard, _key) = key.lock(&set); +/// *guard += 1; +/// }); +/// ``` +pub fn lock_scope(f: F) -> Ret +where + F: for<'scope> FnOnce(LockKey<'scope, Bottom>) -> Ret, +{ + #[cfg(feature = "std")] + { + std::thread_local! { + static SCOPE_ACTIVE: core::cell::Cell = + const { core::cell::Cell::new(false) }; + } + + SCOPE_ACTIVE.with(|active| { + assert!( + !active.replace(true), + "nested lock_scope — use key.subscope() instead" + ); + }); + + let result = f(LockKey { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + }); + + SCOPE_ACTIVE.with(|active| { + active.set(false); + }); + + result + } + + #[cfg(not(feature = "std"))] + { + f(LockKey { + _brand: PhantomData, + _not_send: PhantomData, + _level: PhantomData, + }) + } +} diff --git a/surelock/src/level.rs b/surelock/src/level.rs new file mode 100644 index 0000000..a42f62d --- /dev/null +++ b/surelock/src/level.rs @@ -0,0 +1,52 @@ +//! Lock levels and ordering traits. +//! +//! Levels are marker types that represent a position in a lock +//! acquisition ordering. The [`LockAfter`] trait declares that locks +//! at one level may be acquired after locks at another. +//! +//! The [`lock_order!`](surelock_macros::lock_order) macro generates +//! `LockAfter` impls (including transitive edges) from a declared chain. +//! For most users, the default [`Base`] level is sufficient — levels are +//! only needed for incremental cross-level acquisition. + +/// Marker trait for lock level types. +/// +/// Implemented by [`Bottom`], [`Base`], and any user-declared level +/// types (via [`lock_order!`](surelock_macros::lock_order) or manual +/// implementation). +pub trait Level {} + +/// Marker trait: locks at level `Self` may be acquired after locks at +/// level `Before` are already held. +/// +/// This is a _safe_ trait. A cyclic implementation defeats deadlock +/// prevention but cannot cause undefined behavior — cycles cause a +/// liveness failure (potential deadlock), not a safety failure (memory +/// corruption). The [`lock_order!`](surelock_macros::lock_order) +/// macro prevents cycles by construction. +pub trait LockAfter: Level {} + +/// The bottom of the ordering lattice. +/// +/// This is the initial state of [`super::LockKey`] when created by +/// [`super::lock_scope`]. Not a valid level for mutexes — only used +/// internally as the starting point for the key's level parameter. +#[derive(Debug, Clone, Copy)] +pub struct Bottom; + +impl Level for Bottom {} + +/// Default lock level. +/// +/// [`super::Mutex`] without an explicit level parameter defaults +/// to `Base`. Every user-declared level (via +/// [`lock_order!`](surelock_macros::lock_order)) is implicitly +/// `LockAfter`. +/// +/// If you only use `Base`-level locks, you can acquire one +/// [`super::LockSet`] per scope — no level declarations needed. +#[derive(Debug, Clone, Copy)] +pub struct Base; + +impl Level for Base {} +impl LockAfter for Base {} diff --git a/surelock/src/lib.rs b/surelock/src/lib.rs new file mode 100644 index 0000000..07b74fe --- /dev/null +++ b/surelock/src/lib.rs @@ -0,0 +1,51 @@ +//! # Surelock: `no_std` Deadlock-Free Locks for Rust +//! +//! Surelock prevents deadlocks by breaking the circular-wait Coffman +//! condition via two complementary mechanisms: +//! +//! 1. [`set::LockSet`] — atomic acquisition of multiple locks at the +//! same level, sorted by a monotonic [`id::LockId`] assigned at +//! creation. Safe by construction, infallible. +//! 2. **Levels** — named types declaring a compile-time ordering via +//! [`level::LockAfter`] trait bounds. A consumed-and-re-emitted +//! [`key::LockKey`] tracks the current level as a type parameter. +//! Wrong-order acquisition is a compile error, not a runtime failure. +//! +//! Every lock call is infallible or doesn't compile. No `Result`, +//! no `Option`, no panic on any lock acquisition path. +//! +//! # Quick Start +//! +//! ```rust,ignore +//! use surelock::{key::lock_scope, mutex::Mutex, set::LockSet}; +//! use spin::Mutex as RawSpin; +//! +//! let counter: Mutex, u32> = Mutex::new(0); +//! +//! lock_scope(|key| { +//! let set = LockSet::new(&counter); +//! let (mut guard, _key) = key.lock(&set); +//! *guard += 1; +//! }); +//! ``` +//! +//! # Backend Agnostic +//! +//! [`mutex::Mutex`] is generic over any [`lock_api::RawMutex`] +//! implementation. Use `spin` for `no_std`, `parking_lot` for std, +//! or any other backend. The level parameter defaults to [`level::Base`] +//! — levels are opt-in for incremental cross-level acquisition. + +// Internal unsafe is required to implement lock/unlock around lock_api::RawMutex. +// All unsafe is confined to this crate; the public API is fully safe. +#![allow(unsafe_code)] +#![no_std] + +extern crate alloc; + +pub mod id; +pub mod key; +pub mod level; +pub mod lockable; +pub mod mutex; +pub mod set; diff --git a/surelock/src/lockable.rs b/surelock/src/lockable.rs new file mode 100644 index 0000000..7661c0a --- /dev/null +++ b/surelock/src/lockable.rs @@ -0,0 +1,94 @@ +//! Trait for types that can be locked atomically. +//! +//! [`Lockable`] abstracts over single mutexes, tuples of mutexes, +//! and slices of mutexes. All elements in a `Lockable` must share +//! the same lock [`Level`](crate::level::Level). + +pub mod tuples; + +use alloc::vec::Vec; + +use lock_api::RawMutex; + +use crate::{ + id::LockId, + level::Level, + mutex::{Mutex, guard::MutexGuard}, +}; + +/// A type (or collection of types) that can be locked atomically. +/// +/// All locks in a single `Lockable` must be at the same level. +/// +/// # Safety +/// +/// Implementors must: +/// - Correctly report their [`LockId`]s via [`collect_ids`](Lockable::collect_ids). +/// - Perform lock/unlock operations matching those IDs. +/// - Ensure all locks share the same level type `Lvl`. +pub unsafe trait Lockable { + /// The lock level shared by all locks in this collection. + type Lvl: Level; + + /// The guard type(s) returned when all locks are held. + type Guards<'set> + where + Self: 'set; + + /// Collect the [`LockId`] of each lock into `out`. + fn collect_ids(&self, out: &mut Vec); + + /// Acquire locks in the order given by `sorted_indices`. + /// + /// `sorted_indices` are indices into the conceptual array of locks, + /// ordered by their [`LockId`]. For a single lock this is trivial. + /// + /// # Safety + /// + /// `sorted_indices` must be a valid permutation produced by + /// [`LockSet`](crate::LockSet). + unsafe fn lock_in_order(&self, sorted_indices: &[usize]); + + /// Construct guards assuming all locks are held. + /// + /// # Safety + /// + /// All locks must currently be held by the caller. + unsafe fn make_guards(&self) -> Self::Guards<'_>; + + /// Release all locks. + /// + /// # Safety + /// + /// All locks must currently be held by the caller. + unsafe fn unlock_all(&self); +} + +// -- Single Mutex Implementation -- + +unsafe impl Lockable for &Mutex { + type Lvl = Lvl; + type Guards<'set> + = MutexGuard<'set, R, T> + where + Self: 'set; + + fn collect_ids(&self, out: &mut Vec) { + out.push(self.id()); + } + + unsafe fn lock_in_order(&self, _sorted_indices: &[usize]) { + self.raw.lock(); + } + + unsafe fn make_guards(&self) -> Self::Guards<'_> { + MutexGuard { + raw: &self.raw, + data: &self.data, + } + } + + unsafe fn unlock_all(&self) { + unsafe { self.raw.unlock() }; + } +} diff --git a/surelock/src/lockable/tuples.rs b/surelock/src/lockable/tuples.rs new file mode 100644 index 0000000..cedb959 --- /dev/null +++ b/surelock/src/lockable/tuples.rs @@ -0,0 +1,69 @@ +//! [`Lockable`] implementations for tuples of mutex references. +//! +//! Generated via macro for arities 2 through 12. All elements must +//! share the same lock [`Level`](crate::level::Level) and +//! [`RawMutex`](lock_api::RawMutex) backend. + +use alloc::vec::Vec; + +use lock_api::RawMutex; + +use crate::{ + id::LockId, + level::Level, + lockable::Lockable, + mutex::{Mutex, guard::MutexGuard}, +}; + +macro_rules! impl_lockable_tuple { + // Base case: 2-tuple + ($n:literal: $($idx:tt $T:ident),+) => { + unsafe impl Lockable + for ($(&Mutex,)+) + { + type Lvl = Lvl; + type Guards<'set> + = ($(MutexGuard<'set, R, $T>,)+) + where + Self: 'set; + + fn collect_ids(&self, out: &mut Vec) { + $(out.push(self.$idx.id());)+ + } + + unsafe fn lock_in_order(&self, sorted_indices: &[usize]) { + for &idx in sorted_indices { + match idx { + $($idx => self.$idx.raw.lock(),)+ + _ => unreachable!(), + } + } + } + + unsafe fn make_guards(&self) -> Self::Guards<'_> { + ($( + MutexGuard { + raw: &self.$idx.raw, + data: &self.$idx.data, + }, + )+) + } + + unsafe fn unlock_all(&self) { + $(unsafe { self.$idx.raw.unlock() };)+ + } + } + }; +} + +impl_lockable_tuple!( 2: 0 A, 1 B); +impl_lockable_tuple!( 3: 0 A, 1 B, 2 C); +impl_lockable_tuple!( 4: 0 A, 1 B, 2 C, 3 D); +impl_lockable_tuple!( 5: 0 A, 1 B, 2 C, 3 D, 4 E); +impl_lockable_tuple!( 6: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F); +impl_lockable_tuple!( 7: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G); +impl_lockable_tuple!( 8: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H); +impl_lockable_tuple!( 9: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I); +impl_lockable_tuple!(10: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J); +impl_lockable_tuple!(11: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K); +impl_lockable_tuple!(12: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H, 8 I, 9 J, 10 K, 11 L); diff --git a/surelock/src/mutex.rs b/surelock/src/mutex.rs new file mode 100644 index 0000000..b62563f --- /dev/null +++ b/surelock/src/mutex.rs @@ -0,0 +1,120 @@ +//! Deadlock-free mutex, generic over backend and lock level. +//! +//! [`Mutex`] wraps a [`lock_api::RawMutex`] implementation +//! and a `T`, tagging the pair with a lock [`Level`]. All ordered +//! locking goes through [`LockKey`](super::LockKey) + +//! [`LockSet`](super::LockSet) — there is no public `lock()` method +//! by default. + +pub mod guard; + +use core::{cell::UnsafeCell, fmt, marker::PhantomData}; + +use lock_api::RawMutex; + +use crate::{ + id::LockId, + level::{Base, Level}, +}; + +/// A deadlock-free mutex generic over backend and lock level. +/// +/// `R` is any [`lock_api::RawMutex`] implementation (`spin`, `parking_lot`, +/// etc.). `Lvl` defaults to [`Base`] — levels are opt-in for incremental +/// cross-level acquisition. +/// +/// All ordered locking goes through [`LockKey::lock`](super::LockKey::lock). +/// There is no public `lock()` method unless the `escape-hatch` feature +/// is enabled. +/// +/// # Examples +/// +/// ```rust,ignore +/// use surelock::{lock_scope, LockSet, Mutex}; +/// use spin::Mutex as RawSpin; +/// +/// let counter: Mutex, u32> = Mutex::new(0); +/// +/// lock_scope(|key| { +/// let set = LockSet::new(&counter); +/// let (mut guard, _key) = key.lock(&set); +/// *guard += 1; +/// }); +/// ``` +pub struct Mutex { + id: LockId, + pub(crate) raw: R, + pub(crate) data: UnsafeCell, + _level: PhantomData, +} + +impl Mutex { + /// Create a new mutex with the given data. + /// + /// A unique [`LockId`] is assigned from a global atomic counter. + pub fn new(data: T) -> Self { + Self { + id: LockId::next(), + raw: R::INIT, + data: UnsafeCell::new(data), + _level: PhantomData, + } + } + + /// Returns this mutex's unique [`LockId`]. + pub const fn id(&self) -> LockId { + self.id + } + + /// Exclusive access via `&mut` — no locking needed. + /// + /// Since the caller has exclusive ownership, no other thread can + /// be holding the lock. + pub const fn get_mut(&mut self) -> &mut T { + self.data.get_mut() + } + + /// Consume the mutex and return the inner data. + pub fn into_inner(self) -> T { + self.data.into_inner() + } +} + +// Escape hatch: direct lock bypassing the ordering system. +#[cfg(feature = "escape-hatch")] +impl Mutex { + /// Acquire this mutex directly, bypassing the ordering system. + /// + /// This has the same semantics as `std::sync::Mutex::lock()`: + /// no key is needed, no ordering is checked, and deadlock + /// prevention is the caller's responsibility. + /// + /// Only available with the `escape-hatch` feature enabled. + pub fn unchecked_lock(&self) -> MutexGuard<'_, R, T> { + self.raw.lock(); + // SAFETY: we just acquired the lock. + MutexGuard { + raw: &self.raw, + data: &self.data, + } + } +} + +impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Mutex") + .field("id", &self.id) + .finish_non_exhaustive() + } +} + +// SAFETY: UnsafeCell: Send when T: Send. Explicit for clarity since +// the Sync impl below is manual. +unsafe impl Send for Mutex {} + +// SAFETY: UnsafeCell is unconditionally !Sync, but the RawMutex lock +// guarantees exclusive access to the inner T. R: Sync because &Mutex +// shares &R across threads. T: Send (not Sync) because the mutex +// provides exclusive access — we're sending T between threads, not +// sharing it. +unsafe impl Sync for Mutex {} diff --git a/surelock/src/mutex/guard.rs b/surelock/src/mutex/guard.rs new file mode 100644 index 0000000..2835236 --- /dev/null +++ b/surelock/src/mutex/guard.rs @@ -0,0 +1,65 @@ +//! RAII guard for [`super::Mutex`]. +//! +//! A [`MutexGuard`] provides exclusive access to the data protected +//! by a mutex. Dropping the guard releases the lock. + +use core::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, +}; + +use lock_api::RawMutex; + +/// RAII guard for a [`super::Mutex`]. +/// +/// Provides `Deref` and `DerefMut` access to the protected data. +/// The lock is released when the guard is dropped. +/// +/// Guards are not constructible by users — they are returned by +/// [`LockKey::lock`](crate::LockKey::lock) (via [`LockSet`](crate::LockSet)) +/// or [`Mutex::unchecked_lock`](super::Mutex::unchecked_lock) (if the +/// `escape-hatch` feature is enabled). +#[must_use = "if unused, the Mutex will immediately unlock"] +pub struct MutexGuard<'a, R: RawMutex, T: ?Sized> { + pub(crate) raw: &'a R, + pub(crate) data: &'a UnsafeCell, +} + +impl Deref for MutexGuard<'_, R, T> { + type Target = T; + + fn deref(&self) -> &T { + // SAFETY: the guard guarantees exclusive access. + unsafe { &*self.data.get() } + } +} + +impl DerefMut for MutexGuard<'_, R, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: the guard guarantees exclusive access. + unsafe { &mut *self.data.get() } + } +} + +impl Drop for MutexGuard<'_, R, T> { + fn drop(&mut self) { + // SAFETY: the guard holds the lock. + unsafe { + self.raw.unlock(); + } + } +} + +impl core::fmt::Debug for MutexGuard<'_, R, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&**self, f) + } +} + +// MutexGuard is Send if T is Send and the raw mutex supports it — +// transferring a guard to another thread transfers exclusive access. +unsafe impl Send for MutexGuard<'_, R, T> {} + +// MutexGuard is Sync if T is Sync — a shared reference to the guard +// gives a shared reference to the data. +unsafe impl Sync for MutexGuard<'_, R, T> {} diff --git a/surelock/src/set.rs b/surelock/src/set.rs new file mode 100644 index 0000000..4bd9494 --- /dev/null +++ b/surelock/src/set.rs @@ -0,0 +1,86 @@ +//! Pre-sorted lock collection. +//! +//! A [`LockSet`] wraps a [`Lockable`] and pre-computes the acquisition +//! order (sorted by [`LockId`]). Construct once, lock many times via +//! [`LockKey::lock`](crate::LockKey::lock). + +use alloc::vec::Vec; + +use crate::lockable::Lockable; + +/// A prepared set of locks, pre-sorted by [`LockId`](crate::LockId). +/// +/// All locks in a `LockSet` must be at the same level. Construct once, +/// lock many times via [`LockKey::lock`](crate::LockKey::lock). +/// +/// # Examples +/// +/// ```rust,ignore +/// use surelock::{LockSet, Mutex}; +/// use spin::Mutex as RawSpin; +/// +/// let a: Mutex, u32> = Mutex::new(1); +/// let b: Mutex, u32> = Mutex::new(2); +/// +/// // Sort happens once at construction time. +/// let set = LockSet::new((&a, &b)); +/// ``` +pub struct LockSet { + pub(crate) lockable: L, + /// Indices into the lockable, sorted by `LockId`. + pub(crate) sorted_indices: Vec, +} + +impl core::fmt::Debug for LockSet { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LockSet") + .field("sorted_indices", &self.sorted_indices) + .finish_non_exhaustive() + } +} + +impl LockSet { + /// Create a new `LockSet`, pre-sorting by [`LockId`](crate::LockId). + /// + /// # Panics + /// + /// Debug-asserts that no duplicate locks are present. + #[allow(clippy::indexing_slicing)] // indices are 0..ids.len(), always in bounds + pub fn new(lockable: L) -> Self { + let mut ids = Vec::new(); + lockable.collect_ids(&mut ids); + + // Build index permutation sorted by LockId. + let mut indices: Vec = (0..ids.len()).collect(); + indices.sort_by_key(|&i| ids[i]); + + debug_assert!( + indices.windows(2).all(|w| ids[w[0]] != ids[w[1]]), + "LockSet contains duplicate locks" + ); + + Self { + lockable, + sorted_indices: indices, + } + } + + /// Acquire all locks in sorted order. + /// + /// # Safety + /// + /// Caller must ensure this is called at most once before a + /// corresponding `unlock_all` (i.e., no double-locking). + pub(crate) unsafe fn lock_in_order(&self) { + unsafe { self.lockable.lock_in_order(&self.sorted_indices) }; + } + + /// Construct guards for all held locks. + /// + /// # Safety + /// + /// All locks must be currently held. + pub(crate) unsafe fn make_guards(&self) -> L::Guards<'_> { + unsafe { self.lockable.make_guards() } + } +} diff --git a/surelock/tests/integration.rs b/surelock/tests/integration.rs new file mode 100644 index 0000000..8b7fd71 --- /dev/null +++ b/surelock/tests/integration.rs @@ -0,0 +1,114 @@ +//! Integration tests exercising the full lock acquisition flow. + +use surelock::{key::lock_scope, mutex::Mutex, set::LockSet}; +use surelock_macros::lock_order; + +/// Raw spin mutex that implements `lock_api::RawMutex`. +type RawSpin = spin::mutex::SpinMutex<()>; +type SpinMutex = Mutex; + +// -- Single LockSet (no levels needed) -- + +#[test] +fn single_lock() { + let mut counter: SpinMutex = Mutex::new(0); + + lock_scope(|key| { + let set = LockSet::new(&counter); + let (mut guard, _key) = key.lock(&set); + *guard += 1; + }); + + assert_eq!(*counter.get_mut(), 1); +} + +#[test] +fn single_lock_returned_value() { + let counter: SpinMutex = Mutex::new(42); + + let value = lock_scope(|key| { + let set = LockSet::new(&counter); + let (guard, _key) = key.lock(&set); + *guard + }); + + assert_eq!(value, 42); +} + +#[test] +fn two_locks_atomic() { + let mut a: SpinMutex = Mutex::new(10); + let mut b: SpinMutex = Mutex::new(20); + + lock_scope(|key| { + let set = LockSet::new((&a, &b)); + let ((mut ga, mut gb), _key) = key.lock(&set); + *ga += 1; + *gb += 1; + }); + + assert_eq!(*a.get_mut(), 11); + assert_eq!(*b.get_mut(), 21); +} + +// -- Cross-level incremental acquisition -- + +struct Low; +struct High; + +lock_order! { Low => High } + +type LowMutex = Mutex; +type HighMutex = Mutex; + +#[test] +fn cross_level_acquisition() { + let config: LowMutex = Mutex::new(5); + let mut account: HighMutex = Mutex::new(100); + + lock_scope(|key| { + let config_set = LockSet::new(&config); + let (cfg, key) = key.lock(&config_set); + + let acct_set = LockSet::new(&account); + let (mut acct, _key) = key.lock(&acct_set); + + *acct += *cfg; + }); + + assert_eq!(*account.get_mut(), 105); +} + +// -- Subscope -- + +#[test] +fn subscope_inherits_level() { + let config: LowMutex = Mutex::new(42); + let mut account: HighMutex = Mutex::new(0); + + lock_scope(|key| { + let config_set = LockSet::new(&config); + let (cfg, key) = key.lock(&config_set); + let val = *cfg; + + let (result, _key) = key.subscope(|inner_key| { + let acct_set = LockSet::new(&account); + let (mut acct, _inner_key) = inner_key.lock(&acct_set); + *acct = val; + *acct + }); + + assert_eq!(result, 42); + }); + + assert_eq!(*account.get_mut(), 42); +} + +// -- Mutable access without locking -- + +#[test] +fn get_mut_without_lock() { + let mut m: SpinMutex = Mutex::new("hello".into()); + m.get_mut().push_str(" world"); + assert_eq!(m.into_inner(), "hello world"); +} diff --git a/surelock/tests/lock_order.rs b/surelock/tests/lock_order.rs new file mode 100644 index 0000000..7df3768 --- /dev/null +++ b/surelock/tests/lock_order.rs @@ -0,0 +1,54 @@ +//! Integration tests for the `lock_order!` macro. +//! +//! If this file compiles, the macro generated all expected trait impls. + +#![allow(dead_code, clippy::missing_const_for_fn)] + +use surelock::level::{Base, Bottom, LockAfter}; +use surelock_macros::lock_order; + +struct Config; +struct Account; +struct Transaction; + +lock_order! { Config => Account => Transaction } + +fn _assert_level_impls() +where + Config: surelock::level::Level, + Account: surelock::level::Level, + Transaction: surelock::level::Level, +{ +} + +fn _assert_lock_after_bottom() +where + Config: LockAfter, + Account: LockAfter, + Transaction: LockAfter, +{ +} + +fn _assert_lock_after_base() +where + Config: LockAfter, + Account: LockAfter, + Transaction: LockAfter, +{ +} + +fn _assert_direct_edges() +where + Account: LockAfter, + Transaction: LockAfter, +{ +} + +fn _assert_transitive() +where + Transaction: LockAfter, +{ +} + +#[test] +fn lock_order_compiles() {} diff --git a/deathproof_macros/Cargo.toml b/surelock_macros/Cargo.toml similarity index 62% rename from deathproof_macros/Cargo.toml rename to surelock_macros/Cargo.toml index 2da3067..eb06102 100644 --- a/deathproof_macros/Cargo.toml +++ b/surelock_macros/Cargo.toml @@ -1,10 +1,13 @@ [package] -name = "deathproof_macros" +name = "surelock_macros" version = "0.1.0" -description = "Proc macros for the deathproof deadlock-free locking library" +description = "Proc macros for the surelock deadlock-free locking library" +categories.workspace = true edition.workspace = true +keywords.workspace = true license.workspace = true +readme = "../README.md" repository.workspace = true rust-version.workspace = true authors.workspace = true diff --git a/surelock_macros/src/lib.rs b/surelock_macros/src/lib.rs new file mode 100644 index 0000000..fb2775e --- /dev/null +++ b/surelock_macros/src/lib.rs @@ -0,0 +1,152 @@ +//! Proc macros for the [`surelock`] deadlock-free locking library. +//! +//! Provides the [`lock_order!`] macro for declaring compile-time lock +//! level orderings. The macro generates [`Level`] and [`LockAfter`] +//! trait impls (including transitive edges) from a declared chain, +//! making cyclic acquisition a type error. +//! +//! # Example +//! +//! ```rust,ignore +//! use surelock_macros::lock_order; +//! +//! struct Config; +//! struct Account; +//! struct Transaction; +//! +//! lock_order! { Config => Account => Transaction } +//! ``` + +#![forbid(unsafe_code)] + +use std::collections::HashSet; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{ + Ident, Token, + parse::{Parse, ParseStream}, + punctuated::Punctuated, +}; + +/// A chain of identifiers separated by `=>`. +struct LockOrderInput { + levels: Punctuated]>, +} + +impl Parse for LockOrderInput { + fn parse(input: ParseStream<'_>) -> syn::Result { + let levels = Punctuated::]>::parse_terminated(input)?; + Ok(Self { levels }) + } +} + +/// Declare a lock acquisition ordering for compile-time enforced locking. +/// +/// Generates [`Level`] and [`LockAfter`] trait implementations (including +/// transitive edges) from a chain of level types. The input must be a +/// non-empty, duplicate-free chain of identifiers separated by `=>`. +/// +/// For N levels, this produces: +/// - N `impl Level` impls +/// - N `impl LockAfter` impls +/// - N(N-1)/2 `impl LockAfter for Y` impls (all pairs where X precedes Y) +/// +/// # Syntax +/// +/// ```rust,ignore +/// lock_order! { Config => Account => Transaction } +/// ``` +/// +/// Expands to: +/// +/// ```rust,ignore +/// impl ::surelock::level::Level for Config {} +/// impl ::surelock::level::Level for Account {} +/// impl ::surelock::level::Level for Transaction {} +/// +/// impl ::surelock::level::LockAfter<::surelock::level::Base> for Config {} +/// impl ::surelock::level::LockAfter<::surelock::level::Base> for Account {} +/// impl ::surelock::level::LockAfter<::surelock::level::Base> for Transaction {} +/// +/// impl ::surelock::level::LockAfter for Account {} +/// impl ::surelock::level::LockAfter for Transaction {} +/// impl ::surelock::level::LockAfter for Transaction {} +/// ``` +/// +/// # Errors +/// +/// - Empty input: `lock_order! requires at least one level` +/// - Duplicate identifier: `duplicate level 'X' in lock ordering` +/// +/// # Notes +/// +/// Keep N small (5–10 levels). These are coarse-grained levels, not +/// per-lock-instance identifiers. The impl count grows quadratically. +#[proc_macro] +pub fn lock_order(input: TokenStream) -> TokenStream { + let parsed = syn::parse_macro_input!(input as LockOrderInput); + let levels: Vec<&Ident> = parsed.levels.iter().collect(); + + if levels.is_empty() { + return syn::Error::new(Span::call_site(), "lock_order! requires at least one level") + .to_compile_error() + .into(); + } + + // Detect duplicates. + let mut seen = HashSet::new(); + for ident in &levels { + if !seen.insert(ident.to_string()) { + return syn::Error::new( + ident.span(), + format!("duplicate level `{ident}` in lock ordering"), + ) + .to_compile_error() + .into(); + } + } + + // Generate impl Level for each type. + let level_impls = levels.iter().map(|ident| { + quote! { + impl ::surelock::level::Level for #ident {} + } + }); + + // Generate impl LockAfter and impl LockAfter for each type. + // Bottom is needed because the initial LockKey starts at Bottom. + // Base is needed because Base-level locks can precede user-declared levels. + let bottom_impls = levels.iter().map(|ident| { + quote! { + impl ::surelock::level::LockAfter<::surelock::level::Bottom> for #ident {} + } + }); + + let base_impls = levels.iter().map(|ident| { + quote! { + impl ::surelock::level::LockAfter<::surelock::level::Base> for #ident {} + } + }); + + // Generate impl LockAfter for levels[j] for all i < j. + let mut transitive_impls = Vec::new(); + for (i, before) in levels.iter().enumerate() { + #[allow(clippy::indexing_slicing)] // i < levels.len(), so i+1 is in bounds + for after in &levels[i + 1..] { + transitive_impls.push(quote! { + impl ::surelock::level::LockAfter<#before> for #after {} + }); + } + } + + let expanded = quote! { + #(#level_impls)* + #(#bottom_impls)* + #(#base_impls)* + #(#transitive_impls)* + }; + + expanded.into() +}