Closed eqrion closed 5 months ago
Note, there is no
exn.convert_any
orexn.convert_extern
as we don't want to assume in core wasm that exnref can hold any host value. That's true on JS hosts though, which is why the JS-API allows conversions on the boundary.
Can you walk me through why this is true on JS hosts? I wouldn't necessarily expect that a exception object already materialised on the JS side could be passed to Wasm as an exnref
through a function argument (I'd expect it to appear as an externref
); even if such an object, when thrown in JS, could be caught in Wasm as an exnref
.
So JS code can do things like:
throw 'JS string primitive';
and wasm can catch that and receive an exnref:
block $l (result exnref)
try (catch_all $l)
call $JS
end
end
;; exnref which contains a JS string primitive
And in JS this thrown value doesn't have any identity or stack trace associated with it (AFAIK), it's simply just the primitive without any wrapper exception object.
So that's why I believe we need to allow for exnref
to contain any host value on JS hosts, which is why I think the JS-API ToWebAssembly/ToJSValue
semantics I propose are safe.
However, other hosts could have (simpler) semantics that only objects in an exception class hierarchy can be thrown. So we wouldn't want to force those hosts to be able to convert anyref to exnref inside core wasm. It seems safe to allow the other direction though.
Ah ok, I misunderstood. This makes sense, thanks! Do we expect that such an exnref
is catchable with tag externref
(to immediately get at the externref
view of the thrown JS value) or would the Wasm code need to rely on catch_all
and extern.convert_exn
?
Unfortunately I think extern.convert_exn
would be a problem for wasm2c and other consumers who want to implement exception-handling without a dependency on GC. :-(
Per https://github.com/WebAssembly/exception-handling/issues/280#issuecomment-1664945711, my understanding had been that exnref
would be representable as a sum type (big enough to contain any plausible set of tag values) that can reasonably be stored on the stack and copied when needed.
If Wasm code has the ability to convert an exnref into an externref (which for us is basically a void*
), that would throw a wrench into that plan -- the lifetime of the tag values would become independent of any exnref
value.
@keithw Ah, good point. I did not know that wasm2c implemented externref
, is that a GC or ref-counted value? What is it used for?
Ah ok, I misunderstood. This makes sense, thanks! Do we expect that such an
exnref
is catchable with tagexternref
(to immediately get at theexternref
view of the thrown JS value) or would the Wasm code need to rely oncatch_all
andextern.convert_exn
?
I think that would be really tricky to specify, as tags are generative and so we'd need to have the tag for 'host values' that is importable. And then engines would need to know that there's a special tag that non-wasm exception objects all implicitly have. I think the catch_all
with conversion would be cleaner.
@eqrion It's not GCed or ref-counted -- for wasm2c, externref
is just an opaque type (by default, a void*
). I think it's up to the host API to define the purpose (other than "so wasm2c can pass the spec testsuite"), but in our own work we've been using externref
to pass arbitrary capabilities/handles to a Wasm module.
I think we need to distinguish exnref values from host values thrown as exceptions. When you catch a host value, then the exnref you get conceptually carries that value, but it isn't the same as that value — I think of exnref as more like an instance of WebAssembly.Exception and the value is its arg. It may happen to work in JS to identify the two (and an implementation may do so), but I doubt it necessarily works for other embeddings to make that observable and allow converting between extern and exnref as if that maintained identity.
If we want to allow Wasm to extract a JS value as an externref when catching a JS exception, then we should rather materialise JS exceptions with a special tag defined by the JS API. Then you can do:
(tag $JSexn (import "JS" "JSexn") (param externref))
(block $on_JSexn (result externref exnref)
(try (catch_all $JSexn $on_JSexn)
call $JS
)
...
)
(drop) ;; ignore exnref
;; JS value on top of stack as externref
With that there is no reason to allow converting exnref to externref or anyref, or to pass it to JS. If we don't, then I think an engine choosing to identify JS exn and its argument as mentioned above would become an unobservable optimisation, which seems preferable to hardwiring it into the semantics.
@rossberg I think that could work, but I'll need to think about it more. I'm a bit nervous that long-term the implicit WebAssembly.Exception wrapping of JS values could become accidentally observable in a future extension. But maybe it's fine.
@keithw Okay, that makes sense. In that case we should not have the conversion instructions I listed.
We discussed exposing a special exception tag for JavaScript and allowing importing it (the payload would be an externref that represents the thrown object): https://github.com/WebAssembly/exception-handling/pull/269 I think something like that would still make sense in an exnref world.
+1 on what @rossberg and @dschuff said.
In the current JS API, we have WebAssembly.Exception
in JS side, which is an object containing a tag, thrown values, and possibly other metadata like stack traces. The current Phase 3 JS API spec was hard to write because there was no direct corresponding object for it in the Wasm side, so we had to do some handwaving. When we have exnref
, many things in the current JS API can be simplified.
So the drift of that is, I think exnref
should correspond 1:1 with WebAssembly.Exception
, not the thrown object (without a tag).
To support catching of JS exceptions, #269 proposed a special "JS tag" so that the web engine can recognize it and treat it as a special case. So when wasm code uses try
-catch
with the JS tag, even though a random JS object thrown from JS side doesn't have a "tag" attached to it when thrown, the engine treats it like a WebAssembly.Exception
thrown with a JS tag. AFAIK this has been already implemented in v8:
https://github.com/v8/v8/commit/16f0553dc8cb2747e14bfb66e784b7eee674e146
https://github.com/v8/v8/commit/4e79015dc28659a8a031f06580e902720b35674c
Another reason in favor of having JS values auto-boxed in a WebAssembly.Exception is around null.
In JS it's valid to throw/catch null:
try {
throw null;
} catch (value);
assertEq(value, null);
}
While it also seems desirable to have throw_ref
trap if it receives a ref.null exn
. If JS null is boxed in a WebAssembly.Exception when in WebAssembly, that allows the rethrow semantics you'd expect.
This all may have been discussed before, I'm just catching up :)
Now the #301 has landed I did want to briefly bring back up the question of passing exnrefs into JS via function return, global getters, etc. @rossberg mentioned there and above that we don't want exnref to be convertible to anyref or externref and I agree that makes sense.
But could still allow exnrefs to go to JS via this route if we wrap them in WebAssembly.Exception
at each of the points where they go to JS (perhaps in ToJSValue
) and unwrap them going the other direction. This would not cause any of the aforementioned issues with the type system in wasm, nor would it force non-JS engines to use full GC instead of refcounting; but it would still allow JS users to store an exnref or a JS-wrapped equivalent wherever they wanted.
In fact, it's of course already possible to do this; you just have to throw the exnref out of wasm instead of passing it some other way, and it's exactly equivalent, just awkward. So I don't think the current restriction really buys much compared to just allowing the wrapping and unwrapping in more places.
I think that would still be problematic, because ToWasmValue with target type exnref would then have to convert non-WA.Exception values to exnrefs with the JS tag.
But doing so it wouldn't be able to distinguish a WA.Exception that was originally an exnref (and hence needs to be converted back into one, using the original tag and throw context) from a random object manually created via new WA.Exception
(which would need the JS tag and a payload that is this object as an externref).
So AFAICS, this would still lead to the conflation of different value domains: exnrefs and their payloads. The former represent thrown values, including the context of the throw, not the values themselves.
Unless you somehow want to treat manually created WA.Exception objects as actual exnrefs with some dummy throw context. But I'm not convinced that's desirable or useful.
(Note that this conflation does not occur when merely throwing a manual WA.Exception, because that explicitly turns it into a thrown value, producing an actual throw context.)
I think the issues here have been resolved and the JS API spec now reflects what we discussed here. Let's open specific new issues if there's anything new.
I don't believe this was discussed elsewhere, apologies if I missed it.
Here's a proposal for the semantics of the new exnref value type.
catch
orcatch_all
may receiveToWebAssemblyValue(value, 'exnref')
succeeds for any value (like externref)ToJSValue(value, 'exnref')
succeeds for any value (like externref)extern.convert_exn
instruction for getting an externref from an exnrefany.convert_exn
instruction for getting an anyref from an exnrefnoexn
heap type to mirror the other bottom typesnullexnref
alias for(ref null noexn)
ref.cast exnref/nullexnref
are valid instructions similar, but do very little like in the externref hierarchy.Note, there is no
exn.convert_any
orexn.convert_extern
as we don't want to assume in core wasm thatexnref
can hold any host value. That's true on JS hosts though, which is why the JS-API allows conversions on the boundary.Did I miss anything?
There is a world where we do something more conservative and don't allow exnref in
ToWebAssemblyValue
ToJSValue
or introduce the conversion instructions. But I don't think they are much work, and would be useful.