From 1ac0b49ac3f3e0a668886b98bd8f3fcfc94979d7 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Wed, 9 Mar 2022 21:00:16 +0100 Subject: [PATCH] feat(solc): add scoped reporter (#1000) * feat(solc): add scoped reporter * fix: race in tests --- ethers-solc/src/report.rs | 233 +++++++++++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 16 deletions(-) diff --git a/ethers-solc/src/report.rs b/ethers-solc/src/report.rs index 812adee6..b4b38253 100644 --- a/ethers-solc/src/report.rs +++ b/ethers-solc/src/report.rs @@ -1,17 +1,52 @@ //! Subscribe to events in the compiler pipeline +//! +//! The _reporter_ is the component of the [`Project::compile()`] pipeline which is responsible +//! for reporting on specific steps in the process. +//! +//! By default, the current reporter is a noop that does +//! nothing. +//! +//! To use another report implementation, it must be set as the current reporter. +//! There are two methods for doing so: [`with_scoped`] and +//! [`set_global`]. `with_scoped` sets the reporter for the +//! duration of a scope, while `set_global` sets a global default report +//! for the entire process. + +// https://github.com/tokio-rs/tracing/blob/master/tracing-core/src/dispatch.rs use crate::{CompilerInput, CompilerOutput, Solc}; use semver::Version; use std::{ + any::{Any, TypeId}, + cell::RefCell, error::Error, fmt, path::Path, + ptr::NonNull, sync::{ - atomic::{AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, }; +thread_local! { + static CURRENT_STATE: State = State { + scoped: RefCell::new(Report::none()), + }; +} + +static EXISTS: AtomicBool = AtomicBool::new(false); +static SCOPED_COUNT: AtomicUsize = AtomicUsize::new(0); + +// tracks the state of `GLOBAL_REPORTER` +static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET); + +const UN_SET: usize = 0; +const SETTING: usize = 1; +const SET: usize = 2; + +static mut GLOBAL_REPORTER: Option = None; + /// Install this `Reporter` as the global default if one is /// not already set. /// @@ -69,28 +104,70 @@ pub trait Reporter: 'static { /// Invoked if the import couldn't be resolved fn on_unresolved_import(&self, _import: &Path) {} + + /// If `self` is the same type as the provided `TypeId`, returns an untyped + /// [`NonNull`] pointer to that type. Otherwise, returns `None`. + /// + /// If you wish to downcast a `Reporter`, it is strongly advised to use + /// the safe API provided by [`downcast_ref`] instead. + /// + /// This API is required for `downcast_raw` to be a trait method; a method + /// signature like [`downcast_ref`] (with a generic type parameter) is not + /// object-safe, and thus cannot be a trait method for `Reporter`. This + /// means that if we only exposed `downcast_ref`, `Reporter` + /// implementations could not override the downcasting behavior + /// + /// # Safety + /// + /// The [`downcast_ref`] method expects that the pointer returned by + /// `downcast_raw` points to a valid instance of the type + /// with the provided `TypeId`. Failure to ensure this will result in + /// undefined behaviour, so implementing `downcast_raw` is unsafe. + unsafe fn downcast_raw(&self, id: TypeId) -> Option> { + if id == TypeId::of::() { + Some(NonNull::from(self).cast()) + } else { + None + } + } +} + +impl dyn Reporter { + /// Returns `true` if this `Reporter` is the same type as `T`. + pub fn is(&self) -> bool { + self.downcast_ref::().is_some() + } + + /// Returns some reference to this `Reporter` value if it is of type `T`, + /// or `None` if it isn't. + pub fn downcast_ref(&self) -> Option<&T> { + unsafe { + let raw = self.downcast_raw(TypeId::of::())?; + Some(&*(raw.cast().as_ptr())) + } + } } pub(crate) fn solc_spawn(solc: &Solc, version: &Version, input: &CompilerInput) { - with_global(|r| r.reporter.on_solc_spawn(solc, version, input)); + get_default(|r| r.reporter.on_solc_spawn(solc, version, input)); } pub(crate) fn solc_success(solc: &Solc, version: &Version, output: &CompilerOutput) { - with_global(|r| r.reporter.on_solc_success(solc, version, output)); + get_default(|r| r.reporter.on_solc_success(solc, version, output)); } #[allow(unused)] pub(crate) fn solc_installation_start(version: &Version) { - with_global(|r| r.reporter.on_solc_installation_start(version)); + get_default(|r| r.reporter.on_solc_installation_start(version)); } #[allow(unused)] pub(crate) fn solc_installation_success(version: &Version) { - with_global(|r| r.reporter.on_solc_installation_success(version)); + get_default(|r| r.reporter.on_solc_installation_success(version)); } pub(crate) fn unresolved_import(import: &Path) { - with_global(|r| r.reporter.on_unresolved_import(import)); + get_default(|r| r.reporter.on_unresolved_import(import)); } fn get_global() -> Option<&'static Report> { @@ -106,10 +183,97 @@ fn get_global() -> Option<&'static Report> { } } +/// Executes a closure with a reference to this thread's current [reporter]. +#[inline(always)] +pub fn get_default(mut f: F) -> T +where + F: FnMut(&Report) -> T, +{ + if SCOPED_COUNT.load(Ordering::Acquire) == 0 { + // fast path if no scoped reporter has been set; use the global + // default. + return if let Some(glob) = get_global() { f(glob) } else { f(&Report::none()) } + } + + get_default_scoped(f) +} + +#[inline(never)] +fn get_default_scoped(mut f: F) -> T +where + F: FnMut(&Report) -> T, +{ + CURRENT_STATE + .try_with(|state| { + let scoped = state.scoped.borrow_mut(); + f(&*scoped) + }) + .unwrap_or_else(|_| f(&Report::none())) +} + /// Executes a closure with a reference to the `Reporter`. pub fn with_global(f: impl FnOnce(&Report) -> T) -> Option { - let dispatch = get_global()?; - Some(f(dispatch)) + let report = get_global()?; + Some(f(report)) +} + +/// Sets this reporter as the scoped reporter for the duration of a closure. +pub fn with_scoped(report: &Report, f: impl FnOnce() -> T) -> T { + // When this guard is dropped, the scoped reporter will be reset to the + // prior reporter. Using this (rather than simply resetting after calling + // `f`) ensures that we always reset to the prior reporter even if `f` + // panics. + let _guard = set_scoped(report); + f() +} + +/// The report state of a thread. +struct State { + /// This thread's current scoped reporter. + scoped: RefCell, +} + +impl State { + /// Replaces the current scoped reporter on this thread with the provided + /// reporter. + /// + /// Dropping the returned `ResetGuard` will reset the scoped reporter to + /// the previous value. + #[inline] + fn set_scoped(new_report: Report) -> ScopeGuard { + let prior = CURRENT_STATE.try_with(|state| state.scoped.replace(new_report)).ok(); + EXISTS.store(true, Ordering::Release); + SCOPED_COUNT.fetch_add(1, Ordering::Release); + ScopeGuard(prior) + } +} + +/// A guard that resets the current scoped reporter to the prior +/// scoped reporter when dropped. +#[derive(Debug)] +pub struct ScopeGuard(Option); + +impl Drop for ScopeGuard { + #[inline] + fn drop(&mut self) { + SCOPED_COUNT.fetch_sub(1, Ordering::Release); + if let Some(report) = self.0.take() { + // Replace the reporter and then drop the old one outside + // of the thread-local context. + let prev = CURRENT_STATE.try_with(|state| state.scoped.replace(report)); + drop(prev) + } + } +} + +/// Sets the reporter as the scoped reporter for the duration of the lifetime +/// of the returned DefaultGuard +#[must_use = "Dropping the guard unregisters the reporter."] +pub fn set_scoped(reporter: &Report) -> ScopeGuard { + // When this guard is dropped, the scoped reporter will be reset to the + // prior default. Using this ensures that we always reset to the prior + // reporter even if the thread calling this function panics. + State::set_scoped(reporter.clone()) } /// A no-op [`Reporter`] that does nothing. @@ -165,6 +329,7 @@ impl fmt::Display for SetGlobalReporterError { impl Error for SetGlobalReporterError {} /// `Report` trace data to a [`Reporter`]. +#[derive(Clone)] pub struct Report { reporter: Arc, } @@ -184,16 +349,20 @@ impl Report { { Self { reporter: Arc::new(reporter) } } + + /// Returns `true` if this `Report` forwards to a reporter of type + /// `T`. + #[inline] + pub fn is(&self) -> bool { + ::is::(&*self.reporter) + } } -// tracks the state of `GLOBAL_REPORTER` -static GLOBAL_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET); - -const UN_SET: usize = 0; -const SETTING: usize = 1; -const SET: usize = 2; - -static mut GLOBAL_REPORTER: Option = None; +impl fmt::Debug for Report { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.pad("Report(...)") + } +} /// Sets this report as the global default for the duration of the entire program. /// @@ -216,3 +385,35 @@ fn set_global_reporter(report: Report) -> Result<(), SetGlobalReporterError> { Err(SetGlobalReporterError { _priv: () }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scoped_reporter_works() { + struct TestReporter; + impl Reporter for TestReporter {} + + with_scoped(&Report::new(TestReporter), || { + get_default(|reporter| assert!(reporter.is::())) + }); + } + + #[test] + fn global_and_scoped_reporter_works() { + get_default(|reporter| { + assert!(reporter.is::()); + }); + + set_global_reporter(Report::new(BasicStdoutReporter::default())).unwrap(); + struct TestReporter; + impl Reporter for TestReporter {} + + with_scoped(&Report::new(TestReporter), || { + get_default(|reporter| assert!(reporter.is::())) + }); + + get_default(|reporter| assert!(reporter.is::())) + } +}