1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
//! Wasm stack backtraces (wasmtime-compatible): [`WasmBacktrace`] and its [`FrameInfo`] /
//! [`FrameSymbol`] frames. Source-level symbolication is resolved lazily from the module's DWARF
//! (#29a); throw-time capture that fills a backtrace lands in #29d.
use std::sync::{Arc, OnceLock};
use crate::module::Module;
use crate::store::AsContext;
/// A captured wasm stack backtrace, mirroring `wasmtime::WasmBacktrace`.
#[derive(Clone, Debug)]
pub struct WasmBacktrace {
frames: Vec<FrameInfo>,
/// Indices into `frames` where a host↔wasm re-entry boundary sits (a host fn re-entered wasm
/// via `Func::call`): a boundary precedes `frames[i]`. Presentational — `frames()` stays pure
/// wasm (wasmtime-compatible); `Display` renders a `<host>` line at each.
host_boundaries: Vec<usize>,
}
impl std::fmt::Display for WasmBacktrace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "wasm backtrace:")?;
for (i, frame) in self.frames.iter().enumerate() {
if self.host_boundaries.contains(&i) {
writeln!(f, " <host>")?;
}
let name = frame.func_name().unwrap_or("<unknown>");
writeln!(f, " {i}: func[{}] {name}", frame.func_index())?;
}
if self.host_boundaries.contains(&self.frames.len()) {
writeln!(f, " <host>")?;
}
Ok(())
}
}
// A backtrace is attached as the *source* of a trap/exception error (the trap is the context), so
// the error's `Display` stays the trap message while `downcast_ref::<WasmBacktrace>()` recovers it.
impl std::error::Error for WasmBacktrace {}
impl WasmBacktrace {
/// Captures the wasm backtrace of the current execution if `Config::wasm_backtrace` is on,
/// else an empty trace. Meaningful when called from within a host function (it walks the wasm
/// frames that called it); empty outside an active call.
pub fn capture(store: impl AsContext) -> WasmBacktrace {
let ctx = store.as_context();
if !ctx.engine().wasm_backtrace_enabled() {
return WasmBacktrace::from_frames(Vec::new());
}
crate::exec::trace::from_parked(ctx.inner())
}
/// Like [`capture`](Self::capture) but ignores `Config::wasm_backtrace`.
pub fn force_capture(store: impl AsContext) -> WasmBacktrace {
crate::exec::trace::from_parked(store.as_context().inner())
}
/// Visits active frame modules, innermost first, without allocating or
/// symbolizing a backtrace. `Break` stops the walk; backtrace settings do
/// not affect caller attribution.
pub fn visit_modules(
store: impl AsContext,
visit: impl FnMut(&Module) -> std::ops::ControlFlow<()>,
) -> crate::Result<()> {
crate::exec::trace::visit_modules(store.as_context().inner(), visit)
}
/// The captured frames, most-recent first.
pub fn frames(&self) -> &[FrameInfo] {
&self.frames
}
/// Builds a backtrace from already-collected frames (used by capture, #29d).
pub(crate) fn from_frames(frames: Vec<FrameInfo>) -> WasmBacktrace {
WasmBacktrace {
frames,
host_boundaries: Vec::new(),
}
}
/// Builds a backtrace plus the host-re-entry boundary positions (for `Display` markers).
pub(crate) fn from_frames_with_boundaries(
frames: Vec<FrameInfo>,
host_boundaries: Vec<usize>,
) -> WasmBacktrace {
WasmBacktrace {
frames,
host_boundaries,
}
}
}
/// Information about a single wasm stack frame, mirroring `wasmtime::FrameInfo`.
///
/// Symbolication (file/line/column) is resolved lazily from the module's DWARF on first
/// [`symbols`](Self::symbols), keeping the trap/capture path off the DWARF cost.
#[derive(Debug)]
pub struct FrameInfo {
module: Module,
func_index: u32,
/// Module-relative wasm byte offset of the trapping instruction (`CompiledFunc::offsets[ip]`);
/// `None` when the offset table wasn't retained.
code_offset: Option<u32>,
symbols: OnceLock<Box<[FrameSymbol]>>,
}
impl Clone for FrameInfo {
/// Clones the frame's identity; the lazily-symbolicated cache is not carried (re-resolved on
/// demand). `OnceLock` isn't `Clone`, and the result is identical either way.
fn clone(&self) -> Self {
FrameInfo {
module: self.module.clone(),
func_index: self.func_index,
code_offset: self.code_offset,
symbols: OnceLock::new(),
}
}
}
impl FrameInfo {
pub(crate) fn new(module: Module, func_index: u32, code_offset: Option<u32>) -> FrameInfo {
FrameInfo {
module,
func_index,
code_offset,
symbols: OnceLock::new(),
}
}
/// The index of the function in its defining module.
pub fn func_index(&self) -> u32 {
self.func_index
}
/// The module this frame executes in.
pub fn module(&self) -> &Module {
&self.module
}
/// The function's name from the `name` custom section, if present.
pub fn func_name(&self) -> Option<&str> {
self.module.inner().debug.func_name(self.func_index)
}
/// The trapping instruction's offset relative to the start of the module's code section.
pub fn module_offset(&self) -> Option<usize> {
self.code_offset.map(|o| o as usize)
}
/// The trapping instruction's offset relative to the start of this function's body.
pub fn func_offset(&self) -> Option<usize> {
let off = self.code_offset?;
off.checked_sub(self.func_start()?).map(|o| o as usize)
}
/// Source-level symbols for this frame, resolved lazily from DWARF. Empty when neither a name
/// nor a source location is available (e.g. no debug info).
pub fn symbols(&self) -> &[FrameSymbol] {
self.symbols.get_or_init(|| self.build_symbols())
}
/// The module-relative offset of this function's first instruction, from the offset table.
fn func_start(&self) -> Option<u32> {
// Imported functions have no compiled body (`code()` expects a defined index).
self.func_index
.checked_sub(self.module.inner().num_imported_funcs)?;
self.module
.code(self.func_index)
.offsets()?
.first()
.copied()
}
fn build_symbols(&self) -> Box<[FrameSymbol]> {
let debug = &self.module.inner().debug;
// Prefer the `name` section (func-index-keyed, exact); fall back to the DWARF subprogram
// name at this offset, so modules shipping only DWARF (no `name` section) still get names —
// matching wasmtime's `wasm_backtrace_details`.
let name = debug
.func_name(self.func_index)
.map(Arc::from)
.or_else(|| self.code_offset.and_then(|o| debug.dwarf_func_name(o)));
let loc = self.code_offset.and_then(|o| debug.lookup(o));
if name.is_none() && loc.is_none() {
return Box::new([]);
}
let nonzero = |v: u32| (v != 0).then_some(v);
Box::new([FrameSymbol {
name,
file: loc.as_ref().map(|l| Arc::clone(&l.file)),
line: loc.as_ref().and_then(|l| nonzero(l.line)),
column: loc.as_ref().and_then(|l| nonzero(l.column)),
}])
}
}
/// A source-level symbol for a [`FrameInfo`], mirroring `wasmtime::FrameSymbol`.
#[derive(Clone, Debug)]
pub struct FrameSymbol {
name: Option<Arc<str>>,
file: Option<Arc<str>>,
line: Option<u32>,
column: Option<u32>,
}
impl FrameSymbol {
/// The function name at this symbol.
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
/// The source file path.
pub fn file(&self) -> Option<&str> {
self.file.as_deref()
}
/// The 1-based source line.
pub fn line(&self) -> Option<u32> {
self.line
}
/// The 1-based source column.
pub fn column(&self) -> Option<u32> {
self.column
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::{FrameInfo, FrameSymbol, WasmBacktrace};
use crate::{Config, Engine, Module, Trap};
/// Load-bearing (#29d): with the backtrace as the error's *source* and the `Trap` as *context*,
/// both `downcast_ref`s resolve AND `Display` stays the trap message (not the backtrace) — the
/// spec harness and embedders read the trap text.
#[test]
fn backtrace_source_keeps_trap_display() {
let err = crate::Error::new(WasmBacktrace::from_frames(Vec::new()))
.context(Trap::UnreachableCodeReached);
assert!(matches!(
err.downcast_ref::<Trap>(),
Some(Trap::UnreachableCodeReached)
));
assert!(err.downcast_ref::<WasmBacktrace>().is_some());
assert_eq!(err.to_string(), Trap::UnreachableCodeReached.to_string());
}
/// Real DWARF + `name` section module (`boom`, defined func 0, body on line 13). See
/// `src/module/debug/testdata/fixture.rs`.
const FIXTURE: &[u8] = include_bytes!("module/debug/testdata/fixture.wasm");
#[test]
fn frame_symbolicates_against_dwarf() {
// The default engine no longer retains DWARF (#29c); opt in for file/line.
let engine = Engine::new(Config::new().debug_info(true)).unwrap();
let module = Module::new(&engine, FIXTURE).unwrap();
let offset = module.code(0).offsets().unwrap()[0];
let frame = FrameInfo::new(module.clone(), 0, Some(offset));
assert_eq!(frame.func_index(), 0);
assert_eq!(frame.func_name(), Some("boom"));
assert_eq!(frame.module_offset(), Some(offset as usize));
assert_eq!(frame.func_offset(), Some(0)); // first op of the function
let symbols = frame.symbols();
assert_eq!(symbols.len(), 1);
assert_eq!(symbols[0].name(), Some("boom"));
assert!(symbols[0].file().unwrap().ends_with("fixture.rs"));
assert_eq!(symbols[0].line(), Some(13));
let _clone: FrameSymbol = symbols[0].clone(); // FrameSymbol: Clone
}
/// #29e: DWARF is parsed only when `symbols()` is inspected, never at frame construction
/// (capture). The frame's `Module` clone shares the original's `OnceLock`, so we observe it
/// through the original handle.
#[test]
fn symbolication_is_lazy() {
let engine = Engine::new(Config::new().debug_info(true)).unwrap();
let module = Module::new(&engine, FIXTURE).unwrap();
let offset = module.code(0).offsets().unwrap()[0];
let frame = FrameInfo::new(module.clone(), 0, Some(offset));
assert!(
!module.inner().debug.index_built(),
"constructing a frame must not parse DWARF"
);
let _ = frame.symbols();
assert!(
module.inner().debug.index_built(),
"inspecting symbols() parses DWARF"
);
}
#[test]
fn frame_without_debug_info_is_empty() {
let engine = Engine::default();
let module = Module::new(&engine, wat::parse_str("(module (func))").unwrap()).unwrap();
let frame = FrameInfo::new(module, 0, None);
assert!(frame.symbols().is_empty());
assert_eq!(frame.func_name(), None);
assert_eq!(frame.module_offset(), None);
assert_eq!(frame.func_offset(), None);
}
}