Skip to content
This repository was archived by the owner on Apr 25, 2025. It is now read-only.
This repository was archived by the owner on Apr 25, 2025. It is now read-only.

[js-api] Custom conversions between exceptions in wasm versus in JS #190

Closed
@RossTate

Description

@RossTate

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 Kotlin Throwable, just make it the payload of the exception, and otherwise wrap the JS value as an externref field of some (internal) JSException subclass of Throwable. Similarly, Kotlin will want Kotlin exceptions that escape to JS to be converted into JS exceptions as follows: if the value is a JSException, extract and throw its externref, and otherwise throw the ref $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's defer, and Java's finally. With custom conversions, wasm generators can assume all exceptions have a specific tag, and they can explicitly propagate debugging information such as stack traces (as externref 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 the externref that was thrown and throws the corresponding wasm exception; if the function returns (void), then the externref 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 funcrefs from wasm functions even if the functions are not explicitly exported, and these funcrefs 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 distinguished start 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 wasm funcrefs with just a type signature. So they'd require extending that proposal to create a funcref with an exception handler (that would typically be exported from the wasm instance the funcrefs eventually get propagated with). To potentially make matters worse, the purpose of WebAssembly/design#1408 is to let tooling bypass js-types and just use table.set and the like, but the funcrefs 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 adding throws $__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 the throws 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 that throws $exn uses that conversion function. (If the throws clause specifies multiple exceptions, you try their conversion functions in the sequence listed.)

This works for funcrefs created by WebAssembly/design#1408. It also works for hooking up two wasm instances (via the JS API or ESM): if the imported function throws tags not accepted by the expected function signature, they are converted to externref using their associated conversion-to-JS functions, and that externref is then converted to the expected exception tags using their associated conversion-from-JS functions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions