Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/debugger/src/lib.rs
3073 views
1
//! Wasmtime debugger functionality.
2
//!
3
//! This crate builds on top of the core Wasmtime crate's
4
//! guest-debugger APIs to present an environment where a debugger
5
//! runs as a "co-running process" and sees the debugee as a a
6
//! provider of a stream of events, on which actions can be taken
7
//! between each event.
8
//!
9
//! In the future, this crate will also provide a WIT-level API and
10
//! world in which to run debugger components.
11
12
use std::{any::Any, sync::Arc};
13
use tokio::{
14
sync::{Mutex, mpsc},
15
task::JoinHandle,
16
};
17
use wasmtime::{
18
AsContextMut, DebugEvent, DebugHandler, ExnRef, OwnedRooted, Result, Store, StoreContextMut,
19
Trap,
20
};
21
22
/// A `Debugger` wraps up state associated with debugging the code
23
/// running in a single `Store`.
24
///
25
/// It acts as a Future combinator, wrapping an inner async body that
26
/// performs some actions on a store. Those actions are subject to the
27
/// debugger, and debugger events will be raised as appropriate. From
28
/// the "outside" of this combinator, it is always in one of two
29
/// states: running or paused. When paused, it acts as a
30
/// `StoreContextMut` and can allow examining the paused execution's
31
/// state. One runs until the next event suspends execution by
32
/// invoking `Debugger::run`.
33
pub struct Debugger<T: Send + 'static> {
34
/// The inner task that this debugger wraps.
35
inner: Option<JoinHandle<Result<Store<T>>>>,
36
/// State: either a task handle or the store when passed out of
37
/// the complete task.
38
state: DebuggerState,
39
in_tx: mpsc::Sender<Command<T>>,
40
out_rx: mpsc::Receiver<Response>,
41
}
42
43
/// State machine from the perspective of the outer logic.
44
///
45
/// The intermediate states here, and the separation of these states
46
/// from the `JoinHandle` above, are what allow us to implement a
47
/// cancel-safe version of `Debugger::run` below.
48
///
49
/// The state diagram for the outer logic is:
50
///
51
/// ```plain
52
/// (start)
53
/// v
54
/// |
55
/// .--->---------. v
56
/// | .----< Paused <-----------------------------------------------.
57
/// | | v |
58
/// | | | (async fn run() starts, sends Command::Continue) |
59
/// | | | |
60
/// | | v ^
61
/// | | Running |
62
/// | | v v (async fn run() receives Response::Paused, returns) |
63
/// | | | |_____________________________________________________|
64
/// | | |
65
/// | | | (async fn run() receives Response::Finished, returns)
66
/// | | v
67
/// | | Complete
68
/// | |
69
/// ^ | (async fn with_store() starts, sends Command::Query)
70
/// | v
71
/// | Queried
72
/// | |
73
/// | | (async fn with_store() receives Response::QueryResponse, returns)
74
/// `---<-'
75
/// ```
76
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77
enum DebuggerState {
78
/// Inner body is running in an async task and not in a debugger
79
/// callback. Outer logic is waiting for a `Response::Paused` or
80
/// `Response::Complete`.
81
Running,
82
/// Inner body is running in an async task and at a debugger
83
/// callback (or in the initial trampoline waiting for the first
84
/// `Continue`). `Response::Paused` has been received. Outer
85
/// logic has not sent any commands.
86
Paused,
87
/// We have sent a command to the inner body and are waiting for a
88
/// response.
89
Queried,
90
/// Inner body is complete (has sent `Response::Finished` and we
91
/// have received it). We may or may not have joined yet; if so,
92
/// the `Option<JoinHandle<...>>` will be `None`.
93
Complete,
94
}
95
96
/// Message from "outside" to the debug hook.
97
///
98
/// The `Query` catch-all with a boxed closure is a little janky, but
99
/// is the way that we provide access
100
/// from outside to the Store (which is owned by `inner` above)
101
/// only during pauses. Note that the future cannot take full
102
/// ownership or a mutable borrow of the Store, because it cannot
103
/// hold this across async yield points.
104
///
105
/// Instead, the debugger body sends boxed closures which take the
106
/// Store as a parameter (lifetime-limited not to escape that
107
/// closure) out to this crate's implementation that runs inside of
108
/// debugger-instrumentation callbacks (which have access to the
109
/// Store during their duration). We send return values
110
/// back. Return values are boxed Any values.
111
///
112
/// If we wanted to make this a little more principled, we could
113
/// come up with a Command/Response pair of enums for all possible
114
/// closures and make everything more statically typed and less
115
/// Box'd, but that would severely restrict the flexibility of the
116
/// abstraction here and essentially require writing a full proxy
117
/// of the debugger API.
118
///
119
/// Furthermore, we expect to rip this out eventually when we move
120
/// the debugger over to an async implementation based on
121
/// `run_concurrent` and `Accessor`s (see #11896). Building things
122
/// this way now will actually allow a less painful transition at
123
/// that time, because we will have a bunch of closures accessing
124
/// the store already and we can run those "with an accessor"
125
/// instead.
126
enum Command<T: 'static> {
127
Continue,
128
Query(Box<dyn FnOnce(StoreContextMut<'_, T>) -> Box<dyn Any + Send> + Send>),
129
}
130
131
enum Response {
132
Paused(DebugRunResult),
133
QueryResponse(Box<dyn Any + Send>),
134
Finished,
135
}
136
137
struct HandlerInner<T: Send + 'static> {
138
in_rx: Mutex<mpsc::Receiver<Command<T>>>,
139
out_tx: mpsc::Sender<Response>,
140
}
141
142
struct Handler<T: Send + 'static>(Arc<HandlerInner<T>>);
143
144
impl<T: Send + 'static> std::clone::Clone for Handler<T> {
145
fn clone(&self) -> Self {
146
Handler(self.0.clone())
147
}
148
}
149
150
impl<T: Send + 'static> DebugHandler for Handler<T> {
151
type Data = T;
152
async fn handle(&self, mut store: StoreContextMut<'_, T>, event: DebugEvent<'_>) {
153
let mut in_rx = self.0.in_rx.lock().await;
154
155
let result = match event {
156
DebugEvent::HostcallError(_) => DebugRunResult::HostcallError,
157
DebugEvent::CaughtExceptionThrown(exn) => DebugRunResult::CaughtExceptionThrown(exn),
158
DebugEvent::UncaughtExceptionThrown(exn) => {
159
DebugRunResult::UncaughtExceptionThrown(exn)
160
}
161
DebugEvent::Trap(trap) => DebugRunResult::Trap(trap),
162
DebugEvent::Breakpoint => DebugRunResult::Breakpoint,
163
DebugEvent::EpochYield => DebugRunResult::EpochYield,
164
};
165
self.0
166
.out_tx
167
.send(Response::Paused(result))
168
.await
169
.expect("outbound channel closed prematurely");
170
171
while let Some(cmd) = in_rx.recv().await {
172
match cmd {
173
Command::Query(closure) => {
174
let result = closure(store.as_context_mut());
175
self.0
176
.out_tx
177
.send(Response::QueryResponse(result))
178
.await
179
.expect("outbound channel closed prematurely");
180
}
181
Command::Continue => {
182
break;
183
}
184
}
185
}
186
}
187
}
188
189
impl<T: Send + 'static> Debugger<T> {
190
/// Create a new Debugger that attaches to the given Store and
191
/// runs the given inner body.
192
///
193
/// The debugger is always in one of two states: running or
194
/// paused.
195
///
196
/// When paused, the holder of this object can invoke
197
/// `Debugger::run` to enter the running state. The inner body
198
/// will run until paused by a debug event. While running, the
199
/// future returned by either of these methods owns the `Debugger`
200
/// and hence no other methods can be invoked.
201
///
202
/// When paused, the holder of this object can access the `Store`
203
/// indirectly by providing a closure
204
pub fn new<F, I>(mut store: Store<T>, inner: F) -> Debugger<T>
205
where
206
I: Future<Output = Result<Store<T>>> + Send + 'static,
207
F: for<'a> FnOnce(Store<T>) -> I + Send + 'static,
208
{
209
let (in_tx, mut in_rx) = mpsc::channel(1);
210
let (out_tx, out_rx) = mpsc::channel(1);
211
212
let inner = tokio::spawn(async move {
213
// Receive one "continue" command on the inbound channel
214
// before continuing.
215
match in_rx.recv().await {
216
Some(cmd) => {
217
assert!(matches!(cmd, Command::Continue));
218
}
219
None => {
220
// Premature exit due to closed channel. Just drop `inner`.
221
wasmtime::bail!("Debugger channel dropped");
222
}
223
}
224
225
let out_tx_clone = out_tx.clone();
226
store.set_debug_handler(Handler(Arc::new(HandlerInner {
227
in_rx: Mutex::new(in_rx),
228
out_tx,
229
})));
230
let result = inner(store).await;
231
let _ = out_tx_clone.send(Response::Finished).await;
232
result
233
});
234
235
Debugger {
236
inner: Some(inner),
237
state: DebuggerState::Paused,
238
in_tx,
239
out_rx,
240
}
241
}
242
243
/// Is the inner body done running?
244
pub fn is_complete(&self) -> bool {
245
match self.state {
246
DebuggerState::Complete => true,
247
_ => false,
248
}
249
}
250
251
/// Run the inner body until the next debug event.
252
///
253
/// This method is cancel-safe, and no events will be lost.
254
pub async fn run(&mut self) -> Result<DebugRunResult> {
255
log::trace!("running: state is {:?}", self.state);
256
match self.state {
257
DebuggerState::Paused => {
258
log::trace!("sending Continue");
259
self.in_tx
260
.send(Command::Continue)
261
.await
262
.map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?;
263
log::trace!("sent Continue");
264
265
// If that `send` was canceled, the command was not
266
// sent, so it's fine to remain in `Paused`. If it
267
// succeeded and we reached here, transition to
268
// `Running` so we don't re-send.
269
self.state = DebuggerState::Running;
270
}
271
DebuggerState::Running => {
272
// Previous `run()` must have been canceled; no action
273
// to take here.
274
}
275
DebuggerState::Queried => {
276
// We expect to receive a `QueryResponse`; drop it if
277
// the query was canceled, then transition back to
278
// `Paused`.
279
log::trace!("in Queried; receiving");
280
let response =
281
self.out_rx.recv().await.ok_or_else(|| {
282
wasmtime::format_err!("Premature close of debugger channel")
283
})?;
284
log::trace!("in Queried; received, dropping");
285
assert!(matches!(response, Response::QueryResponse(_)));
286
self.state = DebuggerState::Paused;
287
288
// Now send a `Continue`, as above.
289
log::trace!("in Paused; sending Continue");
290
self.in_tx
291
.send(Command::Continue)
292
.await
293
.map_err(|_| wasmtime::format_err!("Failed to send over debug channel"))?;
294
self.state = DebuggerState::Running;
295
}
296
DebuggerState::Complete => {
297
panic!("Cannot `run()` an already-complete Debugger");
298
}
299
}
300
301
// At this point, the inner task is in Running state. We
302
// expect to receive a message when it next pauses or
303
// completes. If this `recv()` is canceled, no message is
304
// lost, and the state above accurately reflects what must be
305
// done on the next `run()`.
306
log::trace!("waiting for response");
307
let response = self
308
.out_rx
309
.recv()
310
.await
311
.ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?;
312
313
match response {
314
Response::Finished => {
315
log::trace!("got Finished");
316
self.state = DebuggerState::Complete;
317
Ok(DebugRunResult::Finished)
318
}
319
Response::Paused(result) => {
320
log::trace!("got Paused");
321
self.state = DebuggerState::Paused;
322
Ok(result)
323
}
324
Response::QueryResponse(_) => {
325
panic!("Invalid debug response");
326
}
327
}
328
}
329
330
/// Run the debugger body until completion, with no further events.
331
pub async fn finish(&mut self) -> Result<()> {
332
if self.is_complete() {
333
return Ok(());
334
}
335
loop {
336
match self.run().await? {
337
DebugRunResult::Finished => break,
338
e => {
339
log::trace!("finish: event {e:?}");
340
}
341
}
342
}
343
assert!(self.is_complete());
344
Ok(())
345
}
346
347
/// Perform some action on the contained `Store` while not running.
348
///
349
/// This may only be invoked before the inner body finishes and
350
/// when it is paused; that is, when the `Debugger` is initially
351
/// created and after any call to `run()` returns a result other
352
/// than `DebugRunResult::Finished`. If an earlier `run()`
353
/// invocation was canceled, it must be re-invoked and return
354
/// successfully before a query is made.
355
///
356
/// This is cancel-safe; if canceled, the result of the query will
357
/// be dropped.
358
pub async fn with_store<
359
F: FnOnce(StoreContextMut<'_, T>) -> R + Send + 'static,
360
R: Send + 'static,
361
>(
362
&mut self,
363
f: F,
364
) -> Result<R> {
365
assert!(!self.is_complete());
366
367
match self.state {
368
DebuggerState::Queried => {
369
// Earlier query canceled; drop its response first.
370
let response =
371
self.out_rx.recv().await.ok_or_else(|| {
372
wasmtime::format_err!("Premature close of debugger channel")
373
})?;
374
assert!(matches!(response, Response::QueryResponse(_)));
375
self.state = DebuggerState::Paused;
376
}
377
DebuggerState::Running => {
378
// Results from a canceled `run()`; `run()` must
379
// complete before this can be invoked.
380
panic!("Cannot query in Running state");
381
}
382
DebuggerState::Complete => {
383
panic!("Cannot query when complete");
384
}
385
DebuggerState::Paused => {
386
// OK -- this is the state we want.
387
}
388
}
389
390
self.in_tx
391
.send(Command::Query(Box::new(|store| Box::new(f(store)))))
392
.await
393
.map_err(|_| wasmtime::format_err!("Premature close of debugger channel"))?;
394
self.state = DebuggerState::Queried;
395
396
let response = self
397
.out_rx
398
.recv()
399
.await
400
.ok_or_else(|| wasmtime::format_err!("Premature close of debugger channel"))?;
401
let Response::QueryResponse(resp) = response else {
402
wasmtime::bail!("Incorrect response from debugger task");
403
};
404
self.state = DebuggerState::Paused;
405
406
Ok(*resp.downcast::<R>().expect("type mismatch"))
407
}
408
409
/// Drop the Debugger once complete, returning the inner `Store`
410
/// around which it was wrapped.
411
///
412
/// Only valid to invoke once `run()` returns
413
/// `DebugRunResult::Finished` or after calling `finish()` (which
414
/// finishes execution while dropping all further debug events).
415
///
416
/// This is cancel-safe, but if canceled, the Store is lost.
417
pub async fn take_store(&mut self) -> Result<Option<Store<T>>> {
418
match self.state {
419
DebuggerState::Complete => {
420
let inner = match self.inner.take() {
421
Some(inner) => inner,
422
None => return Ok(None),
423
};
424
let mut store = inner.await??;
425
store.clear_debug_handler();
426
Ok(Some(store))
427
}
428
_ => panic!("Invalid state: debugger not yet complete"),
429
}
430
}
431
}
432
433
/// The result of one call to `Debugger::run()`.
434
///
435
/// This is similar to `DebugEvent` but without the lifetime, so it
436
/// can be sent across async tasks, and incorporates the possibility
437
/// of completion (`Finished`) as well.
438
#[derive(Debug)]
439
pub enum DebugRunResult {
440
/// Execution of the inner body finished.
441
Finished,
442
/// An error was raised by a hostcall.
443
HostcallError,
444
/// Wasm execution was interrupted by an epoch change.
445
EpochYield,
446
/// An exception is thrown and caught by Wasm. The current state
447
/// is at the throw-point.
448
CaughtExceptionThrown(OwnedRooted<ExnRef>),
449
/// An exception was not caught and is escaping to the host.
450
UncaughtExceptionThrown(OwnedRooted<ExnRef>),
451
/// A Wasm trap occurred.
452
Trap(Trap),
453
/// A breakpoint was reached.
454
Breakpoint,
455
}
456
457
#[cfg(test)]
458
mod test {
459
use super::*;
460
use wasmtime::*;
461
462
#[tokio::test]
463
#[cfg_attr(miri, ignore)]
464
async fn basic_debugger() -> wasmtime::Result<()> {
465
let _ = env_logger::try_init();
466
467
let mut config = Config::new();
468
config.guest_debug(true);
469
let engine = Engine::new(&config)?;
470
let module = Module::new(
471
&engine,
472
r#"
473
(module
474
(func (export "main") (param i32 i32) (result i32)
475
local.get 0
476
local.get 1
477
i32.add))
478
"#,
479
)?;
480
481
let mut store = Store::new(&engine, ());
482
let instance = Instance::new_async(&mut store, &module, &[]).await?;
483
let main = instance.get_func(&mut store, "main").unwrap();
484
485
let mut debugger = Debugger::new(store, move |mut store| async move {
486
let mut results = [Val::I32(0)];
487
store.edit_breakpoints().unwrap().single_step(true).unwrap();
488
main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
489
.await?;
490
assert_eq!(results[0].unwrap_i32(), 3);
491
main.call_async(&mut store, &[Val::I32(3), Val::I32(4)], &mut results[..])
492
.await?;
493
assert_eq!(results[0].unwrap_i32(), 7);
494
Ok(store)
495
});
496
497
let event = debugger.run().await?;
498
assert!(matches!(event, DebugRunResult::Breakpoint));
499
// At (before executing) first `local.get`.
500
debugger
501
.with_store(|store| {
502
let mut frame = store.debug_frames().unwrap();
503
assert!(!frame.done());
504
assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
505
assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 36);
506
assert_eq!(frame.num_locals(), 2);
507
assert_eq!(frame.num_stacks(), 0);
508
assert_eq!(frame.local(0).unwrap_i32(), 1);
509
assert_eq!(frame.local(1).unwrap_i32(), 2);
510
assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
511
assert!(frame.done());
512
})
513
.await?;
514
515
let event = debugger.run().await?;
516
// At second `local.get`.
517
assert!(matches!(event, DebugRunResult::Breakpoint));
518
debugger
519
.with_store(|store| {
520
let mut frame = store.debug_frames().unwrap();
521
assert!(!frame.done());
522
assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
523
assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 38);
524
assert_eq!(frame.num_locals(), 2);
525
assert_eq!(frame.num_stacks(), 1);
526
assert_eq!(frame.local(0).unwrap_i32(), 1);
527
assert_eq!(frame.local(1).unwrap_i32(), 2);
528
assert_eq!(frame.stack(0).unwrap_i32(), 1);
529
assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
530
assert!(frame.done());
531
})
532
.await?;
533
534
let event = debugger.run().await?;
535
// At `i32.add`.
536
assert!(matches!(event, DebugRunResult::Breakpoint));
537
debugger
538
.with_store(|store| {
539
let mut frame = store.debug_frames().unwrap();
540
assert!(!frame.done());
541
assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
542
assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 40);
543
assert_eq!(frame.num_locals(), 2);
544
assert_eq!(frame.num_stacks(), 2);
545
assert_eq!(frame.local(0).unwrap_i32(), 1);
546
assert_eq!(frame.local(1).unwrap_i32(), 2);
547
assert_eq!(frame.stack(0).unwrap_i32(), 1);
548
assert_eq!(frame.stack(1).unwrap_i32(), 2);
549
assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
550
assert!(frame.done());
551
})
552
.await?;
553
554
let event = debugger.run().await?;
555
// At return point.
556
assert!(matches!(event, DebugRunResult::Breakpoint));
557
debugger
558
.with_store(|store| {
559
let mut frame = store.debug_frames().unwrap();
560
assert!(!frame.done());
561
assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
562
assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 41);
563
assert_eq!(frame.num_locals(), 2);
564
assert_eq!(frame.num_stacks(), 1);
565
assert_eq!(frame.local(0).unwrap_i32(), 1);
566
assert_eq!(frame.local(1).unwrap_i32(), 2);
567
assert_eq!(frame.stack(0).unwrap_i32(), 3);
568
assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
569
assert!(frame.done());
570
})
571
.await?;
572
573
// Now disable breakpoints before continuing. Second call should proceed with no more events.
574
debugger
575
.with_store(|store| {
576
store
577
.edit_breakpoints()
578
.unwrap()
579
.single_step(false)
580
.unwrap();
581
})
582
.await?;
583
584
let event = debugger.run().await?;
585
assert!(matches!(event, DebugRunResult::Finished));
586
587
assert!(debugger.is_complete());
588
589
// Ensure the store still works and the debug handler is
590
// removed.
591
let mut store = debugger.take_store().await?.unwrap();
592
let mut results = [Val::I32(0)];
593
main.call_async(&mut store, &[Val::I32(10), Val::I32(20)], &mut results[..])
594
.await?;
595
assert_eq!(results[0].unwrap_i32(), 30);
596
597
Ok(())
598
}
599
600
#[tokio::test]
601
#[cfg_attr(miri, ignore)]
602
async fn early_finish() -> Result<()> {
603
let _ = env_logger::try_init();
604
605
let mut config = Config::new();
606
config.guest_debug(true);
607
let engine = Engine::new(&config)?;
608
let module = Module::new(
609
&engine,
610
r#"
611
(module
612
(func (export "main") (param i32 i32) (result i32)
613
local.get 0
614
local.get 1
615
i32.add))
616
"#,
617
)?;
618
619
let mut store = Store::new(&engine, ());
620
let instance = Instance::new_async(&mut store, &module, &[]).await?;
621
let main = instance.get_func(&mut store, "main").unwrap();
622
623
let mut debugger = Debugger::new(store, move |mut store| async move {
624
let mut results = [Val::I32(0)];
625
store.edit_breakpoints().unwrap().single_step(true).unwrap();
626
main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
627
.await?;
628
assert_eq!(results[0].unwrap_i32(), 3);
629
Ok(store)
630
});
631
632
debugger.finish().await?;
633
assert!(debugger.is_complete());
634
635
Ok(())
636
}
637
638
#[tokio::test]
639
#[cfg_attr(miri, ignore)]
640
async fn drop_debugger_and_store() -> Result<()> {
641
let _ = env_logger::try_init();
642
643
let mut config = Config::new();
644
config.guest_debug(true);
645
let engine = Engine::new(&config)?;
646
let module = Module::new(
647
&engine,
648
r#"
649
(module
650
(func (export "main") (param i32 i32) (result i32)
651
local.get 0
652
local.get 1
653
i32.add))
654
"#,
655
)?;
656
657
let mut store = Store::new(&engine, ());
658
let instance = Instance::new_async(&mut store, &module, &[]).await?;
659
let main = instance.get_func(&mut store, "main").unwrap();
660
661
let mut debugger = Debugger::new(store, move |mut store| async move {
662
let mut results = [Val::I32(0)];
663
store.edit_breakpoints().unwrap().single_step(true).unwrap();
664
main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
665
.await?;
666
assert_eq!(results[0].unwrap_i32(), 3);
667
Ok(store)
668
});
669
670
// Step once, then drop everything at the end of this
671
// function. Wasmtime's fiber cleanup should safely happen
672
// without attempting to raise debug async handler calls with
673
// missing async context.
674
let _ = debugger.run().await?;
675
676
Ok(())
677
}
678
}
679
680