Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for LazyLock #38

Merged
merged 3 commits into from
Jan 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [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

- Reworked CI to better test continued support for the minimum supported Rust version
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ backtraces = []
# Feature names do not match crate names pending namespaced features.
lockapi = ["lock_api"]
parkinglot = ["parking_lot", "lockapi"]

[build-dependencies]
autocfg = "1.4.0"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Add this dependency to your `Cargo.lock` file like any other:

```toml
[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.
Expand Down
10 changes: 10 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
8 changes: 8 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ impl MutexId {
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 {
Expand Down
6 changes: 2 additions & 4 deletions src/parkinglot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,16 +138,14 @@ pub mod tracing {
/// 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.
pub fn call_once(&self, f: impl FnOnce()) {
let _borrow = self.id.get_borrowed();
self.inner.call_once(f);
self.id.with_held(|| self.inner.call_once(f));
}

/// Performs the given initialization routine once and only once.
///
/// This method is identical to [`Once::call_once`] except it ignores poisoning.
pub fn call_once_force(&self, f: impl FnOnce(OnceState)) {
let _borrow = self.id.get_borrowed();
self.inner.call_once_force(f);
self.id.with_held(|| self.inner.call_once_force(f));
}
}
}
Expand Down
25 changes: 16 additions & 9 deletions src/stdsync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ pub use tracing::{
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`].
pub mod tracing {
use std::fmt;
Expand All @@ -47,6 +53,12 @@ pub mod tracing {
use crate::BorrowedMutex;
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`].
///
/// Refer to the [crate-level][`crate`] documentation for the differences between this struct and
Expand Down Expand Up @@ -460,8 +472,7 @@ pub mod tracing {
where
F: FnOnce(),
{
let _guard = self.mutex_id.get_borrowed();
self.inner.call_once(f);
self.mutex_id.with_held(|| self.inner.call_once(f))
}

/// Performs the same operation as [`call_once`][Once::call_once] except it ignores
Expand All @@ -475,8 +486,7 @@ pub mod tracing {
where
F: FnOnce(&OnceState),
{
let _guard = self.mutex_id.get_borrowed();
self.inner.call_once_force(f);
self.mutex_id.with_held(|| self.inner.call_once_force(f))
}

/// Returns true if some `call_once` has completed successfully.
Expand Down Expand Up @@ -550,9 +560,7 @@ pub mod tracing {
/// As this method may block until initialization is complete, it participates in cycle
/// detection.
pub fn set(&self, value: T) -> Result<(), T> {
let _guard = self.id.get_borrowed();

self.inner.set(value)
self.id.with_held(|| self.inner.set(value))
}

/// Gets the contents of the cell, initializing it with `f` if the cell was empty.
Expand All @@ -562,8 +570,7 @@ pub mod tracing {
where
F: FnOnce() -> T,
{
let _guard = self.id.get_borrowed();
self.inner.get_or_init(f)
self.id.with_held(|| self.inner.get_or_init(f))
}

/// Takes the value out of this `OnceLock`, moving it back to an uninitialized state.
Expand Down
117 changes: 117 additions & 0 deletions src/stdsync/tracing/lazy_lock.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading