JavaScript Promise Integration (JSPI) is a Phase 4 (Standardize the Feature) WebAssembly proposal for running asynchronous Web APIs alongside synchronous code.
JSPI is supported under the following browsers/engines through enabling a feature flag:
Chrome: chrome://flags/#enable-experimental-webassembly-jspi
Firefox: javascript.options.wasm_js_promise_integration
Node.js: --experimental-wasm-jspi
Deno: --v8-flags=--experimental-wasm-jspi
For a quick demo, let's write some async code in C with Emscripten.
Run this command in Docker to get an Emscripten shell:
docker run \
--interactive \
--rm \
--tty \
--volume "./:/src/" \
--workdir /src/ \
emscripten/emsdk \
/bin/bash
Consider the following code:
// main.c
#include <stdio.h>
#include <emscripten.h>
EM_ASYNC_JS(void, readFile, (const char *path), {
const fsPromises = await import('fs/promises');
const data = await fsPromises.readFile(UTF8ToString(path));
console.log(`File contents: ${data}`);
});
int EMSCRIPTEN_KEEPALIVE main() {
printf("Hello, WebAssembly!\n");
readFile("./password.txt");
printf("File read request sent.\n");
return 0;
}
Compile it:
emcc \
-sENVIRONMENT=node \
-sMODULARIZE=1 \
-sJSPI=1 \
-o ./output/module.js \
./main.c
and run it with node --experimental-wasm-jspi index.js
:
// index.js
import module from "./output/module.js";
const myModule = await module();
Hello, WebAssembly!
File contents: Hunter2
File read request sent.
This code isn't particularly useful though, we could already do this without C. Suppose instead of just printing to the console, we actually want it to set a buffer?
// main.c
#include <stdio.h>
#include <emscripten.h>
EM_ASYNC_JS(void, readFile, (const char *path, void *buffer), {
// If you don't believe this is async, uncomment the next line
// await new Promise((resolve) => setTimeout(resolve, 5000));
const fsPromises = await import('fs/promises');
const data = await fsPromises.readFile(UTF8ToString(path));
const bufferToFill = _malloc(data.length * data.BYTES_PER_ELEMENT);
HEAPU8.set(data, bufferToFill);
setValue(buffer, bufferToFill, "*");
});
int EMSCRIPTEN_KEEPALIVE main() {
void *buffer = NULL;
printf("Hello, WebAssembly!\n");
readFile("./password.txt", &buffer);
printf("File buffer pointer: %p\n", buffer);
char* currChar = (char *)buffer;
while (*currChar != '\0') {
printf("%c", *currChar);
currChar++;
}
printf("*currChar is NULL: %d\n", *currChar == '\0');
free(buffer);
return 0;
}
And run the previous compile command with -sEXPORTED_FUNCTIONS='["_malloc", "_main"]'
added:
Hello, WebAssembly!
File buffer pointer: 0x11488
Hunter2
*currChar is NULL: 1
Now, this specific file is pretty dumb but the general gist of it is: you can run asynchronous JavaScript code in synchoronous C code. This is a big gamechanger for bringing in common async use cases (fetching data, filesystem etc.) with C libraries.
One MAJOR caveat is: any functions that you can expect to run any async function (in this case, main()
and readFile
) must be added to sJSPI_EXPORTS=
if it isn't obvious (e.g. in this case, since we used the EM_ASYNC_JS
macro, it isn't necessary). Otherwise, you will get the error RuntimeError: attempting to suspend without a WebAssembly.promising export
(add -g2
for the error stack to have reasonable function names, and add all the function names up to that async function).
The reason this is I imagine is that unlike Asyncify, Emscripten can't automatically instrument function as async or not. I couldn't find any issues on this, and I've had trouble replicating it in a more easily reproducible form.