Description
Does WASM share a stack with JS? As in, can JS call into WASM and WASM into JS, and all of those invocations occur on the same trusted stack? I think the answer to this speaks directly to fundamental assumptions about what WASM actually is.
Allowing synchronous calls between languages in both directions is equivalent to sharing a stack because of reentrancy and the inability to unwind the stacks independently.
The cost of sharing a stack is that it implicitly requires some meta-spec defining how a "common stack" behaves for WASM and JS. And that doesn't even consider WASM in non-JS environments. How do the following things work with a mixed-language stack?
Exception propagation and handling.
Crash dumps.
Stack inspection for GC.
Coroutines - would need to be supported by all languages on the stack.
Tail call elimination, inlining, etc.
For cases like C and Python, there is a strict embeder / embedee relationship for language mixing that I don't see applying in this case. I can't think of a precedent, but maybe .NET has some examples? I am not familiar.
These issues could be avoided by keeping WASM’s stack separate from JS. This would look like isolates with async messaging. Basically, WASM threads would run in their own "worker" with postMessage, or some sort of program-defined inter-language RPC interface. (This RPC interface could use promises on the JS side to make it more palatable.)
Because WASM threads can block, it would still be possible to emulate sync outcalls from WASM to JS. (And deadlock when talking between WASM modules!) Sync outcalls from WASM could be explicitly supported by the system to allow optimizations (if desired) such as running JS on top of the WASM stack. (But since WASM is always on the bottom of the stack, the spec can pretend as if there is no stack sharing. Coroutines could be supported without taking JS into account, etc.)
Of course, at this point it should be obvious that not sharing takes some of the shine off of the “WASM libraries for JS” use case. Promise-based RPC could not be used to implement JS getters, etc. (Although plumbing APIs such as event handling into WASM as “system APIs” would allow WASM to produce synchronous responses to Web API callbacks. It’s only the user JS => user WASM calls that would need to be async to eliminate stack sharing.)
It might be possible to support stack sharing on some WASM threads but not on others... but that seems like complications would accumulate. You’d still need to specify mixed language stacks, there would be two execution modes, certain functionality wouldn’t work on certain threads, etc.
So, I think the question boils down to this: are we running two interoperable languages in the same VM, or are we running two separate languages that talk to each other? Or are we willing to pay the cost of trying to do both?
I currently believe there should be no stack sharing because trying to share is a huge can of worms we do not want to open. It’s always possible to add sync incalls / stack sharing later and it’s more difficult to take it away. On the other hand, I understand the charm of not making a hard distinction between WASM code and JS code and let them run in the same VM. I just don’t think that’s what we’re working towards. We’re working towards a world where native code can evolve on the web without being constrained by JS. Sharing a stack creates a fundamental coupling between WASM and JS.