Skip to main content

oopsie_core/
backtrace.rs

1use std::cell::Cell;
2#[cfg(test)]
3use std::sync::atomic::AtomicBool;
4use std::sync::atomic::AtomicU8;
5use std::sync::atomic::Ordering::Relaxed;
6use std::sync::{Arc, LazyLock};
7use std::{env, fmt};
8
9/// Whether backtrace capture is enabled, and how verbosely it should render.
10///
11/// Resolved once from the environment (see [`rust_backtrace`]) following the
12/// same convention as `std`: `RUST_LIB_BACKTRACE` is consulted first and, if
13/// unset, `RUST_BACKTRACE`.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[repr(u8)]
16pub enum RustBacktrace {
17    /// Capture disabled — no frames are recorded.
18    Disabled = 1,
19    /// Capture enabled, rendered as the trimmed "short" view.
20    Enabled = 2,
21    /// Capture enabled, rendered untrimmed — every frame is shown.
22    Full = 3,
23}
24
25impl RustBacktrace {
26    /// The environment-derived setting, or `None` when neither
27    /// `RUST_LIB_BACKTRACE` nor `RUST_BACKTRACE` is set. The environment is
28    /// read once and cached for the life of the process.
29    pub fn detect_opt() -> Option<Self> {
30        const NONE: u8 = u8::MAX;
31        const NOT_SET: u8 = 0;
32
33        static ENABLED: AtomicU8 = AtomicU8::new(NOT_SET);
34        if let Some(cached) = match ENABLED.load(Relaxed) {
35            1 => Some(Some(Self::Disabled)),
36            2 => Some(Some(Self::Enabled)),
37            3 => Some(Some(Self::Full)),
38            NONE => Some(None),
39            NOT_SET => None,
40            _ => unreachable!(),
41        } {
42            return cached;
43        }
44
45        // `RUST_LIB_BACKTRACE` wins when set; otherwise fall back to
46        // `RUST_BACKTRACE`. `or_else` only fires on `Err` (var unset or
47        // non-unicode), so an explicit `RUST_LIB_BACKTRACE=0` disables even
48        // when `RUST_BACKTRACE=1`.
49        let raw = env::var("RUST_LIB_BACKTRACE").or_else(|_| env::var("RUST_BACKTRACE"));
50        let enabled = match raw.as_deref() {
51            Ok("0") => Some(Self::Disabled),
52            Ok("full") => Some(Self::Full),
53            Ok(_) => Some(Self::Enabled),
54            Err(_) => None,
55        };
56        ENABLED.store(enabled.map_or(NONE, |f| f as u8), Relaxed);
57        enabled
58    }
59
60    /// Panic-path detection: consults only `RUST_BACKTRACE`, mirroring std's
61    /// panic handler. `RUST_LIB_BACKTRACE` deliberately has no effect here —
62    /// it exists to control library error capture independently of panics.
63    pub fn detect_panic_opt() -> Option<Self> {
64        const NONE: u8 = u8::MAX;
65        const NOT_SET: u8 = 0;
66
67        static ENABLED: AtomicU8 = AtomicU8::new(NOT_SET);
68        if let Some(cached) = match ENABLED.load(Relaxed) {
69            1 => Some(Some(Self::Disabled)),
70            2 => Some(Some(Self::Enabled)),
71            3 => Some(Some(Self::Full)),
72            NONE => Some(None),
73            NOT_SET => None,
74            _ => unreachable!(),
75        } {
76            return cached;
77        }
78
79        let enabled = match env::var("RUST_BACKTRACE").as_deref() {
80            Ok("0") => Some(Self::Disabled),
81            Ok("full") => Some(Self::Full),
82            Ok(_) => Some(Self::Enabled),
83            Err(_) => None,
84        };
85        ENABLED.store(enabled.map_or(NONE, |f| f as u8), Relaxed);
86        enabled
87    }
88
89    /// Whether frames should be captured at all.
90    #[inline]
91    #[must_use]
92    pub const fn is_enabled(self) -> bool {
93        matches!(self, Self::Full | Self::Enabled)
94    }
95
96    /// Whether rendering should show every frame, skipping the trimming that
97    /// hides capture machinery and runtime setup/teardown frames.
98    #[inline]
99    #[must_use]
100    pub const fn is_full(self) -> bool {
101        matches!(self, Self::Full)
102    }
103}
104
105thread_local! {
106    /// Per-thread override of the backtrace setting. `None` falls back to the
107    /// environment.
108    static OVERRIDE: Cell<Option<RustBacktrace>> = const { Cell::new(None) };
109}
110
111/// Force [`rust_backtrace`] to return `value` **on the current thread**, taking
112/// precedence over the environment until [`clear_rust_backtrace_override`].
113///
114/// This is the precise, scoped lever — it affects only backtraces captured on
115/// this thread, not threads spawned afterwards. For process-wide control use
116/// the `RUST_LIB_BACKTRACE` / `RUST_BACKTRACE` environment variables instead.
117#[inline]
118pub fn set_rust_backtrace_override(value: RustBacktrace) {
119    OVERRIDE.with(|o| o.set(Some(value)));
120}
121
122/// Remove any override set by [`set_rust_backtrace_override`] on the current
123/// thread, reverting [`rust_backtrace`] to the environment-derived value.
124#[inline]
125pub fn clear_rust_backtrace_override() {
126    OVERRIDE.with(|o| o.set(None));
127}
128
129/// The effective backtrace setting for the current thread.
130///
131/// Returns the thread-local override set via [`set_rust_backtrace_override`] if
132/// present; otherwise the value derived from `RUST_LIB_BACKTRACE` then
133/// `RUST_BACKTRACE`. Like `std`, the environment is read once and cached — only
134/// an override can change the result afterwards.
135#[must_use]
136#[inline]
137pub fn rust_backtrace() -> RustBacktrace {
138    if let Some(over) = OVERRIDE.with(Cell::get) {
139        return over;
140    }
141    RustBacktrace::detect_opt().unwrap_or(RustBacktrace::Disabled)
142}
143
144/// The effective *panic* backtrace setting for the current thread: the
145/// thread-local override if present, else `RUST_BACKTRACE` (only).
146#[must_use]
147#[inline]
148pub fn rust_panic_backtrace() -> RustBacktrace {
149    if let Some(over) = OVERRIDE.with(Cell::get) {
150        return over;
151    }
152    RustBacktrace::detect_panic_opt().unwrap_or(RustBacktrace::Disabled)
153}
154
155/// Run `f` with the thread's backtrace setting forced to `value`, restoring
156/// the previous override (or lack of one) afterwards — including on unwind.
157#[inline]
158pub fn with_rust_backtrace_override<R>(value: RustBacktrace, f: impl FnOnce() -> R) -> R {
159    struct Restore(Option<RustBacktrace>);
160    impl Drop for Restore {
161        fn drop(&mut self) {
162            OVERRIDE.with(|o| o.set(self.0));
163        }
164    }
165    let _restore = Restore(OVERRIDE.with(Cell::get));
166    OVERRIDE.with(|o| o.set(Some(value)));
167    f()
168}
169
170// Clones share one `Arc`-held capture, so cloning is a refcount bump and the
171// first frame access resolves symbols exactly once for all clones.
172#[derive(Clone)]
173enum Inner {
174    Captured(Arc<Lazy>),
175    Disabled(backtrace::Backtrace), // Always empty, used when capture is disabled to avoid the LazyLock indirection.
176}
177
178/// A captured stack backtrace with deferred symbol resolution.
179///
180/// Captured via [`Capturable`](crate::Capturable). When backtrace capture is
181/// disabled (see [`rust_backtrace`]) the capture is empty and records no
182/// frames. Symbols are resolved lazily on first frame access and cached;
183/// clones share one capture, so cloning is a refcount bump and resolution
184/// happens once for all clones.
185#[derive(Clone)]
186pub struct Backtrace {
187    inner: Inner,
188    marker: Option<crate::marker::TraceMarker>,
189}
190
191impl crate::Capturable for Backtrace {
192    #[inline]
193    fn capture() -> Self {
194        if rust_backtrace().is_enabled() {
195            Self {
196                inner: Inner::Captured(Arc::new(Lazy::new(helper::lazy_resolve(Capture {
197                    backtrace: backtrace::Backtrace::new_unresolved(),
198                })))),
199                marker: crate::marker::current(),
200            }
201        } else {
202            Self {
203                inner: Inner::Disabled(backtrace::Backtrace::from(vec![])),
204                marker: None,
205            }
206        }
207    }
208
209    #[inline]
210    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
211        match source.oopsie_backtrace() {
212            // Keep the source's trace only if capture actually succeeded; an
213            // empty trace carries nothing worth preserving over a fresh
214            // capture at the wrap site.
215            Some(trace) if trace.is_captured() => trace.clone(),
216            _ => Self::capture(),
217        }
218    }
219}
220
221/// Source directory of this crate; frames whose filename lives here are the
222/// capture machinery itself.
223pub const CORE_SRC_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/src/");
224
225impl fmt::Debug for Backtrace {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        fmt::Debug::fmt(self.as_backtrace(), f)
228    }
229}
230
231impl Backtrace {
232    /// Returns a reference to the inner [`backtrace::Backtrace`].
233    ///
234    /// Forces and caches full symbol resolution on first access.
235    // Hidden from the public API: it surfaces `backtrace` 0.x types, whose minor
236    // releases are breaking. Kept `pub` only for the renderer and the erased
237    // conversion, which need the raw capture; consumers use the owned frames.
238    #[doc(hidden)]
239    #[must_use]
240    #[inline]
241    pub fn as_backtrace(&self) -> &backtrace::Backtrace {
242        match &self.inner {
243            Inner::Captured(bt) => &bt.force().backtrace,
244            Inner::Disabled(bt) => bt,
245        }
246    }
247
248    /// `true` if frames were recorded at construction (capture was enabled and
249    /// the platform produced a stack). An empty backtrace renders nothing and
250    /// is not worth propagating over a fresh capture.
251    ///
252    /// Never forces symbol resolution.
253    #[must_use]
254    #[inline]
255    pub const fn is_captured(&self) -> bool {
256        match &self.inner {
257            Inner::Captured(_) => true,
258            Inner::Disabled(_) => false,
259        }
260    }
261
262    /// Returns the frames of the backtrace.
263    ///
264    /// Forces and caches full symbol resolution on first access.
265    #[doc(hidden)]
266    #[must_use]
267    #[inline]
268    pub fn frames(&self) -> &[backtrace::BacktraceFrame] {
269        self.as_backtrace().frames()
270    }
271
272    /// Number of trailing frames hidden by this trace's marker: the suffix
273    /// shared with the marker stack, counted in rendered (symbol-level)
274    /// frames. `None` when no marker was set on the capturing thread, the
275    /// stacks share no frames (e.g. the marker came from another thread), the
276    /// cut would hide every frame, or none of the hidden frames carry symbols.
277    ///
278    /// Forces symbol resolution, like [`frames`](Self::frames).
279    #[must_use]
280    pub fn marker_hidden_frames(&self) -> Option<usize> {
281        let marker = self.marker.as_ref()?;
282        let frames = self.frames();
283        let cut = marker.cut_len(frames);
284        if cut == 0 || cut >= frames.len() {
285            return None;
286        }
287        // The render view is symbol-level: inlined frames expand to several
288        // rendered frames, unresolvable ones to none. Convert the physical
289        // cut into that currency — and re-apply the never-hide-everything
290        // guard in it, since the kept physical frames may carry no symbols.
291        let rendered = |frames: &[backtrace::BacktraceFrame]| {
292            frames
293                .iter()
294                .map(|frame| frame.symbols().len())
295                .sum::<usize>()
296        };
297        let hidden = rendered(&frames[frames.len() - cut..]);
298        (hidden > 0 && hidden < rendered(frames)).then_some(hidden)
299    }
300
301    /// Force symbol resolution now, caching the result in place.
302    ///
303    /// Resolution is otherwise deferred until the first frame access
304    /// ([`as_backtrace`](Self::as_backtrace) / [`frames`](Self::frames)). Call
305    /// this to pay that cost at a controlled point — e.g. once up front rather
306    /// than during rendering.
307    #[inline]
308    pub fn resolve(&self) {
309        let _ = self.as_backtrace();
310    }
311
312    #[cfg(test)]
313    fn is_resolved(&self) -> bool {
314        match &self.inner {
315            Inner::Captured(bt) => bt.is_resolved(),
316            Inner::Disabled(_) => true,
317        }
318    }
319}
320
321struct Capture {
322    backtrace: backtrace::Backtrace,
323}
324
325/// A lazily symbol-resolved [`Capture`], shared across clones via the enclosing
326/// `Arc` so resolution happens once for the whole family.
327struct Lazy {
328    cell: LazyLock<Capture, helper::LazyResolve>,
329    // Test-only mirror of `cell`'s forced state: the direct `LazyLock::get` probe
330    // isn't stable on this crate's MSRV, and a test's needs must not raise the
331    // library's floor. `force` is the only path that resolves `cell`, so the two
332    // stay in step.
333    #[cfg(test)]
334    forced: AtomicBool,
335}
336
337impl Lazy {
338    fn new(resolve: helper::LazyResolve) -> Self {
339        Self {
340            cell: LazyLock::new(resolve),
341            #[cfg(test)]
342            forced: AtomicBool::new(false),
343        }
344    }
345
346    /// Force symbol resolution (idempotent) and return the cached capture.
347    #[inline]
348    fn force(&self) -> &Capture {
349        let capture = &*self.cell;
350        #[cfg(test)]
351        self.forced.store(true, Relaxed);
352        capture
353    }
354
355    #[cfg(test)]
356    fn is_resolved(&self) -> bool {
357        self.forced.load(Relaxed)
358    }
359}
360
361mod helper {
362    use std::panic::UnwindSafe;
363
364    use super::*;
365
366    // `LazyLock<T, F>`'s second parameter must be a *named* type, but
367    // `lazy_resolve` returns a state-capturing closure — and stable Rust cannot
368    // name an unboxed closure type. Boxing erases it behind `dyn FnOnce`, which
369    // itself satisfies `F`, at the cost of one allocation per captured trace.
370    // Once `type_alias_impl_trait` stabilizes, replace this alias with
371    // `impl FnOnce() -> Capture + Send + Sync + UnwindSafe` (+ `#[define_opaque]`
372    // on `lazy_resolve`) to store the closure inline and drop the allocation —
373    // exactly what `std`'s own `Backtrace` does.
374    pub(super) type LazyResolve = Box<dyn FnOnce() -> Capture + Send + Sync + UnwindSafe>;
375
376    pub(super) fn lazy_resolve(mut capture: Capture) -> LazyResolve {
377        Box::new(move || {
378            capture.backtrace.resolve();
379            capture
380        })
381    }
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use crate::{Capturable, Diagnostic};
388
389    #[derive(Debug)]
390    struct ErrorWithBacktrace {
391        backtrace: Backtrace,
392    }
393
394    impl fmt::Display for ErrorWithBacktrace {
395        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
396            write!(f, "error with backtrace")
397        }
398    }
399
400    impl std::error::Error for ErrorWithBacktrace {}
401
402    impl Diagnostic for ErrorWithBacktrace {
403        fn oopsie_backtrace(&self) -> Option<&Backtrace> {
404            Some(&self.backtrace)
405        }
406    }
407
408    #[derive(Debug)]
409    struct ErrorWithoutBacktrace;
410
411    impl fmt::Display for ErrorWithoutBacktrace {
412        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
413            write!(f, "error without backtrace")
414        }
415    }
416
417    impl std::error::Error for ErrorWithoutBacktrace {}
418
419    impl Diagnostic for ErrorWithoutBacktrace {}
420
421    #[test]
422    fn test_rust_backtrace_override() {
423        set_rust_backtrace_override(RustBacktrace::Enabled);
424        assert_eq!(rust_backtrace(), RustBacktrace::Enabled);
425    }
426
427    #[test]
428    fn test_backtrace_capture_produces_backtrace() {
429        let bt = Backtrace::capture();
430        // Backtrace should exist (may be empty on some platforms, but should not panic)
431        let _ = bt.as_backtrace().frames();
432    }
433
434    #[test]
435    fn test_backtrace_capture_or_extract_with_existing_backtrace() {
436        let original_bt = Backtrace::capture();
437        let error = ErrorWithBacktrace {
438            backtrace: original_bt.clone(),
439        };
440
441        // Should extract the existing backtrace, not capture a new one
442        let extracted = Backtrace::capture_or_extract(&error);
443        // Both should have the same frames count
444        assert_eq!(
445            extracted.as_backtrace().frames().len(),
446            original_bt.as_backtrace().frames().len()
447        );
448    }
449
450    #[test]
451    fn test_backtrace_capture_or_extract_without_existing_backtrace() {
452        let error = ErrorWithoutBacktrace;
453
454        // Should capture a fresh backtrace
455        let captured = Backtrace::capture_or_extract(&error);
456        // Should produce a valid backtrace (frames list is valid, may be empty)
457        let _ = captured.as_backtrace().frames();
458    }
459
460    #[test]
461    fn test_backtrace_inner_returns_reference() {
462        let bt = Backtrace::capture();
463        let inner = bt.as_backtrace();
464        // Should be able to call methods on the inner backtrace
465        let _ = inner.frames();
466    }
467
468    #[test]
469    fn test_capture_is_empty_when_disabled() {
470        set_rust_backtrace_override(RustBacktrace::Disabled);
471        let bt = Backtrace::capture();
472        clear_rust_backtrace_override();
473        assert!(
474            bt.frames().is_empty(),
475            "disabled capture must record no frames, got {}",
476            bt.frames().len()
477        );
478    }
479
480    #[test]
481    fn test_capture_or_extract_is_empty_when_disabled() {
482        set_rust_backtrace_override(RustBacktrace::Disabled);
483        // No existing backtrace on the source, so this exercises the fresh
484        // capture path the derive macro uses.
485        let bt = Backtrace::capture_or_extract(&ErrorWithoutBacktrace);
486        clear_rust_backtrace_override();
487        assert!(bt.frames().is_empty());
488    }
489
490    #[test]
491    fn capture_or_extract_recaptures_over_empty_source_backtrace() {
492        let src = with_rust_backtrace_override(RustBacktrace::Disabled, || ErrorWithBacktrace {
493            backtrace: Backtrace::capture(),
494        });
495        assert!(
496            src.backtrace.frames().is_empty(),
497            "precondition: source bt empty"
498        );
499        let extracted = with_rust_backtrace_override(RustBacktrace::Enabled, || {
500            <Backtrace as Capturable>::capture_or_extract(&src)
501        });
502        assert!(!extracted.frames().is_empty());
503    }
504
505    #[test]
506    fn clone_does_not_force_resolution() {
507        let bt = with_rust_backtrace_override(RustBacktrace::Enabled, Backtrace::capture);
508        let clone = bt.clone();
509        assert!(!bt.is_resolved(), "clone must not symbolicate");
510        assert!(!clone.is_resolved());
511        let _ = clone.frames();
512        assert!(bt.is_resolved(), "clones share a single resolution");
513    }
514
515    #[test]
516    fn debug_never_renders_empty_for_nonempty_capture() {
517        let bt = with_rust_backtrace_override(RustBacktrace::Enabled, Backtrace::capture);
518        assert!(!bt.frames().is_empty());
519        let rendered = with_rust_backtrace_override(RustBacktrace::Enabled, || format!("{bt:?}"));
520        // Whatever the platform's symbol situation, a non-empty capture must
521        // render at least one frame entry.
522        assert!(rendered.contains("0:"), "rendered: {rendered}");
523    }
524
525    #[test]
526    fn test_capture_records_frames_when_enabled() {
527        set_rust_backtrace_override(RustBacktrace::Enabled);
528        let bt = Backtrace::capture();
529        clear_rust_backtrace_override();
530        // Contrast with the disabled case: capture genuinely records frames
531        // when enabled, so the empty-when-disabled assertions aren't vacuous.
532        assert!(!bt.frames().is_empty());
533    }
534
535    #[test]
536    fn with_override_scopes_and_restores() {
537        let baseline = rust_backtrace();
538        let panic_baseline = rust_panic_backtrace();
539        let inside = with_rust_backtrace_override(RustBacktrace::Full, rust_backtrace);
540        assert_eq!(inside, RustBacktrace::Full);
541        assert_eq!(rust_backtrace(), baseline);
542        let inside = with_rust_backtrace_override(RustBacktrace::Disabled, rust_panic_backtrace);
543        assert_eq!(inside, RustBacktrace::Disabled);
544        assert_eq!(rust_panic_backtrace(), panic_baseline);
545    }
546
547    #[test]
548    fn with_override_restores_on_unwind() {
549        let baseline = rust_backtrace();
550        let _ = std::panic::catch_unwind(|| {
551            with_rust_backtrace_override(RustBacktrace::Full, || panic!("boom"))
552        });
553        assert_eq!(rust_backtrace(), baseline);
554    }
555
556    const _: () = {
557        // Verify that Backtrace implements Capturable
558        const fn is_capturable<T: crate::Capturable>() {}
559        is_capturable::<Backtrace>();
560    };
561
562    #[test]
563    fn capture_embeds_marker_and_computes_hidden_frames() {
564        crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
565            let _restore_guard = {
566                // Isolate the TLS slot from other tests on this thread.
567                let prev = crate::marker::current();
568                scopeguard(prev)
569            };
570            let _ = crate::__private::set_marker();
571            let bt = <Backtrace as crate::Capturable>::capture();
572            let hidden = bt
573                .marker_hidden_frames()
574                .expect("same-thread marker must produce a cut");
575            assert!(hidden > 0);
576            // The count is in rendered (symbol-level) currency and must not
577            // swallow the whole trace.
578            let total_rendered: usize = bt.frames().iter().map(|f| f.symbols().len()).sum();
579            assert!(hidden < total_rendered);
580        });
581    }
582
583    #[test]
584    fn capture_without_marker_has_no_hidden_frames() {
585        crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
586            let _restore_guard = scopeguard(crate::marker::current());
587            crate::__private::restore_marker(None);
588            let bt = <Backtrace as crate::Capturable>::capture();
589            assert!(bt.marker_hidden_frames().is_none());
590        });
591    }
592
593    #[test]
594    fn marker_does_not_cross_threads() {
595        crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
596            let _ = crate::__private::set_marker();
597            let bt = std::thread::spawn(|| {
598                crate::with_rust_backtrace_override(RustBacktrace::Enabled, || {
599                    <Backtrace as crate::Capturable>::capture()
600                })
601            })
602            .join()
603            .unwrap();
604            // The spawned thread has no marker of its own.
605            assert!(bt.marker_hidden_frames().is_none());
606        });
607    }
608
609    /// Restore the previous marker when the test scope ends.
610    fn scopeguard(prev: Option<crate::marker::TraceMarker>) -> impl Drop {
611        struct Restore(Option<crate::marker::TraceMarker>);
612        impl Drop for Restore {
613            fn drop(&mut self) {
614                crate::__private::restore_marker(self.0.take());
615            }
616        }
617        Restore(prev)
618    }
619}