From 1a48005eed8d5da1dfc8eda91992d97931fd5060 Mon Sep 17 00:00:00 2001 From: Brooklyn Zelenka Date: Sun, 5 Apr 2026 16:35:06 -0700 Subject: [PATCH] Fix thumbv6m tests --- .woodpecker/test.yml | 2 +- Cargo.lock | 9 +++++++++ Cargo.toml | 4 +++- NO_STD_GUIDE.md | 33 +++++++++++++++++++++++++++------ README.md | 40 ++++++++++++++++++++++++++-------------- design/ROADMAP.md | 22 ---------------------- 6 files changed, 66 insertions(+), 44 deletions(-) diff --git a/.woodpecker/test.yml b/.woodpecker/test.yml index 94f7717..f8d4cf8 100644 --- a/.woodpecker/test.yml +++ b/.woodpecker/test.yml @@ -54,7 +54,7 @@ steps: commands: - nix profile add nixpkgs#cachix - cachix use expede - - nix develop .#ci --command cargo check --no-default-features --features portable-atomic --target thumbv6m-none-eabi + - nix develop .#ci --command cargo check --no-default-features --features cortex-m --target thumbv6m-none-eabi depends_on: [] - name: msrv diff --git a/Cargo.lock b/Cargo.lock index be97158..ebdc0ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -123,6 +123,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "either" version = "1.15.0" @@ -289,6 +295,9 @@ name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "critical-section", +] [[package]] name = "ppv-lite86" diff --git a/Cargo.toml b/Cargo.toml index cf6e1b4..d5ba32b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ authors = [ [dependencies] lock_api = { version = "0.4", default-features = false, optional = true } -portable-atomic = { version = "1", optional = true } +portable-atomic = { version = "1", optional = true, default-features = false } thiserror = { version = "2.0", default-features = false } [dev-dependencies] @@ -34,6 +34,8 @@ lock-api = ["dep:lock_api"] levels-32 = [] levels-64 = [] portable-atomic = ["dep:portable-atomic"] +critical-section = ["portable-atomic", "portable-atomic/critical-section"] +cortex-m = ["critical-section"] [lints.rust] future_incompatible = { level = "warn", priority = -1 } diff --git a/NO_STD_GUIDE.md b/NO_STD_GUIDE.md index 272bf95..0c09884 100644 --- a/NO_STD_GUIDE.md +++ b/NO_STD_GUIDE.md @@ -6,14 +6,17 @@ Surelock supports `no_std` environments when default features are disabled. The ```toml [dependencies] -# Minimal no_std (AtomicU32 for LockId, works on Cortex-M4+, RISC-V, etc.) +# Minimal no_std (AtomicU32 for LockId, works on Cortex-M4+, RISC-V with CAS, etc.) surelock = { version = "0.1", default-features = false } # no_std with u64 LockId (requires native AtomicU64) surelock = { version = "0.1", default-features = false, features = ["atomic-u64"] } -# no_std on targets without CAS (e.g., Cortex-M0 / thumbv6m) -surelock = { version = "0.1", default-features = false, features = ["portable-atomic"] } +# no_std for Cortex-M0 / thumbv6m (no native CAS) +surelock = { version = "0.1", default-features = false, features = ["cortex-m"] } + +# no_std for other targets without CAS +surelock = { version = "0.1", default-features = false, features = ["critical-section"] } # no_std with lock_api backend (parking_lot, spin, etc.) surelock = { version = "0.1", default-features = false, features = ["lock-api"] } @@ -119,13 +122,31 @@ If you need nested locking contexts, use `key.subscope()` within an existing sco Cortex-M0 and similar targets lack hardware compare-and-swap (CAS) operations. Surelock uses `fetch_add`, `compare_exchange`, and `fetch_update` on atomics, which require CAS. -Enable the `portable-atomic` feature, which provides CAS emulation via critical sections: +For Cortex-M targets, use the `cortex-m` convenience feature: ```toml -surelock = { version = "0.1", default-features = false, features = ["portable-atomic"] } +surelock = { version = "0.1", default-features = false, features = ["cortex-m"] } +``` + +This enables `portable-atomic` with `critical-section` support. You will also need a `critical-section` implementation for your target. For Cortex-M, the [`cortex-m`](https://crates.io/crates/cortex-m) crate provides one: + +```toml +[dependencies] +surelock = { version = "0.1", default-features = false, features = ["cortex-m"] } +cortex-m = { version = "0.7", features = ["critical-section-single-core"] } ``` -This uses the [`portable-atomic`](https://crates.io/crates/portable-atomic) crate. You may also need to configure a critical section implementation for your target (e.g., via the [`critical-section`](https://crates.io/crates/critical-section) crate). +For other targets without CAS, use the `critical-section` feature directly and provide your own `critical-section` implementation: + +```toml +surelock = { version = "0.1", default-features = false, features = ["critical-section"] } +``` + +For targets that have native CAS but lack `AtomicU64` (e.g., some RISC-V), use `portable-atomic` without `critical-section`: + +```toml +surelock = { version = "0.1", default-features = false, features = ["portable-atomic"] } +``` ## Summary diff --git a/README.md b/README.md index edbe808..5856785 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,9 @@ handle.scope(|key| { ## How It Works -### `LockSet`: Fine-Grained, Implicitly Ordered +### `LockSet`: Fine-Grained, Implicitly Runtime Sorted -When you need multiple locks at the _same level_, `LockSet` handles it. Each `Mutex` gets a unique, monotonic `LockId` at creation. `LockSet` pre-sorts by these IDs and acquires in that order, every time: +When you need multiple locks at the _same level_, `LockSet` handles it. Each `Mutex` gets a unique, monotonic `LockId` at creation. `LockSet` pre-sorts by these IDs and acquires in that order, every time. The below is an exaggerated timeline to help explain how these locks work: ```text Thread A Thread B @@ -85,13 +85,23 @@ LockSet::new((&acct_1, &acct_2)) LockSet::new((&acct_2, &acct_1)) ▼ ▼ sorted: [acct_1, acct_2] sorted: [acct_1, acct_2] │ │ - ├─ lock acct_1 ├─ lock acct_1 (waits) - ├─ lock acct_2 ├─ lock acct_2 + ├─ takes acct_1 lock │ + │ ├─ waits for acct_1 lock + ├─ takes acct_2 lock │ + │ │ + ~~~~~~~~~~~~~~~~~~~~~~~TIME PASSES~~~~~~~~~~~~~~~~~~~~~~~ + │ │ + ├─ release acct_2 │ + │ │ (still waiting for lock 1 first) + ├─ release acct_1 │ + │ ├─ takes acct_1 lock + │ ├─ takes acct_2 lock + │ │ ▼ ▼ OK OK (no cycle possible) ``` -### Levels: Coarse-Grained, Compile-Time +### Levels: Coarse-Grained, Compile-Time Ordered Use `Level` types with semantic aliases: @@ -208,15 +218,17 @@ spin = { version = "0.9", features = ["lock_api", "spin_mutex"] } ## Feature Flags -| Feature | Default | Description | -|---------------------|---------|-------------------------------------------------------------------| -| `std` | yes | `StdMutex` default backend, `thread_local!` scope check | -| `atomic-u64` | yes | Use `AtomicU64` for `LockId` (default `AtomicU32` otherwise) | -| `lock-api` | no | Blanket `RawMutex` impl for any `lock_api::RawMutex` backend | -| `escape-hatch` | no | `Mutex::unchecked_lock()` -- std-like direct lock | -| `portable-atomic` | no | Use `portable-atomic` for all atomics (for targets without CAS) | -| `levels-32` | no | Extend numbered levels from 16 to 32 | -| `levels-64` | no | Extend numbered levels from 16 to 64 | +| Feature | Default | Description | +|--------------------|---------|-------------------------------------------------------------------------| +| `std` | yes | `StdMutex` default backend, `thread_local!` scope check | +| `atomic-u64` | yes | Use `AtomicU64` for `LockId` (default `AtomicU32` otherwise) | +| `lock-api` | no | Blanket `RawMutex` impl for any `lock_api::RawMutex` backend | +| `escape-hatch` | no | `Mutex::unchecked_lock()` -- std-like direct lock | +| `portable-atomic` | no | Use `portable-atomic` for all atomics | +| `critical-section` | no | Enable CAS emulation via `critical-section` (implies `portable-atomic`) | +| `cortex-m` | no | Convenience: enables `critical-section` for Cortex-M targets | +| `levels-32` | no | Extend numbered levels from 16 to 32 | +| `levels-64` | no | Extend numbered levels from 16 to 64 | ## Modules diff --git a/design/ROADMAP.md b/design/ROADMAP.md index dc262b8..240d561 100644 --- a/design/ROADMAP.md +++ b/design/ROADMAP.md @@ -2,22 +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. -## `const fn` Mutex Construction - -Static mutexes cannot currently be created directly because `LockId::next()` calls `fetch_add`, which is not `const fn`. Users work around this with `LazyLock` on `std` or runtime initialization on `no_std`. - -The most promising approach is a _sentinel ID_: `Mutex::new_static(data)` creates a mutex with a placeholder ID (`u32::MAX`). On first use (e.g., when `collect_ids` is called by `LockSet`), the real ID is assigned via a CAS operation. After initialization, the mutex behaves identically to a runtime-created one. - -```rust -static CONFIG: Mutex = Mutex::new_static(AppConfig::default()); -``` - -Open questions: - -- Two `LockSet`s containing the same static mutex could race on initialization. The CAS ensures only one thread assigns the ID, but both threads need to see the same result. `Relaxed` ordering should be sufficient since we only need uniqueness, not happens-before. -- `new_static` requires the `RawMutex` backend to be const-constructible. `StdMutex` and `spin::Mutex` both support this. -- Should the sentinel be `u32::MAX` (or `u64::MAX` with the `atomic-u64` feature), reserving it from normal allocation? - ## RwLock Support @@ -84,10 +68,4 @@ A type-state builder for creating mutex hierarchies with explicit depth control. The main use case: complex tree structures where explicit depth transitions are valuable. `new_after` covers linear chains and simple trees. `OrderChain` would add explicitness about depth transitions at the cost of more API surface. -## Numbered Levels on Nightly - -The `Level` type currently requires macro-generated impls for a fixed range (16/32/64). With `generic_const_exprs` (nightly), blanket impls like `impl LockAfter> for Level where Assert<{ B > A }>: IsTrue` would eliminate the ceiling entirely. - -Track: -When stabilized, surelock could provide a `nightly` feature that replaces the macro-generated impls with blanket const-generic impls, removing the level count limitation. -- 2.51.2