-
Notifications
You must be signed in to change notification settings - Fork 305
[ php-wasm ] Add xdebug
shared extension to @php-wasm/node JSPI
#2248
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
[ php-wasm ] Add xdebug
shared extension to @php-wasm/node JSPI
#2248
Conversation
xdebug
dynamic extension to @php wasm node JSPIxdebug
dynamic extension to @php-wasm/node JSPI
Here is a mega-comment containing all our comments and investigation while working in a private repo. Original PR DescriptionMotivation for the change, related issuesIt would be great to offer XDebug support with Studio, and in general, developers using @php-wasm/node could benefit from more debugging tools. Implementation detailsTBD Testing Instructions (or ideally a Blueprint)TBD Comments from Pull Request #60 in Automattic/wordpress-playground-privateComment by brandonpayton at 2025-02-26This is a work-in-progress. Currently, building XDebug succeeds, but it is producing the a static build rather than an .so file. And XDebug has to be a dynamically loaded module. Here is a clue from the build output:
Comment by adamziel at 2025-02-26AFAIR you need to use -sSIDE_MODULE=2 to build a dynamic library. The regular ./configure shared option may or may not apply. Comment by brandonpayton at 2025-02-26Thanks for the tips, @adamziel. I updated the configure invocation to include --disable-static --enable-shared which nicely says what we want, but the build still produces a static library. So far, using SIDE_MODULE=2 does not seem to affect whether the result is static or shared. Based on the docs, I think that option controls dead code elimination, and I'm not sure whether that aspect is related to this issue. Regardless, the current version of the Dockerfile uses SIDE_MODULE=2 just in case it is a factor. I'll keep digging into this. Comment by adamziel at 2025-02-27Check the emmake and emcc overrides in the main Dockerfile we use to build php. You need to pass -sSIDE_MODULE to every atomic emcc call triggered by emmake Comment by adamziel at 2025-02-27Ah you already do it, nevermind them :D Comment by adamziel at 2025-02-27I vaguely remember I had to actually edit the xdebug makefile FWIW Comment by brandonpayton at 2025-02-27
Ah, that's a good clue. Thank you! Comment by brandonpayton at 2025-03-04
Ah, yep. The issue is that the Makefile tries to link against libm, but Emscripten apparently links to an implementation implicitly. If I edit the Makefile and remove the Comment by brandonpayton at 2025-03-04It was so handy to run a shell within that image: Then I could just install vim, look at the actual source files, and re-run emscripten build commands manually. Comment by brandonpayton at 2025-03-05I am testing loading the extension like this:
I have @adamziel's old XDebug draft PR for reference, and there are some differences between our builds. I plan to look at those. But my understanding is that dynamic linking should already be enabled based on the current options in the Dockerfile. Dynamic linking is enabled here in Emscripten based on the RELOCATABLE flag, and according to the docs, the RELOCATABLE flag should be set because of us setting MAIN_MODULE and SIDE_MODULE during build:
Comment by adamziel at 2025-03-05I documented my dynamic linking discoveries in #673 Comment by brandonpayton at 2025-03-05
Thank you for kindly pointing to that again. You mentioned it elsewhere, but I hadn't reviewed it yet. And it is covering the same load failure territory. 🙇 Comment by brandonpayton at 2025-03-05
It's also been helpful to see some of the individual commits from that PR to see how you approached solving similar issues. I was testing with an asyncify build, but I'm exploring with a JSPI build because I think it will likely involve fewer hurdles to get something working and maybe fewer issues to troubleshoot. Then, once we have something working with JSPI, we can fix issues related to asyncify. (We'll see. The previous PR was just with asyncify, so if I'm struggling with the JSPI end, I'll probably revisit this) Comment by brandonpayton at 2025-03-15I spent more time on this this evening. The XDebug build is producing a shared library, and I am working on getting it loading. There were problems with duplicate symbols when enabling - After that, there was one more symbol conflict around Now, I'm running into errors like this when trying to load the xdebug.so extension:
It appears that this is something to do with dlopen(), and I should be able to instrument the emscripten-generated JS code to get a better idea. That's the next thing. Hopefully, we can get back to the place where we can load the extension and start troubleshooting debugger connection and step debugging soon. Comment by brandonpayton at 2025-03-15Does the error go away with jspi? Or do the dlopen have to be declared async? Also, the JS code updates from the other PR may be relevant, especially those sections that mark the act of loading the library as async Comment by brandonpayton at 2025-03-17
I hadn't tried yet with JSPI, but taking the JS code from your previous PR which actually marks
Now I am running into an issue with sleep() callbacks in JS.
This may be some kind of linking-related error. Planning to push the current config shortly... Comment by brandonpayton at 2025-03-17
I said this a while ago but apparently forgot to test with JSPI when getting back to this PR. It's too bad that bun doesn't support JSPI yet because it's much faster to test with bun than to rebuild and test via node. Comment by brandonpayton at 2025-03-18UPDATE: This was due to an accidental 64-bit architecture declaration left in place for the XDebug compilation. There seem to be some ASSERTION-related issues with the recent 64-bit changes, so I am temporarily reverting those in this PR to remove distraction. I spent some time fixing the build and code to run with ASSERTIONS and ERROR_ON_UNDEFINED_SYMBOLS enabled to hopefully track down the issue. I don't know what meaningfully could have changed, but now I'm seeing a different set of errors. When synchronous dlopen() is used, there is an memory access out-of-bounds error like:
When async dlopen() is used, there is an "unreachable code should not be executed" error:
I've read some Emscripten issues today involving asyncify, dlopen(), and possible stack corruption but don't have the links handy. Planning to look them up and share later. Comment by brandonpayton at 2025-03-18The weird memory-related errors were due to a 64-bit architecture declaration that was accidentally left in place. Now, I am back to the "Unreachable code should not be executed" error and am looking into that with ASSERTIONS enabled. Also there is some suggestion to expand the asyncify stack size, so I will try that. But this does not feel like that kind of issue... Comment by brandonpayton at 2025-03-18A JSPI build appears to show the same issue, but I'm not 100% it is the JSPI build that is running in my test. Planning to resume in the morning. Comment by brandonpayton at 2025-03-18It looks like there are some conflicts with 64-bit support. I thought that I had temporarily reverted the 64-bit support changes but had not reverted them on my main working/testing branch. Without 64-bit support, the xdebug.so module is loading well with both JSPI and Asyncify. Instead of pushing a bunch of changes due to the temporary 64-bit revert, let's rebase this branch on the commit immediately preceding the 64-bit support and work from there. That will make it easier to see the meaningful changes in this PR. Comment by brandonpayton at 2025-03-18Correction: The JSPI build has not been properly tested yet. It only appeared that it had. But it's promising that the Asyncify version is loading. Comment by brandonpayton at 2025-03-18
This is because Node.js v22, the version I was testing with, has the old JSPI WebAssembly.Suspender rather than the new JSPI WebAssembly.Suspending member. Our JSPI test is for Suspending, and that is what the Emscripten JSPI builds target as well. Comment by brandonpayton at 2025-03-18Node v23 with the Comment by brandonpayton at 2025-03-19We could consider whether it is valuable to merge an xdebug extension even before enabling step debugging. But it isn't working after applying the 64-bit int changes.
IIRC, when I looked into this error recently, I'm also wondering whether it would be cleaner or less problematic to define Google's summary of
And PHP enables 64-bit integers when that is defined:
There are few other mentions of I may try switching to Comment by brandonpayton at 2025-03-19
A couple notes:
Comment by adamziel at 2025-03-19
Comment by adamziel at 2025-03-20
This sounds related to Comment by adamziel at 2025-03-20I couldn't find that in the PR – are you building XDebug with 64bit integers, too? Could it be a mismatch between the dynamically loaded library and the base program? Comment by adamziel at 2025-03-20Also, what would be a JSPI stack trace of the error? Asyncify loses a lot of that contextual information and I wonder if the full trace would give us more hints. Comment by brandonpayton at 2025-03-20
Cool!
I was originally building xdebug with 64-bit integers (using the same
Good idea. Here's a stacktrace from a JSPI build of PHP 8.3:
There is some mention of an error like this here and the related Emscripten bug here. It's possible I'm not building xdebug with the right ASYNCIFY/JSPI flags (and will try to share the latest build config tomorrow). Comment by brandonpayton at 2025-03-20I'm also curious if upgrading to the latest Emscripten version would have any impact here. Comment by brandonpayton at 2025-03-20I disabled optimizations and built xdebug with debug symbols, and call stack with JSPI is more informative:
Comment by brandonpayton at 2025-03-20Ok! @adamziel, I was able to get the xdebug extension loading with both JSPI and ASYNCIFY with This is wonderful news. I'll clean up the build portion of this PR and start looking into step debugging. Comment by brandonpayton at 2025-03-21I pushed JSPI and ASYNCIFY builds for PHP 8.3. To see the extension loading, you can run: Comment by brandonpayton at 2025-03-21export default async function runExecutor(options: BuiltScriptExecutorSchema) {
const args = [
+ ...(options.nodeArg || []), brandonpayton [on Mar 21] Comment by brandonpayton at 2025-03-21async function run() {
// @ts-ignore
- const defaultPhpIniPath = await import('./php.ini');
+ const defaultPhpIniPath = (await import('./php.ini')).default; brandonpayton[on Mar 21] Comment by brandonpayton at 2025-03-21 then \
set -euxo pipefail;\
echo -n ' --enable-libxml --with-libxml --with-libxml-dir=/root/lib --enable-dom --enable-xml --enable-simplexml --enable-xmlreader --enable-xmlwriter' >> /root/.php-configure-flags && \
- echo -n ' -I /root/lib -I /root/lib/include/libxml2 -lxml2' >> /root/.emcc-php-wasm-flags && \
+ # TODO: Look at restoring -lxml2 here, probably by eliminating it from library pre-build brandonpayton [on Mar 21] There are other places here where the same is true for -lz. I'm planning to check our static lib builds and see if adjustments there can fix this. brandonpayton[on Mar 21] If I change the commands from EMCC_SKIP="..." export EMCC_SKIP="..."; brandonpayton [on Mar 21]
Nah, that seems fine. It looks like the real issue is that, in the MAIN_MODULE compilation, we are specifying both I think we need to pick one way or the other to link to those libs. If we use the
brandonpayton [on Mar 21] I'm not sure why we use both the brandonpayton [on Mar 22] adamziel [on Mar 22] brandonpayton [on Mar 23] Aw, thanks, Adam! Comment by adamziel at 2025-03-24So far so good here. Great work and great documentation. I wish I left more writing from my ancient explorations on this. This is looking really optimistic. Comment by brandonpayton at 2025-03-25
:) Agreed. I am hopeful as well. Some additional notes:
For the moment, we can configure the build to ignore these older PHP versions and focus on new PHP versions. Comment by adamziel at 2025-03-25
No blockers from my end, AFAIR that solved some built-time error. Let's see what @bgrgicak says. Comment by brandonpayton at 2025-03-25I'm temporarily setting aside the PHP 7.2 and 7.3 issues, but here are more notes from my exploration due to those issues: To potentially avoid the PHP 7.2 and 7.3 symbol conflicts, I explored switching from It would be annoying to have to maintain a list of symbols like we do for ASYNCIFY. Fortunately, we may be able to detect which symbols
There are naming conventions Emscripten uses to segment imports, and once we have the list from Comment by brandonpayton at 2025-03-25
As a semi-related aside: Comment by adamziel at 2025-03-25Emscripten even has an option to autogenerate such a list. Unfortunately, it seems way too eager. It lists the majority of the functions which slows down the final binary by a lot. If we could do better than that, it would be great, but first I'd like to explore that WASM-land JSPI polyfill. Maybe the performance wouldn't be that terrible? Comment by brandonpayton at 2025-03-26
Interesting! Looking at their implementation could be helpful as well.
Nice! I was wondering about this as well. Just noticed today that you had commented last month on theJSPI-skeptical comment from the WebKit dev and mentioned his JSPI polyfill. Comment by brandonpayton at 2025-04-07My next step here is to examine XDebug attempting to connect with a local debugger and figure out whether that is still failing and how we might fix it. My guess is that it will fail due to our current Emscripten networking mode. Will see. Comment by adamziel at 2025-04-07
Yes! It was one of the ASYNCIFY_ options. AFAIR the dynamic calls were the largest problems there - you need to assume a set of possible function calls which, if you go by signature, generates a large cartesian product. Comment by brandonpayton at 2025-04-15I think this is in a place where it would be good to debug the XDebug Wasm, see where it is creating its socket and reading from it, and see how Emscripten is handling that. It's what made me work on getting set up for faster, unbundled Node.js debugging in VSCode. Hopefully I can merge that debugging PR yet today. There is just one more thing I want to check first. Comment by brandonpayton at 2025-04-15There is a VSCode extension for debugging WebAssembly with DWARF debug info: Hopefully we will be able to use it here. Comment by mho22 at 2025-04-23I found out this line was creating the socket in Xdebug :
I decided to add a bunch of
I got these informations in the
I
As you can see, the socket options are supported but unfortunately getAllWebSockets(10) is an empty array. My assumption is that xdebug is correctly creating a socket and file descriptor but this one is undefined in wasm. I suspect the issue is related to how the @brandonpayton You did a fantastic job here, I regret not contributing sooner. Thank you very much. Below are the steps I followed to obtain the logs mentioned above.
+ COPY ./php/xdebug-com.c /root/debug/xdebug-com.c
RUN set -euxo pipefail; \
if ( \
# [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ] && \
# TODO: Fix XDebug build and runtime problems with PHP 7.2 and 7.3 or drop support.
([[ "${PHP_VERSION:0:1}" -ge 8 ]] || [[ "${PHP_VERSION:0:3}" == "7.4" ]]) \
); then \
export PHP_VERSION_ESCAPED="${PHP_VERSION//./_}"; \
if [[ "${PHP_VERSION:0:1}" -ge 8 ]]; then \
export XDEBUG_BRANCH="xdebug_3_4"; \
else \
export XDEBUG_BRANCH="xdebug_3_1"; \
fi; \
cd /root; \
git clone https://github.com/xdebug/xdebug.git xdebug \
--branch "${XDEBUG_BRANCH}" \
--single-branch \
--depth 1; \
cd xdebug; \
( \
cd /root/php-src; \
# Install php build scripts like phpize
make install; \
); \
+ cp /root/debug/xdebug-com.c /root/xdebug/src/debugger/com.c; \
...
- export EMCC_FLAGS="-sSIDE_MODULE -D__x86_64__ -sWASM_BIGINT=1; \
+ export EMCC_FLAGS="-sSIDE_MODULE -D__x86_64__ -sWASM_BIGINT=1 -Dsetsockopt=wasm_setsockopt"; \ The complete xdebug-com.c file is in this gist Secondarily, I quickly made it "work" on By adding this in _dlopen_js__deps: ['$dlopenInternal'],
+ _dlopen_js__async: false Here is the code I use to run xdebug in
import { PHP, setPhpIniEntries } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
import fs from 'fs';
const php = new PHP( await loadNodeRuntime( '8.3' ) );
const data = fs.readFileSync( 'node_modules/@php-wasm/node/jspi/8_3_0/xdebug.so' );
php.mkdir( '/extensions' );
php.writeFile( '/extensions/xdebug.so', new Uint8Array( data ) );
setPhpIniEntries( php, {
'zend_extension' : '/extensions/xdebug.so',
'html_errors' : 'Off',
'xdebug.mode' : 'debug',
'xdebug.start_with_request' : 'yes',
'xdebug.client_host' : '127.0.0.1',
'xdebug.client_port' : '9003',
'xdebug.idekey' : 'VSCODE',
'xdebug.log' : '/xdebug.log'
} );
await php.run( { code : "<?php echo xdebug_info();" } );
console.log( php.readFileAsText( '/xdebug.log' ) ); JSPI command :
Comment by adamziel at 2025-04-23I just can't overstate how happy I am about the work happening here. Thank you both ❤️ Comment by adamziel at 2025-04-23
Weird! How is it different from what fsockopen() is doing? Since it works. Any chance this is created as a non-websocket socket? Comment by adamziel at 2025-04-23Also, look into the TLS -> fetch() handler. It wraps emscripten websocket creation, maybe the execution flow gets there, finds no handler, and closes the socket? Comment by adamziel at 2025-04-23Wait, is this an outbound socket or a listening socket? For the latter, we need a server handler. I've dine one here: Comment by mho22 at 2025-04-23@adamziel I followed your advice, corrected the errors and noticed a break in the Xdebug process logs :
My script :
import { PHP, __private__dont__use, setPhpIniEntries } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
import fs from 'fs';
const php = new PHP( await loadNodeRuntime( '8.3' ) );
const data = fs.readFileSync( 'node_modules/@php-wasm/node/jspi/8_3_0/xdebug.so' );
php.mkdir( '/extensions' );
php.writeFile( '/extensions/xdebug.so', new Uint8Array( data ) );
setPhpIniEntries( php, {
'zend_extension' : '/extensions/xdebug.so',
'html_errors' : 'Off',
'xdebug.mode' : 'debug',
'xdebug.start_with_request' : 'yes',
'xdebug.client_host' : '127.0.0.1',
'xdebug.client_port' : '9003',
'xdebug.log' : '/xdebug.log'
} );
const script = `<?php
echo "Hello Xdebug World";
xdebug_break();
echo xdebug_info();
`
php.writeFile( '/xdebug.php', script );
await php.run( { scriptPath : '/xdebug.php' } );
console.log( php.readFileAsText( '/xdebug.log' ) ); But when I
The first one is PHP DEBUG, the second one is php-wasm/xdebug. I think the second one shouldn't be listening. And when I log the resulting websocket._url from
@brandonpayton I modified getAllWebSockets: function (sock) {
+ const socket = FS.getStream(sock).node.sock;
const webSockets = /* @__PURE__ */ new Set();
- if (sock.server) {
+ if (socket.server) {
- sock.server.clients.forEach((ws) => {
+ socket.server.clients.forEach((ws) => {
webSockets.add(ws);
});
}
for (const peer of PHPWASM.getAllPeers(sock)) {
webSockets.add(peer.socket);
}
return Array.from(webSockets);
},
getAllPeers: function (sock) {
+ const socket = FS.getStream(sock).node.sock;
const peers = new Set();
- if (sock.server) {
+ if (socket.server) {
- sock.pending
+ socket.pending
.filter((pending) => pending.peers)
.forEach((pending) => {
for (const peer of Object.values(pending.peers)) {
peers.add(peer);
}
});
}
- if (sock.peers) {
+ if (socket.peers) {
- for (const peer of Object.values(sock.peers)) {
+ for (const peer of Object.values(socket.peers)) {
peers.add(peer);
}
}
return Array.from(peers);
},
...
const SOL_SOCKET = 1;
const SO_KEEPALIVE = 9;
const IPPROTO_TCP = 6;
const TCP_NODELAY = 1;
+ const TCP_KEEPIDLE = 4;
+ const TCP_KEEPINTVL = 5;
+ const TCP_KEEPCNT = 6;
const isSupported =
(level === SOL_SOCKET && optionName === SO_KEEPALIVE) ||
- (level === IPPROTO_TCP && optionName === TCP_NODELAY);
+ (level === IPPROTO_TCP && optionName === TCP_NODELAY) ||
+ (level === IPPROTO_TCP && optionName === TCP_KEEPIDLE) ||
+ (level === IPPROTO_TCP && optionName === TCP_KEEPCNT) ||
+ (level === IPPROTO_TCP && optionName === TCP_KEEPINTVL); My code should be taken with caution. Especially the modifications here as I expect sock to be an integer. I also added a tiny line in
To display the correct port in the Xdebug logs, without that it was confusing to be connected to Comment by mho22 at 2025-04-24I made a lot of mistakes here. I am discovering things as well as I go forward in the issues I face. What I am certain of is that the websocket created by xdebug is not connecting to the websocketServer.
Xdebug should call 5 times
and returns this :
There seems to be a problem when connecting to the webserver. Even if :
Something is perhaps happening during handshake. So I should look for Or maybe things are processing too quickly, and the connection can't be established before Xdebug stops? Comment by mho22 at 2025-04-24It was indeed a matter of time. The connection couldn't be established quickly enough. To correct that I modified the
running the script returns this when VSCode extension PHP DEBUG is running on port 9003 :
else it returns this when VSCode extension PHP DEBUG port 9003 is closed :
However, nothing more happens for now. But it's a start! @brandonpayton This is what I added :
export function addSocketOptionsSupportToWebSocketClass(
WebSocketConstructor: typeof WebSocket
) {
return class PHPWasmWebSocketConstructor extends WebSocketConstructor {
+ public sendQueue: {commandType: number,chunk: string | ArrayBuffer | ArrayLike<number>, callback: any}[] = [];
+ constructor(url: string | URL, protocols?: string | string[]) {
+ super(url, protocols);
+ this.addEventListener( 'open', () => {
+ this.sendQueue.forEach( ( { commandType, chunk, callback } ) => {
+ return (WebSocketConstructor.prototype.send as any).call(
+ this,
+ prependByte(chunk, commandType),
+ callback
+ );
+ });
+ });
+ }
// @ts-ignore
send(chunk: any, callback: any) {
return this.sendCommand(COMMAND_CHUNK, chunk, callback);
}
setSocketOpt(
optionClass: number,
optionName: number,
optionValue: number
) {
return this.sendCommand(
COMMAND_SET_SOCKETOPT,
new Uint8Array([optionClass, optionName, optionValue]).buffer,
() => undefined
);
}
sendCommand(
commandType: number,
chunk: string | ArrayBuffer | ArrayLike<number>,
callback: any
) {
+ if (this.readyState !== WebSocket.OPEN) {
+ this.sendQueue.push({ commandType, chunk, callback });
+ return;
+ }
return (WebSocketConstructor.prototype.send as any).call(
this,
prependByte(chunk, commandType),
callback
);
}
};
} And I slightly modified getAllWebSockets: function (sock) {
- const socket = FS.getStream(sock).node.sock;
+ const socket = getSocketFromFD(sock);
getAllPeers: function (sock) {
- const socket = FS.getStream(sock).node.sock;
+ const socket = getSocketFromFD(sock); Comment by mho22 at 2025-04-29@adamziel @brandonpayton I made the step debugger work! It was indeed an issue with execution and sleep. Xdebug's handler_dbgp.c file has a loop called Under normal circumstances, Adding a while loop with if (type == FD_RL_FILE) {
newl = read(socketfd, buffer, READ_BUFFER_SIZE);
} else {
- newl = recv(socketfd, buffer, READ_BUFFER_SIZE, 0);
+ while( true )
+ {
+ newl = recv(socketfd, buffer, READ_BUFFER_SIZE, 0);
+
+ if (newl > 0)
+ {
+ break;
+ }
+
+ emscripten_sleep(10);
+ }
} I'm not sure if this is the right approach, but it works, and it's definitely surprising to debug a PHP file using Node! I added these two lines in /root/replace.sh 's/, XINI_DBG\(client_port\)/, (long int) XINI_DBG(client_port)/g' /root/xdebug/src/debugger/com.c; \
+ /root/replace.sh 's/#include <fcntl.h>/#include <fcntl.h>\n#include <emscripten.h>/g' /root/xdebug/src/debugger/handler_dbgp.c; \
+ /root/replace.sh 's/newl = recv\(socketfd, buffer, READ_BUFFER_SIZE, 0\);/while ((newl = recv(socketfd, buffer, READ_BUFFER_SIZE, 0)) <= 0) emscripten_sleep(10);/g' /root/xdebug/src/debugger/handler_dbgp.c; \
# Prepare the XDebug module for build
phpize . ; \ I also found out that previous corrections were not working correctly when other functions were called so I kind of improved my previous corrections in getAllWebSockets: function (sock) {
- const socket = getSocketFromFD(sock);
getAllPeers: function (sock) {
- const socket = getSocketFromFD(sock);
...
wasm_setsockopt: function (
socketd,
level,
optionName,
optionValuePtr,
optionLen
) {
...
+ const sock = getSocketFromFD(socketd);
- const ws = PHPWASM.getAllWebSockets(socketd)[0];
+ const ws = PHPWASM.getAllWebSockets(sock)[0];damziel @brandonpayton I made the step debugger work! Comment by adamziel at 2025-04-30NICE! This is such a profound milestone @mho22! As for the approach. I think waiting with a timeout is as good as we can do. Typically, the waiting would be done by the recv() call – let's do it that way to avoid modifying xdebug. It will help us with all other servers that also rely on recv(). It will involve a custom syscall handler. Here's a few pointers:
Comment by brandonpayton at 2025-04-30
This is awesome, @mho22! Comment by mho22 at 2025-05-06This is what I learned so far : How Xdebug runs in php-wasm/node with VSCode : With
When running
Now that we have a connection and the socket options are correctly bound between Wasm Websocket and VSCode TCP socket, it needs to understand the communication. With the help of the while loop in xdebug A last error occurs. VSCode needs to recognize the file based on current working directory :
And because it needs an absolute path. We will have to mount it :
A transaction between xdebug and vscode will now look like this :
And step debugging will occur. Pretty simple in the end. how Xdebug works in php-wasm/web with VSCode : It is pretty similar of course, but the main difference is the absence of a WebsocketServer on the browser. I roughly rewrote the
async send(data: ArrayBuffer) {
await this.fetchOverTCP( data );
}
setSocketOpt(optionClass : number, optionName : number, optionValue : number) {
}
async fetchOverTCP( message : string | ArrayBufferLike | Blob | ArrayBufferView )
{
const socket = new WebSocket( 'ws://127.0.0.1:22028', 'binary' );
await new Promise( resolve => socket.addEventListener( 'open', resolve ) );
socket.send( message );
socket.onmessage = async event =>
{
this.emit( 'message', { data : await event.data.arrayBuffer() } )
socket.close();
}
}
import { WebSocketServer } from 'ws';
import net from 'net';
const wss = new WebSocketServer( { port : 22028 } );
console.log( 'Running WebSocketServer on port 22028.' );
let tcpClient = null;
let connected = false;
const queue = [];
let current = null;
wss.on( 'connection', ws =>
{
current = ws;
console.log( 'WebSocket connected on port 22028.' );
ws.on( 'message', message =>
{
console.log( 'Received from WebSocket:', message.toString() );
if( message.toString().includes( '<init' ) )
{
tcpClient = connectTcpAndFlush( tcpClient, queue, ws );
}
if( connected )
{
tcpClient.write( message );
}
else
{
queue.push( message );
}
});
ws.on( 'error', error => console.error( 'WebSocket Error:', error.message ) );
ws.on( 'close', () => console.log( 'WebSocket closed' ) );
} );
function connectTcpAndFlush( tcpClient, queue )
{
if( tcpClient )
{
console.log( 'Closing existing TCP connection...' );
tcpClient.destroy();
}
tcpClient = new net.Socket();
tcpClient.connect( 9003, '127.0.0.1', () =>
{
console.log( 'TCP connected' );
connected = true;
while( queue.length > 0 )
{
tcpClient.write( queue.shift() );
}
} );
tcpClient.on( 'data', data =>
{
console.log( 'Received from TCP:', data.toString() );
current.send( data );
} );
tcpClient.on( 'error', error =>
{
console.error( 'TCP Client Error:', error.message );
current.close();
} );
return tcpClient;
} The Xdebug websockets will connect with the websocketserver and with the TCP socket from VSCode. The file paths in PHP-WASM FS must be the same as on your local project to allow VSCode to break correctly. And step debugging will occur. how Xdebug works in I think it won't. For now. For several reasons that we will maybe handle in the future but I wanted to list them and separate it from this PR. :
I will create a new Draft in which I write my findings about Comment by adamziel at 2025-05-06Fantastic work @mho22! Let's focus on the node.js version and then follow up with the web version. As for running this with Chrome devtools in the future:
Comment by bgrgicak at 2025-05-16
Sorry @brandonpayton, I somehow missed this ping and found it today while researching dynamic library loading. TBH I don't know more about this, I just fixed the PHP 7.4 issue, but from the docs it looks like we should be able to use a separate lib version by passing --with-onig[=DIR].
Comment by bgrgicak at 2025-05-21
🤦 😅 @bgrgicak, I saw this and also forgot to reply immediately. It's no problem. Thanks for your feedback here. It would be cool if we could just use the separate onig lib for the older PHP version builds. @mho22, I don't recall whether I tried that or not already, but it sounds like a good idea if we want to support those older versions. Comment by adamziel at 2025-05-28Any updates here @mho22 ? Comment by mho22 at 2025-05-29I am still in the middle of my experiments, but here's a brief summary so far. I started testing with I found out a way to show verbosity in socket processes :
This gave me key insights. I noticed that the values expected by Xdebug’s I decided to investigate this anomaly further, so I temporarily set aside Since any changes I make in php-wasm don’t affect Xdebug directly, I realized that I needed to translate Xdebug’s recv into an asynchronous function. Here's the best approach I’ve found so far: Add
And, it works. 🎉 I confirmed that including This setup doesn’t yet qualify as full Questions : What if
And, this may be too far-fetched, but if the above approach works, would building with What do you think is the better approach: continuing with Comment by mho22 at 2025-05-29@brandonpayton, the only reference to versioning appears in the Xdebug branch when cloning the Git repository. But I’ll definitely try the
Comment by adamziel at 2025-05-29
Comment by mho22 at 2025-05-29While I was testing PHP-WASM NODE JSPI version 7.4 with Xdebug enabled, I faced a particular issue :
Strange, when it comes to JSPI. I found out that part was adding an option in php/Dockerfile :
Commenting it solved the issue. Comment by mho22 at 2025-05-29@brandonpayton I have temporarily resolved the issue related to PHP 7.2 and 7.3 builds by commenting out the following section in :
Currently, I have successful step debugging for PHP versions Next steps:
Comment by mho22 at 2025-06-02I managed to run step debugging with PHP 8.4 Asyncify! It required a lot of self control to find the complete list of ASYNCIFY_ONLY functions, both for php and for xdebug. For php: + zend_extension_statement_handler
+ zend_llist_apply_with_argument
+ ZEND_EXT_STMT_SPEC_HANDLER
+ zend_post_deactivate_modules
+ call
+ zend_observer_fcall_begin_prechecked
+ zend_observer_fcall_begin For Xdebug : + zm_post_zend_deactivate_xdebug
+ xdebug_debugger_post_deactivate
+ xdebug_dbgp_deinit
+ xdebug_execute_begin
+ xdebug_execute_user_code_begin
+ xdebug_init_debugger
+ xdebug_debug_init_if_requested_at_startup
+ xdebug_dbgp_init
+ xdebug_execute_ex
+ xdebug_statement_call
+ xdebug_debugger_statement_call
+ xdebug_dbgp_breakpoint
+ xdebug_dbgp_cmdloop
+ xdebug_fd_read_line_delim We must keep in mind that these lists are certainly incomplete, since I just tested the step debugging of my simple And
It follows the same logic as Let's test the other PHP versions. Comment by adamziel at 2025-06-02Amazing work! Yay! For other asyncify functions you could set up a console.trace() statement in handleAsync() in the JSPI build. You'd then use xdebug manually, step through some code, set up watchers etc., and log all the unique C function names that come up in the process. You'll need a These might also be good code paths to write unit tests for - that way we could easily re-run it on JSPI for PHP 7.2+ to source those symbols as well. Then we could reuse the same test suite to confirm it all works on asyncify builds. In fact, I wonder if there's a way we could run some existing XDebug test suite with our wasm build - either unit tests or some tests that send mock packets over the network. Comment by adamziel at 2025-06-02Also, what would it take to ship XDebug with JSPI before we ship asyncify support? If it creates a significant overhead, a ton of Comment by mho22 at 2025-06-03
Ow, I should have thought about this sooner. Thank you!
I was thinking about this yesterday. I'm not sure if this will be straightforward, but, yeah, if we can retrieve the tests, run them and return results, that would be great. I found this file in the Xdebug repository. Let's dig into it.
I can't answer precisely since I am still experimenting. For now, I am building xdebug.so files from within the php/Dockerfile file but this can't be the definitive approach. So my next goal will be to :
But I suppose it won't be complicated to enable these for JSPI only. I am on it. Also, now that the public repository has reopened, should I start a new pull request with my findings? Comment by adamziel at 2025-06-03Fresh public pr -> yes. I'll answer the rest later |
The next main steps are :
Side steps :
Next follow-up PRs suggestion :
|
This command will compile a given version of xdebug as a dynamic extension :
And copy/paste it in the correct |
While currently on this task, I found out I couldn't mount the current working directory and set some php ini entries without having access to And even if I find a way to set the php ini entries inside the @adamziel Where would you add the code relative to mounting the current working directory and setting additional php ini entries relative to Xdebug ? BTW, it works when I separate it in a script like this : import { PHP, setPhpIniEntries } from '@php-wasm/universal';
import { createNodeFsMountHandler, loadNodeRuntime } from '@php-wasm/node';
const php = new PHP( await loadNodeRuntime( '8.4', { withXdebug : true } ) );
php.mkdir( process.cwd() );
php.mount( process.cwd(), createNodeFsMountHandler( process.cwd() ) );
php.chdir( process.cwd() );
setPhpIniEntries( php, {
'zend_extension' : '/extensions/xdebug.so',
'html_errors' : 'Off',
'xdebug.mode' : 'debug',
'xdebug.start_with_request' : 'yes',
'xdebug.log' : '/xdebug.log'
} );
const result = await php.run( { scriptPath : `php/xdebug.php` } );
console.log( result.text ); |
We might need to add a new semantic to define initial php.ini entries during the runtime initialization. That could mean either actually creating the php.ini file or just storing a key/value record for the PHP class to handle. |
PHP supports multiple php.ini files, right? That could be the way to go here, every add-on would create a dedicated partial config file |
I followed your suggestion and used the
Additionally, when phpRuntime.FS.writeFile( '/internal/shared/extensions/xdebug.ini', [
'zend_extension=/internal/shared/extensions/xdebug.so',
'html_errors=Off',
'xdebug.mode=debug',
'xdebug.start_with_request=yes',
'xdebug.log=/xdebug.log'
].join('\n') ); The next step is related to the current working directory mount. And while I thought this would be difficult to implement since we don't have access to phpRuntime.FS.mkdirTree(process.cwd());
phpRuntime.FS.mount(phpRuntime.FS.filesystems["NODEFS"], {root: process.cwd()}, process.cwd());
phpRuntime.FS.chdir(process.cwd());
Here's the return {
ENV: {
...options.ENV,
PHP_INI_SCAN_DIR: '/internal/shared/extensions',
},
onRuntimeInitialized: (phpRuntime: PHPRuntime) => {
if (options.onRuntimeInitialized) {
options.onRuntimeInitialized(phpRuntime);
}
/* The extension file previously read
* is written inside the /extensions directory
*/
phpRuntime.FS.mkdirTree('/internal/shared/extensions');
phpRuntime.FS.writeFile(
`/internal/shared/extensions/${fileName}`,
new Uint8Array(extension)
);
/* The extension has its share of ini entries
* to write in a separate ini file
*/
phpRuntime.FS.writeFile( '/internal/shared/extensions/xdebug.ini', [
'zend_extension=/internal/shared/extensions/xdebug.so',
'html_errors=Off',
'xdebug.mode=debug',
'xdebug.start_with_request=yes',
'xdebug.log=/xdebug.log'
].join('\n') );
/* The extension needs to mount the current
* working directory in order to sync with
* the debugger
*/
phpRuntime.FS.mkdirTree(process.cwd());
phpRuntime.FS.mount(phpRuntime.FS.filesystems["NODEFS"], {root: process.cwd()}, process.cwd());
phpRuntime.FS.chdir(process.cwd());
},
}; And this is my current simplified test code :
<?php
$test = 42; // I set a breakpoint here
echo "Hello Xdebug World\n";
import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
const php = new PHP( await loadNodeRuntime( '8.4', { withXdebug : true } ) );
const result = await php.run( { scriptPath : `php/xdebug.php` } );
console.log( result.text ); Aaaand step debugging occurs! |
This command will compile every version of xdebug as a dynamic extension :
And copy/paste it in the correct node jspi directory. Now every NODE JSPI version of PHP is Xdebug compatible! 🎉 |
@mho22 Great news! What's missing before this one can be reviewed? Edit: Ah, nevermind, I just saw the checklist :) |
I have written new tests related to the dynamic loading of Xdebug. Currently, there are 4 tests for each PHP version.
I consider this is not complete since it can't manage two last tests that I tried to implement, without success :
1 : I am not sure if it has to throw an error when the vscode debugger port is not open. I added it during development to be warned when I forgot to open the vscode debugger port. So we could consider removing that error and not implement that test. 2 : But the last test should be implemented. Unfortunately, even if I create a server on port 9003, with Do you have a better approach on this? I guess after that last test is coded, the pull request is ready to be reviewed. I am also recompiling PHP WASM NODE JSPI entirely due to conflicts. |
No error is fine. We'll add some problem signaling if and when it's needed – perhaps things will "just work" often enough that we won't need it. |
You may need to rebase before rebuilding to resolve the conflicts |
@adamziel Yes! My bad |
Good point! There must be a way to display it in the editor, though, right? Editors have supported "Remote debug sessions" for a while where not having all the sources locally is the baseline. Yesterday I found Chrome DevTools Protocol has a Note this doesn't have to block this PR. I just mean it as a short-term follow-up that we should track separately. |
I still have to do one thing :
|
Something strange is happening with sqlite and PHP version 7.3 and 7.2 :
|
Oh no! I wonder if libsqlite shipped with these older php releases is somehow patched in php-src. Not good. What shows up when you diff it against the official release? If we can apply the same patch and continue using the recent version, let's do that. I worry that downgrading will break a lot of the SQLite integration features |
It could also be the older PHP buildconf assuming a custom libsqlite means a dynamic library |
So this line :
Coupled with this option :
Returns the error. So I should replace these lines : # Add sqlite3 if needed
RUN if [ "$WITH_SQLITE" = "yes" ]; \
then \
echo -n ' --with-sqlite3 --enable-pdo --with-pdo-sqlite=/root/lib ' >> /root/.php-configure-flags && \
- echo -n ' -I ext/pdo_sqlite -lsqlite3 ' >> /root/.emcc-php-wasm-flags; \
+ echo -n ' /root/lib/lib/libsqlite3.a ' >> /root/.emcc-php-wasm-sources; \
else \
echo -n ' --without-sqlite3 --disable-pdo ' >> /root/.php-configure-flags; \
fi Since it is the new way to implement static libraries. However, for version PHP7.2 and PHP7.3 It returns this list of errors :
Indicating that there are duplicate symbols between libphp and libsqlite3. That is why it was only sourced for PHP version over 7.3 This is exactly what said @brandonpayton in the original Pull Request : |
|
I reset the php version to reactivate the sqlite static extension for php7.2 and php7.3, in order to verify their versions and you were right :
While in another project it shows this :
I have no clue how to fix this but, I am about to investigate this further. |
|
https://github.com/php/php-src/commits/PHP-7.2/ext/sqlite3/libsqlite/sqlite3.c the history seems to be just pulling the latest SQLite3 amalgamation every time so perhaps 🤞 |
I guess this is the magic sauce? https://github.com/php/php-src/blob/PHP-7.2/ext/sqlite3/libsqlite/sqlite3ext.h |
Or maybe we could unbundle libsqlite3 like they did for PHP 7.4 ? |
Whatever is the easiest to maintain |
It needed two files and 4 lines in
I deduced it was probably the easiest to maintain 😄 . |
Lovely, thank you! Let's document the rationale for unbundling inline including the error message and a link to this discussion. Also, let's use the directory structure to indicate these two files are only used in older php versions. Maybe by storing them inside in |
# enable the flag and pay the price of the additional overhead. | ||
# https://emscripten.org/docs/porting/guidelines/function_pointer_issues.html | ||
RUN if [ "${PHP_VERSION:0:1}" -lt "8" ]; then \ | ||
echo -n ' -s EMULATE_FUNCTION_POINTER_CASTS=1' >> /root/.emcc-php-wasm-flags; \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the consequences of removing this? I remember seeing some crashes recently on a php 7.4 build without this option enabled. I think it was related to proc_open(), http requests, and fclose(). I wish I written down the full error. Is there something else in this PR that prevents those types of errors? Or is this flag just removed to make the build work, but without addressing the documented problem?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to find a case that breaks without pointer events casts before this pr. If it works with this PR applied, bueno. If it breaks, we need to add a failing unit test and make it pass.
I wonder if the failures were specific to asyncify? I want to say hes but I vaguely remember struggling with then in a JSPI build. Maybe I remember wrong? Not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, I removed that part earlier to make the builds work in JSPI. I rebuilt PHP 7.4 after re-adding that if statement and, as expected, some tests failed. So there is a direct link between EMULATE_FUNCTION_POINTER_CASTS
and Xdebug's failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's find a reproducible way of crashing PHP without pointer casts - there may be a way to fix the underlying problem and get rid of the flash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the commit that added that portion of code.
Unfortunately as a lot of things have changed since Dec 28 2022 in php/Dockerfile
, I don't know if something has solved that part since then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found a way to crash PHP without the pointer cast with this script :
import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';
const php = new PHP( await loadNodeRuntime( '7.4' ) );
const result = await php.runStream( { code : `<?php
function test_curl() {
for ($i = 0; $i < 10; $i++) {
var_dump( $i );
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://example.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_exec($ch);
curl_close($ch); // This could trigger zend_list_free() with indirect pointer
}
}
echo "STARTED";
test_curl();
echo "STOPPED";`
} );
console.log( await result.stdoutText, await result.stderrText );
It crashes with node --experimental-wasm-stack-switching scripts/pointer.js
:
Error: null function or function signature mismatch
at file:///Users/mho/xdebug/template/node_modules/@php-wasm/universal/index.js:1714:80 {
cause: RuntimeError: null function or function signature mismatch
at php.wasm.ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER (wasm://wasm/php.wasm-06f290ba:wasm-function[7637]:0x616df8)
at php.wasm.execute_ex (wasm://wasm/php.wasm-06f290ba:wasm-function[7463]:0x60bee2)
at php.wasm.zend_execute (wasm://wasm/php.wasm-06f290ba:wasm-function[7465]:0x60c0e8)
at php.wasm.zend_execute_scripts (wasm://wasm/php.wasm-06f290ba:wasm-function[6607]:0x5c6b63)
at php.wasm.php_execute_script (wasm://wasm/php.wasm-06f290ba:wasm-function[5701]:0x565190)
at php.wasm.wasm_sapi_handle_request (wasm://wasm/php.wasm-06f290ba:wasm-function[8574]:0x668a2d)
at async file:///Users/mho/xdebug/template/node_modules/@php-wasm/universal/index.js:1687:14
}
node:internal/modules/run_main:104
triggerUncaughtException(
^
RuntimeError: null function or function signature mismatch
at php.wasm.ZEND_ASSIGN_SPEC_CV_VAR_RETVAL_UNUSED_HANDLER (wasm://wasm/php.wasm-06f290ba:wasm-function[7637]:0x616df8)
at php.wasm.execute_ex (wasm://wasm/php.wasm-06f290ba:wasm-function[7463]:0x60bee2)
at php.wasm.zend_execute (wasm://wasm/php.wasm-06f290ba:wasm-function[7465]:0x60c0e8)
at php.wasm.zend_execute_scripts (wasm://wasm/php.wasm-06f290ba:wasm-function[6607]:0x5c6b63)
at php.wasm.php_execute_script (wasm://wasm/php.wasm-06f290ba:wasm-function[5701]:0x565190)
at php.wasm.wasm_sapi_handle_request (wasm://wasm/php.wasm-06f290ba:wasm-function[8574]:0x668a2d)
at async file:///Users/mho/xdebug/template/node_modules/@php-wasm/universal/index.js:1687:14
while it doesn't with node scripts/pointer.js
since I didn't modify Asyncify versions of PHP :
STARTEDint(0)
int(1)
int(2)
int(3)
int(4)
int(5)
int(6)
int(7)
int(8)
int(9)
STOPPED
However, PHP 7.2 is not crashing, only 7.4 and 7.3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another example that crashes PHP is as follows :
<?php
function test_proc() {
for ($i = 0; $i < 10; $i++) {
$h = proc_open("echo hello", [
["pipe", "r"],
["pipe", "w"],
["pipe", "w"]
], $pipes);
// simulate some usage
fwrite($pipes[0], "test\n");
fclose($pipes[0]);
fclose($pipes[1]);
fclose($pipes[2]);
proc_close($h); // triggers php_proc_close
}
}
test_proc();
and returns the following error :
RuntimeError: null function or function signature mismatch
at php.wasm.zend_array_destroy (wasm://wasm/php.wasm-06f290ba:wasm-function[6874]:0x5d8636)
at php.wasm.zval_ptr_dtor (wasm://wasm/php.wasm-06f290ba:wasm-function[6568]:0x5c2f0d)
at php.wasm.zif_proc_open (wasm://wasm/php.wasm-06f290ba:wasm-function[8538]:0x6671f1)
at php.wasm.execute_internal (wasm://wasm/php.wasm-06f290ba:wasm-function[7449]:0x60af68)
at wasm://wasm/000dda0a:wasm-function[232]:0xb182
at php.wasm.ZEND_DO_FCALL_SPEC_RETVAL_USED_HANDLER (wasm://wasm/php.wasm-06f290ba:wasm-function[7866]:0x634513)
at php.wasm.execute_ex (wasm://wasm/php.wasm-06f290ba:wasm-function[7463]:0x60bee2)
at wasm://wasm/000dda0a:wasm-function[231]:0xaf05
at php.wasm.ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER (wasm://wasm/php.wasm-06f290ba:wasm-function[7861]:0x633bf4)
at php.wasm.execute_ex (wasm://wasm/php.wasm-06f290ba:wasm-function[7463]:0x60bee2)
I used xdebug to step debug in it and it actually throws the Error when the second proc_open(...)
is called.
Same for the second curl_init()
in the first example above.
I am glad to have Xdebug enabled. Yet, this won't be easy to solve.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mho22 how is the relevant function signature different with pointer casts enabled? Maybe we just need to patch one or two PHP function signatures to resolve it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to be related to the destructor logic around here – it's interesting to see how it evolved throughout PHP releases:
- https://github.com/php/php-src/blob/PHP-7.2.22/Zend/zend_variables.c
- https://github.com/php/php-src/blob/PHP-7.3.22/Zend/zend_variables.c
- https://github.com/php/php-src/blob/PHP-8.0.22/Zend/zend_variables.c
PHP 7.2 uses a switch statement in _zval_dtor_func that defaults to no-op behavior. PHP 7.3 switched to declaring destructors in an object and calling them like zend_rc_dtor_func[GC_TYPE(p)](p);
which could lead to a null pointer exception. PHP 8.0 seems to have more assertions in place to protect from that.
zend_array_destroy is also interesting to observe between PHP releases:
- https://github.com/php/php-src/blob/PHP-7.3.22/Zend/zend_hash.c
- https://github.com/php/php-src/blob/PHP-8.0.22/Zend/zend_hash.c
- https://github.com/php/php-src/blob/PHP-8.0.22/Zend/zend_hash.c
I think our most important clue is in the function signatures – pointer event casts somehow resolves the crash.
Btw, @brandonpayton had a setup in place to debug the C code with source maps, you may want to step through that code to pinpoint where exactly it crashes. See https://github.com/Automattic/wordpress-playground-private/pull/47. @brandonpayton – let's create a doc page for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes! I was tracking a possible function signature mismatch
related to zend_register_list_destructors_ex
yesterday. I'll keep digging into this today. There is a paragraph in emscripten documentation :
Debugging function pointer issues
TheSAFE_HEAP
andASSERTION
options can catch some of these errors at runtime and provide useful information. You can also see ifEMULATE_FUNCTION_POINTER_CASTS
fixes things for you, but see later down about the overhead.
Thank you, I'll analyze your findings. I hope this won't lead to a list of arbitrary wrappers to add in specific files.
I also found out pointer casts are not compatible with dynamic linking so we have to get rid of that EMULATE_FUNCTION_POINTER_CASTS
option anyways.
packages/php-wasm/node/build.js
Outdated
|
||
return { | ||
contents: contents.replace( | ||
/import\.meta\.dirname,\s*`\.\.\/\.\.\/\.\.\//g, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! My only note is - there's nothing in the xdebug file to indicate this specific pattern matters during the build. How about we surround it with a comment explaining this maneuver and match on something dedicated? E.g.
/**
* Hack: Keeping the path working in
* both the source file and the final
* bundle requires...
*
* ...This is auto replaced in esbuild.js to...
*
* ... always modify both places. you can test if it worked like this:...
*/
readFile(
// XDEBUG_PATH_START
import.meta.dirname
// XDEBUG_PATH_END
, otherArg
);
...also, I thought esbuild supported custom loaders such as ?url
via plugins. Was that approach more complicated?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adamziel I can't find a way to make the dynamic importing with ?url
in esbuild work as it is a runtime import based on dynamic variables. Esbuild doesn't support that. I maybe missed something here but I haven't found a better solution than the direct url rewrite during build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I was missing the dynamic import part. Makes sense 👌 then yes, let's just overcommunicate the replacement
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, maybe ?url
and the dynamic import can still work together? see #2247 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On it. I need to try that again but when I meant dynamic importing
I forgot to mention the wildcard
characteristic of this line :
`../../../jspi/extensions/xdebug/${directoryName}/${fileName}`
Meaning esbuild won't process that line because of the unknown directoryName
.
But I need to try that again to be sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we populate that line statically somehow and avoid runtime interpolation? Like when we load the wasm module we have a static switch statement with a separate case
for each PHP version for this exact reason
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wordpress-playground/packages/php-wasm/web/src/lib/get-php-loader-module.ts
Lines 16 to 41 in d5fc6f5
switch (version) { | |
case '8.4': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_8_4.js'); | |
case '8.3': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_8_3.js'); | |
case '8.2': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_8_2.js'); | |
case '8.1': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_8_1.js'); | |
case '8.0': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_8_0.js'); | |
case '7.4': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_7_4.js'); | |
case '7.3': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_7_3.js'); | |
case '7.2': | |
// @ts-ignore | |
return await import('../../public/php/jspi/php_7_2.js'); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel guilty for not thinking about that before 😅 .
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@adamziel I made a commit dedicated to that improvement. It is also heavily related to the issue on static files you opened today.
As discussed, There are multiple things needed here :
- Use of await import with
?url
- Resolving imported files for Vitest using a Vite Plugin named
import-url
- Resolving imported files for ESBuild using a ESBuild Plugin named
import-url
The particularity between Vitest and ESBuild :
Vitest returns :
export default ${JSON.stringify(absoluteFilePath)};
ESBuild returns :
import path from 'path';
export default path.resolve(__dirname, ${JSON.stringify(relativePath)});`,
Some remarks :
- ESBuild: import
path
is needed even if there's a ton of path inindex.js
but they all have associated numbers. - ESBuild: __dirname is needed for CommonJS.
- Vitest: An absolute path is enough to make the test pass.
Some thoughts to myself :
- ESBuild really do need statically analyzable imports to resolve them in a plugin ?
I've left a few last notes, other than that we're there. Let's figure out this last stretch and set this PR aside until Brandon we merge fcntl. Asyncify support could be a stacked pr against this one. |
xdebug
dynamic extension to @php-wasm/node JSPIxdebug
shared extension to @php-wasm/node JSPI
…ment for xdebug extension
Done. Three last things !
|
… so?url imports relative to tests and builds
Motivation for the change, related issues
Based on @brandonpayton's original pull request and his briliant contribution on this.
This is a work in progress to dynamically enable xdebug in
@php-wasm
.Implementation details
The first step is to compile PHP as a main module, with
-s MAIN_MODULE
. This addition resulted in numerouswasm-ld
errors, which necessitated a comprehensive reformatting of the library load and skip sections within the Dockerfile. A significant amount of further understanding, analysis, and testing is required to ensure that no critical components were inadvertently removed. However, the current setup is functional, primarily for testing Xdebug.The second step is to populate the Xdebug extensions for each PHP version. A dedicated Dockerfile file is created and first builds PHP wasm in its minimal form. Then, XDebug is built with PHP wasm's
phpize
and--disable-static --enable-shared
options.The last step is the dynamic loading of the previously created
xdebug.so
file populated by the second step. To achieve this, an option is added during the runtime loading inloadNodeRuntime
. AddingwithXdebug : true
will execute multiple tasks from functionwithXdebug
inwith-xdebug.ts
:.so
file from the Node filesystem./internal/shared/extensions
.A
PHP_INI_SCAN_DIR
environment variable with value/internal/shared/extensions
is necessary to allow the use of multiple.ini
files. This is why any dynamically loaded extension should add that environment.Xdebug and the debugger he communicates with have to use the same work environment, the files used to step debug need to have the same paths. This is why we have to mount the current working directory.
Adding the option will run Xdebug. It will automatically try to connect to a socket at default port
9003
and open a communication tunnel between them. Nothing happens if no port is detected. Adding a breakpoint to the file ran byphp-wasm
will trigger it if that tunnel is set.Zend extensions and PHP versions
Important
Each version of PHP requires its own version of Xdebug because each version of PHP has a specific Zend Engine API version. Consequently, each version of Xdebug is designed to be compatible with a particular Zend Engine API version.
Example using PHP
8.2
with Xdebug8.3
:Testing Instructions
Two files are needed. A PHP file to test the step debugger. A JS file to run PHP 8.4 node JSPI.
php/xdebug.php
scripts/node.js
To achieve the test, you first need to start debugging [ with F5 or Run > Start debugging in VSCode ], then run :