Repository navigation
[PHP] Recompile PHP.wasm Node JSPI with MAIN_MODULE set to 2 - #3244
Conversation
03ecdb4 to
0dfdff9
Compare
commented
Feb 9, 2026
|
I think what caused |
|
Xdebug globals list includes |
commented
Feb 9, 2026
|
The following issue occurred in JSPI [ due to By printing the symbols from Xdebug I found this line : I added this simple workaround to see what would happen : Which added a new WebAssembly.Tag inside the wasmImports object, and it ran successfully. I will find a better way to add programmatically these necessary tags inside the wasmImports object later. Maybe some other extensions will need to instantiate tags in env too. |
1730ee0 to
49224b7
Compare
commented
Feb 9, 2026
|
🎉 |
22405e6 to
0b69c67
Compare
commented
Feb 10, 2026
|
🎉 🎉 🎉 🎉 🎉 🎉 🎉 |
|
It looks like this pull request is ready for review. Here’s a summary of how I implemented this pull request : Adding And that is what happened first, when running the Actually, Emscripten's documentation says this about So I needed to keep the While running a script to list all the functions of xdebug : import fs from "fs";
const wasm = fs.readFileSync("packages/php-wasm/node-builds/8-4/jspi/extensions/xdebug/xdebug.so");
const module = new WebAssembly.Module(wasm);
// const imports = WebAssembly.Module.imports(module);
const imports = WebAssembly.Module.imports(module).filter(element => element.kind === "global");
fs.writeFileSync('imports-xdebug.json', JSON.stringify(imports, null, 2));I found this : {
"module": "env",
"name": "__stack_pointer",
"kind": "global"
},
Which could mean, every global imports should be kept alive by PHP.wasm... But how? My first attempt was to add the The compilation was successful, but this added the entire function lists to the PHP.js file. Which is not the optimization we needed with My second attempt was to add It also compiled but running Which in the functions list matches this : I had to create a list of required With a lot of back and forth, it turned out two lists were necessary :
Globals are the ones that are in "env" with "func" and "globals" and are NOT "invokes" I added two new steps in my xdebug Dockerfile that will print these symbols inside two dedicated files inside the dedicated xdebug extension directory. Generating this :
The main problem happened here : what about "wasm_recv".
Because of dead code elimination, if a method is not used in the main module, it is not listed in the After lots of attempts, the only way to inject Then : Something quite interesting happened : VS in trunk Nice. I then focused on Intl. And it was another level here.
Pretty much the same except some symbols I had to add manually, + __ZNSt3__211__call_onceERVmPvPFvS2_E
+ __ZNSt3__218condition_variable10notify_allEv
+ __ZNSt3__25mutex4lockEv
+ __ZNSt3__25mutex6unlockEvThis seems to be related to Intl and ICU using libc++ symbols and some of these symbols need to be executed by PHP. These four elements made the tests pass. Xdebug and Redis needed to be a little bit improved by forcing them to use functions from the main module instead of new ones. So :
On the other hand, as Memcached used The only way I found was to create two wrappers in a shims file #include <sys/types.h>
#include <sys/socket.h>
extern ssize_t wasm_recv(int, void*, size_t, int);
extern int wasm_setsockopt(int, int, int, const void*, socklen_t);
ssize_t recv(int s, void* b, size_t l, int f) {
return wasm_recv(s, b, l, f);
}
int setsockopt(int s, int lvl, int opt, const void* v, socklen_t n) {
return wasm_setsockopt(s, lvl, opt, v, n);
}And inject it inside Now that I got these symbol files from every side modules, I needed to find a way to inject them in PHP.wasm for compilation. I focused on the PHP Dockerfile and created two new temporary files :
After that, the different PHP versions compiled successfully. Something important to mention : every symbol list files are necessary, as they are different from one version to another. It is still a bit messy I admit. After tha, all the PHP versions compiled and the tests passed successfully 🎉 N.B.: I also made some minor changes :
|
commented
Feb 25, 2026
|
@brandonpayton After the multi-worker rebase. Some things broke :
So I first added - $PHPWASM__deps: ['$allocateUTF8OnStack'],
+ $PHPWASM__deps: ['$allocateUTF8OnStack', '$addOnInit'],And injected the other ones in - wasmImports,
+ wasmImports: Object.assign({}, wasmImports,
+ typeof _builtin_fd_close === 'function' ? { builtin_fd_close: _builtin_fd_close } : {},
+ typeof _builtin_fcntl64 === 'function' ? { builtin_fcntl64: _builtin_fcntl64 } : {}
+ ),Without that, the dead code elimination would eliminate these functions. I also :
So, the main logic of this pull request resides in these lines : # List global and exports symbols
COPY ./compile/php/exported-functions.list /root/exported-functions.list
RUN cat /root/exported-functions.list >> /root/.JS_ABI_EXPORTS
COPY ./${EMSCRIPTEN_ENVIRONMENT}-builds/ /root/${EMSCRIPTEN_ENVIRONMENT}-builds
COPY ./compile/shared/ /root/shared/
RUN if [ "$WITH_JSPI" = "yes" ]; then \
set -eux; \
PHP_VERSION_DIR="$(echo "$PHP_VERSION" | cut -d. -f1,2 | tr . -)"; \
EXTENSIONS_DIR_PATH="/root/${EMSCRIPTEN_ENVIRONMENT}-builds/${PHP_VERSION_DIR}/jspi/extensions"; \
for path in "$EXTENSIONS_DIR_PATH"/*; do \
extension="${path##*/}"; \
# List globals symbols [ env func and globals without invoke_ and libc++(_Z) prefixed ones ]
/root/emsdk/upstream/bin/wasm-dis "$EXTENSIONS_DIR_PATH/${extension}/${extension}.so" \
| sed -n 's/^[[:space:]]*(import "env" "\([^"]*\)"[[:space:]]*(\(func\|global\).*/\1/p' \
| sed '/^invoke_/d' \
| sed '/^_Z/d' \
| sed 's/^/_/' \
| sort \
>> /root/.JS_ABI_EXPORTS; \
# List exports symbols [ GOT.mem and GOT.func globals ]
/root/emsdk/upstream/bin/wasm-dis "$EXTENSIONS_DIR_PATH/${extension}/${extension}.so" \
| sed -n 's/^[[:space:]]*(import "\(GOT\.mem\|GOT\.func\)" "\([^"]*\)"[[:space:]]*(global.*/\2/p' \
| sort \
>> /root/.WASM_ABI_EXPORTS; \
# Inject extension-specific exported functions
if [ -f /root/shared/${extension}/exported-functions.list ]; then \
cat /root/shared/${extension}/exported-functions.list >> /root/.JS_ABI_EXPORTS; \
fi; \
done \
fi;
RUN set -eux; \
sort -u /root/.JS_ABI_EXPORTS | \
grep -v '^[[:space:]]*$' | \
sed 's/^/ "/; s/$/",/' | \
sed '$ s/,$//' | \
{ echo '['; cat; echo ']'; } \
> /root/.exported-functions
RUN set -eux; \
sort -u /root/.WASM_ABI_EXPORTS | \
grep -v '^[[:space:]]*$' | \
sed 's/^/-Wl,--export=/' \
> /root/.wasm-exports |
commented
Feb 28, 2026
It's strange those were eliminated as dead code since we declare them as deps like: I wonder if it is a bug or expected behavior for some reason. |
left a comment
There was a problem hiding this comment.
Hi @mho22, I don't see any issue with this and am very excited about getting this in. It looks like xdebug binaries need to be rebuilt, but after that, please feel free to merge this and make any changes under a follow-up PR.
Thank you very much!
| __ZNSt3__211__call_onceERVmPvPFvS2_E | ||
| __ZNSt3__218condition_variable10notify_allEv | ||
| __ZNSt3__25mutex4lockEv | ||
| __ZNSt3__25mutex6unlockEv |
There was a problem hiding this comment.
Some questions:
- How did you identify which functions to include here?
- Why are we hard-coding the list? Is there a way to update this list while building the PHP extension?
There was a problem hiding this comment.
I identified these functions the same way I identified __stack_pointer, recompiling PHP crashed without these specific ones. I should investigate to find out if we should also "accept" ZNSt3 functions. I will do that in the next pull request.
| '-f', | ||
| 'php/Dockerfile', | ||
| '.', | ||
| '..', |
There was a problem hiding this comment.
There's no problem here, but because I think there is probably something for me to learn:
Why was this change needed?
There was a problem hiding this comment.
This was needed to have access to the node-builds package to reach the .so files depending on the php version we recompile.
…idually inside each side modules
Motivation for the change, related issues
Based on exploratory pull request: #3086
This change enables
MAIN_MODULE=2for PHP.wasm Node JSPI while preserving compatibility with dynamically loaded side modules (Intl, Memcached, Redis, and Xdebug).Using
MAIN_MODULE=2removes unused symbols from the main module via Dead Code Elimination (DCE). However, side modules depend on specific globals and exports from the main module. Without explicitly keeping those symbols alive, dynamic loading fails at runtime.The goal of this PR is to:
MAIN_MODULE=2Implementation details
1. Automatic Symbol Extraction for Side Modules
Each side module now generates two symbol lists during its Docker build:
envimports (funcandglobal) used to populate the main module'sEXPORTED_FUNCTIONSfileGOT.memandGOT.funcglobals used to generate-Wl,--export=flagsThese lists are version-specific and stored alongside each extension.
2. Main Module Integration
Two temporary aggregation files are used during the PHP build:
.JS_ABI_EXPORTS— converted toEXPORTED_FUNCTIONS.WASM_ABI_EXPORTS— converted to linker--exportflagsThey are normalized and transformed into:
/root/.exported-functions/root/.wasm-exportsThis ensures only the required symbols are retained for side-module compatibility while keeping DCE enabled.
3. Handling
wasm_recvand Other ReplacementsBecause
MAIN_MODULE=2eliminates unused symbols from the main module, functions only required by side modules are not included inwasmImports.To fix this,
wasm_recvis explicitly injected intowasmImports. Socket-related calls were normalized:For Memcached (which uses
recvinternally via libmemcached), a C shim was introduced. This preserves compatibility without modifying upstream libraries.4. Cleanup and Improvements
recv/setsockopthandling in main module_freewith_wasm_freeinphpwasm-emscripten-libraryResults
Compatibility
Size Reduction
.wasmfile reduced by ~4 MB.jsfile reduced by ~22,000 linesCPU Benchmark (3 runs, 6 test groups each)
Benchmarked with constrained environment :
Total CPU time (user) — 3.19x less CPU on average
Total wall-clock time (real) — roughly equivalent
Total sys time — 1.69x less
Why is wall-clock time the same?
Trunk saturates nearly all 14 cores to finish in the same wall-clock time.
MAIN_MODULE=2achieves the same result with ~4 cores:On resource-constrained environments (CI runners, containers with CPU limits), the 3.19x CPU reduction translates directly into faster wall-clock time:
What gets faster for end users:
What stays the same:
Testing Instructions
CI