Skip to content

Conversation

@mho22
Copy link
Collaborator

@mho22 mho22 commented Aug 12, 2025

Motivation for the change, related issues

This is a pull request to dynamically load Intl in @php-wasm Node JSPI.

Roadmap

Related issues and pull requests

Issues

Pull requests

Implementation details

Intl Dynamic Extension Compilation JSPI

  • Creation of a dedicated shared directory in php-wasm/compile which will store the dynamic extensions build processes and files.
  • Creation of a main build.js script with options related to the dynamic extensions
  • Creation of a specific Dockerfile for the creation of the Intl extension .so file based on PHP versions and JSPI
  • Creation of a dedicated project.json file which will store the list of compilation commands related to each dynamic extension for JSPI
  • Compilation of every version of Intl Dynamic Extension JSPI

PHP.wasm Node WithIntl option

  • Loading of Intl extension based on the option withIntl [ same logic as Xdebug ]. This loads dynamically the needed version of the dynamic extension. Stores it in the filesystem. Prepare the related php ini file and load the related ICU data file.
  • Test the correct use of the extension in the php-dynamic-loading.spec.ts file.
  • Keep the Intl static extension working for PHP.wasm Node Asyncify.
  • Keep the Intl static extension compilation process for PHP.wasm Node Asyncify and Web.

Testing Instructions (or ideally a Blueprint)

test.js

import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';

const script = `<?php

$formatter = numfmt_create('en-US', NumberFormatter::CURRENCY);
echo numfmt_format($formatter, 100.00);
$formatter = numfmt_create('fr-FR', NumberFormatter::CURRENCY);
echo numfmt_format($formatter, 100.00);

?>`;

const php = new PHP( await loadNodeRuntime( '8.3', { withIntl : true } ) );

const result = await php.runStream( { code : script } );

console.log( await result.stdoutText );
> node --experimental-wasm-jspi scripts/example.js

//withIntl : true
$100.00100,00 €

//withIntl : false
<br />
<b>Fatal error</b>:  Uncaught Error: Call to undefined function numfmt_create() in /internal/eval.php:3
Stack trace:
#0 {main}
  thrown in <b>/internal/eval.php</b> on line <b>3</b><br />

Next steps

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

@mho22
Copy link
Collaborator Author

mho22 commented Aug 13, 2025

Running the following script crashes :

const response = await php.runStream({
    code: `<?php

        $data = array(
            'ET' => 'Éthiopie',
            'ES' => 'Espagne',
            'AF' => 'Afghanistan',
            'AX' => 'Åland Islands',
        );

        $collator = new Collator( 'en_US' );

        $collator->asort( $data, Collator::SORT_STRING );`
});

With stack trace :

RuntimeError: trying to suspend JS frames
 ❯ close ../../../wasm:/wasm/php.wasm-06334dee:1:11554387
 ❯ uprv_mapFile_74 ../../../wasm:/wasm/intl.so-037462a2:1:8481515
 ❯ openCommonData(char const*, int, UErrorCode*) ../../../wasm:/wasm/intl.so-037462a2:1:8234337
 ❯ extendICUData(UErrorCode*) ../../../wasm:/wasm/intl.so-037462a2:1:8235090
 ❯ doLoadFromCommonData(signed char, char const*, char const*, char const*, char const*, char const*, char const*, char const*, signed char ../../../*)(void*, char const*, char const*, UDataInfo const*), void*, UErrorCode*, UErrorCode*) (wasm:/wasm/intl.so-037462a2:1:8230142
 ❯ doOpenChoice(char const*, char const*, char const*, signed char ../../../*)(void*, char const*, char const*, UDataInfo const*), void*, UErrorCode*) (wasm:/wasm/intl.so-037462a2:1:8226724
 ❯ udata_openChoice_74 ../../../wasm:/wasm/intl.so-037462a2:1:8230772
 ❯ icu_74::CollationRoot::load(char const*, UErrorCode&) ../../../wasm:/wasm/intl.so-037462a2:1:2233607
 ❯ void icu_74::umtx_initOnce<char const*>(icu_74::UInitOnce&, void ../../../*)(char const*, UErrorCode&), char const*, UErrorCode&) (wasm:/wasm/intl.so-037462a2:1:2235560
 ❯ ret.<computed> jspi/php_8_3.js:17015:24
    17013|             }
    17014|             ret[x] = (...args) => {
    17015|                 return original(...args);
       |                        ^
    17016|             };
    17017|             ret[x].orig = original;
 ❯ stubs.<computed> jspi/php_8_3.js:1243:24
 ❯ icu_74::CollationRoot::getRootCacheEntry(UErrorCode&) ../../../wasm:/wasm/intl.so-037462a2:1:2235110
 ❯ icu_74::CollationLoader::loadTailoring(icu_74::Locale const&, UErrorCode&) ../../../wasm:/wasm/intl.so-037462a2:1:5573222
 ❯ icu_74::Collator::makeInstance(icu_74::Locale const&, UErrorCode&) ../../../wasm:/wasm/intl.so-037462a2:1:1962284
 ❯ icu_74::Collator::createInstance(icu_74::Locale const&, UErrorCode&) ../../../wasm:/wasm/intl.so-037462a2:1:1963488
 ❯ ucol_open_74 ../../../wasm:/wasm/intl.so-037462a2:1:5577060
 ❯ collator_ctor ../../../wasm:/wasm/intl.so-037462a2:1:9488925
 ❯ zim_Collator___construct ../../../wasm:/wasm/intl.so-037462a2:1:9489311
 ❯ ZEND_DO_FCALL_SPEC_RETVAL_UNUSED_HANDLER ../../../wasm:/wasm/php.wasm-06334dee:1:5697443
 ❯ execute_ex ../../../wasm:/wasm/php.wasm-06334dee:1:5528658
 ❯ zend_execute ../../../wasm:/wasm/php.wasm-06334dee:1:5529256
 ❯ zend_execute_scripts ../../../wasm:/wasm/php.wasm-06334dee:1:5369001
 ❯ php_execute_script ../../../wasm:/wasm/php.wasm-06334dee:1:4903544
 ❯ wasm_sapi_handle_request ../../../wasm:/wasm/php.wasm-06334dee:1:6500971
 ❯ ../universal/src/lib/php.ts:647:12
 ❯ runExecutionFunction ../universal/src/lib/php.ts:977:22

I have no idea why but that could be related to the fact that the main module PHP.wasm and side module Intl.so need to coordinate their JSPI-wrapped functions. The file I/O operations need to be consistently wrapped across both modules.

@mho22
Copy link
Collaborator Author

mho22 commented Aug 15, 2025

I finally found out why I had the RuntimeError: trying to suspend JS frames crash. It was related to the close(fd) call inside the uprv_mapFile function in icu4c/source/common/umapfile.cpp on line 236 :

close(fd); /* no longer needed */

Removing that line made the extension compile. This means the file descriptor will remain open until process teardown. If this is not what we expected we can look for an improvement later.

Intl dynamic extension for every PHP version Node JSPI are now available next to Xdebug! 🎉

And all checks passed!

@mho22 mho22 marked this pull request as ready for review August 16, 2025 09:52
@brandonpayton brandonpayton requested a review from a team August 20, 2025 17:48
/**
* intl support
*/
describe('intl extension support', { skip: options.withXdebug }, () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should keep testing the asyncify version

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

done.

@adamziel
Copy link
Collaborator

Almost there @mho22, I've left a few logistic notes to make sure the rest of the project continues to work once this PR is merged.

Copy link
Collaborator

@adamziel adamziel left a comment

Choose a reason for hiding this comment

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

See the previous comment

@mho22 mho22 requested a review from adamziel August 26, 2025 15:25
@mho22
Copy link
Collaborator Author

mho22 commented Aug 26, 2025

@adamziel I reverted the code needed for PHP.wasm Node Asyncify and PHP.wasm Web, made some changes and all tests passed. A new pull request is already on the works for Intl dynamic extension on PHP.wasm Node Asyncify.

@adamziel
Copy link
Collaborator

Lovely! So this PR now loads ICU as a dynamic extension in Node + JSPI and as a static extension otherwise, correct? It looks pretty good now! Let's just hold on with merging this one until we merge #2558 and release new packages with just that change.

@mho22
Copy link
Collaborator Author

mho22 commented Aug 27, 2025

@adamziel Yes! In the current state of this pull request, only PHP.wasm Node JSPI has the withIntl dynamic extension loader option. PHP.wasm Node Asyncify and PHP.wasm Web still are compiledbuilt with the static extension and only the web version has the withICU option that loads or not the ICU data file.

I already drafted the "intl dynamic extension for PHP.wasm Node ASYNCIFY" pull request here.

@adamziel adamziel merged commit 2171045 into trunk Aug 28, 2025
51 of 52 checks passed
@adamziel adamziel deleted the add-intl-dynamic-extension-support-to-php-wasm-node-jspi branch August 28, 2025 10:28
@adamziel
Copy link
Collaborator

Yay, this is amazing! Thank you @mho22 ❤️

adamziel pushed a commit that referenced this pull request Sep 2, 2025
…2501 (#2557)

## Motivation for the change, related issues

This is a pull request to dynamically load Intl in @php-wasm Node
ASYNCIFY.

## Related issues and pull requests

Issues 

- #2466
- #2299
- #1295

Pull requests

- #2501
- #2247
- #2187

## Implementation details

### Intl Dynamic Extension Compilation ASYNCIFY 

- Improvement to the specific Intl dynamic extension Dockerfile file
based on PHP versions and ASYNCIFY
- Modification of the dedicated `project.json` file which will store the
list of compilation commands related to each dynamic extension for
asyncify.
- Compilation of every version of Intl Dynamic Extension For Asyncify

### PHP.wasm Node WithIntl option

- Add Intl extension file import for Asyncify
- Test the correct use of the extension in the
`php-dynamic-loading.spec.ts` file.
- Keep the Intl static extension compilation process for PHP.wasm Web.

## Testing Instructions (or ideally a Blueprint)

`test.js` 

```javascript
import { PHP } from '@php-wasm/universal';
import { loadNodeRuntime } from '@php-wasm/node';

const script = `<?php

$formatter = numfmt_create('en-US', NumberFormatter::CURRENCY);
echo numfmt_format($formatter, 100.00);
$formatter = numfmt_create('fr-FR', NumberFormatter::CURRENCY);
echo numfmt_format($formatter, 100.00);

?>`;

const php = new PHP( await loadNodeRuntime( '8.3', { withIntl : true } ) );

const result = await php.runStream( { code : script } );

console.log( await result.stdoutText );
```

```
> node scripts/example.js

//withIntl : true
$100.00100,00 €

//withIntl : false
<br />
<b>Fatal error</b>:  Uncaught Error: Call to undefined function numfmt_create() in /internal/eval.php:3
Stack trace:
#0 {main}
  thrown in <b>/internal/eval.php</b> on line <b>3</b><br />
```

## Next steps

- [x] Experimental PHP Node JSPI 8.3
- [x] PHP.wasm Node JSPI 
- [x] PHP.wasm Node  Asyncify
- [ ] PHP.wasm Web
- [ ] Remove artifacts in PHP.wasm
- [ ] Remove artifacts in Playground
- [ ] Move Xdebug in shared directory alongside Intl
@mho22 mho22 mentioned this pull request Nov 26, 2025
3 tasks
adamziel added a commit that referenced this pull request Nov 28, 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 

- #2466
- #2299
- #1295

Pull requests

- #2557
- #2501
- #2247
- #2187

## Implementation details

- Removal of static Intl options in PHP compilation
- Set up of PHP as a `MAIN_MODULE` in node and web 
- Correction of #2318 by adding`worker` 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
- Playwright 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 Playwright 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

- [x] Experimental PHP.wasm Node JSPI 8.3
- [x] PHP.wasm Node JSPI 
- [x] PHP.wasm Node  Asyncify
- [x] Experimental PHP.wasm Web JSPI 8.3
- [x] Experimental PHP.wasm Web Asyncify 8.3
- [x] PHP.wasm Web JSPI
- [x] PHP.wasm Web Asyncify
- [ ] Implement Intl in Blueprints
- [ ] Remove remaining Intl artifacts in PHP.wasm
- [ ] Remove remaining Intl artifacts in Playground

---------

Co-authored-by: Adam Zieliński <adam@adamziel.com>
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