-
Notifications
You must be signed in to change notification settings - Fork 36
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
[js-api] Custom conversions between exceptions in wasm versus in JS #190
Comments
It sounds like you are expecting that this conversion function would need to be called on every transition during unwinding from JS to or from wasm, else it would be considered unhandled and convert to a trap. I don't think that's actually necessary. The conversion only needs to happen when an exception is actually caught (either in JS or in wasm) or passed to an Also, given that this would be an addition to the existing API, it's not actually necessary that an unconverted exception gets turned into a trap, but I can see how it might make sense to offer that as an alternative to the MVP behavior. Does the type system of the GC spec (or the typed funcref spec) include types that may be thrown? I seem to recall that being discussed in the context of interface types, but not elsewhere. I think for this to be useful for linear-memory languages such as C++, it would be better to make the callback a JS function, rather than a wasm function. Any throw that wants to include metadata such as a stack trace will have to call a JS function to get it, and the final conversion for any unhandled exception will be to JS. (Since the final conversion will need to create a JS object, any wasm callback would just need to call back out to JS to do that). The JS code could of course call any exported wasm function it wanted. |
Or passed to the debugger (the conversion provides the stack trace). One of these cases seems to always happen. I worked through a number of use cases and implementation strategies and found that almost always the conversion-at-the-boundary approach worked better from many perspectives. The only counterexample I could think of is when the matching catch of a thrown wasm exception is a wasm catch of the same tag with a JS frame in between, in which case there's an unnecessary coercion to JS and coercion back. But this seemed like a very uncommon scenario that wasn't worth optimizing for. I can go through the list of reasons if you'd like, but it's a long one, so I don't want to go into it unless you really think it's necessary.
No. As low-level features, GC is orthogonal to exceptional control throw. We would need to make a new EH design to develop a notion of throwable values (though, I'm not sure this would be useful, since in many languages all values are throwable). What there has been discussion of is making GC refs be able to be subclasses of JS classes in the JS API. So Kotlin/Java's
The reason to make it a wasm function is that in many cases it will want to access internal state of the module. For example, to make I worked through a number of cases and found that having the converter be in wasm rather than JS was always at least as efficient and easy to use, and in some cases much more so. |
It mostly sounds like you want to revive |
Wait, @aheejin, your post suggests a potential critical miscommunication between us. What do you mean by the problems for 2PEH would "rematerialize"? I ask because the change from |
I think I answered the same question from you a while ago: #176 (comment) To repeat, it's not Also this writeup kind of assumes the existence of |
I keep hoping it has not. :) |
Thanks for the clarification (and reminder), @aheejin. It was very useful. The issue with that is that the value of It's sad in some ways that that opportunity is gone, but a huge advantage of not striving for that is that it's much easier for producers to generate non-collaborative WebAssembly. With the goal of just non-collaborative EH in mind, there are two things to account for:
The current proposal has the potential to improve on (1) significantly (maybe even optimally?). A So my concern is that the change from Given that, what are thoughts on the motivations and/or solution strategies in the OP? |
We've gotten way off topic from this issue, but we have to finish MVP exceptions. |
@RossTate I have to repeat what I said in the previous comment. The OP is based on the implicit assumption that there is a Also, I simply cannot agree that the current LLVM toolchain not using
I don't think it's gone.
I pushed back for months last year arguing just this, but you said the toolchain could work around the restrictions by various workarounds in many posts. Seeing my arguments coming from you after we and the VM people did all the work second time over is a very interesting situation to be in. |
My understanding is that this is the phase where tooling becomes a concern. Multiple (1PEH) language teams have been invited to look at this proposal, and every team (that I know of) has articulated reasons why this proposal does not suit their needs well. I have independently investigated some challenges of languages as well and also articulated problems, with no solutions offered. The C++ compiler does not support stack-trace-preserving I actually do not know of a 1PEH language team that has been able to successfully compile their language's full EH functionality to this proposal. That does not seem validated.
@aheejin I understand that you are frustrated by my seemingly changing position. After the CG voted to go ahead with a design with
I understand, and my understanding is that this is the phase where the JS API gets developed, and the conversions I am suggesting are executed solely in the JS API. |
I don't think this is accurate. IIRC the decision to remove |
Again, you are conflating the current LLVM toolchain's implementation status with the spec issue. We compile
I am not sure if all that happened in #142 can be characterized as that. In that post, several people expressed concerns (or 👍ed) on why |
@conrad-watt Sorry, to clarify, the unilateral decision was to add
Please get this working (with |
I closed this issue because do not think it will productive and I do not wish to engage anymore. It started off productive until the champion's first post, which focused on attacking me rather than engaging in the ideas. Things escalated from there (and apologies to the group for partaking in that escalation). |
I am sorry if you felt that way; I didn't mean to attack you in my first comment. I was mostly confused that it looked like you wanted to bring back something resembling what you said was problematic in many ways last year. Also maybe one thing I said in the comments was misleading. I agree this can be JS API-only, if we make every language's tag And I don't remember LLVM not using it at the moment played a role in |
Also I am sorry that Michal felt that way. I don't remember all the details of the discussions with him in #142, but I think he was suggesting things including a very different SjLj-like EH scheme for Lisp, in which it takes O(1) from throwing to catching, and I thought at Phase 3 we couldn't afford to change the current proposal in that fundamental way, so I think I argued that a new proposal could be a better way to solve that problem. I still believe that's the case, but I guess I could have been better in listening to him and discussing the issues. |
Thank you very much, @aheejin, for those posts. They're really impactful and greatly appreciated, especially since I recognize how frustrating and problematic all this has been from your position as well. If you want to engage on the topics of the OP, feel free to reopen the issue and prompt a renewed discussion. But I understand if you don't want to. Cheers. |
By request, this is a fork from #183.
Motivation
The basic premise is to enable wasm modules to specify how they would like JS exceptions to converted into their wasm exceptions at the module's entry points, and similarly how they would like their wasm exceptions to be converted into JS exceptions at the exit points.
One use case is JS interop. For example, Kotlin will want to have a
$__kotlin_exception : [(ref $Throwable)]
exception. To match existing JS interop, Kotlin will want all JS exceptions to be converted into Kotlin exceptions at the boundary: if the JS value is already a KotlinThrowable
, just make it the payload of the exception, and otherwise wrap the JS value as anexternref
field of some (internal)JSException
subclass ofThrowable
. Similarly, Kotlin will want Kotlin exceptions that escape to JS to be converted into JS exceptions as follows: if the value is aJSException
, extract and throw itsexternref
, and otherwise throw theref $Throwable
as a JS value (using the JS API for GC that lets GC objects be used as JS objects).Another use case is debugging. The current EH design and JS API only provides good debugging support if you conform to a particular convention. But this convention is too restrictive to conveniently extend this support to features such as C++'s
throw;
, Go'sdefer
, and Java'sfinally
. With custom conversions, wasm generators can assume all exceptions have a specific tag, and they can explicitly propagate debugging information such as stack traces (asexternref
values) so that, at the boundary, they can create JS Errors with this debugging information that already hooks into tooling such as debuggers.Strategy
The high-level strategy is to have "call an Exported Function" in the JS API call a custom-defined wasm function when a wasm exception is thrown, and to have "create a host function" in the JS API call a custom-defined wasm function when a JS exception is thrown. The former function takes the payload of the exception and returns the
externref
to be thrown in JS. The latter function takes theexternref
that was thrown and throws the corresponding wasm exception; if the function returns (void), then theexternref
is considered unhandled and treated as a trap (just as in the pre-EH JS API).To clarify, this strategy does not make use of a
WebAssembly.Exception
class, nor is there a special tag with payload[externref]
for catching/throwing JS exceptions in wasm with; it simply treats all exceptions from JS uniformly and expects them to be explicitly converted to wasm exceptions at the boundary (if they're to be handled at all).Also, while I'm using JS as a concrete example, all the designs here work just as well for other embeddings.
Design
Given the high-level strategy, the key design question is how to specify/determine the custom conversion functions to use.
Wasm-to-JS Exception Conversion
A key challenge to keep in mind is that one can create
funcref
s from wasm functions even if the functions are not explicitly exported, and thesefuncref
s can be passed to and called from JS.So we need to ensure that even the exceptions thrown by these functions are converted, ideally in the same manner as if the function were called as an explicit export on the module.
The design that seems to achieve this most simply is for the module defining an exception tag of type
[t*]
to associate a wasm function of type[t*] -> [externref]
. The engine stores this function in the tag, and the wasm-to-JS stubs generated by engines catch wasm exceptions and call the function stored in the tag with the payload of the exception and throw the returned JS value.We could make this association optional, in which case the default behavior would be to simply trap.
JS-to-Wasm Exception Conversion
A key challenge to keep in mind is that many applications will want to be able to convert arbitrary exceptions from JS into their wasm exceptions.
Any value can be an exception in JS, so this means we cannot make any assumptions about what structure the JS-exception value has.
With this constraint, I can think of three designs.
Instance-Directed Conversion
The first design is for module's to be able to specify a distinguished
[externref] -> []
function, akin to how modules can specify a distinguishedstart
function. When a JS function is used as an import for a wasm instance, that instance's distinguished[externref] -> []
function is used to convert any exceptions it throws. (If none is specified, then the JS exception is propagated as a trap.)Import-Directed Conversion
The second design offers slightly more control. For each import, a module can specify which function to use to handle JS exceptions.
Type-Directed Conversion
Both of the above have some issues. For one, they both only handle the cases where JS functions are converted to explicit imports. But the
js-types
proposal allows you to create wasmfuncref
s with just a type signature. So they'd require extending that proposal to create afuncref
with an exception handler (that would typically be exported from the wasm instance thefuncref
s eventually get propagated with). To potentially make matters worse, the purpose of WebAssembly/design#1408 is to let tooling bypassjs-types
and just usetable.set
and the like, but thefuncref
s created in this manner would also not have any JS exception handler (so they'd just trap).Another problem is that they only let the exception handler kick in when the import is a JS function. If, on the other hand, the function comes from wasm then no conversion will happen, which will violate the expectations of the program. I understand this isn't a big issue right now because C++ wasm modules are always surrounded by JS glue, but the JS API we're working on for the GC proposal would eliminate the need for glue. Furthermore, ESM integration will make it more difficult for users to anticipate when a wasm module will get hooked up with another JS module or another wasm module. So this will likely become a problem soon.
The approach that addresses both of these issues and nicely mirrors the wasm-to-JS design is to use a type-directed approach. Given a wasm module that type-checks with unchecked exceptions but uses only the
$__cpp_exception
tag, just by addingthrows $__cpp_exception
to every function type in the types section (you don't have to touch anything else) you get a wasm module that type-checks using checked exceptions. If you do that, then you can use the exception tag(s) in thethrows
clause of an import to determine which conversion function to use. To do so, you associate with each exception tag$exn
a wasm function of type[externref] -> [] (throws $exn)
, and the JS-to-wasm stub for a function type thatthrows $exn
uses that conversion function. (If thethrows
clause specifies multiple exceptions, you try their conversion functions in the sequence listed.)This works for
funcref
s created by WebAssembly/design#1408. It also works for hooking up two wasm instances (via the JS API or ESM): if the imported functionthrows
tags not accepted by the expected function signature, they are converted toexternref
using their associated conversion-to-JS functions, and thatexternref
is then converted to the expected exception tags using their associated conversion-from-JS functions.The text was updated successfully, but these errors were encountered: