Skip to content

Conversation

@mho22
Copy link
Collaborator

@mho22 mho22 commented Sep 3, 2025

Motivation for the change, related issues

This is a pull request to dynamically load Intl in PHP.wasm Web.

Related issues and pull requests

Issues

Pull requests

Implementation details

  • Removal of static Intl options in PHP compilation
  • Set up of PHP as a MAIN_MODULE in node and web
  • Correction of PHP: Do not pull WebGL in Playground web #2318 by addingworker to the [web] environment
  • Improvement of build file for shared libraries
  • Implementation of Intl dynamic extension lazy loading logic in PHP.wasm web
  • Creation of a ignore-lib-imports Vite plugin
  • Cypress E2E tests implementation for PHP.wasm web by duplicating existing ones from PHP.wasm Node
  • Creation of a virtual alias for wasm-feature-detect to simulate JSPI mode enabled based on Cypress ENV
  • CI jobs implementation to test PHP.wasm web in JSPI and Asyncify mode

Testing Instructions (or ideally a Blueprint)

CI

🧪 test-e2e-php-wasm-web-jspi
🧪 test-e2e-php-wasm-web-asyncify

Next steps

  • Experimental PHP.wasm Node JSPI 8.3
  • PHP.wasm Node JSPI
  • PHP.wasm Node Asyncify
  • Experimental PHP.wasm Web JSPI 8.3
  • Experimental PHP.wasm Web Asyncify 8.3
  • PHP.wasm Web JSPI
  • PHP.wasm Web Asyncify
  • Remove artifacts in PHP.wasm
  • Remove artifacts in Playground
  • Move Xdebug in shared directory alongside Intl

@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch 2 times, most recently from ce5ac2f to 9915b90 Compare September 9, 2025 14:53
@mho22
Copy link
Collaborator Author

mho22 commented Sep 9, 2025

Little summary :

  1. I first retried to build PHP and Intl with MAIN_MODULE for Web. It was a success.
screenshot-020
  1. I then remembered something happened when MAIN_MODULE was enabled [PHP: Do not pull WebGL in Playground web #2318]. I first tried to reproduce the document is not defined error and I easily did that by running npm run dev.

  2. I then tried to set MAIN_MODULE and SIDE_MODULE equal 2 instead of the default 1. It failed. Each file compiled successfully but I always got the same error :

PHP Startup: Invalid library (maybe not a PHP library) '/internal/shared/extensions/intl.so'

I finally found out the resulting intl.so file when compiling with SIDE_MODULE=1 was 5.6 Mb while SIDE_MODULE=2 was 78 bytes. I understood the MAIN_MODULE was then responsible for keeping the resource. I supposed this was not the solution.

  1. I came back with MAIN_MODULE=1 and tried to fix the document is not defined issue. Which I did first by replacing the problematic code :
-  var specialHTMLTargets = [0, document, window];
+  var specialHTMLTargets = [0, typeof document != 'undefined' ? document : 0, typeof window != 'undefined' ? window : 0];
  /** @suppress {duplicate } */
  var findEventTarget = (target) => {
      target = maybeCStringToJsString(target);
-      var domElement = specialHTMLTargets[target] || document.querySelector(target);
+     var domElement = specialHTMLTargets[target] || (typeof document != 'undefined' ? document.querySelector(target) : null);
      return domElement;
    };

And running npm run dev didn't crash.

  1. I kept looking for specialHTMLTargets in emscripten repository and I found these lines in libhtml5.js :
#if ENVIRONMENT_MAY_BE_WORKER || ENVIRONMENT_MAY_BE_NODE || ENVIRONMENT_MAY_BE_SHELL || PTHREADS
  $specialHTMLTargets: "[0, typeof document != 'undefined' ? document : 0, typeof window != 'undefined' ? window : 0]",
#else
  $specialHTMLTargets: "[0, document, window]",
#endif
settings.ENVIRONMENT_MAY_BE_WORKER = not settings.ENVIRONMENT or 'worker' in settings.ENVIRONMENT

 
 So I added worker to the ENVIRONMENT variable and it successfully ran npm run dev again.
 
 @adamziel WDYT? Is this the right approach or do you think I should investigate MAIN_MODULE=2 further?

@mho22
Copy link
Collaborator Author

mho22 commented Sep 9, 2025

I had in mind to also try to add php-wasm-web tests using vitest in jsdom environment and use a vitest.setup.ts file that will emulate fetch with fs.

@adamziel
Copy link
Collaborator

adamziel commented Sep 9, 2025

So I added worker to the ENVIRONMENT variable and it successfully ran npm run dev again.

Good find! It should be fine as long as we're not breaking loading that script on a regular web page (not in a worker). And if we are breaking it, that may still be fine, but let's acknowledge that and discuss any consequences.

@adamziel
Copy link
Collaborator

adamziel commented Sep 9, 2025

I had in mind to also try to add php-wasm-web tests using vitest in jsdom environment and use a vitest.setup.ts file that will emulate fetch with fs.

Let's just use a specific E2E testing setup. I've tried that in the past in this repo and jsdom was notoriously failing to simulate the browser environment or catch any real errors.

@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch 4 times, most recently from f244496 to f54d3c5 Compare September 15, 2025 12:33
@mho22
Copy link
Collaborator Author

mho22 commented Sep 15, 2025

@adamziel

Good find! It should be fine as long as we're not breaking loading that script on a regular web page (not in a worker). And if we are breaking it, that may still be fine, but let's acknowledge that and discuss any consequences.

the environment is now web,worker instead of only web so I suppose the previous behavior for web pages could be fused with the worker behavior?

Let's just use a specific E2E testing setup. I've tried that in the past in this repo and jsdom was notoriously failing to simulate the browser environment or catch any real errors.

I implemented a php-wasm-web:e2e nx command based on the playground-website:e2e one and it runs with success in CI! We could, in another pull request, maybe duplicate the files from Node to Web by replacing everything related to Vitest by Cypress?

I also managed to run the tests in JSPI and Asyncify separately by using chrome for JSPI and electron for Asyncify. I guess we can also run the tests in Asyncify mode with chrome via a different setup. On it.

@mho22
Copy link
Collaborator Author

mho22 commented Sep 15, 2025

I ended up only removing the close function in node because it made the last test crash only for node versions :

if [ "$PLATFORM" = "node" ]; then \
	# Calling close() on a file descriptor acquired through WASI syscalls can trigger a JS call/await
	# during a non-resumable C++ stack frame, leading to "RuntimeError: trying to suspend JS frames".
	# Since ICU maps the file into memory and does not require the descriptor after mapping
	# under our build context, skipping close(fd) avoids that suspension error.
	# NOTE: This means the file descriptor will remain open until process teardown.
	# This is acceptable here because ICU data files are loaded only once at startup.
	/root/replace.sh 's/close\(fd\);//' /root/icu/source/common/umapfile.cpp; \
fi; \

I tried to add the close method in JSPI_IMPORTS and JSPI_EXPORTS but the close function comes from ICU code, not PHP's Intl extension. Like an extension in an extension.

@mho22 mho22 marked this pull request as ready for review September 15, 2025 21:23
@mho22 mho22 requested a review from a team as a code owner September 15, 2025 21:23
@mho22
Copy link
Collaborator Author

mho22 commented Sep 15, 2025

@adamziel That's it I think! The first full dynamic extension with its associated tests in Node, Web and Playground.

I will clean up the old artifacts from static Intl and Playground CLI in the next pull request, to keep this one clean.

Should I leave withICU option in php-wasm-web load-runtime for backwards compatibility?

},
},
{
name: 'ignore-lib-imports',
Copy link
Collaborator

Choose a reason for hiding this comment

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

This code is duplicated – can we move the extension module to packages/vite-extensions/ and import it here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What do you think of this :

To fuse ignore-wasm-imports, ignore-lib-imports and ignore-data-imports into ignore-binary-imports and add it in the /vite-extensions directory. Load it in php-wasm-web vite.config.ts. Remove playground/ignore-wasm-imports.ts and playground/ignore-data-imports and use ignore-binary-imports from vite-extensions instead.

And as this was some modifications in multiple projects I wanted to make those modifications in a follow-up pull request. Is it ok for you?

Copy link
Collaborator

@adamziel adamziel Sep 16, 2025

Choose a reason for hiding this comment

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

A single, configurable plugin sounds lovely. Let's just make sure we can still granularly configure each aspect of it for modules that need just one or two of these options (assuming we have any). A follow-up PR sounds lovely.

* and not
* import("shared/icudt74l.js")
*
* The slice(-2) will ensure the 'public/`
Copy link
Collaborator

@adamziel adamziel Sep 16, 2025

Choose a reason for hiding this comment

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

slice(-6)? Or is the -6 wrong? It sounds like it would preserve more path segments than just shared/icudt74l.js

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

icudt74l.js is in /public/shared while the intl.so files are in /public/php/{mode}/extensions/intl/{php_version} directories this is why it has to slice 6 times instead of 2 for icudt74l.js

): Promise<EmscriptenOptions> {
const extensionName = 'intl.so';
const extensionPath = (await getIntlExtensionModule(version)).default;
const extension = await (await fetch(extensionPath)).arrayBuffer();
Copy link
Collaborator

@adamziel adamziel Sep 16, 2025

Choose a reason for hiding this comment

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

This will be executed every time loadWebRuntime() is called. Let's cache the array response and reuse it when available instead of always calling fetch(). Doing it at the service worker level makes the most sense – it already handles invalidation and makes those files available in the offline mode. Perhaps it's even already happening and we just need to confirm that. In either case, let's clarify the behavior in a comment here.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@mho22 this comment is still relevant

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

On it

@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch 3 times, most recently from 8370eeb to e8d2988 Compare September 17, 2025 17:02
@mho22
Copy link
Collaborator Author

mho22 commented Sep 17, 2025

Currently, only test-e2e-php-wasm-web-asyncify passes. Something is blocking JSPI. the resolvePHP function in load-php-runtime.ts never gets called. In fact, onRuntimeInitialized() isn’t triggered at all. I’ll investigate and figure out what’s going on.

@adamziel
Copy link
Collaborator

Thank you @mho22!

@mho22
Copy link
Collaborator Author

mho22 commented Sep 18, 2025

I had to upgrade playwright version from 1.47.1 to 1.53.2. The new version includes JSPI supported Chrome version 137. Unfortunately the test-e2e-playwright fail now. I guess I'll need to update thes tests now.

@adamziel
Copy link
Collaborator

The error says:

4539 pixels (ratio 0.01 of all image pixels) are different.

This is the diff:

website-old-diff

I think we can just increase the threshold. It doesn't seem like an actual failure. It's weird it would happen in this PR of all 🤷

@adamziel
Copy link
Collaborator

Try putting 10_000 here:

https://github.com/WordPress/wordpress-playground/blob/trunk/packages/playground/website/playwright/e2e/deployment.spec.ts#L18

@mho22
Copy link
Collaborator Author

mho22 commented Oct 27, 2025

I found two issues to fix :

  1. Blueprints V2 doesn't take features : { intl: true } into account. I will have to find how to inject some runtime options into loadNodeRuntime. Probably in a different pull request. So I disabled the playground/blueprints tests, for now.

  2. Playwright/test v1.53.2 is needed to access JSPI in chrome and make the test test-e2e-php-wasm-web-jspi possible.
    However, there is a security alert about that version so I will need to upgrade to v1.56.0 hoping nothing will break.

@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch from a7cba1e to 7469996 Compare October 27, 2025 11:13
@adamziel
Copy link
Collaborator

Blueprints V2 doesn't take features : { intl: true } into account. I will have to find how to inject some runtime options into loadNodeRuntime.

Yeah let's log it and leave it for another PR, we'll need to find the right option – probably runtimeConfiguration.playground or so.

…to version <= 1.55.0 needed to run tests with JSPI
@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch from 7469996 to 4788d0c Compare October 27, 2025 12:31
@adamziel
Copy link
Collaborator

All the tests seem to pass here - yay! Are we missing anything unrelated to test coverage?

@mho22
Copy link
Collaborator Author

mho22 commented Oct 27, 2025

Ow. I haven't recompiled PHP.wasm web with the new emscripten version yet. I just finally succeeded to rebase the project with trunk. I am currently recompiling locally 🤞 .

RUN rm -rf /root/emsdk/upstream/emscripten && \
git clone https://github.com/sbc100/emscripten.git /root/emsdk/upstream/emscripten && \
cd /root/emsdk/upstream/emscripten && \
git checkout main_module_static_v2
Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's use a fixed commit reference to avoid any surprises

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

As you suggested earlier, we are cloning sbc100 own fork of the emscripten repository, so we (but who or how?) should probably also fork its fork since he could delete its own later, right ?

Copy link
Collaborator Author

@mho22 mho22 Oct 27, 2025

Choose a reason for hiding this comment

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

I could also try to patch our emscripten version with : https://patch-diff.githubusercontent.com/raw/emscripten-core/emscripten/pull/25522.patch ? But we're on version 4.0.5 while the latest version is 4.0.18. Maybe I should try to update the version and apply the patch ?

Copy link
Collaborator

Choose a reason for hiding this comment

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

That also makes sense 👍

@mho22
Copy link
Collaborator Author

mho22 commented Oct 27, 2025

It looks like test-e2e-playwright (chromium) is having a Error: trying to suspend JS frames issue. As of Playwright 1.55.1 chromium is now using JSPI mode by default. So I suppose the error is probably related to Intl not being compiled with -sSUPPORT_LONGJMP=wasm -fwasm-exceptions in JSPI mode. I am recompiling Intl JSPI.

@mho22
Copy link
Collaborator Author

mho22 commented Oct 28, 2025

Ah! I recompiled PHP.wasm Web JSPI 8.3 with WITH_DEBUG and i got that stack trace :

Capture d’écran 2025-10-28 à 11 05 12

What did I add in my pull request that could make this part crash 🤔 ?

@mho22
Copy link
Collaborator Author

mho22 commented Oct 28, 2025

OK, this is due to the sbc100 new pull request injection. I should try my patch idea.

# Prevents "document is not defined" errors in Web Workers when using MAIN_MODULE.
# By default, Emscripten assumes MAIN_MODULE runs only in a web environment,
# so it injects WebGL and other browser-only APIs. Explicitly adding "worker"
# ensures the runtime is compatible with Web Workers as well.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Great comment

export JSPI_ADD_IMPORTS=",fd_close"; \
export JSPI_ADD_EXPORTS=",fd_close"; \
fi; \
export ASYNCIFY_FLAGS=" -s ASYNCIFY=2 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=js_open_process,js_fd_read,js_waitpid,js_process_status,js_create_input_device,wasm_setsockopt,wasm_shutdown,wasm_close,wasm_recv,__syscall_fcntl64,js_flock,js_release_file_locks,js_waitpid$JSPI_ADD_IMPORTS -sJSPI_EXPORTS=php_wasm_init,wasm_sleep,wasm_read,emscripten_sleep,wasm_sapi_handle_request,wasm_sapi_request_shutdown,wasm_poll_socket,wrap_select,__wrap_select,select,php_pollfd_for,fflush,wasm_popen,wasm_read,wasm_php_exec,run_cli,wasm_recv$JSPI_ADD_EXPORTS -s EXTRA_EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports,_malloc,setErrNo "; \
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why remove _malloc and setErrNo exports? The latter one is used in the JS library

Copy link
Collaborator Author

@mho22 mho22 Oct 28, 2025

Choose a reason for hiding this comment

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

This is related to the new emscripten version I cloned. It crashed because it used the deprecated EXTRA_EXPORTED_RUNTIME_METHODS instead of the new EXPORTED_RUNTIME_METHODS and after that change it crashed because of _malloc and setErrNo :

11.15 error: /root/emsdk/upstream/emscripten/src/postlibrary.js: undefined exported symbol: "_malloc" in EXPORTED_RUNTIME_METHODS
11.15 error: /root/emsdk/upstream/emscripten/src/postlibrary.js: undefined exported symbol: "setErrNo" in EXPORTED_RUNTIME_METHODS

description: 'The platform to build for',
},
WITH_DEBUG: {
JSPI: {
Copy link
Collaborator

Choose a reason for hiding this comment

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

any chance this name wasn't updated somewhere and we're getting a mixed asyncify/jspi build?

Copy link
Collaborator Author

@mho22 mho22 Oct 28, 2025

Choose a reason for hiding this comment

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

The test-e2e-php-wasm-web-jspi and test-e2e-php-wasm-web-asyncify are passing so I looked elsewhere.

@adamziel
Copy link
Collaborator

Ah! I recompiled PHP.wasm Web JSPI 8.3 with WITH_DEBUG and i got that stack trace :

Inline comments aside, I also wonder if it's a result of having wasm files built with different emscripten versions 🤔

@mho22
Copy link
Collaborator Author

mho22 commented Oct 28, 2025

Inline comments aside, I also wonder if it's a result of having wasm files built with different emscripten versions 🤔

Yep! I replaced my previous Dockerfile implementation with the copy patching, and the crash stopped :

- # NOTE: Temporary use PR #25522 version of Emscripten
- # to build php.wasm with MAIN_MODULE=2 ensurnig
- # correct build without increasing binary size.
- # RUN rm -rf /root/emsdk/upstream/emscripten && \
-     git clone https://github.com/sbc100/emscripten.git /root/emsdk/upstream/emscripten && \
-     cd /root/emsdk/upstream/emscripten && \
-     git checkout main_module_static_v2

- # NOTE: Temporary use PR #25522 version of Emscripten
- # to build php.wasm with MAIN_MODULE=2 ensurnig
- # correct build without increasing binary size.
- # running bootstrap.py to install npm packages.
- RUN python3 /root/emsdk/upstream/emscripten/bootstrap.py

+ # NOTE: Use of Emscripten PR #25522 patch
+ # to build php.wasm with MAIN_MODULE=2 ensuring
+ # correct build without increasing binary size.
+ # npm required to run emscripten/bootstrap.py
+ COPY ./main_module_without_relocatable.patch /root/main_module_without_relocatable.patch
+ RUN cd /root/emsdk/upstream/emscripten && \
+    git apply /root/main_module_without_relocatable.patch

While the patch is stored in packages/php-wasm/compile/base-image/main_module_without_relocatable.patch

I'll recompile PHP.wasm Web JSPI again and cross fingers.

@mho22
Copy link
Collaborator Author

mho22 commented Oct 28, 2025

It was too easy to be true. The wasm file went back to being huge. I have to investigate.

@adamziel
Copy link
Collaborator

I think I've seen MAIN_MODULE somewhere, should it always be MAIN_MODULE=2 now?

@mho22
Copy link
Collaborator Author

mho22 commented Oct 29, 2025

@adamziel Yes! The next step is MAIN_MODULE=2 but I was struggling with the RELOCATABLE part. I made a patch compatible with our current 4.0.5 emscripten version. I now will have to recompile PHP.wasm Web.

@mho22 mho22 force-pushed the add-intl-dynamic-extension-support-to-php-wasm-web branch from 5e0f913 to 4933318 Compare October 29, 2025 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants