Skip to content
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

Expose the JavaScript Exception tag and allow importing it #269

Merged
merged 31 commits into from
Apr 26, 2024
Merged

Conversation

dschuff
Copy link
Member

@dschuff dschuff commented Apr 7, 2023

I believe this is what we discussed in #202

The JS tag has type externref and is exposed on the WebAssembly object, allowing it to be imported.
When such exceptions are caught in wasm, the thrown JS object is passed to the catch block as an externref.
When such exceptions propagate from wasm to JS, the payload value is thrown directly, rather than being
wrapped in a new Exception.

This is a basic start of what we'll need to add to the embedder spec.
I'm not sure if we also need a way to allow exceptions to bubble
from the embedder into wasm ; I don't see any reference to what
happens when the instance wants to call an import, so maybe not.
Also the references aren't quite right, but I may need help
to fix that
Copy link
Member

@aheejin aheejin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically this wraps JS exceptions when entering wasm and unwraps them when exiting wasm, right? If we do this, I guess we don't need a special handling for catch..?

@@ -1383,10 +1388,14 @@ The <dfn attribute for="Exception">stack</dfn> getter steps are:
<h4 id="js-exceptions">JavaScript exceptions</h4>

The <dfn>JavaScript exception tag</dfn> is a [=tag address=] reserved by this
specification to distinguish exceptions originating from JavaScript.
specification to distinguish exceptions originating from JavaScript. It is exposed
to JavaScript code via {{WebAssembly}}.{{JSTag}} a global pre-initailized (TODO: what's ths spec-y way to say this?)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
to JavaScript code via {{WebAssembly}}.{{JSTag}} a global pre-initailized (TODO: what's ths spec-y way to say this?)
to JavaScript code via {{WebAssembly}}.{{JSTag}} a global pre-initialized (TODO: what's ths spec-y way to say this?)

Not sure about more spec-y way to say this though.

@thibaudmichaud
Copy link
Collaborator

I don't have any particular concerns about the implementation here. We could pack/unpack the exception in a WA.Exception at the JS/Wasm boundary as suggested, but I think modifying the catch handler instead is also worth considering: we would only need to add a check to catch blocks where the tag type is externref. Otherwise we know statically that it can't match the JSTag. If the catch tag is the global JSTag and the reference is not a WA.Exception, we push the reference directly on the operand stack. That would remove the need for packing/unpacking at the language boundary and in the catch handler.

@dschuff
Copy link
Member Author

dschuff commented Apr 12, 2023

Yes, this basically wraps/unwraps as you said. Because of that I don't think we need special handling for catch, because it would be the same as catching an exception with any other imported tag with an externref type.
The existing spec is basically written by wrapping/unwrapping at the boundary, so it's a small change
I think for an implementation you could modify the catch handler as @thibaudmichaud said, and it would still be compliant with this spec. If there are no catch handlers the exception would just propagate through (as if it had been wrapped and then unwrapped). Line 1439 (in this PR) also mentions that.

@aheejin
Copy link
Member

aheejin commented Apr 12, 2023

I don't have any particular concerns about the implementation here. We could pack/unpack the exception in a WA.Exception at the JS/Wasm boundary as suggested, but I think modifying the catch handler instead is also worth considering: we would only need to add a check to catch blocks where the tag type is externref. Otherwise we know statically that it can't match the JSTag. If the catch tag is the global JSTag and the reference is not a WA.Exception, we push the reference directly on the operand stack. That would remove the need for packing/unpacking at the language boundary and in the catch handler.

I think this can be treated as internal implementation / optimization details, and this should be still compliant with what @dschuff wrote. Right?

@thibaudmichaud
Copy link
Collaborator

On second thought I think my suggestion doesn't work. If JS explicitly creates and throws a WA.Exception(WA.JSTag, error) and this exception is not handled by wasm, it should be automatically unpacked when we unwind the export according to this spec. That would not happen with my implementation strategy.


For any [=associated store=] |store|, the result of
[=tag_parameters=](|store|, [=JavaScript exception tag=]) must be « ».
[=tag_parameters=](|store|, [=JavaScript exception tag=]) must be « [=externref=] ».

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think what we should be doing (as the "spec-y" thing, and in order to preerve abstraction boundaries) is to invoke tag_alloc from the core spec's embedding interface to create that tag, with the respective tag type. That could happen lazily, i.e., the JS spec does the allocation the first time the tag address is needed and then memoizes it.

Btw, I think the embedding spec should also provide a tag_type function, analogous to the other *_type functions.

Co-authored-by: Andreas Rossberg <[email protected]>
pull bot pushed a commit to jamlee-t/v8 that referenced this pull request May 6, 2023
This is a special tag which matches any JS exception not wrapped in a
WebAssembly.Exception object. See:
WebAssembly/exception-handling#269

This only adds the JSTag object for now. The special behavior of this
tag with respect to JS exceptions will be implemented in a separate CL.

[email protected]

Bug: v8:8091
Change-Id: Ie663791deaf60ac178d7f366ddd007ddfe0e383e
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/4508921
Commit-Queue: Thibaud Michaud <[email protected]>
Reviewed-by: Clemens Backes <[email protected]>
Cr-Commit-Position: refs/heads/main@{#87479}
@ecmziegler
Copy link

@dschuff @thibaudmichaud As discussed earlier, we should rebase this to the new EH API because this is a requirement for our partners.

@thibaudmichaud
Copy link
Collaborator

Rebasing this on the new proposal has some implications so I would like to clarify a few points.

In the old proposal, the implicit wrapping/unwrapping of the JS exception into a WA.Exception at the boundary was only observable through the catch block, using the special JS tag. This allowed us to implement this as a special case of the catch block, and avoid the implicit wrapping/unwrapping (as discussed earlier in this thread).

With the new proposal, it sounds like the WA.Exception could also be observed using catch(_all)_ref. Is that correct? E.g. catching a JS exception with catch_ref will push two references: the WA.Exception (created implicitly at the boundary) and the JS exception (the externref package).

And is it correct that without this PR, catch(_all)_ref would push the original JS exception as the exnref? This contrasts with other places where we throw a type error when we try to initialize an exnref from a JS value (import/export, globals, etc.), so I wonder if this is expected regardless of the special JS tag.

@rossberg
Copy link
Member

rossberg commented Mar 19, 2024

Logically, the exnref of a JS exception is a wrapped value. Internally, it could be the unwrapped JS value itself, depending on implementation choices. That is not observable in Wasm. In particular, there is no way to compare the exnref against the externref that logically represents the actual JS value. From the Wasm perspective, the exnref is opaque.

Moreover, this exnref cannot be passed pack to JS, so JS cannot observe its relation to the JS value either. (That's why we need the type error at the boundary.)

Taken together, this should imply that the previous special-casing still works. An engine is free to represent a JS-exception exnref in whatever way it sees fit, whether wrapped or unwrapped. The only requirement is that throw_ref handles it accordingly.

@dschuff dschuff changed the base branch from main to exnref-js April 5, 2024 22:16
@dschuff
Copy link
Member Author

dschuff commented Apr 5, 2024

I've rebased this against #301 to make it easier to review. One aspect becomes trivial because #301 already handles allocating the JS tag. So this just becomes about the right way to put it on the WebAssembly namespace. In that sense it becomes just like the wasm-specific exception classes, so I refactored that part. It looks like @Ms2ger authored that, WDYT?

@dschuff dschuff requested a review from Ms2ger April 5, 2024 22:21
Copy link
Contributor

@Ms2ger Ms2ger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately I don't have time to follow all the changes in detail, but some comments below.

document/js-api/index.bs Show resolved Hide resolved
document/js-api/index.bs Outdated Show resolved Hide resolved
@dschuff
Copy link
Member Author

dschuff commented Apr 8, 2024

If the attribute works (and I don't see why it wouldn't), we should prefer it over further manual adjustments to the "create a namespace object" algorithm. You'll need to add the getter steps, but those should be trivial.

Agreed; mostly I was wondering how the "create a namespace" bit that we have for the exceptions got to be the way it was in the first place. And I also don't see why the attribute description shouldn't work; byt what exactly do we mean by "work" here? If we write the attribute that way and write a getter, it seems like this is sufficient to describe the desired behavior but I don't have much experience with this kind of spec.

@dschuff
Copy link
Member Author

dschuff commented Apr 9, 2024

Unfortunately I don't have time to follow all the changes in detail, but some comments below.

This change is now pretty trivial; But the exnref change is fairly large, so I'll wait for reviews from @aheejin and @rossberg on that, but won't block on yours.

@Ms2ger
Copy link
Contributor

Ms2ger commented Apr 9, 2024

If the attribute works (and I don't see why it wouldn't), we should prefer it over further manual adjustments to the "create a namespace object" algorithm. You'll need to add the getter steps, but those should be trivial.

Agreed; mostly I was wondering how the "create a namespace" bit that we have for the exceptions got to be the way it was in the first place. And I also don't see why the attribute description shouldn't work; byt what exactly do we mean by "work" here? If we write the attribute that way and write a getter, it seems like this is sufficient to describe the desired behavior but I don't have much experience with this kind of spec.

The reason for the custom algorithm is that the exception classes are modeled after the ones that exist in JavaScript itself, and those couldn't be specified in WebIDL. The attribute should be fine, though; I was just wondering if you discovered an issue that lead you to put in the custom algorithm instead.

Copy link
Member

@rossberg rossberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that remains of this PR is JS/WebIDL stuff, so I'll defer to @Ms2ger. :)

@dschuff
Copy link
Member Author

dschuff commented Apr 9, 2024

The reason for the custom algorithm is that the exception classes are modeled after the ones that exist in JavaScript itself, and those couldn't be specified in WebIDL. The attribute should be fine, though; I was just wondering if you discovered an issue that lead you to put in the custom algorithm instead.

Got it. No, I didn't discover any issues; the attribute is now back, with a getter.

@thibaudmichaud
Copy link
Collaborator

Continuing the discussion from #301 (comment)

if you created a WebAssembly.Exception(WebAssembly.JSTag, payload) and threw it into wasm, it would be treated as a wasm exception on entry and unrapped; and then when it came back out to JS it would not be rewrapped but come out as just payload

It would not be unwrapped on entry (with the optimization I was discussing), so it would come out as the WebAssembly.Exception + payload. We would have to unwrap it either on entry or on exit to ensure that only the payload comes out.

@dschuff
Copy link
Member Author

dschuff commented Apr 11, 2024

@thibaudmichaud @sjrd, continuing the discussion from #301 (comment)

Continuing the discussion from #301 (comment)

if you created a WebAssembly.Exception(WebAssembly.JSTag, payload) and threw it into wasm, it would be treated as a wasm exception on entry and unrapped; and then when it came back out to JS it would not be rewrapped but come out as just payload

It would not be unwrapped on entry (with the optimization I was discussing), so it would come out as the WebAssembly.Exception + payload. We would have to unwrap it either on entry or on exit to ensure that only the payload comes out.

Ok, I don't mind adding this restriction. We can always relax it later if we want.

@thibaudmichaud
Copy link
Collaborator

thibaudmichaud commented Apr 11, 2024

SGTM. For the same reason (and to be consistent) the restriction should also apply to throw-ing with the JSTag in wasm.

@dschuff
Copy link
Member Author

dschuff commented Apr 11, 2024

I could imagine that being actually useful though? If you have a JS object stored in an externref in wasm, I can imagine that you might want to throw it, and have the result come out as an unwrapped JS exception. Especially if it's already an Error object or something.
For that matter, if you catch a JS exception in wasm, you'll get an exnref that you can later rethrow with throw_ref and I would certainly expect that to work. Will that cause the same issues?

@sjrd
Copy link

sjrd commented Apr 11, 2024

SGTM. For the same reason (and to be consistency) the restriction should also apply to throw-ing with the JSTag in wasm.

I disagree. On the Wasm side, this is the only way to throw an exception that will be caught as-is on the JS side. This is important for interoperability with JS, if the JS thing you want to talk to expects specific shapes of exceptions.

The way I see it, by disallowing new WA.Exception(JSTag, x) on the JS side, but allowing throw $jstag on the Wasm side, we establish a strong 1-to-1 correspondence between JS and Wasm exceptions:

  • Wasm sees ($jstag, x) if and only if JS sees x
  • Wasm sees ($not_jstag, x) if and only if JS sees WA.Exception(not_jstag, x)

This is good in terms of implementation because there is a unique representation of exceptions with (by spec) JSTag: the representation that JS exceptions have.

Currently in V8, the catch $jstag must handle 2 possibilities: either no-tag (i.e., JS exception) or with-tag which is JSTag. With what I propose, only the former possibility remains. It does include one more code path for throw, but no more than disallowing throw $jstag in the first place.


Note that if we do disallow it on both sides, it is still kind-of possible for Wasm to throw a bare JS exception, but it requires cooperation from JS. You can write a JS function function throwAsIs(x) { throw x; } which you can import from Wasm. You then use call $throwAsIs and unreachable instead of throw $jstag. But that is inelegant, as it is not symmetric wrt. catch.


Edit: also I'm not only saying this in the abstract. I would definitely use throw $jstag in the Scala-to-Wasm compiler, since our spec says that we can throw JS exceptions and they will be caught as is by JS.

@thibaudmichaud
Copy link
Collaborator

Ok, special-casing throw $jstag is not a big change so if there are valid reasons to use it then that's ok with me.

For that matter, if you catch a JS exception in wasm, you'll get an exnref that you can later rethrow with throw_ref and I would certainly expect that to work. Will that cause the same issues?

That will work, but that does not require any special case because exnref would already be the raw JS value and throw-ref would rethrow it without adding a tag to it.

@dschuff dschuff changed the base branch from exnref-js to main April 11, 2024 20:24
@dschuff
Copy link
Member Author

dschuff commented Apr 24, 2024

Any more comments on this? The current version just uses the WebIDL attribute with a getter, and also contains the restriction that creating a WebAssembly.Exception object using the JS tag is disallowed.

Copy link

@sjrd sjrd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, looks good to me.

@dschuff dschuff merged commit d399b9e into main Apr 26, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants