Fix external PHP extension ABI exports - #4108
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR fixes the external PHP extension ABI surface for PHP.wasm by deriving exports from the matching libphp.a, normalizing phpize headers to ensure stable allocator symbols, and adding a Docker-backed integration test that compiles a real external extension and verifies its imports resolve.
Changes:
- Generate retained export lists from
libphp.a(in addition to bundled extensions) to preserve the public extension ABI while keepingMAIN_MODULE=2. - Patch installed PHP headers in the extension build image to avoid build-specific
_emalloc_<size>symbol references. - Add a Docker-backed fixture and Nx target to compile and validate an external side module against the matching runtime exports.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/php-wasm/compile/php/Dockerfile | Derives ABI export lists from libphp.a via llvm-nm to retain extension ABI under MAIN_MODULE=2. |
| packages/php-wasm/compile-extension/tests/test-external-extension-abi.sh | Adds an integration script that builds a runtime + external module and checks unresolved imports. |
| packages/php-wasm/compile-extension/tests/fixtures/external-abi/external_abi.c | Adds a real external extension that exercises key API/allocator symbols. |
| packages/php-wasm/compile-extension/tests/fixtures/external-abi/config.m4 | Adds phpize config for the external ABI fixture. |
| packages/php-wasm/compile-extension/project.json | Adds an Nx target to run the external ABI integration test. |
| packages/php-wasm/compile-extension/docker/Dockerfile.ext | Patches installed PHP headers to disable constant-size emalloc() specialization. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@mho22 Review follow-up is ready in |
|
Downstream full-stack validation completed against this PR plus #4146 using https://github.com/chubes4/wordpress-playground/tree/proof/bet12-clean-worker-abi. The combined build loaded the external Sodium extension and ran clean PHP workers against managed MariaDB. Real focused WPCOM PHPUnit execution passed 25 CHATGPT-19 media tests across the MCP media-create integration and image/audio upload transport suites. Downstream PR: Automattic/wp-codebox#1938 |
|
Additional downstream proof: Automattic/static-site-importer#656 now publishes a real PHP 8.5 JSPI zstd side module with valid CORS, and deployed Playground crashes at startup with Manifest: https://automattic.github.io/static-site-importer/playground/extensions/v1.3.5/static-site-importer-zstd-php8.5-jspi.manifest.json This validates that the PR fixes an active external consumer, not only the synthetic/Sodium fixture. After merge and runtime deployment, SSI will rerun |
brandonpayton
left a comment
There was a problem hiding this comment.
Hi @chubes4, thank you for this PR. These changes make sense to me, and I really like the expanded test coverage.
I left a few questions and a lot of agreeable comments. This looks good to merge, but I'd like to resolve the questions first.
| git fetch --no-tags --depth=1 origin "$base_sha" | ||
|
|
||
| if git diff --name-only "$base_sha" "$GITHUB_SHA" -- packages/php-wasm/compile-extension/ | grep -q .; then | ||
| if git diff --name-only "$base_sha" "$GITHUB_SHA" -- packages/php-wasm/compile-extension/ packages/php-wasm/compile/php/ | grep -q .; then |
| # _emalloc_<size>() symbols. External side modules must use the stable | ||
| # _emalloc() entry point exported by every matching PHP.wasm main module. |
There was a problem hiding this comment.
Why must they? It would be good for this comment to say why.
There was a problem hiding this comment.
This one is a bit of a mind-bender, but my practical understanding: This normalizes memory allocations onto Zend’s stable entry point, ensuring Playground remains compatible with external extensions built independently from its PHP.wasm runtimes.
a436300 to
e549261
Compare
AI assistance: OpenAI gpt-5.6-sol via OpenCode was used to clarify why external extensions avoid build-specific allocator symbols.
|
Feedback from Codex, which @brandonpayton asked me to post for @chubes4: After filtering my earlier review through the actual scope of #4107 and comparing this PR with current This PR is a meaningful, bounded improvement over trunk. It addresses the original PHP/Zend-symbol failure by retaining the externally visible symbols from the matching I no longer think this PR should be blocked on providing every possible libc or Emscripten system symbol that an arbitrary future side module might import. That is not realistic within the current My revised findings are:
The PR description should also be brought in line with the implementation: it currently says PHP 8.3 while the test uses PHP 8.5, mentions an The longer-term general solution should be a documented PHP-owned ABI plus required-import metadata/preflight validation. That would satisfy #4107's alternative acceptance criterion—load successfully or fail early with a precise unsupported-import diagnostic—without requiring PHP.wasm to contain every possible libc function. With that framing, my revised verdict is: mergeable as a valuable incremental improvement once its scope and runtime-artifact delivery plan are stated accurately; universal libc completeness should not block it. |
Retain the libc symbol required by the published zstd extension and exercise it in the external ABI fixture. AI assistance: OpenAI gpt-5.6-sol via OpenCode identified the missing browser-runtime export and added focused fixture coverage; Chris Huber remains responsible for the change.
|
@brandonpayton The targeted follow-up is complete at
This provides the targeted AI assistance: OpenAI GPT-5.6 Sol via OpenCode implemented the targeted ABI follow-up, ran the browser/runtime/WPCOM validation, and prepared this evidence with Chris Huber. |
Summary
libphp.awhile retainingMAIN_MODULE=2_emallocinstead of build-specific_emalloc_<size>symbolsenvandGOT.*import resolves against the matching runtime or moduleCloses #4107.
Why
The runtime previously generated its retained export list only from bundled extensions.
@php-wasm/compile-extensioncould therefore produce a valid external side module that imported public PHP APIs removed from the matching main module by dead-code elimination. Startup then failed late withTypeError: resolved is not a function.The extension compiler also duplicated PHP patch-version constants. That map drifted to PHP 8.4.20 while the runtime used canonical PHP 8.4.23, undermining matched-header ABI proof. Both paths now consume
supported-php-versions.mjs; the published bundle embeds that canonical data and does not gain a workspace-relative runtime dependency.This keeps the
MAIN_MODULE=2optimization while making the generic public PHP extension ABI available to externally compiled modules. Archive member labels emitted byllvm-nmare filtered before generating Emscripten and Wasm linker export lists.How to test
npm ci.npm exec nx -- run php-wasm-compile-extension:lint.npm exec nx -- run php-wasm-compile-extension:test.npm exec nx -- run php-wasm-compile-extension:typecheck.npm exec nx -- run php-wasm-compile-extension:test-external-extension-abi.Verification
ABI_TEST_EXIT=0.trunkto 23,251,406 bytes on this branch: +412,172 bytes (+1.80%).Compatibility
The supported PHP minor-version list and public package API are unchanged. Extension builds now follow the canonical runtime patch release instead of a stale duplicate, so rebuilding may produce a side module against newer patch-level headers. Published package bundles embed the canonical version data; no extension path or workspace-relative runtime dependency is introduced.
AI assistance
AI assistance: OpenAI gpt-5.6-sol via OpenCode was used to review and implement the follow-up fixes and tests.