New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does WASM share a stack with JS? #126

Closed
ncbray opened this Issue Jun 9, 2015 · 13 comments

Comments

Projects
None yet
10 participants
@ncbray
Copy link

ncbray commented Jun 9, 2015

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.

@ncbray ncbray added the question label Jun 9, 2015

@pizlonator

This comment has been minimized.

Copy link
Contributor

pizlonator commented Jun 9, 2015

On Jun 9, 2015, at 12:41 PM, Nick Bray notifications@github.com wrote:

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.

Such calls are allowed.

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.

It’s true that this behaves “as if” the two were running on the same stack, but the implementation shouldn’t be required to do it that way.

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?

Short answer: I don’t see a need for such a metaspec. We should have an answer for JS<->wasm call semantics and verbiage about what happens when an exception is thrown, but we don’t need to make any mention of any of the other issues.

Exception propagation and handling.

Good question. Shouldn’t be too hard to answer.
Crash dumps.

The spec doesn’t need to say anything about this. It’s up to the implementation.
Stack inspection for GC.

The spec doesn’t need to say anything about this. Wasm locals cannot point into the GC’d heap (they have offsets into a buffer, and that buffer is GC’d but they don’t have control over the buffer variable). It’s up to the JS GC to decide how it scans the JS stack, and how it deals with the wasm stack (either just scan it in a conservative stack GC like ours, or in an accurate GC, you can skip it like you would native frames, or scan it like a JS frame that happens to not have GC heap pointers).
Coroutines - would need to be supported by all languages on the stack.

JS doesn’t have that except by lowering to JS functions and function calls. Wasm doesn’t have them AFAICT (though you could lower coroutines to wasm in any number of ways). So, the spec doesn’t need to say anything about this.
Tail call elimination, inlining, etc.

The spec doesn’t need to say anything about this. The wasm generator could do tail call elimination if it wants to. The wasm implementation could do it, or not. It could inline, or not. The spec doesn’t need to take a position on this.

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.)

Restricting the wasm<->JS interface to just message passing would make wasm significantly harder to use. I don’t like making things harder for users.

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 don’t see the difference between "two interoperable languages in the same VM” and "two separate languages that talk to each other”. These two phrases have the same meaning to me.

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.

I don’t think there exists a can of worms here. All of the issues you brought up are implementation issues that the spec doesn’t have to worry about, except for what happens in case of an exception. That should be easy to answer.

I’m opposed to a message passing approach because it doesn’t help implementors or the spec, and it definitely hurts users.

-Filip

@lukewagner

This comment has been minimized.

Copy link
Member

lukewagner commented Jun 9, 2015

I just don’t think that’s what we’re working towards.

I think that's what many of us are, but I'll let others speak for themselves. Not allowing direct calls significantly limits integration with the rest of the web which is a high-level goal.

We’re working towards a world where native code can evolve on the web without being constrained by JS.

Of the list of complications, all seemed like they had pretty obvious answers/solutions and I don't see how any of this is constraining wasm. It may be more work if you are trying to implement wasm in a separate VM from the JS VM, but that's a downside of having two VMs...

@MikeHolman

This comment has been minimized.

Copy link
Member

MikeHolman commented Jun 9, 2015

I think wasm and js absolutely need to live on the same call stack. For Microsoft, strong interop with JavaScript is one of the most important tenets of this project. This is already working on c++/js with asm.js. @kripken can probably give you a better idea on how things like EH and stack unwinding are handled, but it is certainly possible.

@jfbastien

This comment has been minimized.

Copy link
Member

jfbastien commented Jun 9, 2015

Let's not frame answers in terms of just MVP: things like coroutines, GC and tail call elimination are on the roadmap. If we overconstrain MVP then we won't be able to loosen up the spec to enable these features seamlessly. Let's not overdesign up front, but let's also make sure we can hit the feature list we want in an evolutionary manner. I think that's what Nick is getting at.

@lukewagner

This comment has been minimized.

Copy link
Member

lukewagner commented Jun 9, 2015

As we evolve forward, assuming that we have pure-wasm threads (which, I am in favor of), we can always lock down certain features that don't make sense in a mixed-stack context as only working for pure-wasm-thread-only modules. (In effect, @ncbray's proposal is to preemptively do this for the entirety of wasm.) Coroutines are the only example I'm aware of and this problem isn't specific to JS (e.g., wasm->browser->wasm callstacks would also be problematic). PTCs seem like no big deal: you just don't define cross-wasm-JS calls to be PTCs.

@kg

This comment has been minimized.

Copy link
Contributor

kg commented Jun 10, 2015

setjmp & longjmp are on the table, or at least desired, right? How do those interact with a mixed wasm/js stack?

That aside, I think direct JS->wasm and wasm->JS calls (whether or not they involve actually sharing a stack or cross-thread interop) on the main thread are an absolute requirement. Without them most current emscripten use cases will not work and that is simply not acceptable. Stack sharing seems like the easiest way to achieve this but I can envision ways to do it that don't involve stack sharing.

@jfbastien

This comment has been minimized.

Copy link
Member

jfbastien commented Jun 10, 2015

@kg: I think what we'd agreed on for setjmp & longjmp was to implement them in terms of EH (and turn of dtor calls when unwinding). It's not how it's traditionally done, but will probably simplify implementations quite a bit. The one caveat there is UB when using sjlj for coroutines, but we'll want to support coroutines for real instead so I'd call it a non-issue.

@ncbray

This comment has been minimized.

Copy link

ncbray commented Jun 17, 2015

To explain where I am coming from: MVP means minimal regret. Avoid unnecessary entanglements. Understand the consequences of the entanglements we accept. Make sure we’re actually on a trajectory to run 99% of existing native code on the web. Make sure we don’t hit an evolutionary dead end and fizzle out at “I guess Unity works, good enough?”

Taking a step back, my metagoal is to:

  • understand the picture in everyone’s mind
  • make implicit assumptions explicit
  • figure out where the pictures don’t match or are unclear
  • extrapolate the consequences of different decisions
  • poke the sore areas with a pointy stick

I was pretty sure most everyone had one of two different mental pictures.

One picture is that WASM is about native code. We are designing a portable machine model. JavaScript’s dynamism and concurrency model is fundamentally wrong for native code. We’re carving out a jumping off point where we can build a machine model that suits other languages. And can talk with JS.

Another picture is that WASM is about JavaScript. It’s another way to define functions that run inside the same machine model as JS. If we share a stack, this is implicitly what we’re signing up for.

The gap between these mental pictures seems small, but I think they point to very different endpoints.

Restricting the wasm<->JS interface to just message passing would make wasm significantly harder to use. I don’t like making things harder for users.

Strangely enough - I don’t like making things harder for users, either. I could point at JS and all their rationale for why async messaging is great, but I don’t really believe any of it. It’s just what you do when the alternatives are worse. That’s the subtext of my proposal. I would prefer synchronous calls, but for the consequences. Talking with Chrome folks involved in NPAPI, PPAPI, Flash, etc, the consistent refrain is that reentrancy is the devil. Really what I am proposing is caution - learn from the past and don’t do sync calls until we’ve done our homework. I am not arguing this isn't a feature that would be nice to have, all things being equal.

Exception propagation and handling.

Good question. Shouldn’t be too hard to answer.

Do exceptions implicitly propagate across language boundaries? If they do propagate, what are the performance implications for WASM codegen? If they don’t propagate, what about usability? Can JS catch a WASM exception and vice-versa and what does that look like?

Nothing impossible in there, just a lot of dark corners and the potential for unintended consequences.

(In effect, @ncbray's proposal is to preemptively do this for the entirety of wasm.)

Yep. Default deny. Commit to features with caution.

Coroutines are the only example I'm aware of and this problem isn't specific to JS (e.g., wasm->browser->wasm callstacks would also be problematic).

We should make sure that call pattern never happens.

PTCs seem like no big deal: you just don't define cross-wasm-JS calls to be PTCs.

But maybe people will want it. Which again points to the eventual existence of some sort of “common language” metaspec.

@kg

This comment has been minimized.

Copy link
Contributor

kg commented Jun 17, 2015

We should make sure that call pattern never happens.

Denying wasm->browser->wasm calls is a non-starter. It's simply not an acceptable restriction. Too many real applications and interop scenarios demand the ability to do that and it would be a huge regression vs. asm.js. It's not even a 'feature', it's a requirement, in my opinion.

(Whether you can achieve those call patterns without a shared stack is another question. You probably can, but I'm not sure I'd want to implement it.)

@qwertie

This comment has been minimized.

Copy link

qwertie commented Jun 24, 2015

I was pretty sure most everyone had one of two different mental pictures.

One picture is that WASM is about native code. We are designing a portable machine model. JavaScript’s dynamism and concurrency model is fundamentally wrong for native code....

Another picture is that WASM is about JavaScript. It’s another way to define functions that run inside the same machine model as JS. If we share a stack, this is implicitly what we’re signing up for.

I don't agree with taking one view or the other. It's like asking "Are you Bruce Wayne, or are you Batman?" I'm both! Wasm can serve both equally well. In my view, it should be designed to serve everyone well, not just native code and JS/ES. I see a future in which JS is one of many languages that targets Wasm (but perhaps the only language for which the browser contains a built-in compiler.) Right now Wasm compiles to Javascript (asm.js), but someday the relationship could be flipped.

'Dynamism' (you mean like duck typing?) can be allowed in a 'native code' programming language (e.g. C# - it's not a systems language, but it supports both static and dynamic typing), so can GC (e.g. D), and as for concurrency, well, that's an area in which to tread carefully perhaps, but I'm sure we can find a way to support traditional multithreaded software in the same VM as traditional single-threaded Javascript.

@dwvisser

This comment has been minimized.

Copy link

dwvisser commented Jun 25, 2015

An assembly language that is designed to compile (transpile?) to a high-level language. Mind blown. The real question: Is JavaScript Bruce Wayne or Batman in this metaphor? Does that make Wasm the BatMobile?

@sunfishcode

This comment has been minimized.

Copy link
Member

sunfishcode commented Aug 8, 2015

@ncbray Your comment above recommends we "don’t do sync calls until we’ve done our homework." By my reading, all the specific questions raised in this issue have answers posted in this issue. Are any of these answers unsatisfactory? Can you be more specific about what further homework needs to be done here?

@binji

This comment has been minimized.

Copy link
Member

binji commented Oct 23, 2015

Resolved. Wasm will definitely share a stack with JS. It remains to be seen whether there are pure wasm threads.

@binji binji closed this Oct 23, 2015

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment