Skip to content

Commit 0203385

Browse files
authored
Debug API: switch DebugFrameCursor over to monomorphization on T, and provide AsContextMut. (#11873)
* Debug API: switch `DebugFrameCursor` over to monomorphization on T, and provide `AsContextMut`. This permits use of ordinary store accessors, such as GC object accessors, while viewing the stack. In #11835 and related discussion, we've concluded that (i) it is safe to provide this mutable access, including e.g. ability to do a GC, because debugger access comes only at points that are morally like hostcalls (right now, literally with a `Caller`, and eventually with debug yields); and (ii) we will need this in order to provide the full range of expected debugger functionality. Even before mutation-during-debugging comes into play, with the API before this change, it was not possible to read GC objects at all (because all accessors take a mutable store). * Review feedback. * Fix some disable-feature builds.
1 parent 3c46362 commit 0203385

File tree

6 files changed

+109
-48
lines changed

6 files changed

+109
-48
lines changed

crates/wasmtime/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,3 +416,4 @@ component-model-async-bytes = [
416416

417417
# Enables support for guest debugging.
418418
debug = ['runtime']
419+

crates/wasmtime/src/runtime/debug.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
//! Debugging API.
22
33
use crate::{
4-
AnyRef, ExnRef, ExternRef, Func, Instance, Module, Val,
4+
AnyRef, AsContext, AsContextMut, ExnRef, ExternRef, Func, Instance, Module, StoreContext,
5+
StoreContextMut, Val,
56
store::{AutoAssertNoGc, StoreOpaque},
67
vm::{CurrentActivationBacktrace, VMContext},
78
};
9+
use alloc::vec;
810
use alloc::vec::Vec;
911
use core::{ffi::c_void, ptr::NonNull};
12+
#[cfg(feature = "gc")]
13+
use wasmtime_environ::FrameTable;
1014
use wasmtime_environ::{
1115
DefinedFuncIndex, FrameInstPos, FrameStackShape, FrameStateSlot, FrameStateSlotOffset,
12-
FrameTable, FrameTableDescriptorIndex, FrameValType, FuncKey,
16+
FrameTableDescriptorIndex, FrameValType, FuncKey,
1317
};
1418
use wasmtime_unwinder::Frame;
1519

1620
use super::store::AsStoreOpaque;
1721

18-
impl StoreOpaque {
22+
impl<'a, T> StoreContextMut<'a, T> {
1923
/// Provide an object that captures Wasm stack state, including
2024
/// Wasm VM-level values (locals and operand stack).
2125
///
@@ -30,7 +34,7 @@ impl StoreOpaque {
3034
///
3135
/// Returns `None` if debug instrumentation is not enabled for
3236
/// the engine containing this store.
33-
pub fn debug_frames(&mut self) -> Option<DebugFrameCursor<'_>> {
37+
pub fn debug_frames(self) -> Option<DebugFrameCursor<'a, T>> {
3438
if !self.engine().tunables().debug_guest {
3539
return None;
3640
}
@@ -56,12 +60,12 @@ impl StoreOpaque {
5660
///
5761
/// See the documentation on `Store::stack_value` for more information
5862
/// about which frames this view will show.
59-
pub struct DebugFrameCursor<'a> {
63+
pub struct DebugFrameCursor<'a, T: 'static> {
6064
/// Iterator over frames.
6165
///
6266
/// This iterator owns the store while the view exists (accessible
6367
/// as `iter.store`).
64-
iter: CurrentActivationBacktrace<'a>,
68+
iter: CurrentActivationBacktrace<'a, T>,
6569

6670
/// Is the next frame to be visited by the iterator a trapping
6771
/// frame?
@@ -84,7 +88,7 @@ pub struct DebugFrameCursor<'a> {
8488
current: Option<FrameData>,
8589
}
8690

87-
impl<'a> DebugFrameCursor<'a> {
91+
impl<'a, T: 'static> DebugFrameCursor<'a, T> {
8892
/// Move up to the next frame in the activation.
8993
pub fn move_to_parent(&mut self) {
9094
// If there are no virtual frames to yield, take and decode
@@ -102,8 +106,11 @@ impl<'a> DebugFrameCursor<'a> {
102106
let Some(next_frame) = self.iter.next() else {
103107
return;
104108
};
105-
self.frames =
106-
VirtualFrame::decode(&mut self.iter.store, next_frame, self.is_trapping_frame);
109+
self.frames = VirtualFrame::decode(
110+
self.iter.store.0.as_store_opaque(),
111+
next_frame,
112+
self.is_trapping_frame,
113+
);
107114
debug_assert!(!self.frames.is_empty());
108115
self.is_trapping_frame = false;
109116
}
@@ -139,7 +146,7 @@ impl<'a> DebugFrameCursor<'a> {
139146
/// Get the instance associated with the current frame.
140147
pub fn instance(&mut self) -> Instance {
141148
let instance = self.raw_instance();
142-
Instance::from_wasmtime(instance.id(), self.iter.store.as_store_opaque())
149+
Instance::from_wasmtime(instance.id(), self.iter.store.0.as_store_opaque())
143150
}
144151

145152
/// Get the module associated with the current frame, if any
@@ -190,7 +197,7 @@ impl<'a> DebugFrameCursor<'a> {
190197
let slot_addr = data.slot_addr;
191198
// SAFETY: compiler produced metadata to describe this local
192199
// slot and stored a value of the correct type into it.
193-
unsafe { read_value(&mut self.iter.store, slot_addr, offset, ty) }
200+
unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
194201
}
195202

196203
/// Get the type and value of the given operand-stack value in
@@ -207,7 +214,7 @@ impl<'a> DebugFrameCursor<'a> {
207214
// SAFETY: compiler produced metadata to describe this
208215
// operand-stack slot and stored a value of the correct type
209216
// into it.
210-
unsafe { read_value(&mut self.iter.store, slot_addr, offset, ty) }
217+
unsafe { read_value(&mut self.iter.store.0, slot_addr, offset, ty) }
211218
}
212219
}
213220

@@ -236,11 +243,7 @@ struct VirtualFrame {
236243
impl VirtualFrame {
237244
/// Return virtual frames corresponding to a physical frame, from
238245
/// outermost to innermost.
239-
fn decode(
240-
store: &mut AutoAssertNoGc<'_>,
241-
frame: Frame,
242-
is_trapping_frame: bool,
243-
) -> Vec<VirtualFrame> {
246+
fn decode(store: &mut StoreOpaque, frame: Frame, is_trapping_frame: bool) -> Vec<VirtualFrame> {
244247
let module = store
245248
.modules()
246249
.lookup_module_by_pc(frame.pc())
@@ -336,7 +339,7 @@ impl FrameData {
336339
/// instrumentation is correct, and as long as the tables are
337340
/// preserved through serialization).
338341
unsafe fn read_value(
339-
store: &mut AutoAssertNoGc<'_>,
342+
store: &mut StoreOpaque,
340343
slot_base: *const u8,
341344
offset: FrameStateSlotOffset,
342345
ty: FrameValType,
@@ -399,6 +402,7 @@ unsafe fn read_value(
399402
// Note: ideally this would be an impl Iterator, but this is quite
400403
// awkward because of the locally computed data (FrameStateSlot::parse
401404
// structured result) within the closure borrowed by a nested closure.
405+
#[cfg(feature = "gc")]
402406
pub(crate) fn gc_refs_in_frame<'a>(ft: FrameTable<'a>, pc: u32, fp: *mut usize) -> Vec<*mut u32> {
403407
let fp = fp.cast::<u8>();
404408
let mut ret = vec![];
@@ -429,3 +433,15 @@ pub(crate) fn gc_refs_in_frame<'a>(ft: FrameTable<'a>, pc: u32, fp: *mut usize)
429433
}
430434
ret
431435
}
436+
437+
impl<'a, T: 'static> AsContext for DebugFrameCursor<'a, T> {
438+
type Data = T;
439+
fn as_context(&self) -> StoreContext<'_, Self::Data> {
440+
StoreContext(self.iter.store.0)
441+
}
442+
}
443+
impl<'a, T: 'static> AsContextMut for DebugFrameCursor<'a, T> {
444+
fn as_context_mut(&mut self) -> StoreContextMut<'_, Self::Data> {
445+
StoreContextMut(self.iter.store.0)
446+
}
447+
}

crates/wasmtime/src/runtime/func.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,8 +2235,8 @@ impl<T> Caller<'_, T> {
22352235
///
22362236
/// See ['Store::debug_frames`] for more details.
22372237
#[cfg(feature = "debug")]
2238-
pub fn debug_frames(&mut self) -> Option<crate::DebugFrameCursor<'_>> {
2239-
self.store.debug_frames()
2238+
pub fn debug_frames(&mut self) -> Option<crate::DebugFrameCursor<'_, T>> {
2239+
self.store.as_context_mut().debug_frames()
22402240
}
22412241
}
22422242

crates/wasmtime/src/runtime/store.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1184,8 +1184,8 @@ impl<T> Store<T> {
11841184
/// Returns `None` if debug instrumentation is not enabled for
11851185
/// the engine containing this store.
11861186
#[cfg(feature = "debug")]
1187-
pub fn debug_frames(&mut self) -> Option<crate::DebugFrameCursor<'_>> {
1188-
self.inner.debug_frames()
1187+
pub fn debug_frames(&mut self) -> Option<crate::DebugFrameCursor<'_, T>> {
1188+
self.as_context_mut().debug_frames()
11891189
}
11901190
}
11911191

@@ -1310,16 +1310,6 @@ impl<'a, T> StoreContextMut<'a, T> {
13101310
pub fn has_pending_exception(&self) -> bool {
13111311
self.0.inner.pending_exception.is_some()
13121312
}
1313-
1314-
/// Provide an object that views Wasm stack state, including Wasm
1315-
/// VM-level values (locals and operand stack), when debugging is
1316-
/// enabled.
1317-
///
1318-
/// See ['Store::debug_frames`] for more details.
1319-
#[cfg(feature = "debug")]
1320-
pub fn debug_frames(&mut self) -> Option<crate::DebugFrameCursor<'_>> {
1321-
self.0.inner.debug_frames()
1322-
}
13231313
}
13241314

13251315
impl<T> StoreInner<T> {
@@ -2707,6 +2697,12 @@ impl AsStoreOpaque for dyn VMStore {
27072697
}
27082698
}
27092699

2700+
impl<T: 'static> AsStoreOpaque for Store<T> {
2701+
fn as_store_opaque(&mut self) -> &mut StoreOpaque {
2702+
&mut self.inner.inner
2703+
}
2704+
}
2705+
27102706
impl<T: 'static> AsStoreOpaque for StoreInner<T> {
27112707
fn as_store_opaque(&mut self) -> &mut StoreOpaque {
27122708
self

crates/wasmtime/src/runtime/vm/traphandlers/backtrace.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121
//! exit FP and stopping once we reach the entry SP (meaning that the next older
2222
//! frame is a host frame).
2323
24+
#[cfg(feature = "debug")]
25+
use crate::StoreContextMut;
2426
use crate::prelude::*;
2527
use crate::runtime::store::StoreOpaque;
2628
use crate::runtime::vm::stack_switching::VMStackChain;
2729
use crate::runtime::vm::{
2830
Unwind, VMStoreContext,
2931
traphandlers::{CallThreadState, tls},
3032
};
31-
#[cfg(feature = "debug")]
32-
use crate::store::AutoAssertNoGc;
3333
#[cfg(all(feature = "gc", feature = "stack-switching"))]
3434
use crate::vm::stack_switching::{VMContRef, VMStackState};
3535
use core::ops::ControlFlow;
@@ -330,13 +330,13 @@ impl Backtrace {
330330

331331
/// An iterator over one Wasm activation.
332332
#[cfg(feature = "debug")]
333-
pub(crate) struct CurrentActivationBacktrace<'a> {
334-
pub(crate) store: AutoAssertNoGc<'a>,
333+
pub(crate) struct CurrentActivationBacktrace<'a, T: 'static> {
334+
pub(crate) store: StoreContextMut<'a, T>,
335335
inner: Box<dyn Iterator<Item = Frame>>,
336336
}
337337

338338
#[cfg(feature = "debug")]
339-
impl<'a> CurrentActivationBacktrace<'a> {
339+
impl<'a, T: 'static> CurrentActivationBacktrace<'a, T> {
340340
/// Return an iterator over the most recent Wasm activation.
341341
///
342342
/// The iterator captures the store with a mutable borrow, and
@@ -360,25 +360,24 @@ impl<'a> CurrentActivationBacktrace<'a> {
360360
/// while within host code called from that activation (which will
361361
/// ordinarily be ensured if the `store`'s lifetime came from the
362362
/// host entry point) then everything will be sound.
363-
pub(crate) unsafe fn new(store: &'a mut StoreOpaque) -> CurrentActivationBacktrace<'a> {
363+
pub(crate) unsafe fn new(store: StoreContextMut<'a, T>) -> CurrentActivationBacktrace<'a, T> {
364364
// Get the initial exit FP, exit PC, and entry FP.
365-
let vm_store_context = store.vm_store_context();
365+
let vm_store_context = store.0.vm_store_context();
366366
let exit_pc = unsafe { *(*vm_store_context).last_wasm_exit_pc.get() };
367367
let exit_fp = unsafe { (*vm_store_context).last_wasm_exit_fp() };
368368
let trampoline_fp = unsafe { *(*vm_store_context).last_wasm_entry_fp.get() };
369-
let unwind = store.unwinder();
369+
let unwind = store.0.unwinder();
370370
// Establish the iterator.
371371
let inner = Box::new(unsafe {
372372
wasmtime_unwinder::frame_iterator(unwind, exit_pc, exit_fp, trampoline_fp)
373373
});
374374

375-
let store = AutoAssertNoGc::new(store);
376375
CurrentActivationBacktrace { store, inner }
377376
}
378377
}
379378

380379
#[cfg(feature = "debug")]
381-
impl<'a> Iterator for CurrentActivationBacktrace<'a> {
380+
impl<'a, T: 'static> Iterator for CurrentActivationBacktrace<'a, T> {
382381
type Item = Frame;
383382
fn next(&mut self) -> Option<Self::Item> {
384383
self.inner.next()

tests/all/debug.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
//! Tests for instrumentation-based debugging.
22
3-
use wasmtime::{Caller, Config, Engine, Extern, Func, Instance, Module, Store};
3+
use wasmtime::{AsContextMut, Caller, Config, Engine, Extern, Func, Instance, Module, Store};
44

5-
fn test_stack_values<C: Fn(&mut Config), F: Fn(Caller<'_, ()>) + Send + Sync + 'static>(
6-
wat: &str,
5+
fn get_module_and_store<C: Fn(&mut Config)>(
76
c: C,
8-
f: F,
9-
) -> anyhow::Result<()> {
7+
wat: &str,
8+
) -> anyhow::Result<(Module, Store<()>)> {
109
let mut config = Config::default();
1110
config.guest_debug(true);
1211
config.wasm_exceptions(true);
1312
c(&mut config);
1413
let engine = Engine::new(&config)?;
1514
let module = Module::new(&engine, wat)?;
15+
Ok((module, Store::new(&engine, ())))
16+
}
1617

17-
let mut store = Store::new(&engine, ());
18+
fn test_stack_values<C: Fn(&mut Config), F: Fn(Caller<'_, ()>) + Send + Sync + 'static>(
19+
wat: &str,
20+
c: C,
21+
f: F,
22+
) -> anyhow::Result<()> {
23+
let (module, mut store) = get_module_and_store(c, wat)?;
1824
let func = Func::wrap(&mut store, move |caller: Caller<'_, ()>| {
1925
f(caller);
2026
});
@@ -137,3 +143,46 @@ fn stack_values_dead_gc_ref() -> anyhow::Result<()> {
137143
},
138144
)
139145
}
146+
147+
#[test]
148+
#[cfg_attr(miri, ignore)]
149+
fn gc_access_during_call() -> anyhow::Result<()> {
150+
test_stack_values(
151+
r#"
152+
(module
153+
(type $s (struct (field i32)))
154+
(import "" "host" (func))
155+
(func (export "main")
156+
(local $l (ref null $s))
157+
(local.set $l (struct.new $s (i32.const 42)))
158+
(call 0)))
159+
"#,
160+
|config| {
161+
config.wasm_gc(true);
162+
},
163+
|mut caller: Caller<'_, ()>| {
164+
let mut stack = caller.debug_frames().unwrap();
165+
166+
// Do a GC while we hold the stack cursor.
167+
stack.as_context_mut().gc(None);
168+
169+
assert!(!stack.done());
170+
assert_eq!(stack.num_stacks(), 0);
171+
assert_eq!(stack.num_locals(), 1);
172+
// Note that this struct is dead during the call, and the
173+
// ref could otherwise be optimized away (no longer in the
174+
// stackmap at this point); but we verify it is still
175+
// alive here because it is rooted in the
176+
// debug-instrumentation slot.
177+
let s = stack
178+
.local(0)
179+
.unwrap_any_ref()
180+
.unwrap()
181+
.unwrap_struct(&stack)
182+
.unwrap();
183+
assert_eq!(s.field(&mut stack, 0).unwrap().unwrap_i32(), 42);
184+
stack.move_to_parent();
185+
assert!(stack.done());
186+
},
187+
)
188+
}

0 commit comments

Comments
 (0)