diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index a6a0477..36c8261 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -8,7 +8,22 @@ on: jobs: clippy: runs-on: ubuntu-latest + strategy: + matrix: + include: + - rust: 1.54 + features: '' + - rust: 1.63 + features: '' + - rust: 1.63 + features: 'std' steps: - uses: actions/checkout@v2 - - name: Build - run: cargo clippy + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{matrix.rust}} + components: clippy + override: true + - name: Clippy check + run: cargo clippy --features "${{matrix.features}}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be560a1..b76b293 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,22 @@ on: jobs: test: runs-on: ubuntu-latest + strategy: + matrix: + include: + - rust: 1.54 + features: '' + - rust: 1.63 + features: '' + - rust: 1.63 + features: 'std' steps: - uses: actions/checkout@v2 + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{matrix.rust}} + components: clippy + override: true - name: Test - run: cargo test \ No newline at end of file + run: cargo test --features "${{matrix.features}}" diff --git a/Cargo.toml b/Cargo.toml index 2cae529..c1a6780 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "critical-section" version = "1.0.0" -edition = "2021" +edition = "2018" description = "Cross-platform critical section" repository = "https://github.com/rust-embedded/critical-section" readme = "README.md" @@ -14,6 +14,11 @@ categories = [ [features] +# Enable a critical-section implementation for platforms supporting `std`, based on `std::sync::Mutex`. +# If you enable this, the `critical-section` crate itself provides the implementation, +# you don't have to get another crate to to do it. +std = ["restore-state-bool"] + # Set the RestoreState size. # The crate supplying the critical section implementation can set ONE of them. # Other crates MUST NOT set any of these. diff --git a/README.md b/README.md index b836d50..27897f5 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,16 @@ This crate solves the problem by providing this missing universal API. - It provides functions `acquire`, `release` and `with` that libraries can directly use. - It provides a way for any crate to supply an implementation. This allows "target support" crates such as architecture crates (`cortex-m`, `riscv`), RTOS bindings, or HALs for multicore chips to supply the correct implementation so that all the crates in the dependency tree automatically use it. -## Usage +## Usage in `no-std` binaries. First, add a dependency on a crate providing a critical section implementation. Enable the `critical-section-*` Cargo feature if required by the crate. +Implementations are typically provided by either architecture-support crates (`cortex-m`, `riscv`, etc), or HAL crates. + +For example, for single-core Cortex-M targets, you can use: + ```toml +[dependencies] cortex-m = { version = "0.7.6", features = ["critical-section-single-core"]} ``` @@ -55,14 +60,27 @@ critical_section::with(|cs| { MY_VALUE.borrow(cs).set(42); }); -# struct MyCriticalSection; -# critical_section::set_impl!(MyCriticalSection); -# unsafe impl critical_section::Impl for MyCriticalSection { -# unsafe fn acquire() -> () {} -# unsafe fn release(token: ()) {} +# #[cfg(not(feature = "std"))] // needed for `cargo test --features std` +# mod no_std { +# struct MyCriticalSection; +# critical_section::set_impl!(MyCriticalSection); +# unsafe impl critical_section::Impl for MyCriticalSection { +# unsafe fn acquire() -> () {} +# unsafe fn release(token: ()) {} +# } # } ``` +## Usage in `std` binaries. + +Add the `critical-section` dependency to `Cargo.toml` enabling the `std` feature. This makes the `critical-section` crate itself +provide an implementation based on `std::sync::Mutex`, so you don't have to add any other dependency. + +```toml +[dependencies] +critical-section = { version = "1.1", features = ["std"]} +``` + ## Usage in libraries If you're writing a library intended to be portable across many targets, simply add a dependency on `critical-section` @@ -73,6 +91,20 @@ This has to be done by the end user, enabling the correct implementation for the **Do not** enable any Cargo feature in `critical-section`. +## Usage in `std` tests for `no-std` libraries. + +If you want to run `std`-using tests in otherwise `no-std` libraries, enable the `std` feature in `dev-dependencies` only. +This way the main target will use the `no-std` implementation chosen by the end-user's binary, and only the test targets +will use the `std` implementation. + +```toml +[dependencies] +critical-section = "1.1" + +[dev-dependencies] +critical-section = { version = "1.1", features = ["std"]} +``` + ## Providing an implementation Crates adding support for a particular architecture, chip or operating system should provide a critical section implementation. @@ -93,6 +125,8 @@ critical-section = { version = "1.0", optional = true } Then, provide the critical implementation like this: ```rust +# #[cfg(not(feature = "std"))] // needed for `cargo test --features std` +# mod no_std { // This is a type alias for the enabled `restore-state-*` feature. // For example, it is `bool` if you enable `restore-state-bool`. use critical_section::RawRestoreState; @@ -109,6 +143,7 @@ unsafe impl critical_section::Impl for MyCriticalSection { // TODO } } +# } ``` ## Troubleshooting @@ -160,6 +195,15 @@ be generic. - It would allow mixing different critical section implementations in the same program, which would be unsound. +## Minimum Supported Rust Version (MSRV) + +This crate is guaranteed to compile on the following Rust versions: + +- If the `std` feature is not enabled: stable Rust 1.54 and up. +- If the `std` feature is enabled: stable Rust 1.63 and up. + +It might compile with older versions but that may change in any new patch release. + ## License This work is licensed under either of diff --git a/src/lib.rs b/src/lib.rs index a8744fd..9569250 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,9 @@ -#![no_std] +#![cfg_attr(not(feature = "std"), no_std)] #![doc = include_str!("../README.md")] mod mutex; +#[cfg(feature = "std")] +mod std; use core::marker::PhantomData; @@ -116,7 +118,6 @@ pub type RawRestoreState = RawRestoreStateInner; /// /// User code uses [`RestoreState`] opaquely, critical section implementations /// use [`RawRestoreState`] so that they can use the inner value. -#[repr(transparent)] #[derive(Clone, Copy, Debug)] pub struct RestoreState(RawRestoreState); @@ -238,6 +239,8 @@ pub unsafe trait Impl { /// # Example /// /// ``` +/// # #[cfg(not(feature = "std"))] // needed for `cargo test --features std` +/// # mod no_std { /// use critical_section::RawRestoreState; /// /// struct MyCriticalSection; @@ -252,7 +255,7 @@ pub unsafe trait Impl { /// // ... /// } /// } -/// +/// # } #[macro_export] macro_rules! set_impl { ($t: ty) => { diff --git a/src/std.rs b/src/std.rs new file mode 100644 index 0000000..0e72e02 --- /dev/null +++ b/src/std.rs @@ -0,0 +1,49 @@ +use std::cell::Cell; +use std::mem::MaybeUninit; +use std::sync::{Mutex, MutexGuard}; + +static GLOBAL_MUTEX: Mutex<()> = Mutex::new(()); + +// This is initialized if a thread has acquired the CS, uninitialized otherwise. +static mut GLOBAL_GUARD: MaybeUninit> = MaybeUninit::uninit(); + +std::thread_local!(static IS_LOCKED: Cell = Cell::new(false)); + +struct StdCriticalSection; +crate::set_impl!(StdCriticalSection); + +unsafe impl crate::Impl for StdCriticalSection { + unsafe fn acquire() -> bool { + // Allow reentrancy by checking thread local state + IS_LOCKED.with(|l| { + if l.get() { + // CS already acquired in the current thread. + return true; + } + + // Note: it is fine to set this flag *before* acquiring the mutex because it's thread local. + // No other thread can see its value, there's no potential for races. + // This way, we hold the mutex for slightly less time. + l.set(true); + + // Not acquired in the current thread, acquire it. + let guard = GLOBAL_MUTEX.lock().unwrap(); + GLOBAL_GUARD.write(guard); + false + }) + } + + unsafe fn release(nested_cs: bool) { + if !nested_cs { + // SAFETY: As per the acquire/release safety contract, release can only be called + // if the critical section is acquired in the current thread, + // in which case we know the GLOBAL_GUARD is initialized. + GLOBAL_GUARD.assume_init_drop(); + + // Note: it is fine to clear this flag *after* releasing the mutex because it's thread local. + // No other thread can see its value, there's no potential for races. + // This way, we hold the mutex for slightly less time. + IS_LOCKED.with(|l| l.set(false)); + } + } +}