Lumaktaw patungo sa pangunahing content

Asyncify and JSPI: Stack Switching in PHP WebAssembly

Asyncify lets synchronous C or C++ code interact with asynchronous JavaScript. Technically, it saves the entire C call stack before yielding control back to JavaScript, and then restores it when the asynchronous call is finished. This is called stack switching.

Networking support in the WebAssembly PHP build is implemented using Asyncify. When PHP makes a network request, it yields control back to JavaScript, which makes the request, and then resumes PHP when the response is ready. It works well enough that PHP build can request web APIs, install composer packages, and even connect to a MySQL server.

Asyncify crashes

Stack switching requires wrapping all C functions that may be found at a call stack at a time of making an asynchronous call. Blanket-wrapping of every single C function adds a significant overhead, which is why we maintain a list of specific function names:

https://github.com/WordPress/wordpress-playground/blob/15a660940ee9b4a332965ba2a987f6fda0c159b1/packages/php-wasm/compile/Dockerfile#L624-L632

Unfortunately, missing even a single item from that list results in a WebAssembly crash whenever that function is a part of the call stack when an asynchronous call is made. It looks like this:

A screenshot of an asyncify error in the terminal

Asyncify can auto-list all the required C functions when built without ASYNCIFY_ONLY, but that auto-detection is overeager and ends up listing about 70,000 C functions which increases the startup time to 4.5s. That's why we maintain the list manually.

If you are interested in more details, see GitHub issue 251.

Fixing Asyncify crashes

Pull Request 253 adds a fix-asyncify command that runs a specialized test suite and automatically adds any identified missing C functions to the ASYNCIFY_ONLY list.

If you run into a crash like the one above, you can fix it by:

  1. Identifying a PHP code path that triggers the crash – the stack trace in the terminal should help with that.
  2. Adding a test case that triggers a crash to packages/php-wasm/node/src/test/php-asyncify.spec.ts
  3. Running: npm run fix-asyncify
  4. Committing the test case, the updated Dockerfile, and the rebuilt PHP.wasm

JSPI: The Modern Alternative to Asyncify

The JavaScript Promise Integration (JSPI) API handles stack switching natively in V8, eliminating the need for Asyncify's function wrapping. WordPress Playground now ships JSPI builds alongside Asyncify builds for all PHP versions (7.4–8.5).

Current status:

  • The Playground CLI auto-detects JSPI support and enables it automatically — no manual flags needed
  • Node.js 23+ supports JSPI natively; Node.js 22 requires the --experimental-wasm-jspi flag (handled automatically by the CLI)
  • Node.js 24+ is expected to have JSPI unflagged
  • Browser support varies: JSPI is available in Chrome/Chromium-based browsers behind flags

Binary Size Optimization with MAIN_MODULE=2

Both Asyncify and JSPI builds are compiled with Emscripten's MAIN_MODULE=2 flag, which performs dead code elimination on exported symbols. Only symbols that dynamic extensions actually need are exported.

Impact:

  • Total binary size reduced by 122 MB (13.7%)
  • .wasm files reduced by 109 MB (16%)
  • JavaScript glue code reduced by 14.5 MB (63%)

This optimization applies across all PHP versions (7.4–8.5) for both Node.js and Web targets. The exported symbol list is centrally managed in the Dockerfile, with conditional exports for specific extensions (e.g., __c_longjmp for Xdebug, _wasm_recv for Memcached).