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.
| RUN set -eux; \ | ||
| /root/emsdk/upstream/bin/llvm-nm \ | ||
| --defined-only \ | ||
| --extern-only \ | ||
| --format=just-symbols \ | ||
| /root/lib/libphp.a \ | ||
| | grep -v '^[[:space:]]*$' \ | ||
| | grep -v ':$' \ | ||
| > /root/.PHP_EXTENSION_ABI_EXPORTS; \ | ||
| sed 's/^/_/' /root/.PHP_EXTENSION_ABI_EXPORTS \ | ||
| >> /root/.JS_ABI_EXPORTS; \ | ||
| cat /root/.PHP_EXTENSION_ABI_EXPORTS \ | ||
| >> /root/.WASM_ABI_EXPORTS |
There was a problem hiding this comment.
Addressed in 202bc37. The filtered llvm-nm symbols now pass through LC_ALL=C sort -u before either export list is generated.
| RUN /root/replace.sh \ | ||
| 's/#include "zend_alloc_sizes.h"/#include "zend_alloc_sizes.h"\n#undef HAVE_BUILTIN_CONSTANT_P/' \ | ||
| /usr/local/include/php/Zend/zend_alloc.h && \ | ||
| grep -Fqx '#undef HAVE_BUILTIN_CONSTANT_P' \ | ||
| /usr/local/include/php/Zend/zend_alloc.h |
There was a problem hiding this comment.
Addressed in 202bc37. The Docker layer now checks for the exact #undef first, applies the replacement only when absent, and retains the final verification.
| ROOT_DIR="$(git rev-parse --show-toplevel)" | ||
| WORK_DIR="${ROOT_DIR}/tmp/external-extension-abi" | ||
| PHP_VERSION="${PHP_VERSION:-8.3}" |
There was a problem hiding this comment.
Addressed in 202bc37. Each integration run now uses a unique mktemp -d directory under the repository temp root, with the existing exit trap scoped to that path.
| rm -rf "$WORK_DIR" | ||
| mkdir -p "$WORK_DIR" | ||
| trap 'rm -rf "$WORK_DIR"' EXIT |
There was a problem hiding this comment.
Addressed in 202bc37 by replacing the fixed work directory with a unique mktemp -d path and retaining cleanup through the scoped trap.
|
@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 |
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