use std::sync::{Arc, OnceLock};
use crate::module::Module;
use crate::store::AsContext;
#[derive(Clone, Debug)]
pub struct WasmBacktrace {
frames: Vec<FrameInfo>,
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(())
}
}
impl std::error::Error for WasmBacktrace {}
impl WasmBacktrace {
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())
}
pub fn force_capture(store: impl AsContext) -> WasmBacktrace {
crate::exec::trace::from_parked(store.as_context().inner())
}
pub fn frames(&self) -> &[FrameInfo] {
&self.frames
}
pub(crate) fn from_frames(frames: Vec<FrameInfo>) -> WasmBacktrace {
WasmBacktrace {
frames,
host_boundaries: Vec::new(),
}
}
pub(crate) fn from_frames_with_boundaries(
frames: Vec<FrameInfo>,
host_boundaries: Vec<usize>,
) -> WasmBacktrace {
WasmBacktrace {
frames,
host_boundaries,
}
}
}
#[derive(Debug)]
pub struct FrameInfo {
module: Module,
func_index: u32,
code_offset: Option<u32>,
symbols: OnceLock<Box<[FrameSymbol]>>,
}
impl Clone for FrameInfo {
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(),
}
}
pub fn func_index(&self) -> u32 {
self.func_index
}
pub fn module(&self) -> &Module {
&self.module
}
pub fn func_name(&self) -> Option<&str> {
self.module.inner().debug.func_name(self.func_index)
}
pub fn module_offset(&self) -> Option<usize> {
self.code_offset.map(|o| o as usize)
}
pub fn func_offset(&self) -> Option<usize> {
let off = self.code_offset?;
off.checked_sub(self.func_start()?).map(|o| o as usize)
}
pub fn symbols(&self) -> &[FrameSymbol] {
self.symbols.get_or_init(|| self.build_symbols())
}
fn func_start(&self) -> Option<u32> {
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;
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)),
}])
}
}
#[derive(Clone, Debug)]
pub struct FrameSymbol {
name: Option<Arc<str>>,
file: Option<Arc<str>>,
line: Option<u32>,
column: Option<u32>,
}
impl FrameSymbol {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn file(&self) -> Option<&str> {
self.file.as_deref()
}
pub fn line(&self) -> Option<u32> {
self.line
}
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};
#[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());
}
const FIXTURE: &[u8] = include_bytes!("module/debug/testdata/fixture.wasm");
#[test]
fn frame_symbolicates_against_dwarf() {
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));
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(); }
#[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);
}
}