Skip to content

[PHP.wasm] Support web JSPI side module ABI exports - #3647

Merged
adamziel merged 1 commit into
trunkfrom
adamziel/fix-web-jspi-side-module-longjmp
May 15, 2026
Merged

[PHP.wasm] Support web JSPI side module ABI exports#3647
adamziel merged 1 commit into
trunkfrom
adamziel/fix-web-jspi-side-module-longjmp

Conversation

@adamziel

@adamziel adamziel commented May 15, 2026

Copy link
Copy Markdown
Collaborator

What it does

Makes web JSPI PHP runtimes load external side modules that import wasm longjmp symbols and the PHP/libc symbols used by the published SQLite Database Integration wp_mysql_parser extension.

It also keeps extension ini staging before PHP startup, so externally resolved extensions are available during boot.

Rationale

The wp_mysql_parser module downloaded correctly but crashed or failed to instantiate in the browser runtime. The web JSPI main module did not provide the same dynamic-linking surface as the side module expected: __c_longjmp was synthesized as a JS function instead of a WebAssembly tag, and the parser also imported PHP/libc functions that were not exported by the web 8.3 JSPI build.

This PR also replaces the onRuntimeInitialized Emscripten hook with the preRun hook.

Extension files were already written as .so + generated .ini files before this PR. That worked because PHP startup is lazy: loadPHPRuntime() uses noInitialRun: true, and php_wasm_init() usually runs later on the first PHP request.

The fragile ordering was:

Emscripten creates runtime
  -> preRun
       no external extension files yet
  -> onRuntimeInitialized
       write /internal/shared/extensions/foo.so
       write /internal/shared/extensions/foo.ini
  -> first PHP request
  -> php_wasm_init()
  -> PHP scans php.ini + PHP_INI_SCAN_DIR
  -> PHP loads foo.so from foo.ini

That is only safe while nothing starts PHP before or during onRuntimeInitialized extension staging. If PHP startup happens in that window, PHP scans ini files before foo.ini exists and the extension is missed.

This PR changes the ordering to:

Emscripten creates runtime
  -> preRun
       write /internal/shared/extensions/foo.so
       write /internal/shared/extensions/foo.ini
  -> onRuntimeInitialized
       consumer callbacks can run; extension files already exist
  -> first PHP request
  -> php_wasm_init()
  -> PHP scans php.ini + PHP_INI_SCAN_DIR
  -> PHP loads foo.so from foo.ini

So the fix is not “start using ini files.” The fix is “write the already-generated ini files at the earliest filesystem-ready lifecycle point, before any runtime-initialized code can trigger PHP startup.”

Implementation

The compile Dockerfiles now add the wasm longjmp exports for JSPI builds and patch the generated web loader to provide env.__c_longjmp as a WebAssembly.Tag.

For PHP 8+ JSPI builds, the main module export list now includes the additional wp_mysql_parser imports such as zend_hash_get_current_key_type_ex, zend_hash_get_current_data_ex, zend_hash_move_forward_ex, zend_is_true, clock_gettime, getcwd, and the pthread helpers. The committed PHP 8.3 web JSPI binary was rebuilt to include those exports.

Resolved extension files are staged from preRun, not onRuntimeInitialized, so generated ini files exist before PHP scans them.

The e2e coverage now loads the published wp_mysql_parser manifest and runs WP_MySQL_Native_Lexer over a SQL query, so it catches both load-time and runtime symbol issues.

Testing instructions

Run:

NX_DAEMON=false npx nx test php-wasm-universal --testFile=load-extension.spec.ts
NX_DAEMON=false npx nx typecheck php-wasm-universal
NX_DAEMON=false npx nx lint php-wasm-universal
NX_DAEMON=false npx nx typecheck php-wasm-web
NX_DAEMON=false npx nx lint php-wasm-web
NX_DAEMON=false npx playwright test --config=packages/php-wasm/web/playwright.config.ts -g "loads the published SQLite wp_mysql_parser extension" --workers=1 --project=chromium --reporter=list
git diff --check

I also verified the stacked website PR locally with the SQLite wp_mysql_parser manifest. The WordPress admin notice reported: PHP extension check: wp_mysql_parser extension is loaded; native parser classes available: yes.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds support for loading external JSPI side modules (such as the published SQLite wp_mysql_parser extension) in web PHP runtimes by exposing additional ABI symbols, providing a WebAssembly.Tag for __c_longjmp, and moving extension file staging from onRuntimeInitialized to preRun.

Changes:

  • Patch JSPI web loaders to synthesize env.__c_longjmp as a WebAssembly.Tag and add wasm longjmp / PHP/libc / pthread exports for PHP 8+ JSPI builds.
  • Move resolved extension installation from onRuntimeInitialized to preRun so generated .ini files exist before PHP startup.
  • Add e2e and unit tests covering the new side-module loading path; configure web Playwright to run with JSPI=true.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/php-wasm/compile/php/Dockerfile Add wasm longjmp + PHP 8+ JSPI symbol exports and inject the __c_longjmp Tag patch into the web loader.
packages/php-wasm/compile/php/Dockerfile-5-2 Mirror the wasm longjmp exports and loader patch for the 5.2 build.
packages/php-wasm/web-builds//jspi/php_.js Regenerated loaders containing the __c_longjmp Tag fallback.
packages/php-wasm/web-builds/8-3/jspi/php_8_3.js Rebuilt 8.3.31 binary/loader exporting new wp_mysql_parser ABI symbols.
packages/php-wasm/universal/src/lib/load-extension.ts Stage extension files in preRun instead of onRuntimeInitialized.
packages/php-wasm/universal/src/lib/load-extension.spec.ts Unit test verifying preRun ordering for staged extension files.
packages/php-wasm/web/src/test/php-dynamic-loading.spec.ts E2E test loading wp_mysql_parser manifest and exercising the lexer.
packages/php-wasm/web/playwright.config.ts Run the web dev server with JSPI=true for the e2e tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/php-wasm/web/playwright.config.ts Outdated
Comment thread packages/php-wasm/compile/php/Dockerfile Outdated
Comment thread packages/php-wasm/compile/php/Dockerfile Outdated
Comment thread packages/php-wasm/compile/php/Dockerfile Outdated
Comment thread packages/php-wasm/universal/src/lib/load-extension.spec.ts
Comment thread packages/php-wasm/web/src/test/php-dynamic-loading.spec.ts Outdated
Comment thread packages/php-wasm/compile/php/Dockerfile
@adamziel
adamziel force-pushed the adamziel/fix-web-jspi-side-module-longjmp branch from d85c42d to 8a10166 Compare May 15, 2026 22:06
@adamziel
adamziel merged commit 2d5ece9 into trunk May 15, 2026
53 checks passed
@adamziel
adamziel deleted the adamziel/fix-web-jspi-side-module-longjmp branch May 15, 2026 22:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment