use std::{
any::Any,
future::Future,
pin::Pin,
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use tokio::{
sync::{Mutex, mpsc},
task::JoinHandle,
};
use wasmtime::{
AsContextMut, DebugEvent, DebugHandler, Engine, ExnRef, OwnedRooted, Result, Store,
StoreContextMut, Trap,
};
mod host;
pub use host::{DebuggerComponent, add_debuggee, add_to_linker, wit};
pub struct Debuggee<T: Send + 'static> {
engine: Engine,
state: DebuggeeState,
store: Option<Store<T>>,
in_tx: mpsc::Sender<Command<T>>,
out_rx: mpsc::Receiver<Response<T>>,
handle: Option<JoinHandle<Result<()>>>,
interrupt_pending: Arc<AtomicBool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DebuggeeState {
Initial,
Running,
Paused,
Queried,
Complete,
}
enum Command<T: 'static> {
Continue,
Query(Box<dyn FnOnce(StoreContextMut<'_, T>) -> Box<dyn Any + Send> + Send>),
}
enum Response<T: 'static> {
Paused(DebugRunResult),
QueryResponse(Box<dyn Any + Send>),
Finished(Store<T>),
}
struct HandlerInner<T: Send + 'static> {
in_rx: Mutex<mpsc::Receiver<Command<T>>>,
out_tx: mpsc::Sender<Response<T>>,
interrupt_pending: Arc<AtomicBool>,
}
struct Handler<T: Send + 'static>(Arc<HandlerInner<T>>);
impl<T: Send + 'static> std::clone::Clone for Handler<T> {
fn clone(&self) -> Self {
Handler(self.0.clone())
}
}
impl<T: Send + 'static> DebugHandler for Handler<T> {
type Data = T;
async fn handle(&self, mut store: StoreContextMut<'_, T>, event: DebugEvent<'_>) {
let mut in_rx = self.0.in_rx.lock().await;
let result = match event {
DebugEvent::HostcallError(_) => DebugRunResult::HostcallError,
DebugEvent::CaughtExceptionThrown(exn) => DebugRunResult::CaughtExceptionThrown(exn),
DebugEvent::UncaughtExceptionThrown(exn) => {
DebugRunResult::UncaughtExceptionThrown(exn)
}
DebugEvent::Trap(trap) => DebugRunResult::Trap(trap),
DebugEvent::Breakpoint => DebugRunResult::Breakpoint,
DebugEvent::EpochYield => {
if !self.0.interrupt_pending.swap(false, Ordering::SeqCst) {
return;
}
DebugRunResult::EpochYield
}
};
if self.0.out_tx.send(Response::Paused(result)).await.is_err() {
return;
}
while let Some(cmd) = in_rx.recv().await {
match cmd {
Command::Query(closure) => {
let result = closure(store.as_context_mut());
if self
.0
.out_tx
.send(Response::QueryResponse(result))
.await
.is_err()
{
return;
}
}
Command::Continue => {
break;
}
}
}
}
}
impl<T: Send + 'static> Debuggee<T> {
pub fn new<F>(mut store: Store<T>, inner: F) -> Debuggee<T>
where
F: for<'a> FnOnce(
&'a mut Store<T>,
) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>>
+ Send
+ 'static,
{
let engine = store.engine().clone();
let (in_tx, in_rx) = mpsc::channel(1);
let (out_tx, out_rx) = mpsc::channel(1);
let interrupt_pending = Arc::new(AtomicBool::new(false));
let handle = tokio::spawn({
let interrupt_pending = interrupt_pending.clone();
async move {
let out_tx_clone = out_tx.clone();
let handler = Handler(Arc::new(HandlerInner {
in_rx: Mutex::new(in_rx),
out_tx,
interrupt_pending,
}));
log::trace!("inner debuggee task: first breakpoint");
handler
.handle(store.as_context_mut(), DebugEvent::Breakpoint)
.await;
log::trace!("inner debuggee task: first breakpoint resumed");
store.set_debug_handler(handler);
log::trace!("inner debuggee task: running `inner`");
let result = inner(&mut store).await;
log::trace!("inner debuggee task: done with `inner`");
let _ = out_tx_clone.send(Response::Finished(store)).await;
result
}
});
Debuggee {
engine,
state: DebuggeeState::Initial,
store: None,
in_tx,
out_rx,
interrupt_pending,
handle: Some(handle),
}
}
pub fn is_complete(&self) -> bool {
match self.state {
DebuggeeState::Complete => true,
_ => false,
}
}
pub fn engine(&self) -> &Engine {
&self.engine
}
pub fn interrupt_pending(&self) -> &Arc<AtomicBool> {
&self.interrupt_pending
}
async fn wait_for_initial(&mut self) -> Result<()> {
if let DebuggeeState::Initial = &self.state {
let response = self
.out_rx
.recv()
.await
.ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?;
assert!(matches!(response, Response::Paused(_)));
self.state = DebuggeeState::Paused;
}
Ok(())
}
pub async fn run(&mut self) -> Result<DebugRunResult> {
log::trace!("running: state is {:?}", self.state);
self.wait_for_initial().await?;
match self.state {
DebuggeeState::Initial => unreachable!(),
DebuggeeState::Paused => {
log::trace!("sending Continue");
self.in_tx
.send(Command::Continue)
.await
.map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?;
log::trace!("sent Continue");
self.state = DebuggeeState::Running;
}
DebuggeeState::Running => {
}
DebuggeeState::Queried => {
log::trace!("in Queried; receiving");
let response =
self.out_rx.recv().await.ok_or_else(|| {
wasmtime::format_err!("Premature close of debugger channel")
})?;
log::trace!("in Queried; received, dropping");
assert!(matches!(response, Response::QueryResponse(_)));
self.state = DebuggeeState::Paused;
log::trace!("in Paused; sending Continue");
self.in_tx
.send(Command::Continue)
.await
.map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?;
self.state = DebuggeeState::Running;
}
DebuggeeState::Complete => {
panic!("Cannot `run()` an already-complete Debuggee");
}
}
log::trace!("waiting for response");
let response = self
.out_rx
.recv()
.await
.ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?;
match response {
Response::Finished(store) => {
log::trace!("got Finished");
self.state = DebuggeeState::Complete;
self.store = Some(store);
Ok(DebugRunResult::Finished)
}
Response::Paused(result) => {
log::trace!("got Paused");
self.state = DebuggeeState::Paused;
Ok(result)
}
Response::QueryResponse(_) => {
panic!("Invalid debug response");
}
}
}
pub async fn finish(&mut self) -> Result<()> {
if self.is_complete() {
return Ok(());
}
loop {
match self.run().await? {
DebugRunResult::Finished => break,
e => {
log::trace!("finish: event {e:?}");
}
}
}
if let Some(handle) = self.handle.take() {
handle.await??;
}
assert!(self.is_complete());
Ok(())
}
pub async fn with_store<
F: FnOnce(StoreContextMut<'_, T>) -> R + Send + 'static,
R: Send + 'static,
>(
&mut self,
f: F,
) -> Result<R> {
if let Some(store) = self.store.as_mut() {
return Ok(f(store.as_context_mut()));
}
self.wait_for_initial().await?;
match self.state {
DebuggeeState::Initial => unreachable!(),
DebuggeeState::Queried => {
let response =
self.out_rx.recv().await.ok_or_else(|| {
wasmtime::format_err!("Premature close of debugger channel")
})?;
assert!(matches!(response, Response::QueryResponse(_)));
self.state = DebuggeeState::Paused;
}
DebuggeeState::Running => {
panic!("Cannot query in Running state");
}
DebuggeeState::Complete => {
panic!("Cannot query when complete");
}
DebuggeeState::Paused => {
}
}
log::trace!("sending query in with_store");
self.in_tx
.send(Command::Query(Box::new(|store| Box::new(f(store)))))
.await
.map_err(|_| wasmtime::format_err!("Premature close of debugger channel"))?;
self.state = DebuggeeState::Queried;
let response = self
.out_rx
.recv()
.await
.ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?;
let Response::QueryResponse(resp) = response else {
wasmtime::bail!("Incorrect response from debugger task");
};
self.state = DebuggeeState::Paused;
Ok(*resp.downcast::<R>().expect("type mismatch"))
}
}
#[derive(Debug)]
pub enum DebugRunResult {
Finished,
HostcallError,
EpochYield,
CaughtExceptionThrown(OwnedRooted<ExnRef>),
UncaughtExceptionThrown(OwnedRooted<ExnRef>),
Trap(Trap),
Breakpoint,
}
#[cfg(test)]
mod test {
use super::*;
use wasmtime::*;
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn basic_debugger() -> wasmtime::Result<()> {
let _ = env_logger::try_init();
let mut config = Config::new();
config.guest_debug(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(func (export "main") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
"#,
)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new_async(&mut store, &module, &[]).await?;
let main = instance.get_func(&mut store, "main").unwrap();
let mut debuggee = Debuggee::new(store, move |store| {
Box::pin(async move {
let mut results = [Val::I32(0)];
store.edit_breakpoints().unwrap().single_step(true).unwrap();
main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..])
.await?;
assert_eq!(results[0].unwrap_i32(), 3);
main.call_async(&mut *store, &[Val::I32(3), Val::I32(4)], &mut results[..])
.await?;
assert_eq!(results[0].unwrap_i32(), 7);
Ok(())
})
});
let event = debuggee.run().await?;
assert!(matches!(event, DebugRunResult::Breakpoint));
debuggee
.with_store(|mut store| {
let frame = store.debug_exit_frames().next().unwrap();
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.0
.as_u32(),
0
);
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.1
.raw(),
36
);
assert_eq!(frame.num_locals(&mut store).unwrap(), 2);
assert_eq!(frame.num_stacks(&mut store).unwrap(), 0);
assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1);
assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2);
let frame = frame.parent(&mut store).unwrap();
assert!(frame.is_none());
})
.await?;
let event = debuggee.run().await?;
assert!(matches!(event, DebugRunResult::Breakpoint));
debuggee
.with_store(|mut store| {
let frame = store.debug_exit_frames().next().unwrap();
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.0
.as_u32(),
0
);
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.1
.raw(),
38
);
assert_eq!(frame.num_locals(&mut store).unwrap(), 2);
assert_eq!(frame.num_stacks(&mut store).unwrap(), 1);
assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1);
assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2);
assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 1);
let frame = frame.parent(&mut store).unwrap();
assert!(frame.is_none());
})
.await?;
let event = debuggee.run().await?;
assert!(matches!(event, DebugRunResult::Breakpoint));
debuggee
.with_store(|mut store| {
let frame = store.debug_exit_frames().next().unwrap();
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.0
.as_u32(),
0
);
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.1
.raw(),
40
);
assert_eq!(frame.num_locals(&mut store).unwrap(), 2);
assert_eq!(frame.num_stacks(&mut store).unwrap(), 2);
assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1);
assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2);
assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 1);
assert_eq!(frame.stack(&mut store, 1).unwrap().unwrap_i32(), 2);
let frame = frame.parent(&mut store).unwrap();
assert!(frame.is_none());
})
.await?;
let event = debuggee.run().await?;
assert!(matches!(event, DebugRunResult::Breakpoint));
debuggee
.with_store(|mut store| {
let frame = store.debug_exit_frames().next().unwrap();
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.0
.as_u32(),
0
);
assert_eq!(
frame
.wasm_function_index_and_pc(&mut store)
.unwrap()
.unwrap()
.1
.raw(),
41
);
assert_eq!(frame.num_locals(&mut store).unwrap(), 2);
assert_eq!(frame.num_stacks(&mut store).unwrap(), 1);
assert_eq!(frame.local(&mut store, 0).unwrap().unwrap_i32(), 1);
assert_eq!(frame.local(&mut store, 1).unwrap().unwrap_i32(), 2);
assert_eq!(frame.stack(&mut store, 0).unwrap().unwrap_i32(), 3);
let frame = frame.parent(&mut store).unwrap();
assert!(frame.is_none());
})
.await?;
debuggee
.with_store(|store| {
store
.edit_breakpoints()
.unwrap()
.single_step(false)
.unwrap();
})
.await?;
let event = debuggee.run().await?;
assert!(matches!(event, DebugRunResult::Finished));
assert!(debuggee.is_complete());
Ok(())
}
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn early_finish() -> Result<()> {
let _ = env_logger::try_init();
let mut config = Config::new();
config.guest_debug(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(func (export "main") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
"#,
)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new_async(&mut store, &module, &[]).await?;
let main = instance.get_func(&mut store, "main").unwrap();
let mut debuggee = Debuggee::new(store, move |store| {
Box::pin(async move {
let mut results = [Val::I32(0)];
store.edit_breakpoints().unwrap().single_step(true).unwrap();
main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..])
.await?;
assert_eq!(results[0].unwrap_i32(), 3);
Ok(())
})
});
debuggee.finish().await?;
assert!(debuggee.is_complete());
Ok(())
}
#[tokio::test]
#[cfg_attr(miri, ignore)]
async fn drop_debuggee_and_store() -> Result<()> {
let _ = env_logger::try_init();
let mut config = Config::new();
config.guest_debug(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(func (export "main") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add))
"#,
)?;
let mut store = Store::new(&engine, ());
let instance = Instance::new_async(&mut store, &module, &[]).await?;
let main = instance.get_func(&mut store, "main").unwrap();
let mut debuggee = Debuggee::new(store, move |store| {
Box::pin(async move {
let mut results = [Val::I32(0)];
store.edit_breakpoints().unwrap().single_step(true).unwrap();
main.call_async(&mut *store, &[Val::I32(1), Val::I32(2)], &mut results[..])
.await?;
assert_eq!(results[0].unwrap_i32(), 3);
Ok(())
})
});
let _ = debuggee.run().await?;
Ok(())
}
}