Skip to main content

thruput_sdk/diagnostics/
debug_context.rs

1use core::any::Any;
2use core::fmt::{self, Display, Write};
3use core::panic::Location;
4use core::ptr::NonNull;
5use heapless::String;
6
7pub trait Context: Any + Display {}
8
9impl<T> Context for T where T: Any + Display {}
10
11struct Frame {
12    context: NonNull<dyn Context>,
13    location: LocString,
14    // Read only when rendering a Backtrace, which only the std-gated
15    // abort paths construct.
16    #[cfg_attr(not(feature = "std"), allow(dead_code))]
17    next: Option<NonNull<Frame>>,
18}
19
20type LocString = String<100>;
21
22// On guest targets (single-threaded), use a plain static with ContractCell (which is Sync).
23// On native (multi-threaded), use thread_local with core::cell::Cell.
24// Both provide .get() and .set() for Copy types.
25#[cfg(any(doc, target_arch = "riscv32"))]
26static FRAMES: crate::ContractCell<Option<NonNull<Frame>>> =
27    crate::ContractCell::new(None);
28
29#[cfg(not(any(doc, target_arch = "riscv32")))]
30std::thread_local! {
31    static FRAMES: core::cell::Cell<Option<NonNull<Frame>>> =
32        const { core::cell::Cell::new(None) };
33}
34
35/// Add to the stack of debugging-context values.
36#[track_caller]
37pub fn with_debug_context<F, T>(context: &(impl Context + 'static), f: F) -> T
38where
39    F: FnOnce() -> T,
40{
41    let location_str = write_location(Location::caller());
42
43    let frames = FRAMES.get();
44
45    let frame = Frame {
46        context: NonNull::from(context),
47        location: location_str,
48        next: frames,
49    };
50
51    FRAMES.set(Some(NonNull::from(&frame)));
52
53    let _guard = FramesGuard { prev: frames };
54
55    f()
56}
57
58// Render the location to a fixed-length string, truncating the front of the
59// filepath (and prepending an elipsis) if necessary.
60//
61fn write_location(loc: &Location) -> LocString {
62    let mut out = LocString::new();
63    let mut rem = out.capacity();
64
65    let mut line_col = String::<16>::new();
66    write!(line_col, ":{}:{}", loc.line(), loc.column()).unwrap();
67    rem -= line_col.len();
68
69    let file = loc.file();
70    if file.len() > rem {
71        // Truncate front of filepath
72        let elipsis = "...";
73        out.write_str(elipsis).unwrap();
74        rem -= elipsis.len();
75        out.write_str(&file[file.len() - rem..]).unwrap();
76    } else {
77        out.write_str(file).unwrap();
78    }
79
80    out.write_str(line_col.as_str()).unwrap();
81
82    out
83}
84
85struct FramesGuard {
86    prev: Option<NonNull<Frame>>,
87}
88
89impl Drop for FramesGuard {
90    fn drop(&mut self) {
91        FRAMES.set(self.prev);
92    }
93}
94
95impl Display for Frame {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        // Safety: Frame is only displayed via Backtrace::fmt, which runs in a
98        // non-returning abort context. All `with_debug_context` callers are still
99        // on the stack, so their `context` references remain live.
100        let context = unsafe { self.context.as_ref() };
101        write!(f, "{} at {}", context, self.location)
102    }
103}
104
105/// A snapshot of the `with_debug_context` frame stack.
106///
107/// The frames live on the stacks of the `with_debug_context` callers, so a
108/// `Backtrace` is only valid to display while all of those calls are still
109/// in progress. It is deliberately crate-private, non-`Copy`/`Clone`, and
110/// only constructible via the unsafe [`Backtrace::capture`] so it cannot
111/// escape the abort paths that uphold that invariant.
112// Only the std-gated abort paths in contract_op.rs construct this.
113#[cfg_attr(not(feature = "std"), allow(dead_code))]
114pub(crate) struct Backtrace {
115    top: Option<NonNull<Frame>>,
116}
117
118impl Display for Backtrace {
119    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120        let mut frames = self.top;
121        let mut depth = 1; // depth 0 is the abort call itself
122
123        while let Some(frame) = frames {
124            // Safety: upheld by the caller of `Backtrace::capture`, which
125            // promises the backtrace is only displayed while every captured
126            // `with_debug_context` frame is still on the stack.
127            let frame = unsafe { frame.as_ref() };
128
129            writeln!(f, "  {}: {}", depth, frame)?;
130
131            frames = frame.next;
132            depth += 1;
133        }
134
135        Ok(())
136    }
137}
138
139#[cfg_attr(not(feature = "std"), allow(dead_code))]
140impl Backtrace {
141    /// Capture the current debug-context frame stack.
142    ///
143    /// # Safety
144    ///
145    /// The returned value borrows the stack frames of every
146    /// `with_debug_context` call in progress, but carries no lifetime.
147    /// The caller must not let it outlive those calls: capture and display
148    /// it within a context that never returns into them (i.e. an abort
149    /// path), and do not store it.
150    pub(crate) unsafe fn capture() -> Self {
151        Backtrace { top: FRAMES.get() }
152    }
153}