diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4882f77..5b89bad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,15 @@ jobs: toolchain: ${{matrix.rust}} - run: cargo test - run: cargo check --no-default-features + - run: cargo check --features backtrace + + backtrace: + name: Rust 1.42.0 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: dtolnay/rust-toolchain@1.42.0 + - run: cargo check --features backtrace nostd: name: Rust 1.36.0 @@ -37,6 +46,14 @@ jobs: - uses: dtolnay/rust-toolchain@1.34.0 - run: cargo check + windows: + name: Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - uses: dtolnay/rust-toolchain@stable + - run: cargo check --features backtrace + clippy: name: Clippy runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index e3f92ee..a116f8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,9 @@ categories = ["rust-patterns"] default = ["std"] std = [] +[dependencies] +backtrace = { version = "0.3", optional = true } + [dev-dependencies] futures = { version = "0.3", default-features = false } rustversion = "1.0" diff --git a/build.rs b/build.rs index a7dc8f2..1a184c9 100644 --- a/build.rs +++ b/build.rs @@ -4,6 +4,11 @@ use std::path::Path; use std::process::{Command, ExitStatus, Stdio}; use std::str; +#[cfg(all(feature = "backtrace", not(feature = "std")))] +compile_error! { + "`backtrace` feature without `std` feature is not supported" +} + // This code exercises the surface area that we expect of the std Backtrace // type. If the current toolchain is able to compile it, we go ahead and use // backtrace in anyhow. diff --git a/src/backtrace.rs b/src/backtrace.rs index 81de6a7..1fa20a6 100644 --- a/src/backtrace.rs +++ b/src/backtrace.rs @@ -1,17 +1,34 @@ #[cfg(backtrace)] -pub(crate) use std::backtrace::Backtrace; +pub(crate) use std::backtrace::{Backtrace, BacktraceStatus}; -#[cfg(not(backtrace))] +#[cfg(all(not(backtrace), feature = "backtrace"))] +pub(crate) use self::capture::{Backtrace, BacktraceStatus}; + +#[cfg(not(any(backtrace, feature = "backtrace")))] pub(crate) enum Backtrace {} #[cfg(backtrace)] +macro_rules! impl_backtrace { + () => { + std::backtrace::Backtrace + }; +} + +#[cfg(all(not(backtrace), feature = "backtrace"))] +macro_rules! impl_backtrace { + () => { + impl core::fmt::Debug + core::fmt::Display + }; +} + +#[cfg(any(backtrace, feature = "backtrace"))] macro_rules! backtrace { () => { Some(crate::backtrace::Backtrace::capture()) }; } -#[cfg(not(backtrace))] +#[cfg(not(any(backtrace, feature = "backtrace")))] macro_rules! backtrace { () => { None @@ -28,9 +45,357 @@ macro_rules! backtrace_if_absent { }; } -#[cfg(all(feature = "std", not(backtrace)))] +#[cfg(all(feature = "std", not(backtrace), feature = "backtrace"))] +macro_rules! backtrace_if_absent { + ($err:expr) => { + backtrace!() + }; +} + +#[cfg(all(feature = "std", not(backtrace), not(feature = "backtrace")))] macro_rules! backtrace_if_absent { ($err:expr) => { None }; } + +#[cfg(all(not(backtrace), feature = "backtrace"))] +mod capture { + use backtrace::{BacktraceFmt, BytesOrWideString, Frame, PrintFmt, SymbolName}; + use core::cell::UnsafeCell; + use core::fmt::{self, Debug, Display}; + use core::sync::atomic::{AtomicUsize, Ordering}; + use std::borrow::Cow; + use std::env; + use std::path::{self, Path, PathBuf}; + use std::sync::Once; + + pub(crate) struct Backtrace { + inner: Inner, + } + + pub(crate) enum BacktraceStatus { + Unsupported, + Disabled, + Captured, + } + + enum Inner { + Unsupported, + Disabled, + Captured(LazilyResolvedCapture), + } + + struct Capture { + actual_start: usize, + resolved: bool, + frames: Vec, + } + + struct BacktraceFrame { + frame: Frame, + symbols: Vec, + } + + struct BacktraceSymbol { + name: Option>, + filename: Option, + lineno: Option, + colno: Option, + } + + enum BytesOrWide { + Bytes(Vec), + Wide(Vec), + } + + impl Debug for Backtrace { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let capture = match &self.inner { + Inner::Unsupported => return fmt.write_str(""), + Inner::Disabled => return fmt.write_str(""), + Inner::Captured(c) => c.force(), + }; + + let frames = &capture.frames[capture.actual_start..]; + + write!(fmt, "Backtrace ")?; + + let mut dbg = fmt.debug_list(); + + for frame in frames { + if frame.frame.ip().is_null() { + continue; + } + + dbg.entries(&frame.symbols); + } + + dbg.finish() + } + } + + impl Debug for BacktraceFrame { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let mut dbg = fmt.debug_list(); + dbg.entries(&self.symbols); + dbg.finish() + } + } + + impl Debug for BacktraceSymbol { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{{ ")?; + + if let Some(fn_name) = self.name.as_ref().map(|b| SymbolName::new(b)) { + write!(fmt, "fn: \"{:#}\"", fn_name)?; + } else { + write!(fmt, "fn: ")?; + } + + if let Some(fname) = self.filename.as_ref() { + write!(fmt, ", file: \"{:?}\"", fname)?; + } + + if let Some(line) = self.lineno { + write!(fmt, ", line: {:?}", line)?; + } + + write!(fmt, " }}") + } + } + + impl Debug for BytesOrWide { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + output_filename( + fmt, + match self { + BytesOrWide::Bytes(w) => BytesOrWideString::Bytes(w), + BytesOrWide::Wide(w) => BytesOrWideString::Wide(w), + }, + PrintFmt::Short, + env::current_dir().as_ref().ok(), + ) + } + } + + impl Backtrace { + fn enabled() -> bool { + static ENABLED: AtomicUsize = AtomicUsize::new(0); + match ENABLED.load(Ordering::SeqCst) { + 0 => {} + 1 => return false, + _ => return true, + } + let enabled = match env::var_os("RUST_LIB_BACKTRACE") { + Some(s) => s != "0", + None => match env::var_os("RUST_BACKTRACE") { + Some(s) => s != "0", + None => false, + }, + }; + ENABLED.store(enabled as usize + 1, Ordering::SeqCst); + enabled + } + + #[inline(never)] // want to make sure there's a frame here to remove + pub(crate) fn capture() -> Backtrace { + if Backtrace::enabled() { + Backtrace::create(Backtrace::capture as usize) + } else { + let inner = Inner::Disabled; + Backtrace { inner } + } + } + + // Capture a backtrace which starts just before the function addressed + // by `ip` + fn create(ip: usize) -> Backtrace { + let mut frames = Vec::new(); + let mut actual_start = None; + backtrace::trace(|frame| { + frames.push(BacktraceFrame { + frame: frame.clone(), + symbols: Vec::new(), + }); + if frame.symbol_address() as usize == ip && actual_start.is_none() { + actual_start = Some(frames.len() + 1); + } + true + }); + + // If no frames came out assume that this is an unsupported platform + // since `backtrace` doesn't provide a way of learning this right + // now, and this should be a good enough approximation. + let inner = if frames.is_empty() { + Inner::Unsupported + } else { + Inner::Captured(LazilyResolvedCapture::new(Capture { + actual_start: actual_start.unwrap_or(0), + frames, + resolved: false, + })) + }; + + Backtrace { inner } + } + + pub(crate) fn status(&self) -> BacktraceStatus { + match self.inner { + Inner::Unsupported => BacktraceStatus::Unsupported, + Inner::Disabled => BacktraceStatus::Disabled, + Inner::Captured(_) => BacktraceStatus::Captured, + } + } + } + + impl Display for Backtrace { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + let capture = match &self.inner { + Inner::Unsupported => return fmt.write_str("unsupported backtrace"), + Inner::Disabled => return fmt.write_str("disabled backtrace"), + Inner::Captured(c) => c.force(), + }; + + let full = fmt.alternate(); + let (frames, style) = if full { + (&capture.frames[..], PrintFmt::Full) + } else { + (&capture.frames[capture.actual_start..], PrintFmt::Short) + }; + + // When printing paths we try to strip the cwd if it exists, + // otherwise we just print the path as-is. Note that we also only do + // this for the short format, because if it's full we presumably + // want to print everything. + let cwd = env::current_dir(); + let mut print_path = move |fmt: &mut fmt::Formatter, path: BytesOrWideString| { + output_filename(fmt, path, style, cwd.as_ref().ok()) + }; + + let mut f = BacktraceFmt::new(fmt, style, &mut print_path); + f.add_context()?; + for frame in frames { + let mut f = f.frame(); + if frame.symbols.is_empty() { + f.print_raw(frame.frame.ip(), None, None, None)?; + } else { + for symbol in frame.symbols.iter() { + f.print_raw_with_column( + frame.frame.ip(), + symbol.name.as_ref().map(|b| SymbolName::new(b)), + symbol.filename.as_ref().map(|b| match b { + BytesOrWide::Bytes(w) => BytesOrWideString::Bytes(w), + BytesOrWide::Wide(w) => BytesOrWideString::Wide(w), + }), + symbol.lineno, + symbol.colno, + )?; + } + } + } + f.finish()?; + Ok(()) + } + } + + struct LazilyResolvedCapture { + sync: Once, + capture: UnsafeCell, + } + + impl LazilyResolvedCapture { + fn new(capture: Capture) -> Self { + LazilyResolvedCapture { + sync: Once::new(), + capture: UnsafeCell::new(capture), + } + } + + fn force(&self) -> &Capture { + self.sync.call_once(|| { + // Safety: This exclusive reference can't overlap with any + // others. `Once` guarantees callers will block until this + // closure returns. `Once` also guarantees only a single caller + // will enter this closure. + unsafe { &mut *self.capture.get() }.resolve(); + }); + + // Safety: This shared reference can't overlap with the exclusive + // reference above. + unsafe { &*self.capture.get() } + } + } + + // Safety: Access to the inner value is synchronized using a thread-safe + // `Once`. So long as `Capture` is `Sync`, `LazilyResolvedCapture` is too + unsafe impl Sync for LazilyResolvedCapture where Capture: Sync {} + + impl Capture { + fn resolve(&mut self) { + // If we're already resolved, nothing to do! + if self.resolved { + return; + } + self.resolved = true; + + for frame in self.frames.iter_mut() { + let symbols = &mut frame.symbols; + let frame = &frame.frame; + backtrace::resolve_frame(frame, |symbol| { + symbols.push(BacktraceSymbol { + name: symbol.name().map(|m| m.as_bytes().to_vec()), + filename: symbol.filename_raw().map(|b| match b { + BytesOrWideString::Bytes(b) => BytesOrWide::Bytes(b.to_owned()), + BytesOrWideString::Wide(b) => BytesOrWide::Wide(b.to_owned()), + }), + lineno: symbol.lineno(), + colno: symbol.colno(), + }); + }); + } + } + } + + // Prints the filename of the backtrace frame. + fn output_filename( + fmt: &mut fmt::Formatter, + bows: BytesOrWideString, + print_fmt: PrintFmt, + cwd: Option<&PathBuf>, + ) -> fmt::Result { + let file: Cow = match bows { + #[cfg(unix)] + BytesOrWideString::Bytes(bytes) => { + use std::os::unix::ffi::OsStrExt; + Path::new(std::ffi::OsStr::from_bytes(bytes)).into() + } + #[cfg(not(unix))] + BytesOrWideString::Bytes(bytes) => { + Path::new(std::str::from_utf8(bytes).unwrap_or("")).into() + } + #[cfg(windows)] + BytesOrWideString::Wide(wide) => { + use std::os::windows::ffi::OsStringExt; + Cow::Owned(std::ffi::OsString::from_wide(wide).into()) + } + #[cfg(not(windows))] + BytesOrWideString::Wide(_wide) => Path::new("").into(), + }; + if print_fmt == PrintFmt::Short && file.is_absolute() { + if let Some(cwd) = cwd { + if let Ok(stripped) = file.strip_prefix(&cwd) { + if let Some(s) = stripped.to_str() { + return write!(fmt, ".{}{}", path::MAIN_SEPARATOR, s); + } + } + } + } + Display::fmt(&file.display(), fmt) + } +} + +fn _assert_send_sync() { + fn _assert() {} + _assert::(); +} diff --git a/src/error.rs b/src/error.rs index ca51f28..415fd74 100644 --- a/src/error.rs +++ b/src/error.rs @@ -86,6 +86,8 @@ impl Error { object_downcast: object_downcast::, object_downcast_mut: object_downcast_mut::, object_drop_rest: object_drop_front::, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: no_backtrace, }; // Safety: passing vtable that operates on the right type E. @@ -107,6 +109,8 @@ impl Error { object_downcast: object_downcast::, object_downcast_mut: object_downcast_mut::, object_drop_rest: object_drop_front::, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: no_backtrace, }; // Safety: MessageError is repr(transparent) so it is okay for the @@ -129,6 +133,8 @@ impl Error { object_downcast: object_downcast::, object_downcast_mut: object_downcast_mut::, object_drop_rest: object_drop_front::, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: no_backtrace, }; // Safety: DisplayError is repr(transparent) so it is okay for the @@ -152,6 +158,8 @@ impl Error { object_downcast: context_downcast::, object_downcast_mut: context_downcast_mut::, object_drop_rest: context_drop_rest::, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: no_backtrace, }; // Safety: passing vtable that operates on the right type. @@ -173,6 +181,8 @@ impl Error { object_downcast: object_downcast::>, object_downcast_mut: object_downcast_mut::>, object_drop_rest: object_drop_front::>, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: no_backtrace, }; // Safety: BoxedError is repr(transparent) so it is okay for the vtable @@ -280,6 +290,8 @@ impl Error { object_downcast: context_chain_downcast::, object_downcast_mut: context_chain_downcast_mut::, object_drop_rest: context_chain_drop_rest::, + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: context_backtrace::, }; // As the cause is anyhow::Error, we already have a backtrace for it. @@ -291,9 +303,6 @@ impl Error { /// Get the backtrace for this Error. /// - /// Backtraces are only available on the nightly channel. Tracking issue: - /// [rust-lang/rust#53487][tracking]. - /// /// In order for the backtrace to be meaningful, one of the two environment /// variables `RUST_LIB_BACKTRACE=1` or `RUST_BACKTRACE=1` must be defined /// and `RUST_LIB_BACKTRACE` must not be `0`. Backtraces are somewhat @@ -307,10 +316,24 @@ impl Error { /// - If you want only panics to have backtraces, set `RUST_BACKTRACE=1` and /// `RUST_LIB_BACKTRACE=0`. /// + /// # Stability + /// + /// Standard library backtraces are only available on the nightly channel. + /// Tracking issue: [rust-lang/rust#53487][tracking]. + /// + /// On stable compilers, this function is only available if the crate's + /// "backtrace" feature is enabled, and will use the `backtrace` crate as + /// the underlying backtrace implementation. + /// + /// ```toml + /// [dependencies] + /// anyhow = { version = "1.0", features = ["backtrace"] } + /// ``` + /// /// [tracking]: https://github.com/rust-lang/rust/issues/53487 - #[cfg(backtrace)] - #[cfg_attr(doc_cfg, doc(cfg(nightly)))] - pub fn backtrace(&self) -> &Backtrace { + #[cfg(any(backtrace, feature = "backtrace"))] + #[cfg_attr(doc_cfg, doc(cfg(any(nightly, feature = "backtrace"))))] + pub fn backtrace(&self) -> &impl_backtrace!() { unsafe { ErrorImpl::backtrace(self.inner.by_ref()) } } @@ -520,6 +543,8 @@ struct ErrorVTable { object_downcast: unsafe fn(Ref, TypeId) -> Option>, object_downcast_mut: unsafe fn(Mut, TypeId) -> Option>, object_drop_rest: unsafe fn(Own, TypeId), + #[cfg(all(not(backtrace), feature = "backtrace"))] + object_backtrace: unsafe fn(Ref) -> Option<&Backtrace>, } // Safety: requires layout of *e to match ErrorImpl. @@ -599,6 +624,12 @@ where } } +#[cfg(all(not(backtrace), feature = "backtrace"))] +fn no_backtrace(e: Ref) -> Option<&Backtrace> { + let _ = e; + None +} + // Safety: requires layout of *e to match ErrorImpl>. #[cfg(feature = "std")] unsafe fn context_downcast(e: Ref, target: TypeId) -> Option> @@ -715,6 +746,18 @@ where } } +// Safety: requires layout of *e to match ErrorImpl>. +#[cfg(all(not(backtrace), feature = "backtrace"))] +#[allow(clippy::unnecessary_wraps)] +unsafe fn context_backtrace(e: Ref) -> Option<&Backtrace> +where + C: 'static, +{ + let unerased = e.cast::>>().deref(); + let backtrace = ErrorImpl::backtrace(unerased._object.error.inner.by_ref()); + Some(backtrace) +} + // NOTE: If working with `ErrorImpl<()>`, references should be avoided in favor // of raw pointers and `NonNull`. // repr C to ensure that E remains in the final position. @@ -765,7 +808,7 @@ impl ErrorImpl { (vtable(this.ptr).object_mut)(this) } - #[cfg(backtrace)] + #[cfg(any(backtrace, feature = "backtrace"))] pub(crate) unsafe fn backtrace(this: Ref) -> &Backtrace { // This unwrap can only panic if the underlying error's backtrace method // is nondeterministic, which would only happen in maliciously @@ -773,7 +816,12 @@ impl ErrorImpl { this.deref() .backtrace .as_ref() - .or_else(|| Self::error(this).backtrace()) + .or_else(|| { + #[cfg(backtrace)] + return Self::error(this).backtrace(); + #[cfg(all(not(backtrace), feature = "backtrace"))] + return (vtable(this.ptr).object_backtrace)(this); + }) .expect("backtrace capture failed") } diff --git a/src/fmt.rs b/src/fmt.rs index 68f6755..03d8fd3 100644 --- a/src/fmt.rs +++ b/src/fmt.rs @@ -39,9 +39,9 @@ impl ErrorImpl { } } - #[cfg(backtrace)] + #[cfg(any(backtrace, feature = "backtrace"))] { - use std::backtrace::BacktraceStatus; + use crate::backtrace::BacktraceStatus; let backtrace = Self::backtrace(this); if let BacktraceStatus::Captured = backtrace.status() {