Skip to main content

wasmtime_internal_debugger/
lib.rs

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
12use std::{any::Any, sync::Arc};
13use tokio::{
14    sync::{Mutex, mpsc},
15    task::JoinHandle,
16};
17use 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`.
33pub 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)]
77enum 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.
126enum Command<T: 'static> {
127    Continue,
128    Query(Box<dyn FnOnce(StoreContextMut<'_, T>) -> Box<dyn Any + Send> + Send>),
129}
130
131enum Response {
132    Paused(DebugRunResult),
133    QueryResponse(Box<dyn Any + Send>),
134    Finished,
135}
136
137struct HandlerInner<T: Send + 'static> {
138    in_rx: Mutex<mpsc::Receiver<Command<T>>>,
139    out_tx: mpsc::Sender<Response>,
140}
141
142struct Handler<T: Send + 'static>(Arc<HandlerInner<T>>);
143
144impl<T: Send + 'static> std::clone::Clone for Handler<T> {
145    fn clone(&self) -> Self {
146        Handler(self.0.clone())
147    }
148}
149
150impl<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
189impl<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)]
439pub 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)]
458mod 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}