Skip to content

Commit

Permalink
Implement lazy_static support (#93)
Browse files Browse the repository at this point in the history
* Implement `lazy_static` support

This change adds support for the `lazy_static` crate. It's mostly built
around a `Once` cell for each static plus some global storage.

* Generalize warnings and now warn on lazy_static drops
  • Loading branch information
jamesbornholt authored Jan 25, 2023
1 parent 2f52049 commit 97e0396
Show file tree
Hide file tree
Showing 7 changed files with 466 additions and 9 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 0.6.0 (January 24, 2023)

This version renames the [`silence_atomic_ordering_warning` configuration option](https://docs.rs/shuttle/0.5.0/shuttle/struct.Config.html#structfield.silence_atomic_ordering_warning) to `silence_warnings`, as well as the corresponding environment variables, to enable future warnings to be controlled by the same mechanism.

* Implement `lazy_static` support (#93)

# 0.5.0 (November 22, 2022)

This version updates the embedded `rand` library to v0.8.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "shuttle"
version = "0.5.0"
version = "0.6.0"
edition = "2021"
license = "Apache-2.0"
description = "A library for testing concurrent Rust code"
Expand Down
146 changes: 146 additions & 0 deletions src/lazy_static.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
//! Shuttle's implementation of the [`lazy_static`] crate, v1.4.0.
//!
//! Using this structure, it is possible to have `static`s that require code to be executed at
//! runtime in order to be initialized. Lazy statics should be created with the
//! [`lazy_static!`](crate::lazy_static!) macro.
//!
//! # Warning about drop behavior
//!
//! Shuttle's implementation of `lazy_static` will drop the static value at the end of an execution,
//! and so run the value's [`Drop`] implementation. The actual `lazy_static` crate does not drop the
//! static values, so this difference may cause false positives.
//!
//! To disable the warning printed about this issue, set the `SHUTTLE_SILENCE_WARNINGS` environment
//! variable to any value, or set the [`silence_warnings`](crate::Config::silence_warnings) field of
//! [`Config`](crate::Config) to true.
//!
//! [`lazy_static`]: https://crates.io/crates/lazy_static

use crate::runtime::execution::ExecutionState;
use crate::runtime::storage::StorageKey;
use crate::sync::Once;
use std::marker::PhantomData;

/// Shuttle's implementation of `lazy_static::Lazy` (aka the unstable `std::lazy::Lazy`).
// Sadly, the fields of this thing need to be public because function pointers in const fns are
// unstable, so an explicit instantiation is the only way to construct this struct. User code should
// not rely on these fields.
pub struct Lazy<T: Sync> {
#[doc(hidden)]
pub cell: Once,
#[doc(hidden)]
pub init: fn() -> T,
#[doc(hidden)]
pub _p: PhantomData<T>,
}

impl<T: Sync> std::fmt::Debug for Lazy<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Lazy").finish_non_exhaustive()
}
}

impl<T: Sync> Lazy<T> {
/// Get a reference to the lazy value, initializing it first if necessary.
pub fn get(&'static self) -> &T {
// Safety: see the usage below
unsafe fn extend_lt<'a, T>(t: &'a T) -> &'static T {
std::mem::transmute(t)
}

// We implement lazy statics by using a `Once` to mediate initialization of the static, just
// as the real implementation does. If two threads race on initializing the static, they
// will race on the `Once::call_once`, and only one of them will initialize the static. Once
// it's initialized, all future accesses can bypass the `Once` entirely and just access the
// storage cell.

let initialize = ExecutionState::with(|state| state.get_storage::<_, DropGuard<T>>(self).is_none());

if initialize {
// There's not yet a value for this static, so try to initialize it (possibly racing)
self.cell.call_once(|| {
let value = (self.init)();
ExecutionState::with(|state| state.init_storage(self, DropGuard(value)));
});
}

// At this point we're guaranteed that a value exists for this static, so read it
ExecutionState::with(|state| {
let value: &DropGuard<T> = state.get_storage(self).expect("should be initialized");
// Safety: this *isn't* safe. We are promoting to a `'static` lifetime here, but this
// object does not actually live that long. It would be possible for this reference to
// escape the client code and be used after it becomes invalid when the execution ends.
// But there's not really any way around this -- the semantics of static values and
// Shuttle executions are incompatible.
//
// In reality, this should be safe because any code that uses a Shuttle `lazy_static`
// probably uses a regular `lazy_static` in its non-test version, and if that version
// compiles then the Shuttle version is also safe. We choose this unsafe path because
// it's intended to be used only in testing code, which we want to remain as compatible
// with real-world code as possible and so needs to preserve the semantics of statics.
//
// See also https://github.com/tokio-rs/loom/pull/125
unsafe { extend_lt(&value.0) }
})
}
}

impl<T: Sync> From<&Lazy<T>> for StorageKey {
fn from(lazy: &Lazy<T>) -> Self {
StorageKey(lazy as *const _ as usize, 0x3)
}
}

/// Support trait for enabling a few common operation on lazy static values.
///
/// This is implemented by each defined lazy static, and used by the free functions in this crate.
pub trait LazyStatic {
#[doc(hidden)]
fn initialize(lazy: &Self);
}

/// Takes a shared reference to a lazy static and initializes it if it has not been already.
///
/// This can be used to control the initialization point of a lazy static.
pub fn initialize<T: LazyStatic>(lazy: &T) {
LazyStatic::initialize(lazy);
}

static PRINTED_DROP_WARNING: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);

fn maybe_warn_about_drop() {
use owo_colors::OwoColorize;
use std::sync::atomic::Ordering;

if PRINTED_DROP_WARNING
.compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
if std::env::var("SHUTTLE_SILENCE_WARNINGS").is_ok() {
return;
}

if ExecutionState::with(|state| state.config.silence_warnings) {
return;
}

eprintln!(
"{}: Shuttle runs the `Drop` method of `lazy_static` values at the end of an execution, \
unlike the actual `lazy_static` implementation. This difference may cause false positives. \
See https://docs.rs/shuttle/*/shuttle/lazy_static/index.html#warning-about-drop-behavior \
for details or to disable this warning.",
"WARNING".yellow(),
);
}
}

/// Small wrapper to trigger the warning about drop behavior when a `lazy_static` value drops for
/// the first time.
#[derive(Debug)]
struct DropGuard<T>(T);

impl<T> Drop for DropGuard<T> {
fn drop(&mut self) {
maybe_warn_about_drop();
}
}
78 changes: 74 additions & 4 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@

pub mod future;
pub mod hint;
pub mod lazy_static;
pub mod rand;
pub mod sync;
pub mod thread;
Expand Down Expand Up @@ -213,9 +214,12 @@ pub struct Config {
/// not abort a currently running test iteration; the limit is only checked between iterations.
pub max_time: Option<std::time::Duration>,

/// Whether to enable warnings about [Shuttle's unsound implementation of
/// `atomic`](crate::sync::atomic#warning-about-relaxed-behaviors).
pub silence_atomic_ordering_warning: bool,
/// Whether to silence warnings about Shuttle behaviors that may miss bugs or introduce false
/// positives:
/// 1. [Unsound implementation of `atomic`](crate::sync::atomic#warning-about-relaxed-behaviors)
/// may miss bugs
/// 2. [`lazy_static` values are dropped](mod@crate::lazy_static) at the end of an execution
pub silence_warnings: bool,
}

impl Config {
Expand All @@ -226,7 +230,7 @@ impl Config {
failure_persistence: FailurePersistence::Print,
max_steps: MaxSteps::FailAfter(1_000_000),
max_time: None,
silence_atomic_ordering_warning: false,
silence_warnings: false,
}
}
}
Expand Down Expand Up @@ -414,3 +418,69 @@ macro_rules! __thread_local_inner {
};
}
}

/// Declare a new [lazy static value](crate::lazy_static::Lazy), like the `lazy_static` crate.
// These macros are copied from the lazy_static crate.
#[macro_export]
macro_rules! lazy_static {
($(#[$attr:meta])* static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
// use `()` to explicitly forward the information about private items
$crate::__lazy_static_internal!($(#[$attr])* () static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
$crate::__lazy_static_internal!($(#[$attr])* (pub) static ref $N : $T = $e; $($t)*);
};
($(#[$attr:meta])* pub ($($vis:tt)+) static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
$crate::__lazy_static_internal!($(#[$attr])* (pub ($($vis)+)) static ref $N : $T = $e; $($t)*);
};
() => ()
}

#[macro_export]
#[doc(hidden)]
macro_rules! __lazy_static_internal {
// optional visibility restrictions are wrapped in `()` to allow for
// explicitly passing otherwise implicit information about private items
($(#[$attr:meta])* ($($vis:tt)*) static ref $N:ident : $T:ty = $e:expr; $($t:tt)*) => {
$crate::__lazy_static_internal!(@MAKE TY, $(#[$attr])*, ($($vis)*), $N);
$crate::__lazy_static_internal!(@TAIL, $N : $T = $e);
$crate::lazy_static!($($t)*);
};
(@TAIL, $N:ident : $T:ty = $e:expr) => {
impl ::std::ops::Deref for $N {
type Target = $T;
fn deref(&self) -> &$T {
#[inline(always)]
fn __static_ref_initialize() -> $T { $e }

#[inline(always)]
fn __stability() -> &'static $T {
static LAZY: $crate::lazy_static::Lazy<$T> =
$crate::lazy_static::Lazy {
cell: $crate::sync::Once::new(),
init: __static_ref_initialize,
_p: std::marker::PhantomData,
};
LAZY.get()
}
__stability()
}
}
impl $crate::lazy_static::LazyStatic for $N {
fn initialize(lazy: &Self) {
let _ = &**lazy;
}
}
};
// `vis` is wrapped in `()` to prevent parsing ambiguity
(@MAKE TY, $(#[$attr:meta])*, ($($vis:tt)*), $N:ident) => {
#[allow(missing_copy_implementations)]
#[allow(non_camel_case_types)]
#[allow(dead_code)]
$(#[$attr])*
$($vis)* struct $N {__private_field: ()}
#[doc(hidden)]
$($vis)* static $N: $N = $N {__private_field: ()};
};
() => ()
}
7 changes: 3 additions & 4 deletions src/sync/atomic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,8 @@
//! correctness, the [Loom] crate provides support for reasoning about Acquire and Release orderings
//! and partial support for Relaxed orderings.
//!
//! To disable the warning printed about this issue, set the `SHUTTLE_SILENCE_ORDERING_WARNING`
//! environment variable to any value, or set the
//! [`silence_atomic_ordering_warning`](crate::Config::silence_atomic_ordering_warning) field of
//! To disable the warning printed about this issue, set the `SHUTTLE_SILENCE_WARNINGS` environment
//! variable to any value, or set the [`silence_warnings`](crate::Config::silence_warnings) field of
//! [`Config`](crate::Config) to true.
//!
//! [Loom]: https://crates.io/crates/loom
Expand Down Expand Up @@ -81,7 +80,7 @@ fn maybe_warn_about_ordering(order: Ordering) {
return;
}

if ExecutionState::with(|state| state.config.silence_atomic_ordering_warning) {
if ExecutionState::with(|state| state.config.silence_warnings) {
return;
}

Expand Down
Loading

0 comments on commit 97e0396

Please sign in to comment.