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                    anyhow::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(|_| anyhow::anyhow!("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 = self
281                    .out_rx
282                    .recv()
283                    .await
284                    .ok_or_else(|| anyhow::anyhow!("Premature close of debugger channel"))?;
285                log::trace!("in Queried; received, dropping");
286                assert!(matches!(response, Response::QueryResponse(_)));
287                self.state = DebuggerState::Paused;
288
289                // Now send a `Continue`, as above.
290                log::trace!("in Paused; sending Continue");
291                self.in_tx
292                    .send(Command::Continue)
293                    .await
294                    .map_err(|_| anyhow::anyhow!("Failed to send over debug channel"))?;
295                self.state = DebuggerState::Running;
296            }
297            DebuggerState::Complete => {
298                panic!("Cannot `run()` an already-complete Debugger");
299            }
300        }
301
302        // At this point, the inner task is in Running state. We
303        // expect to receive a message when it next pauses or
304        // completes. If this `recv()` is canceled, no message is
305        // lost, and the state above accurately reflects what must be
306        // done on the next `run()`.
307        log::trace!("waiting for response");
308        let response = self
309            .out_rx
310            .recv()
311            .await
312            .ok_or_else(|| anyhow::anyhow!("Premature close of debugger channel"))?;
313
314        match response {
315            Response::Finished => {
316                log::trace!("got Finished");
317                self.state = DebuggerState::Complete;
318                Ok(DebugRunResult::Finished)
319            }
320            Response::Paused(result) => {
321                log::trace!("got Paused");
322                self.state = DebuggerState::Paused;
323                Ok(result)
324            }
325            Response::QueryResponse(_) => {
326                panic!("Invalid debug response");
327            }
328        }
329    }
330
331    /// Run the debugger body until completion, with no further events.
332    pub async fn finish(&mut self) -> Result<()> {
333        if self.is_complete() {
334            return Ok(());
335        }
336        loop {
337            match self.run().await? {
338                DebugRunResult::Finished => break,
339                e => {
340                    log::trace!("finish: event {e:?}");
341                }
342            }
343        }
344        assert!(self.is_complete());
345        Ok(())
346    }
347
348    /// Perform some action on the contained `Store` while not running.
349    ///
350    /// This may only be invoked before the inner body finishes and
351    /// when it is paused; that is, when the `Debugger` is initially
352    /// created and after any call to `run()` returns a result other
353    /// than `DebugRunResult::Finished`. If an earlier `run()`
354    /// invocation was canceled, it must be re-invoked and return
355    /// successfully before a query is made.
356    ///
357    /// This is cancel-safe; if canceled, the result of the query will
358    /// be dropped.
359    pub async fn with_store<
360        F: FnOnce(StoreContextMut<'_, T>) -> R + Send + 'static,
361        R: Send + 'static,
362    >(
363        &mut self,
364        f: F,
365    ) -> Result<R> {
366        assert!(!self.is_complete());
367
368        match self.state {
369            DebuggerState::Queried => {
370                // Earlier query canceled; drop its response first.
371                let response = self
372                    .out_rx
373                    .recv()
374                    .await
375                    .ok_or_else(|| anyhow::anyhow!("Premature close of debugger channel"))?;
376                assert!(matches!(response, Response::QueryResponse(_)));
377                self.state = DebuggerState::Paused;
378            }
379            DebuggerState::Running => {
380                // Results from a canceled `run()`; `run()` must
381                // complete before this can be invoked.
382                panic!("Cannot query in Running state");
383            }
384            DebuggerState::Complete => {
385                panic!("Cannot query when complete");
386            }
387            DebuggerState::Paused => {
388                // OK -- this is the state we want.
389            }
390        }
391
392        self.in_tx
393            .send(Command::Query(Box::new(|store| Box::new(f(store)))))
394            .await
395            .map_err(|_| anyhow::anyhow!("Premature close of debugger channel"))?;
396        self.state = DebuggerState::Queried;
397
398        let response = self
399            .out_rx
400            .recv()
401            .await
402            .ok_or_else(|| anyhow::anyhow!("Premature close of debugger channel"))?;
403        let Response::QueryResponse(resp) = response else {
404            anyhow::bail!("Incorrect response from debugger task");
405        };
406        self.state = DebuggerState::Paused;
407
408        Ok(*resp.downcast::<R>().expect("type mismatch"))
409    }
410
411    /// Drop the Debugger once complete, returning the inner `Store`
412    /// around which it was wrapped.
413    ///
414    /// Only valid to invoke once `run()` returns
415    /// `DebugRunResult::Finished` or after calling `finish()` (which
416    /// finishes execution while dropping all further debug events).
417    ///
418    /// This is cancel-safe, but if canceled, the Store is lost.
419    pub async fn take_store(&mut self) -> Result<Option<Store<T>>> {
420        match self.state {
421            DebuggerState::Complete => {
422                let inner = match self.inner.take() {
423                    Some(inner) => inner,
424                    None => return Ok(None),
425                };
426                let mut store = inner.await??;
427                store.clear_debug_handler();
428                Ok(Some(store))
429            }
430            _ => panic!("Invalid state: debugger not yet complete"),
431        }
432    }
433}
434
435/// The result of one call to `Debugger::run()`.
436///
437/// This is similar to `DebugEvent` but without the lifetime, so it
438/// can be sent across async tasks, and incorporates the possibility
439/// of completion (`Finished`) as well.
440#[derive(Debug)]
441pub enum DebugRunResult {
442    /// Execution of the inner body finished.
443    Finished,
444    /// An error was raised by a hostcall.
445    HostcallError,
446    /// Wasm execution was interrupted by an epoch change.
447    EpochYield,
448    /// An exception is thrown and caught by Wasm. The current state
449    /// is at the throw-point.
450    CaughtExceptionThrown(OwnedRooted<ExnRef>),
451    /// An exception was not caught and is escaping to the host.
452    UncaughtExceptionThrown(OwnedRooted<ExnRef>),
453    /// A Wasm trap occurred.
454    Trap(Trap),
455    /// A breakpoint was reached.
456    Breakpoint,
457}
458
459#[cfg(test)]
460mod test {
461    use super::*;
462    use wasmtime::*;
463
464    #[tokio::test]
465    #[cfg_attr(miri, ignore)]
466    async fn basic_debugger() -> anyhow::Result<()> {
467        let _ = env_logger::try_init();
468
469        let mut config = Config::new();
470        config.guest_debug(true);
471        config.async_support(true);
472        let engine = Engine::new(&config)?;
473        let module = Module::new(
474            &engine,
475            r#"
476                (module
477                  (func (export "main") (param i32 i32) (result i32)
478                    local.get 0
479                    local.get 1
480                    i32.add))
481            "#,
482        )?;
483
484        let mut store = Store::new(&engine, ());
485        let instance = Instance::new_async(&mut store, &module, &[]).await?;
486        let main = instance.get_func(&mut store, "main").unwrap();
487
488        let mut debugger = Debugger::new(store, move |mut store| async move {
489            let mut results = [Val::I32(0)];
490            store.edit_breakpoints().unwrap().single_step(true).unwrap();
491            main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
492                .await?;
493            assert_eq!(results[0].unwrap_i32(), 3);
494            main.call_async(&mut store, &[Val::I32(3), Val::I32(4)], &mut results[..])
495                .await?;
496            assert_eq!(results[0].unwrap_i32(), 7);
497            Ok(store)
498        });
499
500        let event = debugger.run().await?;
501        assert!(matches!(event, DebugRunResult::Breakpoint));
502        // At (before executing) first `local.get`.
503        debugger
504            .with_store(|store| {
505                let mut frame = store.debug_frames().unwrap();
506                assert!(!frame.done());
507                assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
508                assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 36);
509                assert_eq!(frame.num_locals(), 2);
510                assert_eq!(frame.num_stacks(), 0);
511                assert_eq!(frame.local(0).unwrap_i32(), 1);
512                assert_eq!(frame.local(1).unwrap_i32(), 2);
513                assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
514                assert!(frame.done());
515            })
516            .await?;
517
518        let event = debugger.run().await?;
519        // At second `local.get`.
520        assert!(matches!(event, DebugRunResult::Breakpoint));
521        debugger
522            .with_store(|store| {
523                let mut frame = store.debug_frames().unwrap();
524                assert!(!frame.done());
525                assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
526                assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 38);
527                assert_eq!(frame.num_locals(), 2);
528                assert_eq!(frame.num_stacks(), 1);
529                assert_eq!(frame.local(0).unwrap_i32(), 1);
530                assert_eq!(frame.local(1).unwrap_i32(), 2);
531                assert_eq!(frame.stack(0).unwrap_i32(), 1);
532                assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
533                assert!(frame.done());
534            })
535            .await?;
536
537        let event = debugger.run().await?;
538        // At `i32.add`.
539        assert!(matches!(event, DebugRunResult::Breakpoint));
540        debugger
541            .with_store(|store| {
542                let mut frame = store.debug_frames().unwrap();
543                assert!(!frame.done());
544                assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
545                assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 40);
546                assert_eq!(frame.num_locals(), 2);
547                assert_eq!(frame.num_stacks(), 2);
548                assert_eq!(frame.local(0).unwrap_i32(), 1);
549                assert_eq!(frame.local(1).unwrap_i32(), 2);
550                assert_eq!(frame.stack(0).unwrap_i32(), 1);
551                assert_eq!(frame.stack(1).unwrap_i32(), 2);
552                assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
553                assert!(frame.done());
554            })
555            .await?;
556
557        let event = debugger.run().await?;
558        // At return point.
559        assert!(matches!(event, DebugRunResult::Breakpoint));
560        debugger
561            .with_store(|store| {
562                let mut frame = store.debug_frames().unwrap();
563                assert!(!frame.done());
564                assert_eq!(frame.wasm_function_index_and_pc().unwrap().0.as_u32(), 0);
565                assert_eq!(frame.wasm_function_index_and_pc().unwrap().1, 41);
566                assert_eq!(frame.num_locals(), 2);
567                assert_eq!(frame.num_stacks(), 1);
568                assert_eq!(frame.local(0).unwrap_i32(), 1);
569                assert_eq!(frame.local(1).unwrap_i32(), 2);
570                assert_eq!(frame.stack(0).unwrap_i32(), 3);
571                assert_eq!(frame.move_to_parent(), FrameParentResult::SameActivation);
572                assert!(frame.done());
573            })
574            .await?;
575
576        // Now disable breakpoints before continuing. Second call should proceed with no more events.
577        debugger
578            .with_store(|store| {
579                store
580                    .edit_breakpoints()
581                    .unwrap()
582                    .single_step(false)
583                    .unwrap();
584            })
585            .await?;
586
587        let event = debugger.run().await?;
588        assert!(matches!(event, DebugRunResult::Finished));
589
590        assert!(debugger.is_complete());
591
592        // Ensure the store still works and the debug handler is
593        // removed.
594        let mut store = debugger.take_store().await?.unwrap();
595        let mut results = [Val::I32(0)];
596        main.call_async(&mut store, &[Val::I32(10), Val::I32(20)], &mut results[..])
597            .await?;
598        assert_eq!(results[0].unwrap_i32(), 30);
599
600        Ok(())
601    }
602
603    #[tokio::test]
604    #[cfg_attr(miri, ignore)]
605    async fn early_finish() -> Result<()> {
606        let _ = env_logger::try_init();
607
608        let mut config = Config::new();
609        config.guest_debug(true);
610        config.async_support(true);
611        let engine = Engine::new(&config)?;
612        let module = Module::new(
613            &engine,
614            r#"
615                (module
616                  (func (export "main") (param i32 i32) (result i32)
617                    local.get 0
618                    local.get 1
619                    i32.add))
620            "#,
621        )?;
622
623        let mut store = Store::new(&engine, ());
624        let instance = Instance::new_async(&mut store, &module, &[]).await?;
625        let main = instance.get_func(&mut store, "main").unwrap();
626
627        let mut debugger = Debugger::new(store, move |mut store| async move {
628            let mut results = [Val::I32(0)];
629            store.edit_breakpoints().unwrap().single_step(true).unwrap();
630            main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
631                .await?;
632            assert_eq!(results[0].unwrap_i32(), 3);
633            Ok(store)
634        });
635
636        debugger.finish().await?;
637        assert!(debugger.is_complete());
638
639        Ok(())
640    }
641
642    #[tokio::test]
643    #[cfg_attr(miri, ignore)]
644    async fn drop_debugger_and_store() -> Result<()> {
645        let _ = env_logger::try_init();
646
647        let mut config = Config::new();
648        config.guest_debug(true);
649        config.async_support(true);
650        let engine = Engine::new(&config)?;
651        let module = Module::new(
652            &engine,
653            r#"
654                (module
655                  (func (export "main") (param i32 i32) (result i32)
656                    local.get 0
657                    local.get 1
658                    i32.add))
659            "#,
660        )?;
661
662        let mut store = Store::new(&engine, ());
663        let instance = Instance::new_async(&mut store, &module, &[]).await?;
664        let main = instance.get_func(&mut store, "main").unwrap();
665
666        let mut debugger = Debugger::new(store, move |mut store| async move {
667            let mut results = [Val::I32(0)];
668            store.edit_breakpoints().unwrap().single_step(true).unwrap();
669            main.call_async(&mut store, &[Val::I32(1), Val::I32(2)], &mut results[..])
670                .await?;
671            assert_eq!(results[0].unwrap_i32(), 3);
672            Ok(store)
673        });
674
675        // Step once, then drop everything at the end of this
676        // function. Wasmtime's fiber cleanup should safely happen
677        // without attempting to raise debug async handler calls with
678        // missing async context.
679        let _ = debugger.run().await?;
680
681        Ok(())
682    }
683}