-
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] Should rethrow/br_on_exn trap on undefined? #109
Comments
I think wasm programs should be able to expect that they can |
Oh shoot, apparently that's already not the case. From what I can tell of the JS API in #86, which lines up with the linked Chromium issue, if JavaScript throws |
I was trying to figure out what the interaction of wasm exception handling and JS throws was in more detail so that I could understand the problem more, and #86 was the most recent description I could find. In particular, it says that a JS value being thrown is converted to a wasm value via ToWebAssemblyValue. I then noticed that ToWebAssemblyValue converts a JS null to So I don't know what to do about my first comment. It seems like a useful principle to always be able to assume you can rethrow an exception you caught. But I didn't realize at the time that the principle is incompatible with the existing design, so I'm at a loss for suggestions. My original thinking was that the semantics of |
Hmm, I realized that the JS-interop strategy here has the ability to turn any wasm reference into an To make this concrete, the following module instantiated with the subsequent JS
I'm guessing this behavior was not intended? In particular, it seems to imply that |
Since
I don't really see how this discussion relates to function references though. Ross' example assumes that |
The use of function references in my example is just to make things concrete (and its JS API is in the reference types proposal, which I don't think anyone wants to change). The point of my example is that, with the current JS API for exceptions, an The issue I'm pointing out, the issue raised with "undefined", and the existing issue with null are all related because they all have to do with how wasm/JS values are coerced as the cross the wasm/JS boundaries. |
It occurs to me that when C++ programs throw an exception in wasm, they have to specify an event mark, e.g. |
@backes According to ToWebAssemblyValue and ToJSValue in reference types JS API spec, when Then when a JS function throws an |
I think some of the recent comments are based on domain errors. An exnref is not the value caught, it contains the value caught! Throwing null from JS still materialises as a non-null exnref when caught in Wasm. That contains some abstract host value, which happens to be the JS null value (though there is no way to observe that). That is unrelated to null exnrefs, which are merely uninitialised exnrefs. Similarly if you throw an exported function. ToJSValue and inverse do not even enter the picture. |
Ah, so it sounds like you had in mind something in line with what I was suggesting with making throwing JS exceptions analogous to throwing C++ exceptions. But I think it's oversimplifying to say |
What you're saying boils down to the question what ToJSValue does for exnref values passed back to JS normally. If that were to coerce exnref values to their contained value then that would be an explicit unwrapping that could not be reverted by ToWAValue. That is, passing the value back to JS would not give you an equivalent exnref. That's probably undesirable.
Fair point, but I think separable. We could still define special behaviour when catching an exnref thrown from JS (because JS itself cannot create such values). Whether that's necessary I'm not sure. A better alternative might be to provide a special throw method in the JS API for throwing a proper Wasm exception. In practice, I don't see much of a use case for passing exnrefs to JS, and I'm not sure it should be encouraged. |
Oh, to clarify. I meant to say that specifically JS-values-wrapped-as-exnrefs should be unwrapped when thrown from wasm to JS, not arbitrary exnrefs.
Even give the above clarification, this point of yours suggests that
Nonetheless, it is possible to do and so needs to be specified. And, being a web standard, we cannot really change this specification later on. This means we have to try to anticipate all of the ways it could be practical now and design for those patterns. |
Thank you for the clarification. So let me check a few more points:
I guess these are the points we should state in our JS API. |
@aheejin, conceptually, I would say that an exnref is unwrapped at the moment it's rethrown in Wasm (which is why that instruction traps on null), regardless of where it's caught. If caught in Wasm, a new exnref is produced at the catch site. If caught in JS, you either get the JS value contained or some opaque object representing the Wasm exception. We could define that throwing a Wasm exception on the JS side behaves like a rethrow in Wasm. Yes, the JS API certainly needs to define something. It'll probably need to define a new form of Wasm exception exotic objects to represent Wasm exceptions on the JS side. |
Thanks. So I guess we need to define two things:
@backes, what's the current implementation doing on these two things? |
In V8, Wasm exceptions are currently instances of No special wrapping happens when this object crosses the JS<>Wasm boundary. I.e. JS just catches the |
@backes, okay, that sounds like what I would expect. Maybe modelling this private property in the spec does not even require a new exotic object, just a [[...]] property on an error object. |
What is still missing thought is the wrapping when catching a JS-thrown exception in wasm., and unwrapping when rethrowing to JS. That needs to be spec'ed and implemented. Implementation-wide, an |
Suppose
I'm having a hard time making all these work out simultaneously though. |
@RossTate, under the special case discussed above, (3) would not hold in the case that the value thrown is actually a Wasm exnref. Other than that, what problem do you see? |
Well y'all haven't specified what happens when JS throws a wasm-exnref-that-is-wrapping-a-JS-thrown-JS-value. From your description above, it sounds like when wasm catches that value it will be an exnref wrapping a JS value that is an exnref wrapping a JS value. This breaks item 2 in my list (supposing the ... in the try throws a JS value in JS code). |
Nothing special, same as if throwing any other exnref. It's a subcase of the case I mentioned one up. Assuming the special-case semantics, throwing an exnref in JS would unwrap it, just like rethrow in Wasm. The other possibility is that we don't special-case throwing an exnref in JS. Then Wasm catch would just produce another exnref (unobservably) containing the first one, i.e., forego (2) as you say. That's the simpler and more regular semantics, but not sure if it wouldn't be surprising/inconvenient for JS folks. |
This then violates item 3. If the value thrown by JS is a wasm-exnref-that-is-wrapping-a-JS-thrown-JS-value, then the value caught by JS will be the wrapped value rather than the exnref. That is, |
Yes, isn't that what I said above? |
It wasn't completely clear to me what you were saying. (No fault on your part, just subtle topic.) Would JS people be alright with making |
I'm not sure how much the JS community would care in general -- JS is full of semantic hacks like that -- but it being induced by an API is more questionable, I agree (although that is not unheard of either, the DOM induces a number of behaviours that are not proper JS). That said, this isn't necessarily implied. The way such a semantics should probably work is by special-casing the Wasm catch when what it catches is an exnref object thrown from JS. Your two snippets would still be equivalent. The exception could even pass through Wasm code unchanged. Only when Wasm catches (and possibly rethrows) it you'd observe special behaviour. That is, your condition (3) would still be violated, but not the more specific example you're asking about above. |
Ah, so your suggestion is to make it so that It seems like no matter what we do, the semantics is ugly. But, as you mentioned above, there's not really a use case for giving It also means that, if |
Only in a JS embedding. This would be part of the JS API specification, not core Wasm. Which doesn't say it's nice, but JS is full of discontinuities like that.
The simplest design would be to simply not do anything special for exnref in JS. Then you simply cannot rethrow a Wasm exception from JS (unless we introduce an API function for that, as I suggested earlier). I'm not sure what ruling out exnrefs at the boundary would buy on top of that. |
Just to clarify, by nothing special you mean it gets wrapped just like every other JS value when caught by wasm? |
I'm confused. I'm mostly confused about what people think of If I understand correctly (please correct me if not), @RossTate
Is this not
?
Ditto; shouldn't this be
Here again, isn't what wasm @backes |
Thanks for the explanations, @RossTate and @rossberg. So the basic concern is that any kind of "unwrapping" in Then I still don't see how that solves the issue. @RossTate proposed to still unwrap it in
But then a wasm function taking an The only solution I see to this is to keep a reference to the wrapper when unwrapping, such that re-wrapping creates the same wrapper reference. |
For me, |
The trick is that (1) this conversion happens regardless of which type you're looking at, and (2) it is a 1-to-1 mapping, i.e., when returning the same exnpackage multiple times you ought to see the same ExceptionRef on the JS side -- similar to how it works for functions. The easiest way to implement that obviously is to use the same representation for exnpackage and ExceptionRef(exnpackage), i.e., the conversions become identity function in the implementation. Alternatively, have each object point to the other. |
Oh, so (2) is indeed the "keeping a reference to the wrapper" trick I mentioned. The easiest way to keep a reference of course is to use the same representation. Neat! If we do the conversion regardless of the type ( |
No, So:
All these are agnostic to whether
unlike the special rule for ExceptionRef. That is, exnrefs are opaque in JS (as ExceptionRef objects), whereas RuntimeExceptions are opaque in Wasm (as externrefs). |
Now I wonder again why we can't unify WA.RuntimeException and ExceptionRef. I need a break to get the knot out of my brain. |
Yes, so the other alternative I mentioned above would be to collapse WA.RuntimeException and ExceptionRef on the JS side. Then:
that is, WA.RuntimeException represents an exnref, and throwing it behaves like a rethrow. However, unlike when throwing normal JS values,
gives you a new object when X was a WA.RuntimeException (because Wasm produces a new exnref to be wrapped), but the original X otherwise. That may be confusing. |
This is confusing... We have two different types (I haven't caught up until the end of the thread yet, so I'm currently assuming there are two separate types
Not sure what this part means. Not sure what happened to |
I think we need clear definitions of each of
at this point. This is really confusing now. Someone uses |
I like this a lot! Can you remind me why in these two cases, we need a new exnpackage:
If we change them to
wouldn't that fix your |
None of the explanation I gave was related to object identity. That’s another issue on top of the issues I discussed. Watching kids so can’t help more until later. |
I'm totally at loss at this point.
I'm not sure if terms are defined and people are using the same terms for same meaning at this point. |
Hm, waiting for more explanation here then. I thought your main concerns are the invariants posted in #109 (comment):
The latest proposal from @rossberg (#109 (comment)) with the modification proposed by me in #109 (comment) should satisfy all three points. |
|
Because the Wasm semantics should avoid the hacky JS semantics of mutating an existing object, e.g., to attach a different stack trace when caught and rethrown. So each Wasm catch must observably produce a new, immutable exnref object.
I would rather say that an exnpackage is created each time an exception is caught in Wasm, regardless of what kind. Or, in the second variant, also when a Wasm exception is caught in JS. |
Doesn't |
Ok, understood. I guess I don't care too much about this. We are talking about the JS API here, so inheriting "hacky JS semantics" does not really worry me :)
Agreed, if a wasm catch creates a new exnpackage anyway. Otherwise, I would have said that the exnpackage is created exactly once (on wasm throw), so we can more easily maintain identity. Instead of |
Yes, it creates an exnpackage value, which are the values of type exnref.
The JS hack is just that, a hack. There should not be any mutation behind your back. It particularly doesn't make sense as long as exnrefs aren't linear. For example, that creates pathological behaviour when you rethrow an exn twice, the second time after you have already caught the first:
And this would likely become way more problematic if we wanted to introduce resumption. |
I'm not sure why we introduced the concept of |
Well, it's not completely "behind your back" since stack trace modification only happens on a (re)throw. And there is still quite some "magic" happening behind the scenes anyway. Is it correct that with the modification proposed in #109 (comment) (i.e. preserving If so, it's a "we don't want to copy JS hacks (in our JS API)" vs "we want to preserve |
Well, for now, but I wouldn't assume that it will always stay that way. And more likely, stack traces may occur in some API. So I would be cautious to spec something for JS interop that isn't consistent with a well-behaved internal semantics. |
@backes raised some important issues that have implications beyond the scope of exception handling. In particular, what to do about object identity, which is actually relevant to the following
and to the question of whether |
Thanks for pausing this. It took me a while to figure out how to boil down the cross-cutting concerns to digestible discussion topics, but I believe the community's answers to WebAssembly/design#1351 and WebAssembly/design#1353 will help inform the design decisions to make here. |
Closing this, as we decided to remove |
Fix decription of ref.func
We decided to make
rethrow
andbr_on_exn
trap on a null value in #90. Should we do the same when a givenexnref
is an undefined value thrown from JavaScript?Raised by @backes in https://bugs.chromium.org/p/v8/issues/detail?id=10128#c12.
The text was updated successfully, but these errors were encountered: