Merge pull request #38 from bertptrs/lazylock

Add support for LazyLock
This commit is contained in:
2025-01-18 12:30:56 +01:00
committed by GitHub
9 changed files with 165 additions and 16 deletions

View File

@@ -6,6 +6,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Added
- On Rust 1.80 or newer, a wrapper for `std::sync::LazyLock` is now available. The MSRV has not been
changed; older versions simply don't get this wrapper.
### Changed ### Changed
- Reworked CI to better test continued support for the minimum supported Rust version - Reworked CI to better test continued support for the minimum supported Rust version

5
Cargo.lock generated
View File

@@ -25,9 +25,9 @@ checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.1.0" version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@@ -619,6 +619,7 @@ dependencies = [
name = "tracing-mutex" name = "tracing-mutex"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"autocfg",
"criterion", "criterion",
"lock_api", "lock_api",
"parking_lot", "parking_lot",

View File

@@ -36,3 +36,6 @@ backtraces = []
# Feature names do not match crate names pending namespaced features. # Feature names do not match crate names pending namespaced features.
lockapi = ["lock_api"] lockapi = ["lock_api"]
parkinglot = ["parking_lot", "lockapi"] parkinglot = ["parking_lot", "lockapi"]
[build-dependencies]
autocfg = "1.4.0"

View File

@@ -36,7 +36,7 @@ Add this dependency to your `Cargo.lock` file like any other:
```toml ```toml
[dependencies] [dependencies]
tracing-mutex = "0.2" tracing-mutex = "0.3"
``` ```
Then use the locks provided by this library instead of the ones you would use otherwise. Then use the locks provided by this library instead of the ones you would use otherwise.

10
build.rs Normal file
View File

@@ -0,0 +1,10 @@
use autocfg::AutoCfg;
fn main() {
// To avoid bumping MSRV unnecessarily, we can sniff certain features. Reevaluate this on major
// releases.
let ac = AutoCfg::new().unwrap();
ac.emit_has_path("std::sync::LazyLock");
autocfg::rerun_path("build.rs");
}

View File

@@ -193,6 +193,14 @@ impl MutexId {
unreachable!("Tried to drop lock for mutex {:?} but it wasn't held", self) unreachable!("Tried to drop lock for mutex {:?} but it wasn't held", self)
}); });
} }
/// Execute the given closure while the guard is held.
pub fn with_held<T>(&self, f: impl FnOnce() -> T) -> T {
// Note: we MUST construct the RAII guard, we cannot simply mark held + mark released, as
// f() may panic and corrupt our state.
let _guard = self.get_borrowed();
f()
}
} }
impl Default for MutexId { impl Default for MutexId {

View File

@@ -138,16 +138,14 @@ pub mod tracing {
/// This method will panic if `f` panics, poisoning this `Once`. In addition, this function /// This method will panic if `f` panics, poisoning this `Once`. In addition, this function
/// panics when the lock acquisition order is determined to be inconsistent. /// panics when the lock acquisition order is determined to be inconsistent.
pub fn call_once(&self, f: impl FnOnce()) { pub fn call_once(&self, f: impl FnOnce()) {
let _borrow = self.id.get_borrowed(); self.id.with_held(|| self.inner.call_once(f));
self.inner.call_once(f);
} }
/// Performs the given initialization routine once and only once. /// Performs the given initialization routine once and only once.
/// ///
/// This method is identical to [`Once::call_once`] except it ignores poisoning. /// This method is identical to [`Once::call_once`] except it ignores poisoning.
pub fn call_once_force(&self, f: impl FnOnce(OnceState)) { pub fn call_once_force(&self, f: impl FnOnce(OnceState)) {
let _borrow = self.id.get_borrowed(); self.id.with_held(|| self.inner.call_once_force(f));
self.inner.call_once_force(f);
} }
} }
} }

View File

@@ -30,6 +30,12 @@ pub use tracing::{
Condvar, Mutex, MutexGuard, Once, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard, Condvar, Mutex, MutexGuard, Once, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard,
}; };
#[cfg(all(has_std__sync__LazyLock, debug_assertions))]
pub use tracing::LazyLock;
#[cfg(all(has_std__sync__LazyLock, not(debug_assertions)))]
pub use std::sync::LazyLock;
/// Dependency tracing versions of [`std::sync`]. /// Dependency tracing versions of [`std::sync`].
pub mod tracing { pub mod tracing {
use std::fmt; use std::fmt;
@@ -47,6 +53,12 @@ pub mod tracing {
use crate::BorrowedMutex; use crate::BorrowedMutex;
use crate::LazyMutexId; use crate::LazyMutexId;
#[cfg(has_std__sync__LazyLock)]
pub use lazy_lock::LazyLock;
#[cfg(has_std__sync__LazyLock)]
mod lazy_lock;
/// Wrapper for [`std::sync::Mutex`]. /// Wrapper for [`std::sync::Mutex`].
/// ///
/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and /// Refer to the [crate-level][`crate`] documentation for the differences between this struct and
@@ -460,8 +472,7 @@ pub mod tracing {
where where
F: FnOnce(), F: FnOnce(),
{ {
let _guard = self.mutex_id.get_borrowed(); self.mutex_id.with_held(|| self.inner.call_once(f))
self.inner.call_once(f);
} }
/// Performs the same operation as [`call_once`][Once::call_once] except it ignores /// Performs the same operation as [`call_once`][Once::call_once] except it ignores
@@ -475,8 +486,7 @@ pub mod tracing {
where where
F: FnOnce(&OnceState), F: FnOnce(&OnceState),
{ {
let _guard = self.mutex_id.get_borrowed(); self.mutex_id.with_held(|| self.inner.call_once_force(f))
self.inner.call_once_force(f);
} }
/// Returns true if some `call_once` has completed successfully. /// Returns true if some `call_once` has completed successfully.
@@ -550,9 +560,7 @@ pub mod tracing {
/// As this method may block until initialization is complete, it participates in cycle /// As this method may block until initialization is complete, it participates in cycle
/// detection. /// detection.
pub fn set(&self, value: T) -> Result<(), T> { pub fn set(&self, value: T) -> Result<(), T> {
let _guard = self.id.get_borrowed(); self.id.with_held(|| self.inner.set(value))
self.inner.set(value)
} }
/// Gets the contents of the cell, initializing it with `f` if the cell was empty. /// Gets the contents of the cell, initializing it with `f` if the cell was empty.
@@ -562,8 +570,7 @@ pub mod tracing {
where where
F: FnOnce() -> T, F: FnOnce() -> T,
{ {
let _guard = self.id.get_borrowed(); self.id.with_held(|| self.inner.get_or_init(f))
self.inner.get_or_init(f)
} }
/// Takes the value out of this `OnceLock`, moving it back to an uninitialized state. /// Takes the value out of this `OnceLock`, moving it back to an uninitialized state.

View File

@@ -0,0 +1,117 @@
//! Wrapper implementation for LazyLock
//!
//! This lives in a separate module as LazyLock would otherwise raise our MSRV to 1.80. Reevaluate
//! this in the future.
use std::fmt;
use std::fmt::Debug;
use std::ops::Deref;
use crate::LazyMutexId;
/// Wrapper for [`std::sync::LazyLock`]
///
/// This wrapper participates in cycle detection like all other primitives in this crate. It should
/// only be possible to encounter cycles when acquiring mutexes in the initialisation function.
///
/// # Examples
///
/// ```
/// use tracing_mutex::stdsync::tracing::LazyLock;
///
/// static LOCK: LazyLock<i32> = LazyLock::new(|| {
/// println!("Hello, world!");
/// 42
/// });
///
/// // This should print "Hello, world!"
/// println!("{}", *LOCK);
/// // This should not.
/// println!("{}", *LOCK);
/// ```
pub struct LazyLock<T, F = fn() -> T> {
// MSRV violation is fine, this is gated behind a cfg! check
#[allow(clippy::incompatible_msrv)]
inner: std::sync::LazyLock<T, F>,
id: LazyMutexId,
}
impl<T, F: FnOnce() -> T> LazyLock<T, F> {
/// Creates a new lazy value with the given initializing function.
pub const fn new(f: F) -> LazyLock<T, F> {
Self {
id: LazyMutexId::new(),
// MSRV violation is fine, this is gated behind a cfg! check
#[allow(clippy::incompatible_msrv)]
inner: std::sync::LazyLock::new(f),
}
}
/// Force this lazy lock to be evaluated.
///
/// This is equivalent to dereferencing, but is more explicit.
pub fn force(this: &LazyLock<T, F>) -> &T {
this
}
}
impl<T, F: FnOnce() -> T> Deref for LazyLock<T, F> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.id.with_held(|| &*self.inner)
}
}
impl<T: Default> Default for LazyLock<T> {
/// Return a `LazyLock` that is initialized through [`Default`].
fn default() -> Self {
Self::new(Default::default)
}
}
impl<T: Debug, F> Debug for LazyLock<T, F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// Cannot implement this ourselves because the get() used is nightly, so delegate.
self.inner.fmt(f)
}
}
#[cfg(test)]
mod tests {
use crate::stdsync::Mutex;
use super::*;
#[test]
fn test_only_init_once() {
let mut init_counter = 0;
let lock = LazyLock::new(|| {
init_counter += 1;
42
});
assert_eq!(*lock, 42);
LazyLock::force(&lock);
// Ensure we can access the init counter
drop(lock);
assert_eq!(init_counter, 1);
}
#[test]
#[should_panic(expected = "Found cycle")]
fn test_panic_with_cycle() {
let mutex = Mutex::new(());
let lock = LazyLock::new(|| *mutex.lock().unwrap());
// Establish the relation from lock to mutex
LazyLock::force(&lock);
// Now do it the other way around, which should crash
let _guard = mutex.lock().unwrap();
LazyLock::force(&lock);
}
}