backtrace_ext/lib.rs
1//! Minor conveniences on top of the backtrace crate
2//!
3//! See [`short_frames_strict`][] for details.
4use backtrace::*;
5use std::ops::Range;
6
7#[cfg(test)]
8mod test;
9
10/// Gets an iterator over the frames that are part of Rust's "short backtrace" range.
11/// If no such range is found, the full stack is yielded.
12///
13/// Rust generally tries to include special frames on the stack called `rust_end_short_backtrace`
14/// and `rust_begin_short_backtrace` which delimit the "real" stackframes from "gunk" stackframes
15/// like setting up main and invoking the panic runtime. This yields all the "real" frames between
16/// those two (which theoretically can be nothing with enough optimization, although that's unlikely
17/// for any non-trivial program).
18///
19/// If only one of the special frames is present we will only clamp one side of the stack
20/// (similar to `a..` or `..a`). If the special frames are in the wrong order we will discard
21/// them and produce the full stack. If multiple versions of a special frame are found
22/// (I've seen it in the wild), we will pick the "innermost" ones, producing the smallest
23/// possible backtrace (and excluding all special frames from the output).
24///
25/// Each element of the iterator includes a Range which you should use to slice
26/// the frame's `symbols()` array. This handles the theoretical situation where "real" frames
27/// got inlined together with the special marker frames. I want to believe this can't happen
28/// but you can never trust backtraces to be reasonable! We will never yield a Frame to you
29/// with an empty Range.
30///
31/// Note that some "gunk" frames may still be found within the short backtrace, as there is still some
32/// platform-specific and optimization-specific glue around the edges because compilers are
33/// complicated and nothing's perfect. This can include:
34///
35/// * `core::ops::function::FnOnce::call_once`
36/// * `std::panicking::begin_panic_handler`
37/// * `core::panicking::panic_fmt`
38/// * `rust_begin_unwind`
39///
40/// In the future we may introduce a non-strict short_frames which heuristically filters
41/// those frames out too. Until then, the strict approach is safe.
42///
43/// # Example
44///
45/// Here's an example simple "short backtrace" implementation.
46/// Note the use of `sub_frames` for the inner loop to restrict `symbols`!
47///
48/// This example is based off of code found in `miette` (Apache-2.0), which itself
49/// copied the logic from `human-panic` (MIT/Apache-2.0).
50///
51/// FIXME: it would be nice if this example consulted `RUST_BACKTRACE=full`,
52/// and maybe other vars used by rust's builtin panic handler..?
53///
54/// ```
55/// fn backtrace() -> String {
56/// use std::fmt::Write;
57/// if let Ok(var) = std::env::var("RUST_BACKTRACE") {
58/// if !var.is_empty() && var != "0" {
59/// const HEX_WIDTH: usize = std::mem::size_of::<usize>() + 2;
60/// // Padding for next lines after frame's address
61/// const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6;
62/// let mut backtrace = String::new();
63/// let trace = backtrace::Backtrace::new();
64/// let frames = backtrace_ext::short_frames_strict(&trace).enumerate();
65/// for (idx, (frame, subframes)) in frames {
66/// let ip = frame.ip();
67/// let _ = write!(backtrace, "\n{:4}: {:2$?}", idx, ip, HEX_WIDTH);
68///
69/// let symbols = frame.symbols();
70/// if symbols.is_empty() {
71/// let _ = write!(backtrace, " - <unresolved>");
72/// continue;
73/// }
74///
75/// for (idx, symbol) in symbols[subframes].iter().enumerate() {
76/// // Print symbols from this address,
77/// // if there are several addresses
78/// // we need to put it on next line
79/// if idx != 0 {
80/// let _ = write!(backtrace, "\n{:1$}", "", NEXT_SYMBOL_PADDING);
81/// }
82///
83/// if let Some(name) = symbol.name() {
84/// let _ = write!(backtrace, " - {}", name);
85/// } else {
86/// let _ = write!(backtrace, " - <unknown>");
87/// }
88///
89/// // See if there is debug information with file name and line
90/// if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) {
91/// let _ = write!(
92/// backtrace,
93/// "\n{:3$}at {}:{}",
94/// "",
95/// file.display(),
96/// line,
97/// NEXT_SYMBOL_PADDING
98/// );
99/// }
100/// }
101/// }
102/// return backtrace;
103/// }
104/// }
105/// "".into()
106/// }
107/// ```
108pub fn short_frames_strict(
109 backtrace: &Backtrace,
110) -> impl Iterator<Item = (&BacktraceFrame, Range<usize>)> {
111 short_frames_strict_impl(backtrace)
112}
113
114pub(crate) fn short_frames_strict_impl<B: Backtraceish>(
115 backtrace: &B,
116) -> impl Iterator<Item = (&B::Frame, Range<usize>)> {
117 // Search for the special frames
118 let mut short_start = None;
119 let mut short_end = None;
120 let frames = backtrace.frames();
121 for (frame_idx, frame) in frames.iter().enumerate() {
122 let symbols = frame.symbols();
123 for (subframe_idx, frame) in symbols.iter().enumerate() {
124 if let Some(name) = frame.name_str() {
125 // Yes these ARE backwards, and that's intentional! We want to print the frames from
126 // "newest to oldest" (show what panicked first), and that's the order that Backtrace
127 // gives us, but these magic labels view the stack in the opposite order. So we just
128 // swap it once here and forget about that weirdness.
129 //
130 // Note that due to platform/optimization wobblyness you can end up with multiple frames
131 // that contain these names in sequence. If that happens we just want to pick the two
132 // that are closest together. For the start that means just using the last one we found,
133 // and for the end that means taking the first one we find.
134 if name.contains("rust_end_short_backtrace") {
135 short_start = Some((frame_idx, subframe_idx));
136 }
137 if name.contains("rust_begin_short_backtrace") && short_end.is_none() {
138 short_end = Some((frame_idx, subframe_idx));
139 }
140 }
141 }
142 }
143
144 // Check if these are in the right order, if they aren't, discard them
145 // This also handles the mega-cursed case of "someone made a symbol with both names
146 // so actually they're the exact same subframe".
147 if let (Some(start), Some(end)) = (short_start, short_end) {
148 if start >= end {
149 short_start = None;
150 short_end = None;
151 }
152 }
153
154 // By default we want to produce a full stack trace and now we'll try to clamp it.
155 let mut first_frame = 0usize;
156 let mut first_subframe = 0usize;
157 // NOTE: this is INCLUSIVE
158 let mut last_frame = frames.len().saturating_sub(1);
159 // NOTE: this is EXCLUSIVE
160 let mut last_subframe_excl = backtrace
161 .frames()
162 .last()
163 .map(|frame| frame.symbols().len())
164 .unwrap_or(0);
165
166 // This code tries to be really paranoid about boundary conditions although in practice
167 // most of them are impossible because there's always going to be gunk on either side
168 // of the short backtrace to smooth out the boundaries, and panic_fmt is basically
169 // impossible to optimize out. Still, don't trust backtracers!!!
170 //
171 // This library has a fuckton of tests to try to catch all the little corner cases here.
172
173 // If we found the start bound...
174 if let Some((idx, sub_idx)) = short_start {
175 if frames[idx].symbols().len() == sub_idx + 1 {
176 // If it was the last subframe of this frame, we want to just
177 // use the whole next frame! It's ok if this takes us to `first_frame = len`,
178 // that will be properly handled as an empty output
179 first_frame = idx + 1;
180 first_subframe = 0;
181 } else {
182 // Otherwise use this frame, and all the subframes after it
183 first_frame = idx;
184 first_subframe = sub_idx + 1;
185 }
186 }
187
188 // If we found the end bound...
189 if let Some((idx, sub_idx)) = short_end {
190 if sub_idx == 0 {
191 // If it was the first subframe of this frame, we want to just
192 // use the whole previous frame!
193 if idx == 0 {
194 // If we were *also* on the first frame, set subframe_excl
195 // to 0, indicating an empty output
196 last_frame = 0;
197 last_subframe_excl = 0;
198 } else {
199 last_frame = idx - 1;
200 last_subframe_excl = frames[last_frame].symbols().len();
201 }
202 } else {
203 // Otherwise use this frame (no need subframe math, exclusive bound!)
204 last_frame = idx;
205 last_subframe_excl = sub_idx;
206 }
207 }
208
209 // If the two subframes managed to perfectly line up with eachother, just
210 // throw everything out and yield an empty range. We don't need to fix any
211 // other values at this point as they won't be used for anything with an
212 // empty iterator
213 let final_frames = {
214 let start = (first_frame, first_subframe);
215 let end = (last_frame, last_subframe_excl);
216 if start == end {
217 &frames[0..0]
218 } else {
219 &frames[first_frame..=last_frame]
220 }
221 };
222
223 // Get the index of the last frame when starting from the first frame
224 let adjusted_last_frame = last_frame.saturating_sub(first_frame);
225
226 // finally do the iteration
227 final_frames.iter().enumerate().map(move |(idx, frame)| {
228 // Default to all subframes being yielded
229 let mut sub_start = 0;
230 let mut sub_end_excl = frame.symbols().len();
231 // If we're on first frame, apply its subframe clamp
232 if idx == 0 {
233 sub_start = first_subframe;
234 }
235 // If we're on the last frame, apply its subframe clamp
236 if idx == adjusted_last_frame {
237 sub_end_excl = last_subframe_excl;
238 }
239 (frame, sub_start..sub_end_excl)
240 })
241}
242
243pub(crate) trait Backtraceish {
244 type Frame: Frameish;
245 fn frames(&self) -> &[Self::Frame];
246}
247
248pub(crate) trait Frameish {
249 type Symbol: Symbolish;
250 fn symbols(&self) -> &[Self::Symbol];
251}
252
253pub(crate) trait Symbolish {
254 fn name_str(&self) -> Option<&str>;
255}
256
257impl Backtraceish for Backtrace {
258 type Frame = BacktraceFrame;
259 fn frames(&self) -> &[Self::Frame] {
260 self.frames()
261 }
262}
263
264impl Frameish for BacktraceFrame {
265 type Symbol = BacktraceSymbol;
266 fn symbols(&self) -> &[Self::Symbol] {
267 self.symbols()
268 }
269}
270
271impl Symbolish for BacktraceSymbol {
272 // We need to shortcut SymbolName here because
273 // HRTB isn't in our msrv
274 fn name_str(&self) -> Option<&str> {
275 self.name().and_then(|n| n.as_str())
276 }
277}