diff --git a/HACKING.md b/HACKING.md index 162d156..6373894 100644 --- a/HACKING.md +++ b/HACKING.md @@ -50,9 +50,14 @@ src/ ├── locksmith.rs Locksmith ├── mutex.rs Mutex ├── mutex/guard.rs MutexGuard -├── raw_mutex.rs RawMutex trait -├── raw_mutex/std_mutex.rs StdMutex backend -├── raw_mutex/lock_api_adapter.rs lock_api bridge +├── mutex/raw.rs RawMutex trait +├── mutex/raw/std_mutex.rs StdMutex backend +├── mutex/raw/lock_api_adapter.rs lock_api bridge for Mutex +├── rw_lock.rs RwLock, ReadLock, WriteLock +├── rw_lock/guard.rs RwLockReadGuard, RwLockWriteGuard +├── rw_lock/raw.rs RawRwLock trait +├── rw_lock/raw/std_rw_lock.rs StdRwLock backend +├── rw_lock/raw/lock_api_adapter.rs lock_api bridge for RwLock └── set.rs LockSet tests/ @@ -81,11 +86,16 @@ design/ Published design docs `#![deny(unsafe_code)]` at the crate level. Only modules that need `unsafe` opt in via `#![allow(unsafe_code)]`: -- `raw_mutex.rs` -- `unsafe trait RawMutex` -- `raw_mutex/std_mutex.rs` -- `unsafe impl` for StdMutex -- `raw_mutex/lock_api_adapter.rs` -- blanket `unsafe impl` - `mutex.rs` -- `unsafe impl Send/Sync` - `mutex/guard.rs` -- `Deref`/`DerefMut` on `UnsafeCell` +- `mutex/raw.rs` -- `unsafe trait RawMutex` +- `mutex/raw/std_mutex.rs` -- `unsafe impl` for StdMutex +- `mutex/raw/lock_api_adapter.rs` -- blanket `unsafe impl` +- `rw_lock.rs` -- `unsafe impl Send/Sync` for `RwLock` +- `rw_lock/guard.rs` -- `Deref`/`DerefMut` on `UnsafeCell` +- `rw_lock/raw.rs` -- `unsafe trait RawRwLock` +- `rw_lock/raw/std_rw_lock.rs` -- `unsafe impl` for StdRwLock +- `rw_lock/raw/lock_api_adapter.rs` -- blanket `unsafe impl` - `key_voucher.rs` -- `unsafe impl Send` All other modules are fully safe. Adding `unsafe` to a new module diff --git a/README.md b/README.md index cecd5fe..08efbc6 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,50 @@ let txn = account.new_higher(30u32); // Level<2> Specify just the level to use the default backend: `Mutex>`. +### Reader-Writer Locks + +`RwLock` participates in the same `LockId`-sorted +ordering as `Mutex` and the same compile-time `Level` system. +Mode is chosen at the call site: + +```rust +use surelock::{ + key_handle::KeyHandle, + rw_lock::{ReadLock, RwLock, WriteLock}, + set::LockSet, +}; + +let cache: RwLock = RwLock::new(0); + +let mut handle = KeyHandle::claim(); + +// Single-lock convenience methods. +handle.scope(|key| { + let (val, _key) = key.read(&cache); + assert_eq!(*val, 0); +}); +handle.scope(|key| { + let (mut val, _key) = key.write(&cache); + *val = 42; +}); + +// `ReadLock` / `WriteLock` wrappers let one `LockSet` mix modes. +let a: RwLock = RwLock::new(1); +let b: RwLock = RwLock::new(0); +let set = LockSet::new((ReadLock(&a), WriteLock(&b))); +handle.scope(|key| { + let ((ra, mut wb), _key) = key.lock(&set); + *wb = *ra + 10; +}); +``` + +`RwLock`s and `Mutex`es share the global `LockId` counter, so a +single `LockSet` can hold both: + +```rust,ignore +let set = LockSet::new((&mtx, ReadLock(&rwlock))); +``` + ### Key Patterns ```rust @@ -159,6 +203,10 @@ handle.scope(|key| { /* ... */ }); let (guard, key) = key.lock(&mutex); let ((ga, gb), key) = key.lock(&set); +// Reader-writer lock convenience methods +let (read_guard, key) = key.read(&rwlock); +let (write_guard, key) = key.write(&rwlock); + // Lock with closure (one-shot, sorts inline) let (val, key) = key.lock_with(&mutex, |guard| *guard); @@ -243,18 +291,21 @@ spin = { version = "0.9", features = ["lock_api", "spin_mutex"] } ## Modules -| Module | Key Types | Description | -|---------------|--------------------------------------------|-----------------------------------------| -| `acquirable` | `Acquirable` | Internal trait + impls (single, tuples) | -| `id` | `LockId` | Monotonic global counter | -| `key` | `MutexKey`, `lock_scope`, `try_lock_scope` | Scope token, ambient entry points | -| `key_handle` | `KeyHandle` | Per-thread scope capability | -| `key_voucher` | `KeyVoucher` | Transferable token (`Send`) | -| `level` | `Level`, `IsLevel`, `LockAfter`, `Base` | Numbered levels, ordering traits | -| `locksmith` | `Locksmith` | Factory for `KeyVoucher`s | -| `mutex` | `Mutex`, `MutexGuard` | Deadlock-free mutex, RAII guard | -| `raw_mutex` | `RawMutex`, `StdMutex` | Backend trait, `lock_api` adapter | -| `set` | `LockSet` | Pre-sorted lock collection | +| Module | Key Types | Description | +|-----------------|------------------------------------------------------------|---------------------------------------------------| +| `acquirable` | `Acquirable` | Internal trait + impls (single, tuples) | +| `id` | `LockId` | Monotonic global counter | +| `key` | `MutexKey`, `lock_scope`, `try_lock_scope` | Scope token, ambient entry points | +| `key_handle` | `KeyHandle` | Per-thread scope capability | +| `key_voucher` | `KeyVoucher` | Transferable token (`Send`) | +| `level` | `Level`, `IsLevel`, `LockAfter`, `Base` | Numbered levels, ordering traits | +| `locksmith` | `Locksmith` | Factory for `KeyVoucher`s | +| `mutex` | `Mutex`, `MutexGuard` | Deadlock-free mutex, RAII guard | +| `mutex::raw` | `RawMutex`, `StdMutex` | Mutex backend trait, `lock_api` adapter | +| `rw_lock` | `RwLock`, `ReadLock`, `WriteLock` | Deadlock-free rwlock + mode wrappers | +| `rw_lock::guard`| `RwLockReadGuard`, `RwLockWriteGuard` | RAII guards (shared / exclusive) | +| `rw_lock::raw` | `RawRwLock`, `StdRwLock` | RwLock backend trait, `lock_api` adapter | +| `set` | `LockSet` | Pre-sorted lock collection | ## Prior Art @@ -275,6 +326,23 @@ Grounded in Coffman, Elphick, and Shoshani's classic [System Deadlocks][coffman_ For detailed comparisons, see [design/comparison/happylock.md](design/comparison/happylock.md) and [design/comparison/lock_tree.md](design/comparison/lock_tree.md). +## Examples + +Runnable examples live in [`examples/`](examples/): + +| Example | What it shows | +|------------------|------------------------------------------------------------------------------| +| `quickstart` | Smallest possible surelock program — single mutex, single counter. | +| `bank_transfers` | Classic deadlock case made deadlock-free via `LockSet`. | +| `rwlock_cache` | Read-heavy cache: many readers, one writer. | +| `tiered_levels` | Compile-time level ordering with semantic `type` aliases. | +| `mixed_lockset` | `Mutex` + `RwLock` reader + `RwLock` writer in one `LockSet`. | +| `error_handling` | Walks every typed-error path and recovers from each. | +| `multi_core` | `Locksmith` + `KeyVoucher` to distribute lock scope capability to N workers. | + +Run any of them with `cargo run --example ` (add `--release` +for the contention demos). + ## Design Documents The [design/](design/) directory contains architecture documentation, design rationale, and detailed comparisons with prior art. diff --git a/design/ROADMAP.md b/design/ROADMAP.md index 3871c5d..a2ef62d 100644 --- a/design/ROADMAP.md +++ b/design/ROADMAP.md @@ -2,16 +2,6 @@ Planned features and design sketches for future surelock versions. These are ideas that have been explored in design discussions but are not yet implemented. -## RwLock Support - -A `RwLock` that participates in the same level ordering as `Mutex`. The main design challenge is that `Acquirable` and `LockSet` would need to express read vs write mode: - -- A `LockSet` containing both a `Mutex` and an `RwLock` (read mode) at the same level -- The guard type changes depending on mode (`ReadGuard` vs `WriteGuard`) -- The `RawMutex` trait would need a `RawRwLock` counterpart (or the trait is extended) - -One approach: `RwLock` implements `Acquirable` twice -- once for read mode, once for write. The user specifies mode at `LockSet` construction or via a wrapper type (`ReadLock(&rwlock)`, `WriteLock(&rwlock)`). - ## Poisoning A `Poisonable` wrapper type that detects panics during critical sections and flags the mutex as poisoned for subsequent acquirers. @@ -42,7 +32,7 @@ On single-core targets, surelock's ordering invariant guarantees that a lock is // surelock-cs crate (~30 lines) pub struct CriticalSectionRawMutex; -unsafe impl surelock::raw_mutex::RawMutex for CriticalSectionRawMutex { +unsafe impl surelock::mutex::raw::RawMutex for CriticalSectionRawMutex { type Guard<'a> = CsGuard; fn new() -> Self { Self } diff --git a/examples/bank_transfers.rs b/examples/bank_transfers.rs new file mode 100644 index 0000000..9a70f67 --- /dev/null +++ b/examples/bank_transfers.rs @@ -0,0 +1,85 @@ +#![allow(clippy::unwrap_used)] +//! Classic deadlock case made deadlock-free: cross-account transfers. +//! +//! Run with: `cargo run --release --example bank_transfers` +//! +//! Two worker threads send money in opposite directions +//! (A -> B and B -> A) for two seconds. Without surelock, the naive +//! "lock from, then lock to" pattern can deadlock because the two +//! workers pick opposing acquisition orders. +//! +//! With [`MutexKey::lock_with`] over a 2-tuple, both threads get the +//! locks sorted by [`LockId`](surelock::id::LockId) -- always the +//! same order -- so no cycle can form. The conservation invariant +//! (total balance unchanged) is checked at the end. + +use std::{ + sync::Arc, + thread, + time::{Duration, Instant}, +}; + +use surelock::{key_handle::KeyHandle, mutex::Mutex}; + +type Account = Mutex; + +fn transfer(handle: &mut KeyHandle, from: &Account, to: &Account, amount: i64) -> bool { + handle.scope(|key| { + // The 2-tuple is sorted by LockId, regardless of the source + // order. Both threads observe the same acquisition order. + let (success, _key) = key.lock_with(&(from, to), |(mut fg, mut tg)| { + if *fg >= amount { + *fg -= amount; + *tg += amount; + true + } else { + false + } + }); + success + }) +} + +fn main() { + let a: Arc = Arc::new(Mutex::new(1_000)); + let b: Arc = Arc::new(Mutex::new(1_000)); + + let deadline = Instant::now() + Duration::from_secs(2); + + let workers: Vec<_> = [ + (Arc::clone(&a), Arc::clone(&b)), + (Arc::clone(&b), Arc::clone(&a)), + ] + .into_iter() + .map(|(from, to)| { + thread::spawn(move || { + let mut handle = KeyHandle::claim(); + let mut transfers = 0u64; + while Instant::now() < deadline { + if transfer(&mut handle, &from, &to, 1) { + transfers += 1; + } + } + transfers + }) + }) + .collect(); + + let total_transfers: u64 = workers.into_iter().map(|w| w.join().unwrap()).sum(); + + // Conservation: total balance unchanged. + let mut h = KeyHandle::claim(); + h.scope(|key| { + let ((bal_a, bal_b), _key) = key.lock_with(&(&*a, &*b), |(ga, gb)| (*ga, *gb)); + println!("transfers completed: {total_transfers}"); + println!( + "balance a = {bal_a}, b = {bal_b}, total = {}", + bal_a + bal_b + ); + assert_eq!( + bal_a + bal_b, + 2_000, + "conservation violated -- transfer was not atomic" + ); + }); +} diff --git a/examples/error_handling.rs b/examples/error_handling.rs new file mode 100644 index 0000000..1c3eec7 --- /dev/null +++ b/examples/error_handling.rs @@ -0,0 +1,82 @@ +#![allow(clippy::unwrap_used)] +//! Demonstrate every fallible API and recover from each error. +//! +//! Run with: `cargo run --example error_handling` +//! +//! Each panic-form (`new`, `lock_scope`, `claim`, `create`) has a +//! paired `try_*` form returning a typed `Result<_, FooError>`. +//! This example exercises every typed-error path so you can see +//! the messages. + +use surelock::{ + key::{lock_scope, try_lock_scope}, + key_handle::{ClaimError, KeyHandle}, + locksmith::{IssueError, Locksmith, LocksmithExistsError}, + mutex::Mutex, + set::{LockSet, LockSetError}, +}; + +fn main() { + // 1) LockSet::try_new on duplicate locks -> LockSetError::DuplicateLocks + { + let m: Mutex = Mutex::new(0); + match LockSet::try_new((&m, &m)) { + Ok(_) => unreachable!(), + Err(e) => { + assert_eq!(e, LockSetError::DuplicateLocks); + println!("LockSet::try_new error: {e}"); + } + } + } + + // 2) try_lock_scope while nested -> ClaimError::AlreadyClaimed + lock_scope(|_outer| { + let result = try_lock_scope(|_inner| 42); + match result { + Ok(_) => unreachable!("nested try_lock_scope must fail"), + Err(e) => { + assert_eq!(e, ClaimError::AlreadyClaimed); + println!("try_lock_scope error: {e}"); + } + } + }); + + // 3) KeyHandle::try_claim while claimed -> ClaimError::AlreadyClaimed + { + let _h = KeyHandle::claim(); + match KeyHandle::try_claim() { + Ok(_) => unreachable!(), + Err(e) => { + assert_eq!(e, ClaimError::AlreadyClaimed); + println!("KeyHandle::try_claim error: {e}"); + } + } + } + + // 4) Locksmith::new while one exists -> LocksmithExistsError::AlreadyExists + { + let _s = Locksmith::new(2).unwrap(); + match Locksmith::new(2) { + Ok(_) => unreachable!(), + Err(e) => { + assert_eq!(e, LocksmithExistsError::AlreadyExists); + println!("Locksmith::new error: {e}"); + } + } + } + + // 5) Locksmith::issue past limit -> IssueError::LimitReached + { + let s = Locksmith::new(1).unwrap(); + let _v1 = s.issue().unwrap(); + match s.issue() { + Ok(_) => unreachable!(), + Err(e) => { + assert_eq!(e, IssueError::LimitReached); + println!("Locksmith::issue error: {e}"); + } + } + } + + println!("OK -- every error path is typed, matchable, and `Display`-friendly."); +} diff --git a/examples/mixed_lockset.rs b/examples/mixed_lockset.rs new file mode 100644 index 0000000..2b23234 --- /dev/null +++ b/examples/mixed_lockset.rs @@ -0,0 +1,43 @@ +//! Mix a [`Mutex`], a reader, and a writer in a single [`LockSet`]. +//! +//! Run with: `cargo run --example mixed_lockset` +//! +//! All three locks are acquired atomically, sorted by +//! [`LockId`](surelock::id::LockId). The order on the wire is +//! whatever the global counter handed out; the user doesn't have to +//! think about it. + +use surelock::{ + key::lock_scope, + mutex::Mutex, + rw_lock::{ReadLock, RwLock, WriteLock}, + set::LockSet, +}; + +fn main() { + let m: Mutex = Mutex::new(1); + let source: RwLock = RwLock::new(10); + let sink: RwLock = RwLock::new(0); + + let set = LockSet::new((&m, ReadLock(&source), WriteLock(&sink))); + + lock_scope(|key| { + let ((gm, gsrc, mut gsink), _key) = key.lock(&set); + *gsink = *gm + *gsrc; + println!("m={}, source={}, sink={}", *gm, *gsrc, *gsink); + }); + + // Single-lock convenience methods work without wrappers. + lock_scope(|key| { + let (g, _key) = key.read(&sink); + assert_eq!(*g, 11); + }); + + // Inline wrapper form via lock_owned: no let-binding needed. + lock_scope(|key| { + let (mut g, _key) = key.lock_owned(WriteLock(&sink)); + *g = 0; + }); + + println!("OK"); +} diff --git a/examples/multi_core.rs b/examples/multi_core.rs new file mode 100644 index 0000000..f11eebb --- /dev/null +++ b/examples/multi_core.rs @@ -0,0 +1,76 @@ +//! Distribute lock-scope capability to multiple workers via +//! [`Locksmith`] + [`KeyVoucher`]. +//! +//! Run with: `cargo run --example multi_core` +//! +//! On `std` this is roughly equivalent to "each worker calls +//! `KeyHandle::claim`", but the [`Locksmith`] pattern is necessary on +//! `no_std` where there's no `thread_local!` to enforce per-thread +//! uniqueness. It also generalises beyond OS threads to bare-metal +//! cores. +//! +//! Pattern: +//! +//! 1. Create a [`Locksmith`] with `N` (number of workers). +//! 2. Issue `N` [`KeyVoucher`]s. +//! 3. Send each voucher to its worker. +//! 4. Each worker `redeem`s the voucher for a [`KeyHandle`]. + +#![allow( + clippy::expect_used, + clippy::needless_pass_by_value, + clippy::unwrap_used +)] + +use std::{sync::Arc, thread}; + +use surelock::{key_voucher::KeyVoucher, locksmith::Locksmith, mutex::Mutex}; + +fn worker(id: usize, voucher: KeyVoucher, shared: Arc>>) { + let mut handle = voucher.redeem().expect("first-claim on this thread"); + handle.scope(|key| { + let ((), _key) = key.lock_with(&*shared, |mut g| g.push(id)); + }); +} + +fn main() { + const N: usize = 4; + let shared: Arc>> = Arc::new(Mutex::new(Vec::new())); + + let smith = Locksmith::new(N).expect("locksmith creation"); + + // Issue all vouchers before spawning, so the test of "limit + // reached" is straightforward. + let vouchers: Vec = (0..N) + .map(|_| smith.issue().expect("voucher under limit")) + .collect(); + assert!(smith.issue().is_err(), "fifth voucher should fail"); + + // Distribute. + let handles: Vec<_> = vouchers + .into_iter() + .enumerate() + .map(|(id, v)| { + let shared = Arc::clone(&shared); + thread::spawn(move || worker(id, v, shared)) + }) + .collect(); + + for h in handles { + h.join().unwrap(); + } + + // Once the workers finish, the main thread can use the same + // locksmith pattern for itself. + drop(smith); + let smith = Locksmith::new(1).expect("re-create after drop"); + let voucher = smith.issue().expect("single voucher"); + let mut handle = voucher.redeem().expect("main-thread redeem"); + handle.scope(|key| { + let (g, _key) = key.lock(&*shared); + let mut sorted = g.clone(); + sorted.sort_unstable(); + println!("worker IDs that committed: {sorted:?}"); + assert_eq!(sorted, vec![0, 1, 2, 3]); + }); +} diff --git a/examples/quickstart.rs b/examples/quickstart.rs new file mode 100644 index 0000000..fbc6783 --- /dev/null +++ b/examples/quickstart.rs @@ -0,0 +1,34 @@ +//! The simplest possible surelock program. +//! +//! Run with: `cargo run --example quickstart` +//! +//! Demonstrates the bread-and-butter pattern: +//! +//! 1. Claim a [`KeyHandle`] on the current thread. +//! 2. Enter a scope and receive a [`MutexKey`]. +//! 3. Lock a [`Mutex`] via the key; receive a guard + new key. +//! 4. Drop the guard (and key) at end of scope; the lock releases. + +use surelock::{key_handle::KeyHandle, mutex::Mutex}; + +fn main() { + let counter: Mutex = Mutex::new(0); + + let mut handle = KeyHandle::claim(); + + // Three increments. Each scope is independent; the key starts + // fresh at `Bottom` and the lock is acquired and released within + // the scope. + for _ in 0..3 { + handle.scope(|key| { + let (mut g, _key) = key.lock(&counter); + *g += 1; + }); + } + + handle.scope(|key| { + let (g, _key) = key.lock(&counter); + println!("counter = {}", *g); + assert_eq!(*g, 3); + }); +} diff --git a/examples/rwlock_cache.rs b/examples/rwlock_cache.rs new file mode 100644 index 0000000..78da7c1 --- /dev/null +++ b/examples/rwlock_cache.rs @@ -0,0 +1,83 @@ +#![allow(clippy::indexing_slicing, clippy::unwrap_used)] +//! A read-heavy cache: many readers, occasional writer. +//! +//! Run with: `cargo run --release --example rwlock_cache` +//! +//! Demonstrates the value of [`RwLock`] over [`Mutex`]: readers run +//! concurrently with each other; writers exclude everyone. +//! +//! Eight reader threads and one writer thread hammer a `HashMap` +//! cache for 300 ms. Surelock's `key.read(&rw)` / `key.write(&rw)` +//! convenience methods are the single-lock entry points. + +use std::{ + collections::HashMap, + sync::Arc, + thread, + time::{Duration, Instant}, +}; + +use surelock::{key_handle::KeyHandle, rw_lock::RwLock}; + +fn main() { + let cache: Arc>> = Arc::new(RwLock::new(HashMap::new())); + + // Pre-populate. + { + let mut h = KeyHandle::claim(); + h.scope(|key| { + let (mut g, _key) = key.write(&*cache); + for i in 0..16u32 { + g.insert(format!("k{i}"), u64::from(i)); + } + }); + } + + let deadline = Instant::now() + Duration::from_millis(300); + let mut workers = vec![]; + + // Eight readers. + for r in 0..8 { + let cache = Arc::clone(&cache); + workers.push(thread::spawn(move || -> u64 { + let mut h = KeyHandle::claim(); + let mut reads = 0u64; + while Instant::now() < deadline { + h.scope(|key| { + let (g, _key) = key.read(&*cache); + if g.contains_key(&format!("k{}", r % 16)) { + reads += 1; + } + }); + } + reads + })); + } + + // One writer. + { + let cache = Arc::clone(&cache); + workers.push(thread::spawn(move || -> u64 { + let mut h = KeyHandle::claim(); + let mut writes = 0u64; + while Instant::now() < deadline { + h.scope(|key| { + let (mut g, _key) = key.write(&*cache); + let k = format!("k{}", writes % 16); + *g.entry(k).or_insert(0) = g + .get(&format!("k{}", writes % 16)) + .copied() + .unwrap_or(0) + .wrapping_add(1); + }); + writes += 1; + } + writes + })); + } + + let totals: Vec = workers.into_iter().map(|w| w.join().unwrap()).collect(); + let reads: u64 = totals[..8].iter().sum(); + let writes = totals[8]; + println!("reads completed: {reads}, writes completed: {writes}"); +} diff --git a/examples/tiered_levels.rs b/examples/tiered_levels.rs new file mode 100644 index 0000000..90ed44e --- /dev/null +++ b/examples/tiered_levels.rs @@ -0,0 +1,46 @@ +//! Hierarchical locking with compile-time level ordering. +//! +//! Run with: `cargo run --example tiered_levels` +//! +//! Three semantic levels (Config / Account / Transaction) ordered +//! by [`Level`](surelock::level::Level). The compiler will reject +//! any program that tries to acquire a lower-tier lock after a +//! higher-tier one. +//! +//! Uncomment the line marked `// COMPILE ERROR` to see the +//! diagnostic. + +use surelock::{key::lock_scope, level::Level, mutex::Mutex}; + +type Config = Level<0>; +type Account = Level<1>; +type Txn = Level<2>; + +fn main() { + let cfg: Mutex = Mutex::new(1); + let acct: Mutex = Mutex::new(1_000); + let txn: Mutex, Txn> = Mutex::new(Vec::new()); + + lock_scope(|key| { + // 1) Read config (Level<0>); key advances to Config. + let (cfg_val, key) = key.lock_with(&cfg, |g| *g); + + // 2) Read account balance (Level<1>); key advances to Account. + let (balance, key) = key.lock_with(&acct, |g| *g); + + // 3) Append to transaction log (Level<2>); key advances to Txn. + let ((), _key) = key.lock_with(&txn, |mut g| { + g.push(i64::try_from(balance).unwrap_or(i64::MAX) * i64::from(cfg_val)); + }); + + // Uncomment to see a compile error -- can't go back to Config: + // let (_, _key) = _key.lock(&cfg); // COMPILE ERROR + }); + + // Inspect the final log. + lock_scope(|key| { + let (log, _key) = key.lock_with(&txn, |g| g.clone()); + println!("transaction log: {log:?}"); + assert_eq!(log, vec![1_000]); + }); +} diff --git a/flake.lock b/flake.lock index 2f29ed5..48b3a3c 100644 --- a/flake.lock +++ b/flake.lock @@ -71,11 +71,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1774388614, - "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "lastModified": 1778737229, + "narHash": "sha256-6xWoytx8jFW4PF1GjRm/i/53trbpKGfz6zjzQGBr4cI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "rev": "d7a713c0b7e47c908258e71cba7a2d77cc8d71d5", "type": "github" }, "original": { @@ -99,11 +99,11 @@ ] }, "locked": { - "lastModified": 1774753967, - "narHash": "sha256-HpT5fE8JQSbAxolUnw3VgGAo3urVjcrgtB2rtoxURVw=", + "lastModified": 1778901358, + "narHash": "sha256-n35a8GOPs8zi35GXPe4uBz0Y8xseTkQpNgcrq81gPg0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "405b9b4c2c6c5a2b1d390524ce8a240729f34a96", + "rev": "61ec6a4fc56fe0c2b863f7b3eaba07b6664697d9", "type": "github" }, "original": { diff --git a/src/acquirable.rs b/src/acquirable.rs index 052ee37..98a0296 100644 --- a/src/acquirable.rs +++ b/src/acquirable.rs @@ -9,40 +9,48 @@ use alloc::vec::Vec; use crate::{ id::LockId, level::IsLevel, - mutex::{Mutex, guard::MutexGuard}, - raw_mutex::RawMutex, + mutex::{Mutex, guard::MutexGuard, raw::RawMutex}, }; -/// Uniform access to a [`Mutex`] through any wrapper type. +/// Uniform access to a lock through any reference / wrapper type. /// -/// Implemented for bare references, `Arc`, `Rc`, and `Box`. Used by -/// tuple [`Acquirable`] impls so that `LockSet::new((&arc, &bare))` -/// and `key.lock_with(&(&arc, &rc), ...)` work transparently. +/// Implemented for bare references and smart-pointer wrappers +/// (`Arc`, `Rc`, `Box`) around [`Mutex`], and for the +/// [`ReadLock`](crate::rw_lock::ReadLock) / +/// [`WriteLock`](crate::rw_lock::WriteLock) mode wrappers around +/// [`RwLock`](crate::rw_lock::RwLock) references. Used by tuple +/// [`Acquirable`] impls so that +/// `LockSet::new((&arc_mtx, ReadLock(&rw)))` works transparently. /// -/// Associated types (`Data`, `Lvl`, `RawMtx`) are determined by +/// Associated types (`Data`, `Lvl`, `Guard`) are determined by /// `Self`, avoiding unconstrained type parameter issues in generic -/// tuple impls. +/// tuple impls. The `Guard` GAT lets each impl pick its own guard +/// type -- exclusive [`MutexGuard`] for mutexes, shared +/// [`RwLockReadGuard`](crate::rw_lock::guard::RwLockReadGuard) for +/// readers, exclusive +/// [`RwLockWriteGuard`](crate::rw_lock::guard::RwLockWriteGuard) for +/// writers. pub trait MutexRef<'a> { - /// The data type guarded by the mutex. + /// The data type guarded by the lock. type Data: 'a; - /// The level of the mutex. + /// The level of the lock. type Lvl: IsLevel; - /// The raw mutex backend. - type RawMtx: RawMutex + 'a; + /// The guard type returned by [`lock_ref`](Self::lock_ref). + type Guard; - /// Return this mutex's [`LockId`]. + /// Return this lock's [`LockId`]. fn id(&self) -> LockId; - /// Lock the mutex and return the guard. - fn lock_ref(&'a self) -> MutexGuard<'a, Self::RawMtx, Self::Data>; + /// Lock and return the guard. + fn lock_ref(&'a self) -> Self::Guard; } impl<'a, T: 'a, Lvl: IsLevel, R: RawMutex + 'a> MutexRef<'a> for &'a Mutex { type Data = T; type Lvl = Lvl; - type RawMtx = R; + type Guard = MutexGuard<'a, R, T>; fn id(&self) -> LockId { Mutex::id(self) @@ -62,7 +70,7 @@ impl<'a, T: 'a, Lvl: IsLevel, R: RawMutex + 'a> MutexRef<'a> { type Data = T; type Lvl = Lvl; - type RawMtx = R; + type Guard = MutexGuard<'a, R, T>; fn id(&self) -> LockId { Mutex::id(self) @@ -81,7 +89,7 @@ impl<'a, T: 'a, Lvl: IsLevel, R: RawMutex + 'a> MutexRef<'a> { type Data = T; type Lvl = Lvl; - type RawMtx = R; + type Guard = MutexGuard<'a, R, T>; fn id(&self) -> LockId { Mutex::id(self) @@ -100,7 +108,7 @@ impl<'a, T: 'a, Lvl: IsLevel, R: RawMutex + 'a> MutexRef<'a> { type Data = T; type Lvl = Lvl; - type RawMtx = R; + type Guard = MutexGuard<'a, R, T>; fn id(&self) -> LockId { Mutex::id(self) diff --git a/src/acquirable/tuples.rs b/src/acquirable/tuples.rs index a694c9f..bda691e 100644 --- a/src/acquirable/tuples.rs +++ b/src/acquirable/tuples.rs @@ -1,33 +1,35 @@ -//! [`Acquirable`] implementations for tuples of mutex references. +//! [`Acquirable`] implementations for tuples of mutex / rwlock references. //! //! Generated via macro for arities 2 through 12. //! //! All tuple elements implement [`MutexRef`], so any combination of -//! bare references, `Arc`, `Rc`, and `Box` is accepted. +//! bare references, `Arc`, `Rc`, `Box`, and rwlock mode wrappers +//! ([`ReadLock`](crate::rw_lock::ReadLock) / +//! [`WriteLock`](crate::rw_lock::WriteLock)) is accepted. //! //! The 2-tuple supports different levels per element (multi-level). //! Arities 3--12 require the same level. +//! +//! Tuple elements no longer need to share a backend type -- each +//! element brings its own `Guard` type via the [`MutexRef::Guard`] +//! GAT, and the tuple's overall guard is a tuple of those. use crate::{ acquirable::{Acquirable, MutexRef}, level::{MaxLevel, MinLevel}, - mutex::guard::MutexGuard, }; -// -- 2-tuple: multi-level, different T per element -- +// -- 2-tuple: multi-level, different T / backend per element -- impl<'a, A, B> Acquirable<'a> for (A, B) where A: MutexRef<'a>, - B: MutexRef<'a, RawMtx = A::RawMtx>, + B: MutexRef<'a>, A::Lvl: MinLevel + MaxLevel, { type MinLvl = >::Min; type MaxLvl = >::Max; - type Guard = ( - MutexGuard<'a, A::RawMtx, A::Data>, - MutexGuard<'a, B::RawMtx, B::Data>, - ); + type Guard = (A::Guard, B::Guard); fn collect_ids(&self, out: &mut alloc::vec::Vec) { out.push(self.0.id()); @@ -36,8 +38,8 @@ where #[allow(clippy::expect_used, non_snake_case)] fn lock_sorted(&'a self, sorted_indices: &[usize]) -> Self::Guard { - let mut GA: Option> = None; - let mut GB: Option> = None; + let mut GA: Option = None; + let mut GB: Option = None; for &target in sorted_indices { match target { @@ -54,10 +56,11 @@ where } } -// -- 3-12 tuples: same level, same backend, different T per element -- +// -- 3-12 tuples: same level, different T / backend per element -- // // Each element implements MutexRef with associated types. The macro -// constrains all elements to share the same Lvl and RawMtx. +// constrains all elements to share Lvl only; backends and guard types +// may differ. macro_rules! impl_lockable_tuple_same_level { ($n:literal: $first_idx:tt $first_T:ident $first_G:ident $(, $idx:tt $T:ident $G:ident)+) => { @@ -65,13 +68,13 @@ macro_rules! impl_lockable_tuple_same_level { for ($first_G, $($G,)+) where $first_G: MutexRef<'a>, - $($G: MutexRef<'a, Lvl = $first_G::Lvl, RawMtx = $first_G::RawMtx>,)+ + $($G: MutexRef<'a, Lvl = $first_G::Lvl>,)+ { type MinLvl = $first_G::Lvl; type MaxLvl = $first_G::Lvl; type Guard = ( - MutexGuard<'a, $first_G::RawMtx, $first_G::Data>, - $(MutexGuard<'a, $G::RawMtx, $G::Data>,)+ + $first_G::Guard, + $($G::Guard,)+ ); fn collect_ids(&self, out: &mut alloc::vec::Vec) { @@ -81,8 +84,8 @@ macro_rules! impl_lockable_tuple_same_level { #[allow(clippy::expect_used, non_snake_case)] fn lock_sorted(&'a self, sorted_indices: &[usize]) -> Self::Guard { - let mut $first_T: Option> = None; - $(let mut $T: Option> = None;)+ + let mut $first_T: Option<$first_G::Guard> = None; + $(let mut $T: Option<$G::Guard> = None;)+ for &target in sorted_indices { match target { diff --git a/src/key.rs b/src/key.rs index a0e9249..42369c8 100644 --- a/src/key.rs +++ b/src/key.rs @@ -10,8 +10,7 @@ use core::marker::PhantomData; use crate::{ acquirable::Acquirable, level::{IsLevel, LockAfter}, - mutex::{Mutex, guard::MutexGuard}, - raw_mutex::RawMutex, + mutex::{Mutex, guard::MutexGuard, raw::RawMutex}, set::{LockSet, build_sorted}, }; @@ -98,6 +97,36 @@ impl<'a, L: Acquirable<'a>> Lockable<'a> for LockSet { } } +/// Variant of [`Lockable`] that consumes the lockable by value. +/// +/// Implemented for [`ReadLock`](crate::rw_lock::ReadLock) and +/// [`WriteLock`](crate::rw_lock::WriteLock), so that +/// [`MutexKey::lock_owned`] can accept them inline without the +/// temporary-lifetime trap that bites +/// `key.lock(&WriteLock(&rw))`. The wrappers are zero-cost +/// (`#[repr(transparent)]` newtypes around a reference), so passing +/// them by value is the same as passing the inner reference. +/// +/// Users do not implement this trait directly. +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot be locked by value", + note = "use a `ReadLock(&rw)` / `WriteLock(&rw)` wrapper, or build a `LockSet` for multi-lock acquisition" +)] +pub trait LockableByValue<'a> { + /// The guard type returned when the lock is held. + type Guard; + + /// The minimum level in this target. + type MinLvl: IsLevel; + + /// The maximum level in this target. The key advances to this + /// level after acquisition. + type MaxLvl: IsLevel; + + /// Acquire the lock and return the guard. Consumes `self`. + fn lock_by_value(self) -> Self::Guard; +} + // ── MutexKey ──────────────────────────────────────────────────── /// Scope token for ordered lock acquisition. @@ -171,6 +200,49 @@ impl<'scope, Lvl: IsLevel> MutexKey<'scope, Lvl> { (guards, MutexKey::new_internal()) } + /// Like [`lock`](Self::lock) but takes the lockable by value. + /// + /// Solves the temporary-lifetime trap with + /// [`ReadLock`](crate::rw_lock::ReadLock) / + /// [`WriteLock`](crate::rw_lock::WriteLock): you can write + /// `key.lock_owned(WriteLock(&rw))` inline without a + /// `let writer = WriteLock(&rw)` binding. The wrappers are + /// `Copy` and zero-cost, so this has no performance impact. + /// + /// For the bare `Mutex`/`RwLock` case the convenience methods + /// [`MutexKey::read`] / [`MutexKey::write`] are still more + /// concise; reach for `lock_owned` when you need a wrapper + /// inline that you don't want to name. + /// + /// # Examples + /// + /// ```rust + /// use surelock::{ + /// key::lock_scope, + /// rw_lock::{ReadLock, RwLock, WriteLock}, + /// }; + /// + /// let rw: RwLock = RwLock::new(0); + /// + /// lock_scope(|key| { + /// let (mut g, _key) = key.lock_owned(WriteLock(&rw)); + /// *g = 7; + /// }); + /// + /// lock_scope(|key| { + /// let (g, _key) = key.lock_owned(ReadLock(&rw)); + /// assert_eq!(*g, 7); + /// }); + /// ``` + pub fn lock_owned<'a, L>(self, target: L) -> (L::Guard, MutexKey<'scope, L::MaxLvl>) + where + L: LockableByValue<'a>, + L::MinLvl: LockAfter, + { + let guards = target.lock_by_value(); + (guards, MutexKey::new_internal()) + } + /// Lock one or more mutexes via a closure. /// /// Sorts by [`LockId`](crate::id::LockId) and acquires in one @@ -222,6 +294,84 @@ impl<'scope, Lvl: IsLevel> MutexKey<'scope, Lvl> { (result, MutexKey::new_internal()) } + /// Acquire an [`RwLock`](crate::rw_lock::RwLock) in shared + /// (read) mode. + /// + /// Convenience wrapper around + /// `self.lock(&ReadLock(rw))`. Consumes the key and returns the + /// read guard plus a new key at the rwlock's level. + /// + /// # Examples + /// + /// ```rust + /// use surelock::{key_handle::KeyHandle, rw_lock::RwLock}; + /// + /// let counter: RwLock = RwLock::new(7); + /// + /// let mut handle = KeyHandle::claim(); + /// handle.scope(|key| { + /// let (guard, _key) = key.read(&counter); + /// assert_eq!(*guard, 7); + /// }); + /// ``` + pub fn read<'a, T, ChildLvl, R>( + self, + rw: &'a crate::rw_lock::RwLock, + ) -> ( + crate::rw_lock::guard::RwLockReadGuard<'a, R, T>, + MutexKey<'scope, ChildLvl>, + ) + where + T: 'a, + ChildLvl: IsLevel + LockAfter, + R: crate::rw_lock::raw::RawRwLock + 'a, + { + let guard = crate::rw_lock::guard::RwLockReadGuard { + data: &rw.data, + _raw_guard: rw.raw.read(), + }; + (guard, MutexKey::new_internal()) + } + + /// Acquire an [`RwLock`](crate::rw_lock::RwLock) in exclusive + /// (write) mode. + /// + /// Convenience wrapper around + /// `self.lock(&WriteLock(rw))`. Consumes the key and returns the + /// write guard plus a new key at the rwlock's level. + /// + /// # Examples + /// + /// ```rust + /// use surelock::{key_handle::KeyHandle, rw_lock::RwLock}; + /// + /// let counter: RwLock = RwLock::new(0); + /// + /// let mut handle = KeyHandle::claim(); + /// handle.scope(|key| { + /// let (mut guard, _key) = key.write(&counter); + /// *guard = 42; + /// }); + /// ``` + pub fn write<'a, T, ChildLvl, R>( + self, + rw: &'a crate::rw_lock::RwLock, + ) -> ( + crate::rw_lock::guard::RwLockWriteGuard<'a, R, T>, + MutexKey<'scope, ChildLvl>, + ) + where + T: 'a, + ChildLvl: IsLevel + LockAfter, + R: crate::rw_lock::raw::RawRwLock + 'a, + { + let guard = crate::rw_lock::guard::RwLockWriteGuard { + data: &rw.data, + _raw_guard: rw.raw.write(), + }; + (guard, MutexKey::new_internal()) + } + /// Create a nested scope that inherits the current level. /// /// The inner key starts at the same level as the outer key, so @@ -262,15 +412,22 @@ impl<'scope, Lvl: IsLevel> MutexKey<'scope, Lvl> { /// `scope(&mut self)` provides static nesting prevention via the /// borrow checker, without requiring `thread_local!`. /// -/// Returns `Some(result)` if a scope was entered successfully, or -/// `None` if a scope is already active on the current thread. -/// Use [`lock_scope`] for a panicking convenience wrapper. +/// Returns `Ok(result)` if a scope was entered successfully, or +/// `Err(`[`ClaimError::AlreadyClaimed`](crate::key_handle::ClaimError::AlreadyClaimed)`)` +/// if a scope is already active on the current thread. Use +/// [`lock_scope`] for a panicking convenience wrapper. /// /// 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`. /// +/// # Errors +/// +/// Returns +/// [`ClaimError::AlreadyClaimed`](crate::key_handle::ClaimError::AlreadyClaimed) +/// if a scope is already active on this thread. +/// /// # Examples /// /// ```rust @@ -284,17 +441,17 @@ impl<'scope, Lvl: IsLevel> MutexKey<'scope, Lvl> { /// }); /// /// match result { -/// Some(()) => { /* success */ } -/// None => { /* already inside a scope */ } +/// Ok(()) => { /* success */ } +/// Err(_) => { /* already inside a scope */ } /// } /// ``` #[cfg(feature = "std")] -pub fn try_lock_scope(f: F) -> Option +pub fn try_lock_scope(f: F) -> Result where F: for<'scope> FnOnce(MutexKey<'scope, Bottom>) -> Ret, { let mut handle = crate::key_handle::KeyHandle::try_claim()?; - Some(handle.scope(f)) + Ok(handle.scope(f)) } /// Enter an ordered lock acquisition scope, or panic if one is @@ -335,4 +492,6 @@ where F: for<'scope> FnOnce(MutexKey<'scope, Bottom>) -> Ret, { try_lock_scope(f).expect("nested lock_scope -- use key.subscope() or try_lock_scope()") + // ^ The expected error is ClaimError::AlreadyClaimed; the message + // above is the message the user sees in the panic. } diff --git a/src/key_handle.rs b/src/key_handle.rs index 95d2886..1808732 100644 --- a/src/key_handle.rs +++ b/src/key_handle.rs @@ -2,8 +2,32 @@ use core::marker::PhantomData; +use thiserror::Error; + use crate::{key::MutexKey, level::Bottom}; +/// Error returned by [`KeyHandle::try_claim`] when a handle already +/// exists on this thread (or, on `no_std`, when this is a +/// hypothetical impossibility -- see field docs). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[non_exhaustive] +pub enum ClaimError { + /// A [`KeyHandle`] is already claimed on this thread, or an + /// active [`lock_scope`](crate::key::lock_scope) / + /// [`try_lock_scope`](crate::key::try_lock_scope) call holds the + /// slot. + /// + /// To resolve: drop the existing handle (or exit the active + /// scope) before claiming again. For nested locking within a + /// scope, use [`MutexKey::subscope`](crate::key::MutexKey::subscope) + /// instead of claiming a second handle. + #[error( + "KeyHandle is already claimed on this thread; drop the existing handle, or use \ + MutexKey::subscope for nested locking" + )] + AlreadyClaimed, +} + /// Sentinel type stored in `thread_local!`. Its presence means /// no handle has been claimed on this thread. #[cfg(feature = "std")] @@ -106,10 +130,10 @@ impl KeyHandle { /// Try to claim the `KeyHandle` for the current thread. /// /// On `std`: takes the handle from `thread_local!` storage. - /// Returns `None` if already claimed (by a previous `try_claim` + /// Returns an error if already claimed (by a previous `try_claim` /// or an active `try_lock_scope` / `lock_scope`). /// - /// On `no_std`: always returns `Some` (no `thread_local!` + /// On `no_std`: always returns `Ok` (no `thread_local!` /// available for uniqueness checking). This is **dangerous** -- /// multiple handles on the same execution context can create /// independent keys that defeat the ordering guarantee. On @@ -118,19 +142,25 @@ impl KeyHandle { /// via `&mut` references. The `&mut self` on /// [`scope`](KeyHandle::scope) prevents nesting within a single /// handle, but cannot prevent multiple handles from coexisting. - #[must_use] + /// + /// # Errors + /// + /// Returns [`ClaimError::AlreadyClaimed`] if a handle or scope + /// already exists on this thread (`std` only). #[allow(clippy::missing_const_for_fn)] // std branch calls non-const storage::take() - pub fn try_claim() -> Option { + pub fn try_claim() -> Result { #[cfg(feature = "std")] { - storage::take().map(|_slot| Self { - _not_send: PhantomData, - }) + storage::take() + .map(|_slot| Self { + _not_send: PhantomData, + }) + .ok_or(ClaimError::AlreadyClaimed) } #[cfg(not(feature = "std"))] { - Some(Self { + Ok(Self { _not_send: PhantomData, }) } diff --git a/src/key_voucher.rs b/src/key_voucher.rs index a0811e5..90aab9a 100644 --- a/src/key_voucher.rs +++ b/src/key_voucher.rs @@ -25,7 +25,13 @@ //! }); //! handle.join().unwrap(); //! ``` +//! +//! On `std`, [`redeem`](KeyVoucher::redeem) returns +//! `Result<`[`KeyHandle`]`, `[`ClaimError`]`>`. +//! On `no_std` it is infallible (returns [`KeyHandle`] directly). +#[cfg(feature = "std")] +use crate::key_handle::ClaimError; use crate::key_handle::KeyHandle; /// Transferable token redeemable for a [`KeyHandle`]. @@ -58,9 +64,10 @@ impl KeyVoucher { /// Redeem this voucher for a [`KeyHandle`] on the current thread. /// - /// On `std`: returns `Some(KeyHandle)` if no handle is already - /// claimed on this thread, `None` otherwise. Uses the same - /// `thread_local!` mechanism as + /// On `std`: returns `Ok(KeyHandle)` if no handle is already + /// claimed on this thread, + /// `Err(`[`ClaimError::AlreadyClaimed`]`)` otherwise. Uses the + /// same `thread_local!` mechanism as /// [`KeyHandle::try_claim`](crate::key_handle::KeyHandle::try_claim). /// /// On `no_std`: always returns `KeyHandle` (infallible). Per-context @@ -68,9 +75,13 @@ impl KeyVoucher { /// [`Locksmith`](crate::locksmith::Locksmith)'s limit prevents /// over-issuance, but two vouchers redeemed on the same core /// are not detected. - #[must_use] + /// + /// # Errors + /// + /// On `std`, returns [`ClaimError::AlreadyClaimed`] if a + /// [`KeyHandle`] is already held on this thread. #[cfg(feature = "std")] - pub fn redeem(self) -> Option { + pub fn redeem(self) -> Result { KeyHandle::try_claim() } @@ -82,7 +93,7 @@ impl KeyVoucher { /// /// # Panics /// - /// Panics if `KeyHandle::try_claim` returns `None`. This is + /// Panics if `KeyHandle::try_claim` returns an error. This is /// unreachable on `no_std` (where `try_claim` always succeeds) /// but is retained as a defensive assertion. #[must_use] diff --git a/src/level.rs b/src/level.rs index f17ce6b..6f29992 100644 --- a/src/level.rs +++ b/src/level.rs @@ -120,7 +120,7 @@ where /// let account = config.new_higher(20u32); // Level<1> /// let txn = account.new_higher(30u32); // Level<2> /// ``` -pub trait NewHigher { +pub trait NewHigher { /// The child's level (parent max + 1). type NextLvl: IsLevel; diff --git a/src/lib.rs b/src/lib.rs index d0beba9..cf8418e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,40 @@ //! }); //! ``` //! +//! ## Reader-Writer Locks +//! +//! [`RwLock`](crate::rw_lock::RwLock) is the rwlock +//! counterpart of [`Mutex`](crate::mutex::Mutex). It participates in +//! the same [`LockId`](crate::id::LockId)-sorted ordering used by +//! [`LockSet`](crate::set::LockSet) and the same compile-time +//! [`Level`](crate::level::Level) machinery used by +//! [`MutexKey`](crate::key::MutexKey). Mode is chosen at the call +//! site, either with the convenience methods +//! [`read`](crate::key::MutexKey::read) / +//! [`write`](crate::key::MutexKey::write), or with the +//! [`ReadLock`](crate::rw_lock::ReadLock) / +//! [`WriteLock`](crate::rw_lock::WriteLock) wrappers when composing +//! into a [`LockSet`](crate::set::LockSet): +//! +//! ```rust +//! use surelock::{ +//! key_handle::KeyHandle, +//! rw_lock::{ReadLock, RwLock, WriteLock}, +//! set::LockSet, +//! }; +//! +//! let a: RwLock = RwLock::new(1); +//! let b: RwLock = RwLock::new(0); +//! +//! let set = LockSet::new((ReadLock(&a), WriteLock(&b))); +//! +//! let mut handle = KeyHandle::claim(); +//! handle.scope(|key| { +//! let ((ra, mut wb), _key) = key.lock(&set); +//! *wb = *ra + 10; +//! }); +//! ``` +//! //! ## Nested Scopes //! //! Use [`subscope`](crate::key::MutexKey::subscope) to create a @@ -219,9 +253,9 @@ //! # Backend Agnostic //! //! [`Mutex`](crate::mutex::Mutex) is generic over any -//! [`RawMutex`](crate::raw_mutex::RawMutex) implementation. `Lvl` +//! [`RawMutex`](crate::mutex::raw::RawMutex) implementation. `Lvl` //! defaults to [`Base`](crate::level::Base) (= `Level<0>`) and `R` -//! defaults to [`StdMutex`](crate::raw_mutex::std_mutex::StdMutex) +//! defaults to [`StdMutex`](crate::mutex::raw::std_mutex::StdMutex) //! on `std`. Specify just the level to use the default backend: //! `Mutex>`. For `no_std`, enable the `lock-api` //! feature and use `spin`, `parking_lot`, or any other @@ -232,7 +266,8 @@ //! There are two ways to enter a lock scope: //! //! - **Ambient**: [`lock_scope`](crate::key::lock_scope) / [`try_lock_scope`](crate::key::try_lock_scope) -- -//! convenient, runtime nesting check on `std` (panic / `None`). +//! convenient, runtime nesting check on `std` (panic / +//! [`ClaimError`](crate::key_handle::ClaimError)). //! - **Capability-based**: [`KeyHandle::scope`](crate::key_handle::KeyHandle::scope) -- //! static nesting prevention via `&mut self` (compile error). //! Works on `no_std` without `thread_local!`. @@ -279,7 +314,8 @@ //! directory in the repository. // Unsafe is forbidden by default. Modules that need it opt in with -// #[allow(unsafe_code)]. See: raw_mutex, mutex, mutex/guard, key_voucher. +// #[allow(unsafe_code)]. See: mutex, mutex/guard, mutex/raw, rw_lock, +// rw_lock/guard, rw_lock/raw, key_voucher. #![deny(unsafe_code)] // Doc links to std-gated items (lock_scope, StdMutex, Mutex::new_higher, // etc.) resolve correctly on the default std build (docs.rs), but break @@ -292,6 +328,15 @@ extern crate alloc; +// Module declarations. +// +// `mutex` (with its nested `mutex::raw` submodule for backends) is +// the original exclusive-lock primitive; `rw_lock` (with its own +// nested `rw_lock::raw` submodule) is the reader-writer counterpart. +// Both participate in the same `LockId` counter (`id`), level system +// (`level`), and key/scope machinery (`key`, `key_handle`, +// `key_voucher`, `locksmith`). + pub mod acquirable; mod atomic; pub mod id; @@ -301,5 +346,5 @@ pub mod key_voucher; pub mod level; pub mod locksmith; pub mod mutex; -pub mod raw_mutex; +pub mod rw_lock; pub mod set; diff --git a/src/locksmith.rs b/src/locksmith.rs index af60bb8..6a53d25 100644 --- a/src/locksmith.rs +++ b/src/locksmith.rs @@ -24,15 +24,47 @@ //! let _v1 = smith.issue().unwrap(); //! let _v2 = smith.issue().unwrap(); //! let _v3 = smith.issue().unwrap(); -//! assert!(smith.issue().is_none()); // limit reached +//! assert!(smith.issue().is_err()); // limit reached //! ``` use core::marker::PhantomData; +use thiserror::Error; + use crate::atomic::{AtomicBool, AtomicUsize, Ordering}; use crate::key_voucher::KeyVoucher; +/// Error returned by [`Locksmith::new`] / [`Locksmith::unlimited`] +/// when a `Locksmith` already exists. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[non_exhaustive] +pub enum LocksmithExistsError { + /// A `Locksmith` already exists. Drop the existing instance + /// before creating a new one. Only one can be live at a time. + #[error( + "a Locksmith already exists; only one can be live at a time. Drop the existing \ + instance before creating a new one." + )] + AlreadyExists, +} + +/// Error returned by [`Locksmith::issue`] when the issuance limit is +/// reached. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[non_exhaustive] +pub enum IssueError { + /// The configured voucher limit has been reached. No more + /// vouchers will be issued by this [`Locksmith`]. + /// + /// Note that the counter only ever increments -- dropping a + /// [`KeyVoucher`] or its resulting + /// [`KeyHandle`](crate::key_handle::KeyHandle) does not free a + /// slot. + #[error("Locksmith has exhausted its voucher limit")] + LimitReached, +} + /// Global singleton flag. Only one `Locksmith` can exist at a time. static LOCKSMITH_EXISTS: AtomicBool = AtomicBool::new(false); @@ -67,35 +99,43 @@ pub struct Locksmith { 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`). + /// Returns an error 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 { + /// + /// # Errors + /// + /// Returns [`LocksmithExistsError::AlreadyExists`] if another + /// `Locksmith` is currently live. + pub fn new(limit: usize) -> Result { LOCKSMITH_EXISTS .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) - .ok() .map(|_| Self { issued: AtomicUsize::new(0), limit: Some(limit), _not_send: PhantomData, }) + .map_err(|_| LocksmithExistsError::AlreadyExists) } /// Try to create a `Locksmith` with no voucher limit. /// - /// Returns `None` if a `Locksmith` already exists. + /// Returns an error if a `Locksmith` already exists. /// `issue()` will always succeed (no limit check). - #[must_use] - pub fn unlimited() -> Option { + /// + /// # Errors + /// + /// Returns [`LocksmithExistsError::AlreadyExists`] if another + /// `Locksmith` is currently live. + pub fn unlimited() -> Result { LOCKSMITH_EXISTS .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed) - .ok() .map(|_| Self { issued: AtomicUsize::new(0), limit: None, _not_send: PhantomData, }) + .map_err(|_| LocksmithExistsError::AlreadyExists) } /// Create a `Locksmith` with the given limit, panicking if one @@ -124,8 +164,8 @@ impl Locksmith { /// Issue a [`KeyVoucher`]. /// - /// Returns `None` if the limit has been reached. If the - /// `Locksmith` is unlimited, always returns `Some`. + /// Returns an error if the limit has been reached. If the + /// `Locksmith` is unlimited, always returns `Ok`. /// /// The voucher is `Send` and can be transferred to another /// thread or core. @@ -133,20 +173,24 @@ impl Locksmith { /// 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 { + /// + /// # Errors + /// + /// Returns [`IssueError::LimitReached`] when a finite limit has + /// been exhausted. + pub fn issue(&self) -> Result { match self.limit { None => { self.issued.fetch_add(1, Ordering::Relaxed); - Some(KeyVoucher::new_internal()) + Ok(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()), + .map(|_| KeyVoucher::new_internal()) + .map_err(|_| IssueError::LimitReached), } } diff --git a/src/mutex.rs b/src/mutex.rs index 62fbac7..8a65cb7 100644 --- a/src/mutex.rs +++ b/src/mutex.rs @@ -10,17 +10,18 @@ //! //! The level parameter `Lvl` defaults to [`Base`] (= `Level<0>`). //! The backend parameter `R` defaults to -//! [`StdMutex`](crate::raw_mutex::std_mutex::StdMutex) when the `std` +//! [`StdMutex`](crate::mutex::raw::std_mutex::StdMutex) when the `std` //! feature is enabled (which it is by default). //! //! Users can specify just the level without naming the backend: //! `Mutex>`. pub mod guard; +pub mod raw; use core::{cell::UnsafeCell, fmt, marker::PhantomData}; -use crate::{id::LockId, level::IsLevel, raw_mutex::RawMutex}; +use crate::{id::LockId, level::IsLevel, mutex::raw::RawMutex}; #[cfg(feature = "std")] use crate::level::Base; @@ -33,7 +34,7 @@ use guard::MutexGuard; /// `T` is the protected data. `Lvl` defaults to [`Base`] (= /// `Level<0>`) -- levels are opt-in for incremental cross-level /// acquisition. `R` defaults to -/// [`StdMutex`](crate::raw_mutex::std_mutex::StdMutex) on `std`. +/// [`StdMutex`](crate::mutex::raw::std_mutex::StdMutex) on `std`. /// /// Specify just the level to use the default backend: /// `Mutex>`. @@ -55,7 +56,7 @@ use guard::MutexGuard; /// }); /// ``` #[cfg(feature = "std")] -pub struct Mutex { +pub struct Mutex { id: LockId, pub(crate) raw: R, pub(crate) data: UnsafeCell, @@ -160,10 +161,10 @@ impl Mutex { /// // Level = max(Level<0>, Level<1>) + 1 = Level<2> /// ``` #[must_use] - pub fn new_higher>( + pub fn new_higher>( data: T, parents: Parents, - ) -> Mutex { + ) -> Mutex { parents.new_higher(data) } } diff --git a/src/mutex/guard.rs b/src/mutex/guard.rs index 65a524e..e4e91e1 100644 --- a/src/mutex/guard.rs +++ b/src/mutex/guard.rs @@ -10,7 +10,7 @@ use core::{ ops::{Deref, DerefMut}, }; -use crate::raw_mutex::RawMutex; +use crate::mutex::raw::RawMutex; /// RAII guard for a [`super::Mutex`]. /// diff --git a/src/raw_mutex.rs b/src/mutex/raw.rs similarity index 100% rename from src/raw_mutex.rs rename to src/mutex/raw.rs diff --git a/src/raw_mutex/lock_api_adapter.rs b/src/mutex/raw/lock_api_adapter.rs similarity index 100% rename from src/raw_mutex/lock_api_adapter.rs rename to src/mutex/raw/lock_api_adapter.rs diff --git a/src/raw_mutex/std_mutex.rs b/src/mutex/raw/std_mutex.rs similarity index 100% rename from src/raw_mutex/std_mutex.rs rename to src/mutex/raw/std_mutex.rs diff --git a/src/rw_lock.rs b/src/rw_lock.rs new file mode 100644 index 0000000..a3a509c --- /dev/null +++ b/src/rw_lock.rs @@ -0,0 +1,520 @@ +#![allow(unsafe_code)] +//! Deadlock-free reader-writer lock, generic over lock level and +//! backend. +//! +//! [`RwLock`] is the rwlock counterpart of [`Mutex`](crate::mutex::Mutex). +//! It participates in the same `LockId`-sorted ordering used by +//! [`LockSet`](crate::set::LockSet) and the same compile-time +//! [`Level`](crate::level::Level) machinery used by +//! [`MutexKey`](crate::key::MutexKey). +//! +//! # Read vs Write Mode +//! +//! Mode is chosen at the call site, not baked into the type: +//! +//! ```rust +//! use surelock::{ +//! key_handle::KeyHandle, +//! rw_lock::{RwLock, WriteLock}, +//! }; +//! +//! let cache: RwLock = RwLock::new(0); +//! +//! let mut handle = KeyHandle::claim(); +//! handle.scope(|key| { +//! // Convenience method +//! let (val, _key) = key.read(&cache); +//! assert_eq!(*val, 0); +//! }); +//! +//! // Equivalent via the wrapper form, in a fresh scope. +//! handle.scope(|key| { +//! let writer = WriteLock(&cache); +//! let (mut val, _key) = key.lock(&writer); +//! *val = 42; +//! }); +//! ``` +//! +//! Inside a [`LockSet`](crate::set::LockSet), mode is expressed by +//! wrapping each reference: +//! +//! ```rust +//! use surelock::{ +//! key_handle::KeyHandle, +//! rw_lock::{ReadLock, RwLock, WriteLock}, +//! set::LockSet, +//! }; +//! +//! let a: RwLock = RwLock::new(1); +//! let b: RwLock = RwLock::new(2); +//! +//! let set = LockSet::new((ReadLock(&a), WriteLock(&b))); +//! +//! let mut handle = KeyHandle::claim(); +//! handle.scope(|key| { +//! let ((ra, mut wb), _key) = key.lock(&set); +//! *wb += *ra; +//! }); +//! ``` + +pub mod guard; +pub mod raw; + +use core::{cell::UnsafeCell, fmt, marker::PhantomData}; + +use crate::{id::LockId, level::IsLevel, rw_lock::raw::RawRwLock}; + +#[cfg(feature = "std")] +use crate::level::Base; + +/// A deadlock-free reader-writer lock generic over lock level and +/// backend. +/// +/// `T` is the protected data. `Lvl` defaults to +/// [`Base`] (= `Level<0>`) -- levels are opt-in for incremental +/// cross-level acquisition. `R` defaults to +/// [`StdRwLock`](crate::rw_lock::raw::std_rw_lock::StdRwLock) on +/// `std`. +/// +/// All ordered locking goes through +/// [`MutexKey::read`](crate::key::MutexKey::read) / +/// [`MutexKey::write`](crate::key::MutexKey::write) or via the +/// [`ReadLock`] / [`WriteLock`] wrappers passed to +/// [`MutexKey::lock`](crate::key::MutexKey::lock). +/// +/// # Examples +/// +/// ```rust +/// use surelock::{key_handle::KeyHandle, rw_lock::RwLock}; +/// +/// let counter: RwLock = RwLock::new(0); +/// +/// let mut handle = KeyHandle::claim(); +/// handle.scope(|key| { +/// let (mut guard, _key) = key.write(&counter); +/// *guard += 1; +/// }); +/// ``` +#[cfg(feature = "std")] +pub struct RwLock< + T, + Lvl: IsLevel = Base, + R: RawRwLock = crate::rw_lock::raw::std_rw_lock::StdRwLock, +> { + id: LockId, + pub(crate) raw: R, + pub(crate) data: UnsafeCell, + _level: PhantomData, +} + +/// A deadlock-free reader-writer lock generic over lock level and +/// backend. +/// +/// `no_std` form -- no default backend. Specify `R` explicitly. +#[cfg(not(feature = "std"))] +pub struct RwLock { + id: LockId, + pub(crate) raw: R, + pub(crate) data: UnsafeCell, + _level: PhantomData, +} + +impl RwLock { + /// Create a new rwlock with the given data. + /// + /// A unique [`LockId`] is assigned from the same global atomic + /// counter used by [`Mutex`](crate::mutex::Mutex), so an + /// [`RwLock`] and a [`Mutex`](crate::mutex::Mutex) can sit side + /// by side in a single [`LockSet`](crate::set::LockSet) and sort + /// deterministically. + #[must_use] + pub fn new(data: T) -> Self { + Self { + id: LockId::next(), + raw: RawRwLock::new(), + data: UnsafeCell::new(data), + _level: PhantomData, + } + } + + /// Returns this rwlock's unique [`LockId`]. + #[must_use] + 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. + #[must_use] + pub const fn get_mut(&mut self) -> &mut T { + self.data.get_mut() + } + + /// Consume the rwlock and return the inner data. + #[must_use] + pub fn into_inner(self) -> T { + self.data.into_inner() + } +} + +// -- new_higher: ordered construction with inferred level -- + +/// Associated function for creating rwlocks ordered after existing +/// mutex / rwlock parents, using the default `StdRwLock` backend. +/// +/// For custom backends, use the [`NewHigherRw`] trait method instead: +/// `parent.new_higher_rw(data)`. +#[cfg(feature = "std")] +impl RwLock { + /// Create a new rwlock ordered after one or more parents. + /// + /// The new rwlock's level is `max(parent levels) + 1`. Accepts + /// a single `&Mutex` / `&RwLock` reference or a 2-tuple of those. + /// Smart-pointer wrappers (`Arc`, `Rc`, `Box`) are also accepted. + /// + /// Uses the default `StdRwLock` backend. + /// + /// # Examples + /// + /// ```rust + /// use surelock::{mutex::Mutex, rw_lock::RwLock}; + /// + /// let config: Mutex = Mutex::new(10); + /// let cache = RwLock::new_higher(20u32, &config); // Level<1> + /// ``` + // The `parents` value is consumed only by an auto-ref'd `&self` + // method call on the trait; clippy's `needless_pass_by_value` + // doesn't recognize that path. We mirror the by-value signature + // of `Mutex::new_higher` for API symmetry. + #[allow(clippy::needless_pass_by_value)] + #[must_use] + pub fn new_higher>( + data: T, + parents: Parents, + ) -> RwLock { + parents.new_higher_rw(data) + } +} + +impl fmt::Debug for RwLock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RwLock") + .field("id", &self.id) + .finish_non_exhaustive() + } +} + +// SAFETY: UnsafeCell: Send when T: Send. +unsafe impl Send for RwLock {} + +// SAFETY: Shared (read) access exposes &T across threads, so T: Sync +// is required (unlike Mutex, which only needs T: Send). T: Send is +// also required because write access lets us move T between threads. +unsafe impl Sync for RwLock {} + +// -- MutexLevel impls so RwLock can be a parent in new_higher chains -- + +use crate::level::MutexLevel; + +impl MutexLevel for &RwLock { + type Lvl = Lvl; +} + +#[cfg(target_has_atomic = "ptr")] +impl MutexLevel for &alloc::sync::Arc> { + type Lvl = Lvl; +} + +impl MutexLevel for &alloc::rc::Rc> { + type Lvl = Lvl; +} + +impl MutexLevel for &alloc::boxed::Box> { + type Lvl = Lvl; +} + +// -- NewHigherRw: rwlock-flavored sibling of NewHigher -- +// +// `NewHigher` returns a `Mutex<...>` and inherits the parent's +// backend. The rwlock equivalent has a wrinkle: `Mutex` backends +// (`RawMutex`) and `RwLock` backends (`RawRwLock`) are distinct +// traits, so backend inheritance only makes sense when the parent is +// itself an `RwLock`. To keep the trait usable when the parent is a +// `Mutex` (or any `MutexLevel`-providing reference), we parameterize +// `NewHigherRw` by the *child's* backend `R`, not the parent's. The +// trait then extracts only the parent's level. +// +// In practice that means: +// - `(&rwlock_parent).new_higher_rw(data)` -> RwLock<_, next, R> +// - `(&mutex_parent).new_higher_rw_in::(data)` -> ... +// +// To keep ergonomics simple, the associated function +// `RwLock::new_higher(data, parents)` exists for the default +// `StdRwLock` backend. + +/// Create a child [`RwLock`] ordered after one or more parents. +/// +/// The rwlock counterpart of +/// [`NewHigher`](crate::level::NewHigher). Implemented for +/// [`Mutex`](crate::mutex::Mutex) references, [`RwLock`] references, +/// and smart-pointer wrappers (`Arc`, `Rc`, `Box`). +/// +/// Unlike [`NewHigher`](crate::level::NewHigher), `NewHigherRw` does +/// not inherit the backend type from the parent -- the parent might +/// be a [`Mutex`](crate::mutex::Mutex) (with a `RawMutex` backend) +/// while the child is an [`RwLock`] (with a `RawRwLock` backend), +/// and those are distinct traits. The child's backend `R` is the +/// trait's type parameter. +/// +/// # Examples +/// +/// ```rust +/// use surelock::{mutex::Mutex, rw_lock::{NewHigherRw, RwLock}}; +/// +/// let config: Mutex = Mutex::new(10); +/// // The child's backend defaults to StdRwLock here. +/// let cache: RwLock = (&config).new_higher_rw(20u32); +/// ``` +pub trait NewHigherRw { + /// The child's level (parent's level + 1). + type NextLvl: IsLevel; + + /// Create a child rwlock at the next level. + fn new_higher_rw(&self, data: ChildT) -> RwLock; +} + +// Bare-reference impl: parent is any reference with a MutexLevel. +// This single blanket covers `&Mutex<...>`, `&RwLock<...>`, and the +// smart-pointer wrappers below (via the existing `MutexLevel` impls +// in `mutex.rs` and the ones added above for `RwLock`). + +impl NewHigherRw for Parent +where + R: RawRwLock, + Parent: MutexLevel, + Parent::Lvl: crate::level::NextLevel, +{ + type NextLvl = ::Next; + + fn new_higher_rw(&self, data: ChildT) -> RwLock { + RwLock::new(data) + } +} + +// -- ReadLock / WriteLock mode wrappers -- + +/// Wrap an [`RwLock`] reference (or smart-pointer reference) to +/// acquire it in shared (read) mode. +/// +/// `ReadLock` is zero-cost: it is a `repr(transparent)` newtype +/// around a reference. The mode is encoded in the type, so a single +/// [`LockSet`](crate::set::LockSet) can hold readers and writers +/// side by side: +/// +/// ```rust +/// use surelock::{ +/// key_handle::KeyHandle, +/// rw_lock::{ReadLock, RwLock, WriteLock}, +/// set::LockSet, +/// }; +/// +/// let a: RwLock = RwLock::new(1); +/// let b: RwLock = RwLock::new(2); +/// let set = LockSet::new((ReadLock(&a), WriteLock(&b))); +/// +/// let mut handle = KeyHandle::claim(); +/// handle.scope(|key| { +/// let ((ra, mut wb), _key) = key.lock(&set); +/// *wb = *ra + 10; +/// }); +/// ``` +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct ReadLock(pub R); + +/// Wrap an [`RwLock`] reference (or smart-pointer reference) to +/// acquire it in exclusive (write) mode. +/// +/// See [`ReadLock`] for the shared-mode counterpart and for usage +/// examples. +#[repr(transparent)] +#[derive(Debug, Clone, Copy)] +pub struct WriteLock(pub R); + +// Note: `(ReadLock(&rw), WriteLock(&rw))` referring to the same +// rwlock is rejected at *runtime* by [`LockSet::new`]'s duplicate +// [`LockId`](crate::id::LockId) check (both wrappers report +// `rw.id()`). Stable Rust cannot express "T != U" in trait bounds +// in a way that scales to the open set of `RwLock` type +// parameters, so compile-time enforcement of the foot-gun would +// require either negative impls (nightly only) or an exhaustive +// macro enumeration that conflicts with admitting legitimate mixed +// tuples like `(ReadLock(&a), WriteLock(&b))` where `a` and `b` +// have different `T`s. The runtime check is consistent with the +// existing duplicate detection for `Mutex` and keeps the API +// minimal. + +// -- MutexRef / Acquirable / Lockable impls -- +// +// Each impl is generated for one of the four reference flavors that +// the user might wrap with `ReadLock` / `WriteLock`: +// &'a RwLock +// &'a Arc> (target_has_atomic = "ptr") +// &'a Rc> +// &'a Box> +// +// Two flavors per reference (Read/Write), so 8 impl-bundles total. +// Each bundle gives us MutexRef + Acquirable + Lockable so the +// wrapper works in every API entry point: lock_with, LockSet, and +// the single-lock fast path on MutexKey::lock. + +use crate::{ + acquirable::{Acquirable, MutexRef}, + key::{Lockable, LockableByValue}, + rw_lock::guard::{RwLockReadGuard, RwLockWriteGuard}, +}; + +macro_rules! impl_read_lock_traits { + ($ref:ty) => { + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> MutexRef<'a> for ReadLock<$ref> { + type Data = T; + type Lvl = Lvl; + type Guard = RwLockReadGuard<'a, R, T>; + + fn id(&self) -> LockId { + self.0.id() + } + + fn lock_ref(&'a self) -> Self::Guard { + RwLockReadGuard { + data: &self.0.data, + _raw_guard: self.0.raw.read(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> Acquirable<'a> for ReadLock<$ref> { + type MinLvl = Lvl; + type MaxLvl = Lvl; + type Guard = RwLockReadGuard<'a, R, T>; + + fn collect_ids(&self, out: &mut alloc::vec::Vec) { + out.push(self.0.id()); + } + + fn lock_sorted(&'a self, _sorted_indices: &[usize]) -> Self::Guard { + RwLockReadGuard { + data: &self.0.data, + _raw_guard: self.0.raw.read(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> Lockable<'a> for ReadLock<$ref> { + type Guard = RwLockReadGuard<'a, R, T>; + type MinLvl = Lvl; + type MaxLvl = Lvl; + + fn lock_impl(&'a self) -> Self::Guard { + RwLockReadGuard { + data: &self.0.data, + _raw_guard: self.0.raw.read(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> LockableByValue<'a> for ReadLock<$ref> { + type Guard = RwLockReadGuard<'a, R, T>; + type MinLvl = Lvl; + type MaxLvl = Lvl; + + fn lock_by_value(self) -> Self::Guard { + RwLockReadGuard { + data: &self.0.data, + _raw_guard: self.0.raw.read(), + } + } + } + }; +} + +macro_rules! impl_write_lock_traits { + ($ref:ty) => { + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> MutexRef<'a> for WriteLock<$ref> { + type Data = T; + type Lvl = Lvl; + type Guard = RwLockWriteGuard<'a, R, T>; + + fn id(&self) -> LockId { + self.0.id() + } + + fn lock_ref(&'a self) -> Self::Guard { + RwLockWriteGuard { + data: &self.0.data, + _raw_guard: self.0.raw.write(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> Acquirable<'a> for WriteLock<$ref> { + type MinLvl = Lvl; + type MaxLvl = Lvl; + type Guard = RwLockWriteGuard<'a, R, T>; + + fn collect_ids(&self, out: &mut alloc::vec::Vec) { + out.push(self.0.id()); + } + + fn lock_sorted(&'a self, _sorted_indices: &[usize]) -> Self::Guard { + RwLockWriteGuard { + data: &self.0.data, + _raw_guard: self.0.raw.write(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> Lockable<'a> for WriteLock<$ref> { + type Guard = RwLockWriteGuard<'a, R, T>; + type MinLvl = Lvl; + type MaxLvl = Lvl; + + fn lock_impl(&'a self) -> Self::Guard { + RwLockWriteGuard { + data: &self.0.data, + _raw_guard: self.0.raw.write(), + } + } + } + + impl<'a, T: 'a, Lvl: IsLevel, R: RawRwLock + 'a> LockableByValue<'a> for WriteLock<$ref> { + type Guard = RwLockWriteGuard<'a, R, T>; + type MinLvl = Lvl; + type MaxLvl = Lvl; + + fn lock_by_value(self) -> Self::Guard { + RwLockWriteGuard { + data: &self.0.data, + _raw_guard: self.0.raw.write(), + } + } + } + }; +} + +impl_read_lock_traits!(&'a RwLock); +impl_write_lock_traits!(&'a RwLock); + +impl_read_lock_traits!(&'a alloc::rc::Rc>); +impl_write_lock_traits!(&'a alloc::rc::Rc>); + +impl_read_lock_traits!(&'a alloc::boxed::Box>); +impl_write_lock_traits!(&'a alloc::boxed::Box>); + +// Arc requires pointer-width atomics. +#[cfg(target_has_atomic = "ptr")] +impl_read_lock_traits!(&'a alloc::sync::Arc>); +#[cfg(target_has_atomic = "ptr")] +impl_write_lock_traits!(&'a alloc::sync::Arc>); diff --git a/src/rw_lock/guard.rs b/src/rw_lock/guard.rs new file mode 100644 index 0000000..480329e --- /dev/null +++ b/src/rw_lock/guard.rs @@ -0,0 +1,106 @@ +#![allow(unsafe_code)] +//! RAII guards for [`super::RwLock`]. +//! +//! Two distinct types reflect the read / write mode: +//! +//! - [`RwLockReadGuard`] -- shared access; `Deref` only. +//! - [`RwLockWriteGuard`] -- exclusive access; `Deref` and +//! `DerefMut`. +//! +//! Guards are not constructible by users -- they are returned by +//! [`MutexKey::read`](crate::key::MutexKey::read), +//! [`MutexKey::write`](crate::key::MutexKey::write), or via the +//! [`ReadLock`](super::ReadLock) / [`WriteLock`](super::WriteLock) +//! wrappers passed to [`MutexKey::lock`](crate::key::MutexKey::lock). +//! Dropping the guard releases the underlying lock. + +use core::{ + cell::UnsafeCell, + ops::{Deref, DerefMut}, +}; + +use crate::rw_lock::raw::RawRwLock; + +/// RAII guard for shared (read) access to a [`super::RwLock`]. +/// +/// `Deref` only -- read guards do not expose mutable access. The +/// shared lock is released when the guard is dropped. +#[must_use = "if unused, the RwLock will immediately unlock"] +pub struct RwLockReadGuard<'a, R: RawRwLock + 'a, T: ?Sized> { + // Field order matters: Rust drops fields in declaration order. + // `data` is declared first so its borrow is released before + // `_raw_guard` drops and releases the lock. + pub(crate) data: &'a UnsafeCell, + pub(crate) _raw_guard: R::ReadGuard<'a>, +} + +impl Deref for RwLockReadGuard<'_, R, T> { + type Target = T; + + fn deref(&self) -> &T { + // SAFETY: the shared raw guard guarantees no writer is active. + unsafe { &*self.data.get() } + } +} + +impl core::fmt::Debug for RwLockReadGuard<'_, R, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&**self, f) + } +} + +// SAFETY: shared-mode access exposes &T across threads, so T: Sync is +// the relevant bound. The raw guard's Send/Sync gates the surface. +unsafe impl<'a, R: RawRwLock + 'a, T: ?Sized + Sync> Send for RwLockReadGuard<'a, R, T> where + R::ReadGuard<'a>: Send +{ +} + +unsafe impl<'a, R: RawRwLock + 'a, T: ?Sized + Sync> Sync for RwLockReadGuard<'a, R, T> where + R::ReadGuard<'a>: Sync +{ +} + +/// RAII guard for exclusive (write) access to a [`super::RwLock`]. +/// +/// `Deref` + `DerefMut`. The exclusive lock is released when the +/// guard is dropped. +#[must_use = "if unused, the RwLock will immediately unlock"] +pub struct RwLockWriteGuard<'a, R: RawRwLock + 'a, T: ?Sized> { + pub(crate) data: &'a UnsafeCell, + pub(crate) _raw_guard: R::WriteGuard<'a>, +} + +impl Deref for RwLockWriteGuard<'_, R, T> { + type Target = T; + + fn deref(&self) -> &T { + // SAFETY: the exclusive raw guard guarantees no other access. + unsafe { &*self.data.get() } + } +} + +impl DerefMut for RwLockWriteGuard<'_, R, T> { + fn deref_mut(&mut self) -> &mut T { + // SAFETY: the exclusive raw guard guarantees no other access. + unsafe { &mut *self.data.get() } + } +} + +impl core::fmt::Debug for RwLockWriteGuard<'_, R, T> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + core::fmt::Debug::fmt(&**self, f) + } +} + +// SAFETY: write-mode access is exclusive, mirroring Mutex semantics: +// T: Send is sufficient for cross-thread movement of the lock value. +unsafe impl<'a, R: RawRwLock + 'a, T: ?Sized + Send> Send for RwLockWriteGuard<'a, R, T> where + R::WriteGuard<'a>: Send +{ +} + +unsafe impl<'a, R: RawRwLock + 'a, T: ?Sized + Sync> Sync for RwLockWriteGuard<'a, R, T> where + R::WriteGuard<'a>: Sync +{ +} diff --git a/src/rw_lock/raw.rs b/src/rw_lock/raw.rs new file mode 100644 index 0000000..6aa7c05 --- /dev/null +++ b/src/rw_lock/raw.rs @@ -0,0 +1,85 @@ +#![allow(unsafe_code)] +//! Backend trait for raw reader-writer lock implementations. +//! +//! [`RawRwLock`] is to [`RwLock`](crate::rw_lock::RwLock) what +//! [`RawMutex`](crate::mutex::raw::RawMutex) is to +//! [`Mutex`](crate::mutex::Mutex): a minimal, GAT-based backend +//! abstraction. The trait has two guard GATs -- +//! [`ReadGuard`](RawRwLock::ReadGuard) and +//! [`WriteGuard`](RawRwLock::WriteGuard) -- because the two acquisition +//! modes return distinct types in every realistic backend (`std`, +//! `parking_lot`, `spin`, ...). Dropping a guard releases the lock; no +//! separate `unlock_*` method. +//! +//! # Built-in Backends +//! +//! - [`StdRwLock`](crate::rw_lock::raw::std_rw_lock::StdRwLock): +//! wraps `std::sync::RwLock<()>` (behind the `std` feature, which +//! is the default). +//! +//! # `lock_api` Interop +//! +//! Enable the `lock-api` feature for a blanket impl that bridges any +//! `lock_api::RawRwLock` implementation (`parking_lot`, `spin`, etc.) +//! to surelock's trait. No newtype wrappers needed. + +#[cfg(feature = "lock-api")] +pub mod lock_api_adapter; +#[cfg(feature = "std")] +pub mod std_rw_lock; + +/// A raw reader-writer lock primitive. +/// +/// Implementations provide the low-level shared / exclusive lock +/// operations that [`RwLock`](crate::rw_lock::RwLock) delegates to. +/// Unlocking happens by dropping the associated guard type -- there +/// is no separate `unlock_*` method. +/// +/// # Safety +/// +/// Implementations must guarantee that: +/// - `read()` blocks until a shared lock is acquired. +/// - `write()` blocks until an exclusive lock is acquired. +/// - A successful `read` / `try_read` returns a [`ReadGuard`](Self::ReadGuard) +/// that releases the shared lock on drop. +/// - A successful `write` / `try_write` returns a [`WriteGuard`](Self::WriteGuard) +/// that releases the exclusive lock on drop. +/// - While any [`ReadGuard`](Self::ReadGuard) is alive, no +/// [`WriteGuard`](Self::WriteGuard) may exist. +/// - While a [`WriteGuard`](Self::WriteGuard) is alive, no other +/// guard of any kind may exist. +/// - The implementation is thread-safe. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an rwlock backend", + note = "implement `RawRwLock` for custom backends, or enable the `lock-api` feature for `parking_lot` / `spin` compatibility" +)] +pub unsafe trait RawRwLock { + /// RAII guard for shared (read) access. Dropping the guard releases + /// the shared lock. + type ReadGuard<'a> + where + Self: 'a; + + /// RAII guard for exclusive (write) access. Dropping the guard + /// releases the exclusive lock. + type WriteGuard<'a> + where + Self: 'a; + + /// Create a new unlocked instance. + fn new() -> Self; + + /// Block until a shared lock is acquired. + fn read(&self) -> Self::ReadGuard<'_>; + + /// Try to acquire a shared lock without blocking. Returns + /// `Some(guard)` if acquired, `None` if it would block. + fn try_read(&self) -> Option>; + + /// Block until an exclusive lock is acquired. + fn write(&self) -> Self::WriteGuard<'_>; + + /// Try to acquire an exclusive lock without blocking. Returns + /// `Some(guard)` if acquired, `None` if it would block. + fn try_write(&self) -> Option>; +} diff --git a/src/rw_lock/raw/lock_api_adapter.rs b/src/rw_lock/raw/lock_api_adapter.rs new file mode 100644 index 0000000..123938a --- /dev/null +++ b/src/rw_lock/raw/lock_api_adapter.rs @@ -0,0 +1,88 @@ +#![allow(unsafe_code)] +//! Blanket [`RawRwLock`] impl for `lock_api::RawRwLock`. +//! +//! Available behind the `lock-api` feature. Bridges any +//! `lock_api::RawRwLock` implementation (`parking_lot`, `spin`, etc.) +//! to surelock's trait. No newtype wrappers needed -- use the +//! backend type directly as the `R` parameter on +//! `RwLock`. + +use super::RawRwLock; + +/// RAII guard releasing a shared lock on drop. +pub struct LockApiReadGuard<'a, R: lock_api::RawRwLock>(&'a R); + +impl core::fmt::Debug for LockApiReadGuard<'_, R> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LockApiReadGuard").finish_non_exhaustive() + } +} + +impl Drop for LockApiReadGuard<'_, R> { + fn drop(&mut self) { + // SAFETY: the guard only exists while the shared lock is held. + unsafe { + self.0.unlock_shared(); + } + } +} + +/// RAII guard releasing an exclusive lock on drop. +pub struct LockApiWriteGuard<'a, R: lock_api::RawRwLock>(&'a R); + +impl core::fmt::Debug for LockApiWriteGuard<'_, R> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("LockApiWriteGuard").finish_non_exhaustive() + } +} + +impl Drop for LockApiWriteGuard<'_, R> { + fn drop(&mut self) { + // SAFETY: the guard only exists while the exclusive lock is held. + unsafe { + self.0.unlock_exclusive(); + } + } +} + +unsafe impl RawRwLock for R { + type ReadGuard<'a> + = LockApiReadGuard<'a, R> + where + R: 'a; + + type WriteGuard<'a> + = LockApiWriteGuard<'a, R> + where + R: 'a; + + fn new() -> Self { + R::INIT + } + + fn read(&self) -> Self::ReadGuard<'_> { + lock_api::RawRwLock::lock_shared(self); + LockApiReadGuard(self) + } + + fn try_read(&self) -> Option> { + if lock_api::RawRwLock::try_lock_shared(self) { + Some(LockApiReadGuard(self)) + } else { + None + } + } + + fn write(&self) -> Self::WriteGuard<'_> { + lock_api::RawRwLock::lock_exclusive(self); + LockApiWriteGuard(self) + } + + fn try_write(&self) -> Option> { + if lock_api::RawRwLock::try_lock_exclusive(self) { + Some(LockApiWriteGuard(self)) + } else { + None + } + } +} diff --git a/src/rw_lock/raw/std_rw_lock.rs b/src/rw_lock/raw/std_rw_lock.rs new file mode 100644 index 0000000..d31a7a5 --- /dev/null +++ b/src/rw_lock/raw/std_rw_lock.rs @@ -0,0 +1,98 @@ +#![allow(unsafe_code)] +//! [`RawRwLock`] backend using `std::sync::RwLock`. +//! +//! [`StdRwLock`] wraps `std::sync::RwLock<()>` and implements +//! surelock's [`RawRwLock`] trait. Poisoned rwlocks panic on +//! acquisition -- consistent with the +//! [`StdMutex`](crate::mutex::raw::std_mutex::StdMutex) policy and +//! with surelock's position that poisoning is orthogonal to +//! deadlock prevention. +//! +//! This is the default backend when the `std` feature is enabled. + +extern crate std; + +use std::sync; + +use super::RawRwLock; + +/// A raw rwlock backed by [`std::sync::RwLock`]. +/// +/// Panics if the underlying `std::sync::RwLock` is poisoned (a panic +/// previously unwound through a critical section). This matches the +/// `.read().unwrap()` / `.write().unwrap()` convention used by the +/// vast majority of `std::sync::RwLock` users. +pub struct StdRwLock { + inner: sync::RwLock<()>, +} + +impl StdRwLock { + /// Create a new unlocked `StdRwLock`. + #[must_use] + pub const fn const_new() -> Self { + Self { + inner: sync::RwLock::new(()), + } + } +} + +impl Default for StdRwLock { + fn default() -> Self { + Self::const_new() + } +} + +impl core::fmt::Debug for StdRwLock { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("StdRwLock").finish_non_exhaustive() + } +} + +unsafe impl RawRwLock for StdRwLock { + type ReadGuard<'a> = sync::RwLockReadGuard<'a, ()>; + type WriteGuard<'a> = sync::RwLockWriteGuard<'a, ()>; + + fn new() -> Self { + Self::const_new() + } + + #[allow(clippy::expect_used)] // Intentional -- poisoned rwlock should panic loudly + fn read(&self) -> Self::ReadGuard<'_> { + self.inner + .read() + .expect("surelock: rwlock poisoned (a panic occurred in a critical section)") + } + + #[allow(clippy::panic)] // Intentional -- poisoned rwlock should panic loudly + fn try_read(&self) -> Option> { + match self.inner.try_read() { + Ok(guard) => Some(guard), + Err(sync::TryLockError::WouldBlock) => None, + Err(sync::TryLockError::Poisoned(_)) => { + panic!("surelock: rwlock poisoned (a panic occurred in a critical section)"); + } + } + } + + #[allow(clippy::expect_used)] // Intentional -- poisoned rwlock should panic loudly + fn write(&self) -> Self::WriteGuard<'_> { + self.inner + .write() + .expect("surelock: rwlock poisoned (a panic occurred in a critical section)") + } + + #[allow(clippy::panic)] // Intentional -- poisoned rwlock should panic loudly + fn try_write(&self) -> Option> { + match self.inner.try_write() { + Ok(guard) => Some(guard), + Err(sync::TryLockError::WouldBlock) => None, + Err(sync::TryLockError::Poisoned(_)) => { + panic!("surelock: rwlock poisoned (a panic occurred in a critical section)"); + } + } + } +} + +// SAFETY: std::sync::RwLock<()> is Send + Sync. +unsafe impl Send for StdRwLock {} +unsafe impl Sync for StdRwLock {} diff --git a/src/set.rs b/src/set.rs index 779f1a4..54a854d 100644 --- a/src/set.rs +++ b/src/set.rs @@ -6,8 +6,28 @@ use alloc::vec::Vec; +use thiserror::Error; + use crate::{acquirable::Acquirable, id::LockId}; +/// Error returned by [`LockSet::try_new`] when the group contains +/// the same lock more than once. +/// +/// "Same lock" means same [`LockId`], which catches both literal +/// double references (`(&m, &m)`) and aliases through smart pointers +/// (`(&*arc, &*arc.clone())`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)] +#[non_exhaustive] +pub enum LockSetError { + /// The same [`LockId`] appeared more than once in the group. + /// This includes attempts to acquire the same + /// [`RwLock`](crate::rw_lock::RwLock) in both + /// [`ReadLock`](crate::rw_lock::ReadLock) and + /// [`WriteLock`](crate::rw_lock::WriteLock) modes in one set. + #[error("LockSet contains duplicate locks (the same LockId appears more than once)")] + DuplicateLocks, +} + /// A prepared set of locks, pre-sorted by [`LockId`]. /// /// For tuples of arity 3 and above and for slices, all locks must be @@ -91,13 +111,17 @@ impl LockSet { } } - /// Try to create a new `LockSet`, returning `None` if any locks - /// are duplicated. + /// Try to create a new `LockSet`, returning an error if any + /// locks are duplicated. /// - /// Same as [`new`](LockSet::new) but returns `None` instead of - /// panicking when two locks share the same - /// [`LockId`]. An empty group always returns - /// `Some`. + /// Same as [`new`](LockSet::new) but returns a typed error + /// instead of panicking when two locks share the same + /// [`LockId`]. An empty group always succeeds. + /// + /// # Errors + /// + /// Returns [`LockSetError::DuplicateLocks`] if the group contains + /// two references to the same lock (same [`LockId`]). /// /// # Examples /// @@ -108,23 +132,22 @@ impl LockSet { /// let b: Mutex = Mutex::new(2); /// /// // Distinct locks -- succeeds. - /// assert!(LockSet::try_new((&a, &b)).is_some()); + /// assert!(LockSet::try_new((&a, &b)).is_ok()); /// - /// // Same lock twice -- returns None. - /// assert!(LockSet::try_new((&a, &a)).is_none()); + /// // Same lock twice -- returns an error. + /// assert!(LockSet::try_new((&a, &a)).is_err()); /// ``` - #[must_use] - pub fn try_new<'a>(group: L) -> Option + pub fn try_new<'a>(group: L) -> Result where L: Acquirable<'a>, { let (sorted_indices, has_duplicates) = build_sorted(&group); if has_duplicates { - return None; + return Err(LockSetError::DuplicateLocks); } - Some(Self { + Ok(Self { group, sorted_indices, }) diff --git a/tests/compile_fail/key_escape_from_scope.stderr b/tests/compile_fail/key_escape_from_scope.stderr index 64b3877..997e181 100644 --- a/tests/compile_fail/key_escape_from_scope.stderr +++ b/tests/compile_fail/key_escape_from_scope.stderr @@ -4,7 +4,7 @@ warning: unused imports: `key::MutexKey` and `level::Bottom` 4 | use surelock::{key::lock_scope, key::MutexKey, level::Bottom}; | ^^^^^^^^^^^^^ ^^^^^^^^^^^^^ | - = note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default + = note: `#[warn(unused_imports)]` on by default error: lifetime may not live long enough --> tests/compile_fail/key_escape_from_scope.rs:10:9 diff --git a/tests/compile_fail/key_reuse_after_consumption.stderr b/tests/compile_fail/key_reuse_after_consumption.stderr index fb8935a..ae4f79c 100644 --- a/tests/compile_fail/key_reuse_after_consumption.stderr +++ b/tests/compile_fail/key_reuse_after_consumption.stderr @@ -1,3 +1,17 @@ +warning: unused variable: `guard_a` + --> tests/compile_fail/key_reuse_after_consumption.rs:12:14 + | +12 | let (guard_a, _new_key) = key.lock(&a); + | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_guard_a` + | + = note: `#[warn(unused_variables)]` on by default + +warning: unused variable: `guard_b` + --> tests/compile_fail/key_reuse_after_consumption.rs:16:14 + | +16 | let (guard_b, _) = key.lock(&b); + | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_guard_b` + error[E0382]: use of moved value: `key` --> tests/compile_fail/key_reuse_after_consumption.rs:16:28 | @@ -15,17 +29,3 @@ note: `MutexKey::<'scope, Lvl>::lock` takes ownership of the receiver `self`, wh | | pub fn lock<'a, L: Lockable<'a>>(self, target: &'a L) -> (L::Guard, MutexKey<'scope, L::MaxLvl>) | ^^^^ - -warning: unused variable: `guard_a` - --> tests/compile_fail/key_reuse_after_consumption.rs:12:14 - | -12 | let (guard_a, _new_key) = key.lock(&a); - | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_guard_a` - | - = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default - -warning: unused variable: `guard_b` - --> tests/compile_fail/key_reuse_after_consumption.rs:16:14 - | -16 | let (guard_b, _) = key.lock(&b); - | ^^^^^^^ help: if this is intentional, prefix it with an underscore: `_guard_b` diff --git a/tests/compile_fail/rwlock_read_guard_no_deref_mut.rs b/tests/compile_fail/rwlock_read_guard_no_deref_mut.rs new file mode 100644 index 0000000..0bd395c --- /dev/null +++ b/tests/compile_fail/rwlock_read_guard_no_deref_mut.rs @@ -0,0 +1,14 @@ +//! `RwLockReadGuard` exposes `Deref` only -- writing through it must +//! not compile. + +use surelock::{key::lock_scope, rw_lock::RwLock}; + +fn main() { + let rw: RwLock = RwLock::new(0); + + lock_scope(|key| { + let (mut g, _key) = key.read(&rw); + // Should fail: RwLockReadGuard has no DerefMut. + *g = 42; + }); +} diff --git a/tests/compile_fail/rwlock_read_guard_no_deref_mut.stderr b/tests/compile_fail/rwlock_read_guard_no_deref_mut.stderr new file mode 100644 index 0000000..58da75a --- /dev/null +++ b/tests/compile_fail/rwlock_read_guard_no_deref_mut.stderr @@ -0,0 +1,17 @@ +warning: variable does not need to be mutable + --> tests/compile_fail/rwlock_read_guard_no_deref_mut.rs:10:14 + | +10 | let (mut g, _key) = key.read(&rw); + | ----^ + | | + | help: remove this `mut` + | + = note: `#[warn(unused_mut)]` on by default + +error[E0594]: cannot assign to data in dereference of `surelock::rw_lock::guard::RwLockReadGuard<'_, StdRwLock, u32>` + --> tests/compile_fail/rwlock_read_guard_no_deref_mut.rs:12:9 + | +12 | *g = 42; + | ^^^^^^^ cannot assign + | + = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `surelock::rw_lock::guard::RwLockReadGuard<'_, StdRwLock, u32>` diff --git a/tests/compile_fail/rwlock_wrong_level_order.rs b/tests/compile_fail/rwlock_wrong_level_order.rs new file mode 100644 index 0000000..d08a026 --- /dev/null +++ b/tests/compile_fail/rwlock_wrong_level_order.rs @@ -0,0 +1,21 @@ +//! Attempting to acquire an RwLock at a lower level after a higher +//! level should not compile. + +use surelock::{key::lock_scope, level::Level, rw_lock::RwLock}; + +type High = Level<5>; +type Low = Level<2>; + +fn main() { + let high: RwLock = RwLock::new(1); + let low: RwLock = RwLock::new(2); + + lock_scope(|key| { + // Read high first (Level<5>) + let (_, key) = key.read(&high); + + // Try to write low (Level<2>) -- should fail: + // Level<2> does not implement LockAfter> + let (_, _key) = key.write(&low); + }); +} diff --git a/tests/compile_fail/rwlock_wrong_level_order.stderr b/tests/compile_fail/rwlock_wrong_level_order.stderr new file mode 100644 index 0000000..43c993c --- /dev/null +++ b/tests/compile_fail/rwlock_wrong_level_order.stderr @@ -0,0 +1,21 @@ +error[E0277]: `Level<2>` cannot be acquired after `Level<5>` + --> tests/compile_fail/rwlock_wrong_level_order.rs:19:35 + | +19 | let (_, _key) = key.write(&low); + | ----- ^^^^ the trait `LockAfter>` is not implemented for `Level<2>` + | | + | required by a bound introduced by this call + | + = note: locks must be acquired in ascending level order; once a higher-levelled lock is held, lower-levelled locks cannot be acquired for this key + = help: the following other types implement trait `LockAfter`: + `Level<2>` implements `LockAfter` + `Level<2>` implements `LockAfter>` + `Level<2>` implements `LockAfter>` +note: required by a bound in `MutexKey::<'scope, Lvl>::write` + --> src/key.rs + | + | pub fn write<'a, T, ChildLvl, R>( + | ----- required by a bound in this associated function +... + | ChildLvl: IsLevel + LockAfter, + | ^^^^^^^^^^^^^^ required by this bound in `MutexKey::<'scope, Lvl>::write` diff --git a/tests/compile_fail/wrong_level_order.stderr b/tests/compile_fail/wrong_level_order.stderr index e499bd6..ccd083c 100644 --- a/tests/compile_fail/wrong_level_order.stderr +++ b/tests/compile_fail/wrong_level_order.stderr @@ -7,25 +7,10 @@ error[E0277]: `Level<2>` cannot be acquired after `Level<5>` | required by a bound introduced by this call | = note: locks must be acquired in ascending level order; once a higher-levelled lock is held, lower-levelled locks cannot be acquired for this key -help: the following other types implement trait `LockAfter` - --> src/level.rs - | - | impl LockAfter for Level<$n> {} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Level<2>` implements `LockAfter` -... - | impl LockAfter> for Level<$rest> {} - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - | | - | `Level<2>` implements `LockAfter>` - | `Level<2>` implements `LockAfter>` -... - | / generate_level_impls!( - | | 64, - | | [ - | | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, -... | - | | ); - | |_- in this macro invocation + = help: the following other types implement trait `LockAfter`: + `Level<2>` implements `LockAfter` + `Level<2>` implements `LockAfter>` + `Level<2>` implements `LockAfter>` note: required by a bound in `MutexKey::<'scope, Lvl>::lock` --> src/key.rs | @@ -34,4 +19,3 @@ note: required by a bound in `MutexKey::<'scope, Lvl>::lock` | where | L::MinLvl: LockAfter, | ^^^^^^^^^^^^^^ required by this bound in `MutexKey::<'scope, Lvl>::lock` - = note: this error originates in the macro `generate_level_impls` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/integration.rs b/tests/integration.rs index 7371bc2..fffff8e 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -11,8 +11,7 @@ use surelock::{ key::{lock_scope, try_lock_scope}, key_handle::KeyHandle, level::NewHigher, - mutex::Mutex, - raw_mutex::RawMutex as _, + mutex::{Mutex, raw::RawMutex as _}, set::LockSet, }; @@ -242,13 +241,13 @@ mod lock_set { } #[test] - fn try_new_returns_none_on_duplicate() { + fn try_new_returns_err_on_duplicate() { let m: Mutex = Mutex::new(0); - assert!(LockSet::try_new((&m, &m)).is_none()); + assert!(LockSet::try_new((&m, &m)).is_err()); } #[test] - fn try_new_returns_some_on_distinct() { + fn try_new_returns_ok_on_distinct() { let a: Mutex = Mutex::new(1); let b: Mutex = Mutex::new(2); let set = LockSet::try_new((&a, &b)).unwrap(); @@ -271,9 +270,9 @@ mod lock_set { } #[test] - fn try_new_empty_slice_returns_some() { + fn try_new_empty_slice_returns_ok() { let mutexes: &[Mutex] = &[]; - assert!(LockSet::try_new(mutexes).is_some()); + assert!(LockSet::try_new(mutexes).is_ok()); } #[test] @@ -373,7 +372,7 @@ mod try_lock_scope_tests { use super::*; #[test] - fn returns_some_when_no_scope_active() { + fn returns_ok_when_no_scope_active() { let counter: Mutex = Mutex::new(0); let result = try_lock_scope(|key| { @@ -381,24 +380,24 @@ mod try_lock_scope_tests { v }); - assert_eq!(result, Some(0)); + assert_eq!(result, Ok(0)); } #[test] - fn returns_none_when_nested() { + fn returns_err_when_nested() { let result = lock_scope(|_key| try_lock_scope(|_key2| 42)); - assert_eq!(result, None); + assert!(result.is_err()); } #[test] - fn recovers_after_inner_returns_none() { + fn recovers_after_inner_returns_err() { lock_scope(|_key| { let inner = try_lock_scope(|_key2| 42); - assert_eq!(inner, None); + assert!(inner.is_err()); }); let result = try_lock_scope(|_key| 99); - assert_eq!(result, Some(99)); + assert_eq!(result, Ok(99)); } } @@ -433,14 +432,14 @@ mod key_handle { #[test] fn prevents_second_claim() { let _keeper = KeyHandle::claim(); - assert!(KeyHandle::try_claim().is_none()); + assert!(KeyHandle::try_claim().is_err()); } #[test] fn blocks_try_lock_scope() { let _keeper = KeyHandle::claim(); let result = try_lock_scope(|_key| 42); - assert_eq!(result, None); + assert!(result.is_err()); } #[test] @@ -450,7 +449,7 @@ mod key_handle { } let result = try_lock_scope(|_key| 99); - assert_eq!(result, Some(99)); + assert_eq!(result, Ok(99)); } #[test] @@ -496,7 +495,7 @@ mod locksmith { let smith = surelock::locksmith::Locksmith::new(2).unwrap(); let _v1 = smith.issue().unwrap(); let _v2 = smith.issue().unwrap(); - assert!(smith.issue().is_none()); + assert!(smith.issue().is_err()); } #[test] @@ -534,7 +533,7 @@ mod locksmith { let _handle = voucher.redeem().unwrap(); let result = try_lock_scope(|_key| 42); - assert_eq!(result, None); + assert!(result.is_err()); } #[test] @@ -569,8 +568,8 @@ mod locksmith { #[serial] fn singleton_enforcement() { let smith = surelock::locksmith::Locksmith::new(1).unwrap(); - assert!(surelock::locksmith::Locksmith::new(2).is_none()); - assert!(surelock::locksmith::Locksmith::unlimited().is_none()); + assert!(surelock::locksmith::Locksmith::new(2).is_err()); + assert!(surelock::locksmith::Locksmith::unlimited().is_err()); drop(smith); let _smith2 = surelock::locksmith::Locksmith::new(1).unwrap(); } @@ -1101,14 +1100,14 @@ mod raw_mutex_try_lock { #[test] fn succeeds_when_unlocked() { - let raw = surelock::raw_mutex::std_mutex::StdMutex::new(); + let raw = surelock::mutex::raw::std_mutex::StdMutex::new(); let guard = raw.try_lock(); assert!(guard.is_some(), "try_lock should succeed on unlocked mutex"); } #[test] fn fails_when_locked() { - let raw = surelock::raw_mutex::std_mutex::StdMutex::new(); + let raw = surelock::mutex::raw::std_mutex::StdMutex::new(); let _guard = raw.lock(); let result = raw.try_lock(); assert!(result.is_none(), "try_lock should return None when locked"); diff --git a/tests/property.rs b/tests/property.rs index 745ab67..267d985 100644 --- a/tests/property.rs +++ b/tests/property.rs @@ -2,7 +2,12 @@ #![allow(clippy::unwrap_used, clippy::indexing_slicing)] -use surelock::{key::lock_scope, mutex::Mutex, set::LockSet}; +use surelock::{ + key::lock_scope, + mutex::Mutex, + rw_lock::{ReadLock, RwLock, WriteLock}, + set::LockSet, +}; /// `LockId`s are strictly monotonically increasing: for any sequence /// of N mutexes created in order, each ID is greater than the last. @@ -69,3 +74,49 @@ fn lockset_slice_preserves_values() { }); }); } + +/// For any pair of `RwLock`s and any combination of read/write modes, +/// `LockSet` acquires them and returns guards observing the original +/// values. Verifies that mixed-mode tuples are sound for arbitrary +/// orderings. +#[test] +fn rwlock_mixed_mode_2tuple() { + bolero::check!() + .with_type::<(u32, u32, u8)>() + .for_each(|&(val_a, val_b, modes)| { + let a: RwLock = RwLock::new(val_a); + let b: RwLock = RwLock::new(val_b); + + // Lowest 2 bits select R/W for a and b respectively. + // 0 = read, 1 = write. + let mode_a = modes & 0b01; + let mode_b = (modes >> 1) & 0b01; + + lock_scope(|key| match (mode_a, mode_b) { + (0, 0) => { + let set = LockSet::new((ReadLock(&a), ReadLock(&b))); + let ((ga, gb), _key) = key.lock(&set); + assert_eq!(*ga, val_a); + assert_eq!(*gb, val_b); + } + (0, _) => { + let set = LockSet::new((ReadLock(&a), WriteLock(&b))); + let ((ga, gb), _key) = key.lock(&set); + assert_eq!(*ga, val_a); + assert_eq!(*gb, val_b); + } + (_, 0) => { + let set = LockSet::new((WriteLock(&a), ReadLock(&b))); + let ((ga, gb), _key) = key.lock(&set); + assert_eq!(*ga, val_a); + assert_eq!(*gb, val_b); + } + (_, _) => { + let set = LockSet::new((WriteLock(&a), WriteLock(&b))); + let ((ga, gb), _key) = key.lock(&set); + assert_eq!(*ga, val_a); + assert_eq!(*gb, val_b); + } + }); + }); +} diff --git a/tests/rw_lock.rs b/tests/rw_lock.rs new file mode 100644 index 0000000..e933392 --- /dev/null +++ b/tests/rw_lock.rs @@ -0,0 +1,326 @@ +//! Integration tests for [`surelock::rw_lock::RwLock`]. +//! +//! Uses the default `StdRwLock` backend (std feature, enabled by +//! default). Mirrors `tests/integration.rs` in structure. + +#![allow(clippy::unwrap_used, clippy::expect_used)] + +use std::{ + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + thread, +}; + +use surelock::{ + key::lock_scope, + key_handle::KeyHandle, + mutex::Mutex, + rw_lock::{NewHigherRw, ReadLock, RwLock, WriteLock}, + set::LockSet, +}; + +type Low = surelock::level::Level<1>; +type High = surelock::level::Level<2>; + +mod construction { + use super::*; + + #[test] + fn new_and_into_inner() { + let rw: RwLock = RwLock::new(42); + assert_eq!(rw.into_inner(), 42); + } + + #[test] + fn get_mut_bypasses_lock() { + let mut rw: RwLock = RwLock::new(0); + *rw.get_mut() = 10; + assert_eq!(rw.into_inner(), 10); + } + + #[test] + fn unique_ids() { + let a: RwLock = RwLock::new(0); + let b: RwLock = RwLock::new(0); + let c: Mutex = Mutex::new(0); + // Same global LockId counter, so all three are distinct. + assert_ne!(a.id(), b.id()); + assert_ne!(a.id(), c.id()); + assert_ne!(b.id(), c.id()); + } +} + +mod single_lock { + use super::*; + + #[test] + fn read_via_convenience() { + let rw: RwLock = RwLock::new(7); + let value = lock_scope(|key| { + let (g, _key) = key.read(&rw); + *g + }); + assert_eq!(value, 7); + } + + #[test] + fn write_via_convenience() { + let mut rw: RwLock = RwLock::new(0); + lock_scope(|key| { + let (mut g, _key) = key.write(&rw); + *g = 99; + }); + assert_eq!(*rw.get_mut(), 99); + } + + #[test] + fn read_via_wrapper() { + let rw: RwLock = RwLock::new(7); + let value = lock_scope(|key| { + let reader = ReadLock(&rw); + let (g, _key) = key.lock(&reader); + *g + }); + assert_eq!(value, 7); + } + + #[test] + fn write_via_wrapper() { + let mut rw: RwLock = RwLock::new(0); + lock_scope(|key| { + let writer = WriteLock(&rw); + let (mut g, _key) = key.lock(&writer); + *g = 99; + }); + assert_eq!(*rw.get_mut(), 99); + } + + #[test] + fn lock_with_read_wrapper() { + let rw: RwLock = RwLock::new(13); + let value = lock_scope(|key| { + let (v, _key) = key.lock_with(&ReadLock(&rw), |g| *g); + v + }); + assert_eq!(value, 13); + } + + #[test] + fn lock_with_write_wrapper() { + let mut rw: RwLock = RwLock::new(0); + lock_scope(|key| { + let ((), _key) = key.lock_with(&WriteLock(&rw), |mut g| { + *g = 21; + }); + }); + assert_eq!(*rw.get_mut(), 21); + } +} + +mod concurrent_readers { + use super::*; + + /// Multiple readers may hold the lock simultaneously; observed by + /// having each reader increment a shared counter and verify the + /// peak overlap exceeded 1. + #[test] + fn many_readers_overlap() { + let rw: Arc> = Arc::new(RwLock::new(0)); + let in_flight = Arc::new(AtomicUsize::new(0)); + let peak = Arc::new(AtomicUsize::new(0)); + + let mut handles = Vec::new(); + for _ in 0..8 { + let rw = Arc::clone(&rw); + let in_flight = Arc::clone(&in_flight); + let peak = Arc::clone(&peak); + handles.push(thread::spawn(move || { + let mut handle = KeyHandle::claim(); + handle.scope(|key| { + let (g, _key) = key.read(&rw); + let now = in_flight.fetch_add(1, Ordering::SeqCst) + 1; + peak.fetch_max(now, Ordering::SeqCst); + // Hold the read briefly so others can join. + thread::sleep(std::time::Duration::from_millis(5)); + in_flight.fetch_sub(1, Ordering::SeqCst); + let _ = *g; + }); + })); + } + for h in handles { + h.join().unwrap(); + } + assert!( + peak.load(Ordering::SeqCst) > 1, + "expected concurrent readers, got peak {}", + peak.load(Ordering::SeqCst) + ); + } + + /// One writer excludes all readers and other writers; observed by + /// requiring strict serialization of writes through the rwlock. + #[test] + fn writer_is_exclusive() { + let rw: Arc> = Arc::new(RwLock::new(0)); + let mut handles = Vec::new(); + for _ in 0..32 { + let rw = Arc::clone(&rw); + handles.push(thread::spawn(move || { + let mut handle = KeyHandle::claim(); + handle.scope(|key| { + let (mut g, _key) = key.write(&rw); + let v = *g; + *g = v + 1; + }); + })); + } + for h in handles { + h.join().unwrap(); + } + // Final value reflects every increment. + let final_val = lock_scope(|key| { + let (g, _key) = key.read(&rw); + *g + }); + assert_eq!(final_val, 32); + } +} + +mod lock_set { + use super::*; + + #[test] + fn two_readers_in_set() { + let a: RwLock = RwLock::new(1); + let b: RwLock = RwLock::new(2); + let set = LockSet::new((ReadLock(&a), ReadLock(&b))); + let sum = lock_scope(|key| { + let ((ga, gb), _key) = key.lock(&set); + *ga + *gb + }); + assert_eq!(sum, 3); + } + + #[test] + fn read_and_write_in_set() { + let a: RwLock = RwLock::new(10); + let b: RwLock = RwLock::new(0); + let set = LockSet::new((ReadLock(&a), WriteLock(&b))); + lock_scope(|key| { + let ((ga, mut gb), _key) = key.lock(&set); + *gb = *ga + 5; + }); + let result = lock_scope(|key| { + let (g, _key) = key.read(&b); + *g + }); + assert_eq!(result, 15); + } + + #[test] + fn mixed_mutex_and_rwlock_in_set() { + // A Mutex and an RwLock in one set. Different backend types, + // but the relaxed tuple impl (post-PR1) admits this. + let m: Mutex = Mutex::new(5); + let rw: RwLock = RwLock::new(10); + let set = LockSet::new((&m, ReadLock(&rw))); + let sum = lock_scope(|key| { + let ((gm, grw), _key) = key.lock(&set); + *gm + *grw + }); + assert_eq!(sum, 15); + } + + #[test] + #[should_panic(expected = "duplicate")] + fn same_rwlock_read_and_write_panics() { + let rw: RwLock = RwLock::new(0); + // Runtime duplicate-id detection fires: same LockId twice. + let _set = LockSet::new((ReadLock(&rw), WriteLock(&rw))); + } + + #[test] + fn try_new_rejects_duplicate_modes() { + let rw: RwLock = RwLock::new(0); + let set = LockSet::try_new((ReadLock(&rw), WriteLock(&rw))); + assert!(set.is_err()); + } +} + +mod cross_level { + use super::*; + + #[test] + fn read_then_write_at_higher_level() { + let low: RwLock = RwLock::new(7); + let high: RwLock = RwLock::new(0); + lock_scope(|key| { + let (low_val, key) = key.read(&low); + let v = *low_val; + drop(low_val); + let (mut hg, _key) = key.write(&high); + *hg = v * 2; + }); + let final_val = lock_scope(|key| { + let (g, _key) = key.read(&high); + *g + }); + assert_eq!(final_val, 14); + } + + #[test] + fn new_higher_rw_chains() { + let config: Mutex = Mutex::new(10); + let cache: RwLock = (&config).new_higher_rw(20u32); + // Compile check: cache lives at Mutex's Level<0> + 1 = Level<1>. + let _: RwLock> = cache; + } +} + +mod subscope { + use super::*; + + #[test] + fn nested_read_and_write() { + let a: RwLock = RwLock::new(1); + let b: RwLock = RwLock::new(0); + lock_scope(|key| { + let (ga, key) = key.read(&a); + let v = *ga; + drop(ga); + + let ((), _key) = key.subscope(|inner| { + let (mut gb, _ikey) = inner.write(&b); + *gb = v * 10; + }); + }); + let b_val = lock_scope(|key| { + let (g, _key) = key.read(&b); + *g + }); + assert_eq!(b_val, 10); + } +} + +mod send_sync { + use super::*; + + const fn assert_send() {} + const fn assert_sync() {} + + #[test] + fn rwlock_is_send_and_sync() { + assert_send::>(); + assert_sync::>(); + } + + #[test] + fn rwlock_is_send_with_arc() { + // Compile-only sanity check: Arc> is Send + Sync + // for T: Send + Sync. + assert_send::>>(); + assert_sync::>>(); + } +}