Open adamziel opened 1 year ago
JSPI is available in Node.js behind the --experimental-wasm-stack-switching
flag – thank you for sharing @fmgccabe!
Here's some notes I took to find that flag:
node --v8-options
gives all v8-specific CLI options--experimental-wasm-stack-switching
It's available in Node 17, 18, 19, and 20. Node 16 exists with a bad option
error message.
I tried:
-s ASYNCIFY=2
-s ASYNCIFY_EXPORTS="$EXPORTED_FUNCTIONS" \
-s ASYNCIFY_IMPORTS='["wasm_setsockopt","js_popen_to_file","wasm_socket_has_data","wasm_poll_socket","wasm_close","wasm_shutdown"]' \
(not all of these functions are needed but I just wanted to get something working)
And ran
nx build php-wasm-node
nx build wp-now
node --experimental-wasm-stack-switching --loader=./packages/nx-extensions/src/executors/built-script/loader.mjs --stack-trace-limit=50 dist/packages/wp-now/main.js start --path=./plugin --php=7.4
And got this error:
TypeError: Cannot read properties of undefined (reading '0')
at sigToWasmTypes (/playground/dist/packages/php-wasm/node/index.cjs:30939:19)
at /playground/dist/packages/php-wasm/node/index.cjs:30959:26
at Object.instrumentWasmImports (/playground/dist/packages/php-wasm/node/index.cjs:30966:11)
at Object.init4 (/playground/dist/packages/php-wasm/node/index.cjs:31353:12)
at loadPHPRuntime (/playground/dist/packages/php-wasm/node/index.cjs:67103:38)
at doLoad (/playground/dist/packages/php-wasm/node/index.cjs:67997:31)
at processTicksAndRejections (node:internal/process/task_queues:95:5)
at async Function.load (/playground/dist/packages/php-wasm/node/index.cjs:67982:12)
at async _WPNow.setup_fn (file:///playground/dist/packages/wp-now/main.js:350:14)
at async Function.create (file:///playground/dist/packages/wp-now/main.js:204:5)
at async startServer (file:///playground/dist/packages/wp-now/main.js:469:17)
at async Object.handler (file:///playground/dist/packages/wp-now/main.js:569:9)
Turns out instrumentWasmImports
generated by Emscripten could had no exports signatures stored. I did some monkeypatching and added this:
var Asyncify = {
instrumentWasmImports: function(imports) {
// ...
if (isAsyncifyImport) {
if (x === 'js_popen_to_file') {
sig = "iiii";
} else if (x === 'wasm_close') {
sig = "ii";
} else if (x === 'wasm_shutdown') {
sig = "iii";
} else if (x === 'wasm_poll_socket') {
sig = "iiii";
} else if (x === 'wasm_setsockopt') {
sig = "piiipii";
}
And got one step further:
RuntimeError: null function or function signature mismatch
at call (wasm://wasm/02952aa6:wasm-function[5683]:0x3f303a)
at ret.<computed> (playground/dist/packages/php-wasm/node/index.cjs:30981:31)
at Module.dynCall_v (playground/dist/packages/php-wasm/node/index.cjs:31527:75)
at invoke_v (playground/dist/packages/php-wasm/node/index.cjs:31628:7)
at invoke_v (wasm://wasm/02952aa6:wasm-function[371]:0x2bfad)
at php_module_startup (wasm://wasm/02952aa6:wasm-function[5142]:0x390ac6)
at wasm_sapi_module_startup (wasm://wasm/02952aa6:wasm-function[4570]:0x31b10b)
at byn$fpcast-emu$wasm_sapi_module_startup (wasm://wasm/02952aa6:wasm-function[5725]:0x3f3cc9)
at php_wasm_init (wasm://wasm/02952aa6:wasm-function[9870]:0x560402)
at ret.<computed> (playground/dist/packages/php-wasm/node/index.cjs:30981:31)
at Module._php_wasm_init (playground/dist/packages/php-wasm/node/index.cjs:31467:89)
at Object.ccall (playground/dist/packages/php-wasm/node/index.cjs:31062:20)
at _NodePHP.#initWebRuntime (playground/dist/packages/php-wasm/node/index.cjs:67350:32)
at _NodePHP.run (playground/dist/packages/php-wasm/node/index.cjs:67315:27)
at PHPRequestHandler.#dispatchToPHP (playground/dist/packages/php-wasm/node/index.cjs:66884:29)
at async PHPRequestHandler.request (playground/dist/packages/php-wasm/node/index.cjs:66802:12)
at async PHPBrowser.request (playground/dist/packages/php-wasm/node/index.cjs:66596:22)
Seems like an error in a dynamic call, so I added invoke_*
to the list of wrapped imports:
var isAsyncifyImport = ASYNCIFY_IMPORTS.indexOf(x2) >= 0
|| x2.startsWith("__asyncjs__")
|| x2.startsWith("invoke")
;
if(x === 'invoke_viidii') {
return; // I couldn't get this one right
} else if(x.startsWith('invoke_v')) {
sig = x.substr(7) + 'i';
}
But I'm not sure if that was a good step, the error now is:
RuntimeError: invalid suspender object for suspend
at invoke_v (wasm://wasm/02952aa6:wasm-function[371]:0x2bfad)
at php_module_startup (wasm://wasm/02952aa6:wasm-function[5142]:0x390ac6)
at wasm_sapi_module_startup (wasm://wasm/02952aa6:wasm-function[4570]:0x31b10b)
at byn$fpcast-emu$wasm_sapi_module_startup (wasm://wasm/02952aa6:wasm-function[5725]:0x3f3cc9)
at php_wasm_init (wasm://wasm/02952aa6:wasm-function[9870]:0x560402)
...
I am running on latest emsdk as of today, which wraps the imports in:
imports[x2] = original = new WebAssembly.Function(type, original, {
suspending: "first"
});
And the exports in:
return new WebAssembly.Function({
parameters,
results: ["externref"]
}, original, {
promising: "first"
});
I'm stuck at this point and the JSPI API is still experimental – let's revisit this once the API is stable.
@adamziel is there a short version of what ASYNCIFY_ONLY
does? is it a macro that wraps those functions or rewrites them into some continuable state machine?
@dmsnell pretty much yes – it rewrites these functions as a continuable state machine.
When the async call is made, it saves the call stack and sets a global flag to make all the functions on that call stack short-circuit. Then it synchronously returns.
When the async call is finished, it restores the call stack, sets other global flags, and continues right after the async call.
Note that we need to use ASYNCIFY_ONLY
now when PHP is built with ASYNCIFY=1
. This issue explores switching to a new experimental API called JSPI
that is activated via ASYNCIFY=2
. Thia naming is confusing as these are two completely different APIs. JSPI is smart about the call stack and only requires us to list asynchronous C-level imports and exports.
JSPI seems to be now available via Origin Trials. I wonder if Playground should ship JSPI code for Chrome and Asyncify code for other browsers. It would be a maintenance burden for a while, but it would get a lot more stable in Chrome. CC @bgrgicak
Most visitors use Chrome, so it would be beneficial to do it. Personally I would prefer not to support two versions, but if we can do it in a clean way it should be ok.
The issue of different browsers is likely to be a temporary phenomenon. E.g., Mozilla is already working on their implementation of JSPI in Firefox.
Let's explore switching to JSPI for the Node.js version of Playground as it would solve a lot of the "null function or function signature mismatch" issues. A few questions to answer:
cc @brandonpayton – would you look into that next?
Also CC @bgrgicak – let's hold on with fixing these one-off "null function or function signature mismatch" errors until we can confirm or reject the JSPI usage in the short term. They take a lot of time to fix and that time is not a good long-term investment considering the new API will solve it all in one go.
Also CC @mho22 as that's relevant to your libcurl explorations.
A couple of comments:
I hope that this helps in understanding the current status of JSPI. Note that, given Node.js tracks V8, and that eventually JSPI will be shipping in V8, it seems that JSPI is actually coming to Node.js.
Webassembly indicates that JSPI
can be enabled in Node with the flag --experimental-wasm-jspi
. And it seems like Node has these options written in two V8 tests. The latest V8 version tag on node is 12.3.22
and it has been updated yesterday.
The last [wasm][jspi]
commit made in V8 was added yesterday too on tag 12.6.55
. I suppose it is just a matter of time before node
updates v8
to 12.6.55
patch and using node
with option --experimental-wasm-jspi
. Still not available on node@21.7.3
...
Resource about feature flags – sometimes you use Chrome flags, Node flags, V8 flags – I'm confused which is which, but maybe one of those listed there would work https://webassembly.org/features/
I am planning to look at this next, starting with testing a simple C program with no Playground involved.
From @adamziel:
I got frustrated with an Asyncify error and spent some time exploring this again and writing up some notes. The last time I failed, I couldn't make it past the TypeError: undefined is not a constructor ('WebAssembly.Function')
. This time, I got JSPI to actually run:
Install Emscripten
Create a simple jspi-experiment.c
program:
#include <emscripten.h>
#include <stdlib.h>
#include <stdio.h>
unsigned int async_call_sleep(unsigned int time)
{
emscripten_sleep(time * 1000);
return time;
}
int main()
{
printf("Hello, World (JSPI!)\n");
async_call_sleep(3);
printf("Goodbye, World (JSPI!)\n");
return 0;
}
Create a build.sh
file with the following content:
#!/bin/bash
emcc -O0 -g2 \
-sENVIRONMENT=$1 \
-sASYNCIFY=2 \
-sEXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' \
-sEXPORTED_FUNCTIONS='["_main"]' \
-o jspi-experiment.$2 \
jspi-experiment.c
bash build.sh web html
#enable-experimental-webassembly-jspi
at chrome://flags
.python3 -m http.server
http://localhost:8000/jspi-experiment.html
You should see
Hello, World (JSPI!)
Goodbye, World (JSPI!)
The feature flag isn't a show-stopper. JSPI can be enabled for the entire playground.wordpress.net site via origin trials (https://v8.dev/blog/jspi-ot).
bash build.sh node js
nvm use 22
to switch to Node 22node --experimental-wasm-jspi jspi-experiment.js
You should see
Hello, World (JSPI!)
Goodbye, World (JSPI!)
JSPI is only available in Node.js, not Bun, and only in v22
You can see a list of all V8 options Node supports with node --v8-options
, JSPI is only available in Node 22:
; nvm use 21
; node --v8-options | grep jspi
; nvm use 22
; node --v8-options | grep jspi
--experimental-wasm-jspi (enable javascript promise integration for Wasm (experimental))
type: bool default: --no-experimental-wasm-jspi
Also, it doesn't work with Bun yet:
1269 | type.parameters.unshift('externref');
1270 | imports[x] = original = new WebAssembly.Function(
^
TypeError: undefined is not a constructor (evaluating 'new WebAssembly.Function(type, original, { suspending: "first" })')
@adamziel if we implement JSPI support, would we have access to the kinds of calls that were being made from PHP -> JS and possibly be able to log those if they aren't in our current Asyncify list?
If so, maybe real world usage of JSPI could help us update the Asyncify lists and avoid crashes in the non-JSPI builds.
You sir are a genius @brandonpayton
https://github.com/WordPress/wordpress-playground/issues/134 works in Node.js v22 and Chrome – both require a feature flag or applying to an origin trial.
I don't expect stable JSPI support in all major runtimes (Chrome, Firefox, Safari, mobile browsers, last 3 Node versions) for the next year, two, or even three. I'm happy to be wrong here, but I didn't see any proof of imminent rollout.
Here's what we could do:
This should get us:
Firefox just shipped JSPI support. I'm not sure how extensive it is, but I just managed to run a simple JSPI program in Firefox nightly. I haven't tested PHP.wasm.
Description
We use Asyncify to call asynchronous JavaScript functions from synchronous WebAssembly code.
However, Asyncify forces us to maintain a large list of all the C functions that can be at the call stack at the time of making an asynchronous call must be listed during the build. When we miss even one, that code path triggers a fatal crash (
"unreachable" WASM instruction executed
). People report those crashes rather often.JSPI (proposal) is the new API that doesn't require maintaining that list and produces a smaller and faster binary.
Let's explore migrating to JSPI to solve these issues and more:
Next steps
We have a PR adding JSPI Support. It works in Node.js v22 and Chrome – both require a feature flag or applying to an origin trial.
I don't expect stable JSPI support in all major runtimes (Chrome, Firefox, Safari, mobile browsers, last 3 Node versions) for the next year, two, or even three. I'm happy to be wrong here, but I didn't see any proof of imminent rollout.
Here's what we could do:
This should get us:
Runtime support as of April 29th, 2024
JSPI is supported on:
#enable-experimental-webassembly-jspi
enabled atchrome://flags
, or with sites where the JSPI origin trial is enabled.--experimental-wasm-stack-switching
feature flag.