diff --git a/crates/environ/src/module_artifacts.rs b/crates/environ/src/module_artifacts.rs index f8091f9c848e..853f07095400 100644 --- a/crates/environ/src/module_artifacts.rs +++ b/crates/environ/src/module_artifacts.rs @@ -431,6 +431,19 @@ impl CompiledFunctionsTable { if loc.is_empty() { None } else { Some(loc) } } + /// Get the range of the text section that holds all of `namespace`'s + /// functions, if it has any. + /// + /// A namespace's functions are laid out together, so no other function + /// falls inside this range. + pub fn text_range(&self, namespace: FuncKeyNamespace) -> Option> { + let ns_idx = self.namespace_index(namespace)?; + let Range { start, end } = self.func_loc_range(ns_idx); + let first = self.func_locs.get(start)?; + let last = &self.func_locs[FuncLocIndex::from_u32(end.as_u32() - 1)]; + Some(first.start..last.start + last.length) + } + fn src_loc_index(&self, key: FuncKey) -> Option { let (key_ns, key_index) = key.into_parts(); if !Self::has_src_locs(key_ns.kind()) { diff --git a/crates/wasmtime/src/runtime/debug.rs b/crates/wasmtime/src/runtime/debug.rs index 9df28c0f114a..3b4d99bd0289 100644 --- a/crates/wasmtime/src/runtime/debug.rs +++ b/crates/wasmtime/src/runtime/debug.rs @@ -1260,7 +1260,13 @@ impl<'a> BreakpointEdit<'a> { let table = module .frame_table() .expect("Frame table must be present when guest-debug is enabled"); + // A component's modules share one code image, and so its frame table: leave the + // other modules' code to them, or this module's breakpoints decide theirs. + let own_code = module.compiled_module().defined_funcs_text_range(); for (wasm_pc, patch) in table.breakpoint_patches() { + if !own_code.contains(&patch.offset) { + continue; + } let key = BreakpointKey::from_raw(&module, wasm_pc); let this_enabled = enabled || key_enabled(&key); log::trace!( diff --git a/crates/wasmtime/src/runtime/instantiate.rs b/crates/wasmtime/src/runtime/instantiate.rs index 6d597811131d..9b9baeea64c7 100644 --- a/crates/wasmtime/src/runtime/instantiate.rs +++ b/crates/wasmtime/src/runtime/instantiate.rs @@ -143,6 +143,23 @@ impl CompiledModule { self.engine_code.raw_wasm_to_array_trampoline_data(range) } + /// Returns the range of the text section that holds this module's + /// defined functions. + /// + /// A component's modules share one text section, so this excludes the + /// other modules' functions. + pub fn defined_funcs_text_range(&self) -> Range { + let namespace = + FuncKey::DefinedWasmFunction(self.module_index(), DefinedFuncIndex::from_u32(0)) + .namespace(); + match self.index.text_range(namespace) { + Some(range) => { + usize::try_from(range.start).unwrap()..usize::try_from(range.end).unwrap() + } + None => 0..0, + } + } + /// Lookups a defined function by a program counter value. /// /// Returns the defined function index and the relative address of diff --git a/tests/all/debug.rs b/tests/all/debug.rs index 88885fb03208..378a676d1802 100644 --- a/tests/all/debug.rs +++ b/tests/all/debug.rs @@ -1613,6 +1613,111 @@ async fn breakpoint_slips_to_first_opcode() -> wasmtime::Result<()> { Ok(()) } +/// A component of two core modules, whose exported functions are on different PCs: `$pad` +/// keeps `f2`'s code off the ones `f1` has. +const TWO_MODULES: &str = r#"(component + (core module $m1 + (func (export "f1") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.add)) + (core instance $i1 (instantiate $m1)) + (core module $m2 + (func $pad (result i32) + i32.const 1 + i32.const 2 + i32.add + i32.const 3 + i32.add) + (func (export "f2") (param i32 i32) (result i32) + local.get 0 + local.get 1 + i32.mul)) + (core instance $i2 (instantiate $m2)) + (func (export "f1") (param "a" s32) (param "b" s32) (result s32) + (canon lift (core func $i1 "f1"))) + (func (export "f2") (param "a" s32) (param "b" s32) (result s32) + (canon lift (core func $i2 "f2"))))"#; + +/// Records the PC of each debug event. +#[derive(Clone)] +struct RecordPcs(Arc>>); + +impl DebugHandler for RecordPcs { + type Data = (); + fn handle( + &self, + mut store: StoreContextMut<'_, ()>, + _event: DebugEvent<'_>, + ) -> impl std::future::Future + Send { + let frame = store.debug_exit_frames().next().unwrap(); + let (_, pc) = frame + .wasm_function_index_and_pc(&mut store) + .unwrap() + .unwrap(); + self.0.lock().unwrap().push(pc.raw()); + async {} + } +} + +/// Loads [`TWO_MODULES`], and calls `f1` then `f2`, saying the PCs of each one's events. +/// `edit` sets breakpoints first, given the component's modules. +async fn two_modules( + edit: impl FnOnce(&mut Store<()>, &[Module]) -> wasmtime::Result<()>, +) -> wasmtime::Result<(Vec, Vec)> { + use wasmtime::component::{Component, Linker}; + + let mut config = Config::default(); + config.guest_debug(true); + let engine = Engine::new(&config)?; + let component = Component::new(&engine, TWO_MODULES)?; + let linker: Linker<()> = Linker::new(&engine); + let mut store = Store::new(&engine, ()); + let instance = linker.instantiate_async(&mut store, &component).await?; + let pcs = Arc::new(Mutex::new(Vec::new())); + store.set_debug_handler(RecordPcs(pcs.clone())); + let modules = store.debug_all_modules(); + assert_eq!(modules.len(), 2); + edit(&mut store, &modules)?; + + let f1 = instance.get_typed_func::<(i32, i32), (i32,)>(&mut store, "f1")?; + f1.call_async(&mut store, (3, 5)).await?; + let in_f1 = std::mem::take(&mut *pcs.lock().unwrap()); + let f2 = instance.get_typed_func::<(i32, i32), (i32,)>(&mut store, "f2")?; + f2.call_async(&mut store, (3, 5)).await?; + let in_f2 = std::mem::take(&mut *pcs.lock().unwrap()); + Ok((in_f1, in_f2)) +} + +#[tokio::test] +#[cfg_attr(miri, ignore)] +async fn component_breakpoint_survives_single_step() -> wasmtime::Result<()> { + let _ = env_logger::try_init(); + + // Each function's first PC, from a single step through it. + let (f1, f2) = + two_modules(|store, _| store.edit_breakpoints().unwrap().single_step(true)).await?; + assert!(f1.iter().all(|pc| !f2.contains(pc)), "{f1:?} {f2:?}"); + + // A breakpoint in either module stops only there, after single-stepping has been on and off. + for (at, pc) in [(0, f1[0]), (1, f2[0])] { + let events = two_modules(|store, modules| { + let mut edit = store.edit_breakpoints().unwrap(); + edit.add_breakpoint(&modules[at], ModulePC::new(pc))?; + edit.single_step(true)?; + edit.single_step(false) + }) + .await?; + let wanted = if at == 0 { + (vec![pc], vec![]) + } else { + (vec![], vec![pc]) + }; + assert_eq!(events, wanted, "a breakpoint in module {at}"); + } + Ok(()) +} + #[tokio::test] #[cfg_attr(miri, ignore)] async fn component_module_relative_breakpoint_pcs() -> wasmtime::Result<()> {