Idea: support post-trap call traces for guest interpreters
While discussing this componentize-py issue, I realized it would be useful to allow a component to be entered one last time after it traps in order to get additional debug info.
For example, if a trap occurs while running Python code in the CPython interpreter, the guest stack trace produced by Wasmtime is usually pretty useless; what we really want is a Python-level stack trace, and that can only realistically be produced by executing guest code. More generally, it would be nice to be able to attach a Python-level debugger to the interpreter to do an interactive investigation; either way, we'd need to execute guest code.
One possible solution to this would be to allow the host to export a special on-trap function which the host may call after the trap happens, similar to how a signal handler would run after e.g. a SIGSEGV occurs. Presumably that function would be restricted from calling any imports and, in the simple case, take no parameters and return only a string containing a high-level call trace. Attaching a debugger would require a more sophisticated interface, of course, but we could start simple and expand from there.
That's a neat idea. Riffing on it a bit: perhaps we could define a new component callstack section that is sorta like the start section in that it simply contains a funcidx but this funcidx is not exported as part of the component's public signature. And then this callstack function could be called non-deterministically by the host to generate a nice symbolic backtrace from the stew of core wasm linear memory state. In addition to providing post-mortem backtraces like you're talking about, the callstack function could also be called on live component instances by debuggers or profilers. What's neat is that the cross-component/language scenario just sorta falls out: the runtime calls the callstack function for the component instance of each task on the stack and splices the results together. By making calls to the callstack function host-defined, the runtime can know when it does or doesn't plan to do post-mortem backtraces, which allows the wasm compiler to then optimize more-aggressively when post-mortem backtraces are turned off.
I don't know the right terminology for this, but could you "re-map" the component's memory as read-only before calling callstack?
That's an interesting idea, but if we're running general stack-walking code in the guest, likely it'll do a bunch of innocuous local mutations (for input/output, intermediate data structures, etc).
I suppose the host always has the option to snapshot the memory before calling callstack if it wants to preserve the exact state as of the original trap.
Continuing to run code in an alread-trapped instance sounds potentially hazardous.
As an alternative, maybe components can declare some kind of separate (nested?) "debug" component and move the callstack function to that instead.
Hosts may instantiate such a debug component if they want to, e.g. after a trap of the main component. The debug component instance would (per usual) get its own private mutable linear memory to run its callstack transformation logic. Additionally, the exported callstack function should somehow get readonly access to the state of the main component at time of trapping.