Using the WASM JSPI in Rust
I was working with Rust and WebAssembly, and was using WebAssembly as a means to coordinate function calls. That's pretty easy with synchronous function calls, but if you want to call to asynchronous functions, you have to use the experimental JSPI (JavaScript Promise Integration).
Here's a quick (happy path) example of how you pass an asynchronous JavaScript function into a WebAssembly module, and then have the module coordinate those asynchronous calls. Let's assume the module has an export function named sequence_calls
that just calls the async function and adds up the numbers returned.
const importObject = {
js: {
choose_piece: new WebAssembly.Suspending(() => {
return new Promise(function(resolve, reject) {
window.setTimeout(function() {
console.log('ran timeout after 4000ms');
resolve(1); // return a number
}, 4000)
})
})
}
};
WebAssembly.instantiateStreaming(
fetch("quarto.wasm"),
importObject).then((results) => {
const exports = results.instance.exports;
const sequence_calls = WebAssembly
.promising(exports.sequence_calls);
sequence_calls()
.then(result => {
console.log("result is " + result)
});
}
);
First you wrap the function you're importing into wasm (the choose_piece function) with WebAssembly.Suspending
. Make sure the function returns a Promise.
Second, you need to wrap the export function that calls the async function with WebAssembly.promising
which turns the synchronous WebAssembly export into a asynchronous function.
Here's my implementation of sequence_calls
in Rust and compiled with cargo build --lib --target wasm32-unknown-unknown
.
// lib.rs
mod js {
#[link(wasm_import_module = "js")]
unsafe extern "C" {
pub fn choose_piece() -> i32;
}
}
#[unsafe(no_mangle)]
pub extern "C" fn sequence_calls() -> i32 {
unsafe {
let a = js::choose_piece();
let b = js::choose_piece();
let c = js::choose_piece();
a + b + c
}
}
Finally, you can call the function. The example above outputs...
ran timeout after 4000ms
ran timeout after 4000ms
ran timeout after 4000ms
result is 3
Pretty cool if you're dealing with promises that won't ever fail - in my case I'm running animations and want to wait for the animation to complete before I call the next function.
Now you might notice that the signature of choose_piece
returns an i32 - which is not entirely true, but I don't think it's wrong. The import is returning a Promise
, but practically speaking, there's no way for the Rust/WASM to handle an error. If I'm understanding the proposal correctly, the main benefit of JSPI is so WASM can treat async calls as though they were synchronous, and if an error occurs, it becomes a thrown exception.
"...a WebAssembly module can import a Promise returning JavaScript function so that the WebAssembly code can treat a call to it as a synchronous call."
And if the Promise is rejected, you can't handle that in WASM, you have to handle that in the JavaScript call...
"If the Promise is rejected, then instead of resuming the WebAssembly module with the value, an exception will be propagated into the suspended computation.
Having said that, there is discussion in wasm-bindgen
to possibly add a new type indicating the possibility of error https://github.com/wasm-bindgen/wasm-bindgen/issues/3633. Please comment below if you have some additional insight into this or I'm missing something.
Here's some other interesting reading from the v8 blog: Introducing the WebAssembly JavaScript Promise Integration API
There does appear to be a way to specify types of errors thrown in WASM and allow JS to catch and differentiate errors: