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

Support the WebAssembly javascript-promise integration proposal #3633

Open
Aaron1011 opened this issue Sep 23, 2023 · 5 comments
Open

Support the WebAssembly javascript-promise integration proposal #3633

Aaron1011 opened this issue Sep 23, 2023 · 5 comments

Comments

@Aaron1011
Copy link
Contributor

Motivation

The javascript-promise integration proposal (see also https://v8.dev/blog/jspi) allows automatically suspending and resuming WebAssembly modules when they invoke a properly-annotated async Javascript function (which returns a Promise). From the perspective of WebAssembly, this behaves like a synchronous call. This proposal is currently in Phase 3 (Implementation): https://github.com/WebAssembly/proposals#phase-3---implementation-phase-cg--wg

In the case of Rust, this means the ability to invoke an async Javascript function without needing the Rust function to be async. If the call occurs deep within a Rust library, this would otherwise require making every single caller async (since blocking/looping on a Future is only possible on native application, not wasm).

Proposed Solution

Importing

The #[wasm_bindgen] macro will support a new jspi attribute (name bikesheddable) when used on JavaScript imports:

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(jspi)]
    fn js_async_function() -> Result<i32, JsValue>;
}

fn invoke_it() {
    let res: Result<i32, JsValue> = js_async_function(); // wasm will suspend execution for us
}

This would bind to a JavaScript function:

async function js_async_function() {
    return 42;
}

When wasm-bindgen imports js_async_function, it will use the new WebAssembly.Function API described in the proposal to mark the function as suspending. As a result, the

Exporting

When any functions are imported with the jspi attribute, wasm-bindgen will add an additional suspender parameter to all exported functions. This is necessary to ensure that a suspender object is always available whenever an imported jspi function is invoked. Rust functions will be exported using the new WebAssembly.Function api described in the proposal to mark the function as 'promising'. Internally, wasm-bindgen will need to store the suspender in a global variable, which will be read and used as an argument whenever an imported jspi function is called.

Because of the potential overhead from marking all exported functions as promising, this should require an explicit opt in from a --jspi command-line flag.

Alternatives

  • Do nothing. While Rust's native async support can replace this feature in some places, it requires 'infecting' the entire call stack with async. This feature would allow calling async JavaScript functions from a synchronous context, without needing invasive changes to the entire program (or breaking changes for consumers, in the case of a library).

Additional Context

@daxpedda
Copy link
Collaborator

I don't think this needs an opt-in command-line flag with your proposal, because it's already explicitly opt-in by marking the function with jspi. Though this requires some experimental label somewhere, e.g. experimental_jspi in the attribute name maybe.

PR's welcome!

@Liamolucko
Copy link
Collaborator

Rust functions will be exported using the new WebAssembly.Function api described in the proposal to mark the function as 'promising'.

This part is a bit concerning to me - I've only skimmed the proposal, but I think making functions as promising causes them to return a Promise. So this would mean that every exported function would be forced to become async.

At the very least, this means that a flag is definitely required. It'll also break certain uses of Closures: any closures that need to return something will break as a result of turning on this flag, since they'll return promises instead of whatever value they're supposed to be returning.

@daxpedda
Copy link
Collaborator

Thinking about this a bit more, adding a flag would be good to get a proper compile-time error if users don't support JSPI in their use-case but JSPI is used somewhere.

So this would mean that every exported function would be forced to become async.

Why would we need to make every exported function promising?

@Liamolucko
Copy link
Collaborator

Why would we need to make every exported function promising?

We don't have to, but that's what @Aaron1011 is proposing. The alternative would be to also specify #[wasm_bindgen(jspi)] on exports that you want to make promising; then wasm-bindgen would have to panic if you try to call a suspending function from a non-promising function, since there'd be no Suspender available to call it with.

After thinking about this a bit more, it's harder to implement than I first thought, since there are allowed to be multiple Rust calls suspended at once. So, say that you call f1, it gets suspended, and while it's suspended, you call f2, which also gets suspended. Then if f1 gets resumed, there'll be an issue: the stack now looks like this:

|f1 stack frame 1 | frame 2 | ... | frame n | f2 stack frame 1 | ... | frame m |
                                                                 stack pointer ^

Since the stack pointer's now pointing at f2's last stack frame, it's going to try and read that and get garbage data. So, we'll have to allocate a separate stack for every promising function that gets called, similar to what we do for multithreading, and deal with saving/restoring the stack pointer when suspending functions get called.

@daxpedda
Copy link
Collaborator

Why would we need to make every exported function promising?

We don't have to, but that's what @Aaron1011 is proposing. The alternative would be to also specify #[wasm_bindgen(jspi)] on exports that you want to make promising; then wasm-bindgen would have to panic if you try to call a suspending function from a non-promising function, since there'd be no Suspender available to call it with.

Right. I would like to know exactly what Emscripten did here, because looking through it, it seems they have done something to allowing not every function to require suspending. @Aaron1011 if you are interested in implementing this looking into how Emscripten does it would be the first step imo.

So, we'll have to allocate a separate stack for every promising function that gets called, similar to what we do for multithreading, and deal with saving/restoring the stack pointer when suspending functions get called.

Indeed that is what Emscripten does: https://v8.dev/blog/jspi#limitations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants