cfallin opened issue #11835:
Most of the public API surface for working with Wasm-visible storage in Wasmtime takes an
AsContextMutparameter. For random examples,StructRef::field,Memory::data, andTable::getall take either anAsContext/AsContextMutor anInto<StoreContext{,Mut}>.It makes sense that accessors require "the store" (in some vague sense) to access the linear memories, tables, and GC heap, because all of these units of storage are owned by the store.
However, in the context of the guest-debugging API, it turns out that we need to expose an
AsContextMutfrom the "debug session" handle type in #11826, because without that, while the debug session owns the store, there is no way to actually do anything with GC refs that one reads out, or read data in the linear memories, etc. In other words, regular debugger access will want to get at the guest's data and objects.Likewise, In #11769 I put accessors for the values on stack (locals and operand stack) directly on a
StackView, and this suffices for core Wasm types (ints/floats/vectors) but as soon as one actually wants to examine a GC ref that one reads out, one will want anAsContextMut.This is possible (and I've done it in the draft #11826 in the latest commit), and I believe it's still sound (one can view a "debugger pause" as like a hostcall; so it morally has access to whatever a
Callerhas access to, andCallerimplsAsContextMut), but I don't like it because it forces over-monomorphization: it means that the whole debugger API gets theTfrom theStore. It also interacts somewhat poorly with internals: for example,AutoAssertNoGcholds theStoreOpaque, but we actually want to hold theStoreInner<T>if we want to be able to pass out aStoreContextMut.It seems like we should define traits that morally wrap the
StoreOpaque, and are sufficient to get at Wasm-accessible objects (linear memories, tables, GC), since none of those can be dependent on the host-sideTanyway; and then add the right impls to make that automatically work as before withAsContextMut-types. Function calls still need the monomorphizedStoreContextMutbecause aCallercan pop out the other end that needs theT. Stated succinctly: access to Wasm data should not needTso is not monomorphized; calls to Wasm code does needTso is monomorphized. Thoughts?
cfallin commented on issue #11835:
(cc @alexcrichton @fitzgen)
cfallin commented on issue #11835:
I'll throw out the names
AsOpaqueContext/AsOpaqueContextMutwith the full expectation that the bikeshed machinery will generate better names than that, if we go this way :-)
cfallin added the wasmtime:debugging label to Issue #11835.
cfallin added the wasmtime:api label to Issue #11835.
alexcrichton commented on issue #11835:
My hunch is that this would not be easy to retrofit onto the preexisting API without redesigning/rethinking most of the rest of it. There's no concept, for example, of a
StoreOpaquein Wasmtime's public API. That means that it would only possibly be introduced with these newAsOpaque*traits that would be implemented for various objects. If an API takes aT: AsOpaque*, though, then it probably also wants to takeT: AsContext*which implies a number of blanket impls for these traits too. We would then, in theory, want to go back to all the preexisting APIs in Wasmtime and audit them to see if they need opaque-ness or not. Basically my hunch is that this would be a pretty large undertaking and not something we can just tack on relatively easily.That being said the technical points you're raising are still valid, but they can be solved today. Elsewhere the generic entrypoint with
T: AsContextMutis dispatched with a one-liner to something that takes&StoreOpaqueor similar. That means that the cost of monomorphization is pretty minimal. Data-structure-wise if theTis irrelevant then there could bestruct ThingOpaquewhich everything is implemented in terms of andstruct Thing<T>with aPhantomData<T>or similar. Basically there's various ways to slice and dice the internal implementation to not plumb generics everywhere (which I agree is undesirable, that's the whole pointStoreOpaqueexists)This is possible (and I've done it in the draft https://github.com/bytecodealliance/wasmtime/pull/11826 in the latest commit), and I believe it's still sound (one can view a "debugger pause" as like a hostcall; so it morally has access to whatever a Caller has access to, and Caller impls AsContextMut), but I don't like it because it forces over-monomorphization: it means that the whole debugger API gets the T from the Store. It also interacts somewhat poorly with internals: for example, AutoAssertNoGc holds the StoreOpaque, but we actually want to hold the StoreInner<T> if we want to be able to pass out a StoreContextMut.
There's a lot competing here unfortunately. If the user-visible type is
AsContextMut, then it's impossible to preventthing.as_context_mut().gc()which means thatAutoAssertNoGccan't be what's stored at-rest anyway. In general providingAsContextMutto a suspended computation is also something we've at least, so far, carefully not had to deal with. Not to say it can't be dealt with, but orthogonal to the monomorphization concerns we'll need to carefully map this out as it's a pretty major new power for embedders.
cfallin commented on issue #11835:
There's a lot competing here unfortunately. If the user-visible type is
AsContextMut, then it's impossible to preventthing.as_context_mut().gc()which means thatAutoAssertNoGccan't be what's stored at-rest anyway. In general providingAsContextMutto a suspended computation is also something we've at least, so far, carefully not had to deal with. Not to say it can't be dealt with, but orthogonal to the monomorphization concerns we'll need to carefully map this out as it's a pretty major new power for embedders.I was worried about that too -- but the mental breakthrough for me at least (after also writing the initial issue description, so this is new) came with thinking about debug-step yields as hostcalls rather than (say) future yields from a suspended computation. (That's what I meant by "The dynamic store ownership protocol basically works with the safe Rust restrictions there too: one can get at the store only when the Wasm code yields, which is morally like a hostcall that passes a reborrowed &mut Store back." over in this comment on the draft PR -- apologies for thinking spreading across two places!) In essence, think of every breakpoint sequence-point (a
ud2/brk/... under the hood) or trapping load or whatever as morally containing an optional call that reborrows the store flowing through the Wasm code. The only difference is in the plumbing: the call happens through a mechanism of redirecting PC on a signal and taking the*mut VMContextfrom somewhere else (TLS).In that frame, I don't see this as any different from
Caller. However your example of GC does raise one realization for me: this means that every trapping instruction, including sequence points, now needs to be a safepoint to the user-stack-map code. That's doable I think, and once we do that, I believe it's actually fine to have a GC happen while in a debug-step yield and while examining the stack: the GC values on the stack are rooted now, even in the instrumentation slots.If we agree that makes sense, I don't think we even need to make this distinction in the API for soundness, it's just a monomorphization-minimization thing. I'm actually somewhat ambivalent whether that matters much with the debug host happening mainly on developer platforms (since R/R covers capturing bugs on embedded and that shouldn't require the full instrumentation-based API).
alexcrichton commented on issue #11835:
Ok yeah that all makes sense to me yeah. I also don't know of anything concretely bad that could happen if the store is accessed while wasm is suspended. I think I'm mostly just scared of it as it's a new "superpower" that we haven't previously provided access to.
fitzgen commented on issue #11835:
I think I'm mostly just scared of it as it's a new "superpower" that we haven't previously provided access to.
Yes, debuggers can mess with all your internals in potentially surprising and perhaps unpleasant ways. That's unfortunately just how it goes.
But this does highlight that debugging APIs are a kind of "new"/"separate" API from the "regular" embedding API. Another example of this, beyond being able to pause and run host code that potentially mutates linear memory (for example) at non-call program points, is the ability to get and mutate non-exported entities. We want the debugger API to be able to enumerate/access/mutate all Wasm globals, tables, memories, functions, etc... and not only the ones that are explicitly exported. We also might want to allow the debugger APIs to mutate non-
mutglobals. We definitely want to give the debugger APIs the ability to call into and out of components in ways that would otherwise trap due to the enter/exit component flags.So given that these debugging APIs really do provide new abilities, I think we should also consider the idea that we might not want to reuse the existing types and methods for our new debugging APIs. Maybe we create
Debug{Store,Caller,Context}types and the new debugging APIs explicitly take these things as context instead of a regularStoreor whatever? And you only get these via the top-level callback/coroutine debugging APIs we are discussing in https://github.com/bytecodealliance/wasmtime/pull/11826 ? These new types could use a lifetime, perhaps, to capture the invariants around where you are paused and when you can e.g. access a stack frame's locals.Not actually sure that bifurcating the APIs is the best way forward, but I think it is worth exploring the possibility.
cfallin commented on issue #11835:
All of that could make a lot of sense, and I am just now realizing that indeed we'll need to have a way to get at at least private memories even for the most basic component-debugging use-case. (I was p1-brained and was assuming the memory would be exported; nope!) That said, I think there is also a kind of less abstracted way to get this:
- the
DebugSessioncan directly have methods that reach into instances and provide aMemoryorGlobalor Tablefor a private entity. The resulting handle would work with all the same accessors that take a mut store as today; the only access control is at the point that one wants to get the handle. This replacesDebugContext`, because there's really only one "debug context".- we continue to provide the equivalent of
Caller(AsContextMut) as an impl on theDebugSession; when it yields fromnextand we have mut access to it, we can use the session as context to get at data and mutate arbitrarily. In other words, not do the abstraction to avoid monomorphization that this issue initially suggested.
fitzgen edited a comment on issue #11835:
All of that could make a lot of sense, and I am just now realizing that indeed we'll need to have a way to get at at least private memories even for the most basic component-debugging use-case. (I was p1-brained and was assuming the memory would be exported; nope!) That said, I think there is also a kind of less abstracted way to get this:
- the
DebugSessioncan directly have methods that reach into instances and provide aMemoryorGlobalorTablefor a private entity. The resulting handle would work with all the same accessors that take a mut store as today; the only access control is at the point that one wants to get the handle. This replacesDebugContext, because there's really only one "debug context".- we continue to provide the equivalent of
Caller(AsContextMut) as an impl on theDebugSession; when it yields fromnextand we have mut access to it, we can use the session as context to get at data and mutate arbitrarily. In other words, not do the abstraction to avoid monomorphization that this issue initially suggested.
cfallin closed issue #11835:
Most of the public API surface for working with Wasm-visible storage in Wasmtime takes an
AsContextMutparameter. For random examples,StructRef::field,Memory::data, andTable::getall take either anAsContext/AsContextMutor anInto<StoreContext{,Mut}>.It makes sense that accessors require "the store" (in some vague sense) to access the linear memories, tables, and GC heap, because all of these units of storage are owned by the store.
However, in the context of the guest-debugging API, it turns out that we need to expose an
AsContextMutfrom the "debug session" handle type in #11826, because without that, while the debug session owns the store, there is no way to actually do anything with GC refs that one reads out, or read data in the linear memories, etc. In other words, regular debugger access will want to get at the guest's data and objects.Likewise, In #11769 I put accessors for the values on stack (locals and operand stack) directly on a
StackView, and this suffices for core Wasm types (ints/floats/vectors) but as soon as one actually wants to examine a GC ref that one reads out, one will want anAsContextMut.This is possible (and I've done it in the draft #11826 in the latest commit), and I believe it's still sound (one can view a "debugger pause" as like a hostcall; so it morally has access to whatever a
Callerhas access to, andCallerimplsAsContextMut), but I don't like it because it forces over-monomorphization: it means that the whole debugger API gets theTfrom theStore. It also interacts somewhat poorly with internals: for example,AutoAssertNoGcholds theStoreOpaque, but we actually want to hold theStoreInner<T>if we want to be able to pass out aStoreContextMut.It seems like we should define traits that morally wrap the
StoreOpaque, and are sufficient to get at Wasm-accessible objects (linear memories, tables, GC), since none of those can be dependent on the host-sideTanyway; and then add the right impls to make that automatically work as before withAsContextMut-types. Function calls still need the monomorphizedStoreContextMutbecause aCallercan pop out the other end that needs theT. Stated succinctly: access to Wasm data should not needTso is not monomorphized; calls to Wasm code does needTso is monomorphized. Thoughts?
Last updated: Dec 06 2025 at 07:03 UTC