WordPress / wordpress-playground

Run WordPress in the browser via WebAssembly PHP
https://w.org/playground/
GNU General Public License v2.0
1.61k stars 242 forks source link

PHP-WASM: improve phar support (Asyncify error – null function or function signature mismatch) #1241

Open eliot-akira opened 5 months ago

eliot-akira commented 5 months ago

I'm trying to run PHPUnit (in PHAR format) using wp-now, and encountered an error from @php-wasm/node. I prepared an example repo that reproduces the issue.

git clone https://github.com/eliot-akira/wp-now-phpunit-example
cd wp-now-phpunit-example
npm install
npm run start

The error message:

  WASM ERROR
  null function or function signature mismatch 

PHP CLI error RuntimeError: null function or function signature mismatch
    at php_auto_globals_create_get (wasm://wasm/033de762:wasm-function[10756]:0x5a9c45)
    at php_hash_environment (wasm://wasm/033de762:wasm-function[10670]:0x59d883)
    at dynCall_i (wasm://wasm/033de762:wasm-function[12351]:0x62eee9)
    at ret.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
    at runtime.asm.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at invoke_i `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33833:14)
    at php_request_startup (wasm://wasm/033de762:wasm-function[3796]:0x26c693)
    at dynCall_i (wasm://wasm/033de762:wasm-function[12351]:0x62eee9)
    at ret.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
    at runtime.asm.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at invoke_i `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33833:14)
    at do_cli (wasm://wasm/033de762:wasm-function[12488]:0x636ef0)
    at dynCall_iii (wasm://wasm/033de762:wasm-function[12349]:0x62ed81)
    at ret.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
    at runtime.asm.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at invoke_iii `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33756:14) {
  cause: Error
      at Asyncify.handleSleep `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:34003:45)
      at _wasm_poll_socket `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:32883:21)
      at php_pollfd_for (wasm://wasm/033de762:wasm-function[970]:0x91c74)
      at php_network_connect_socket (wasm://wasm/033de762:wasm-function[5416]:0x3926cc)
      at php_tcp_sockop_set_option (wasm://wasm/033de762:wasm-function[10420]:0x587eb1)
      at php_openssl_sockop_set_option (wasm://wasm/033de762:wasm-function[10549]:0x592aff)
      at _php_stream_set_option (wasm://wasm/033de762:wasm-function[520]:0x37949)
      at dynCall_iiiii (wasm://wasm/033de762:wasm-function[12347]:0x62ebd7)
      at ret.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
      at runtime.asm.<computed> `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
      at invoke_iiiii `/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33767:14)
      at _php_stream_xport_create (wasm://wasm/033de762:wasm-function[1715]:0xfb54c)
      at zif_stream_socket_client (wasm://wasm/033de762:wasm-function[7831]:0x49f2b8)
      at ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED_HANDLER (wasm://wasm/033de762:wasm-function[13699]:0x68d8f6)
      at execute_ex (wasm://wasm/033de762:wasm-function[12148]:0x624deb)
      at zend_execute (wasm://wasm/033de762:wasm-function[3379]:0x21da10)
}
adamziel commented 5 months ago

It could be an Asyncify issue, but it could also be dispatching another PHP Request before the previous one finished running – I can see php_request_startup is later in the call stack than zend_execute.

eliot-akira commented 5 months ago

It's nice how detailed the error stack trace is, with helpful clues to what happened.

I suppose it might help with investigating deeper if the example is not using a PHAR file but a folder of PHP files. Then parts of it could be commented out, or place an exit at various points, to narrow down the specific code that leads to the error.

So I'll try simplifying the example to get closer to the cause.

eliot-akira commented 5 months ago

OK, I tested by running PHPUnit from a Composer-installed vendor folder, and there is no WASM error. So the issue seems specific to the PHAR file.

I've updated the example repo to demonstrate the difference: npm run start for PHAR with WASM error, and npm run start:vendor for vendored folder.

For the latter, it ends with "Program terminated with exit(1)". After digging in the PHPUnit code, I found that this is expected behavior: here, the CLI exits with code 1 if no test file is specified.

For reference, I'll include the stack trace below. What's interesting is that everything is working correctly, but it still shows the "cause" of the error as Asyncify.handleSleep.

$ npm run start:vendor

> wp-now-phpunit-example@1.0.0 start:vendor
> NODE_ENV=test node index.js

Error in argument 1, char 1: no argument for option -
Usage: phpunit [options] [-f] <file> [--] [args...]
   phpunit [options] -r <code> [--] [args...]
   phpunit [options] [-B <begin_code>] -R <code> [-E <end_code>] [--] [args...]
   phpunit [options] [-B <begin_code>] -F <file> [-E <end_code>] [--] [args...]
   phpunit [options] -S <addr>:<port> [-t docroot] [router]
   phpunit [options] -- [args...]
   phpunit [options] -a

  -a               Run as interactive shell
  -c <path>|<file> Look for php.ini file in this directory
  -n               No configuration (ini) files will be used
  -d foo[=bar]     Define INI entry foo with value 'bar'
  -e               Generate extended information for debugger/profiler
  -f <file>        Parse and execute <file>.
  -h               This help
  -i               PHP information
  -l               Syntax check only (lint)
  -m               Show compiled in modules
  -r <code>        Run PHP <code> without using script tags <?..?>
  -B <begin_code>  Run PHP <begin_code> before processing input lines
  -R <code>        Run PHP <code> for every input line
  -F <file>        Parse and execute <file> for every input line
  -E <end_code>    Run PHP <end_code> after processing all input lines
  -H               Hide any passed arguments from external tools.
  -S <addr>:<port> Run with built-in web server.
  -t <docroot>     Specify document root <docroot> for built-in web server.
  -s               Output HTML syntax highlighted source.
  -v               Version number
  -w               Output source with stripped comments and whitespace.
  -z <file>        Load Zend extension <file>.

  args...          Arguments passed to script. Use -- args when first argument
                   starts with - or script is read from stdin

  --ini            Show configuration file names

  --rf <name>      Show information about function <name>.
  --rc <name>      Show information about class <name>.
  --re <name>      Show information about extension <name>.
  --rz <name>      Show information about Zend extension <name>.
  --ri <name>      Show configuration for extension <name>.

PHP CLI error ExitStatus: Program terminated with exit(1)
    at _proc_exit (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:31403:17)
    at exitJS (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:31410:5)
    at dynCall_vi (wasm://wasm/033de762:wasm-function[12342]:0x62e6cf)
    at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
    at runtime.asm.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at invoke_vi (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33745:7)
    at run_cli (wasm://wasm/033de762:wasm-function[10072]:0x55f617)
    at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
    at runtime.asm.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at Module._run_cli (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33570:66)
    at Object.ccall (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33211:20)
    at _NodePHP.cli (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:73875:47)
    at main (file://~/wp-now-phpunit-example/index.js:26:15)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
  status: 1,
  cause: Error
      at Asyncify.handleSleep (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:34003:45)
      at _wasm_poll_socket (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:32883:21)
      at php_pollfd_for (wasm://wasm/033de762:wasm-function[970]:0x91c74)
      at php_network_connect_socket (wasm://wasm/033de762:wasm-function[5416]:0x3926cc)
      at php_tcp_sockop_set_option (wasm://wasm/033de762:wasm-function[10420]:0x587eb1)
      at php_openssl_sockop_set_option (wasm://wasm/033de762:wasm-function[10549]:0x592aff)
      at _php_stream_set_option (wasm://wasm/033de762:wasm-function[520]:0x37949)
      at dynCall_iiiii (wasm://wasm/033de762:wasm-function[12347]:0x62ebd7)
      at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33018:33)
      at runtime.asm.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
      at invoke_iiiii (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:33767:14)
      at _php_stream_xport_create (wasm://wasm/033de762:wasm-function[1715]:0xfb54c)
      at zif_stream_socket_client (wasm://wasm/033de762:wasm-function[7831]:0x49f2b8)
      at ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED_HANDLER (wasm://wasm/033de762:wasm-function[13699]:0x68d8f6)
      at execute_ex (wasm://wasm/033de762:wasm-function[12148]:0x624deb)
      at zend_execute (wasm://wasm/033de762:wasm-function[3379]:0x21da10)
}

The PHAR example doesn't show the help screen, so the WASM error must be happening before it.

Well, I can work around this issue by using vendored files instead of PHAR, but I'm curious to try if the error still happens with a minimal/empty PHAR file.

eliot-akira commented 5 months ago

I added a more minimal example running the PHPUnit PHAR file with PHP-WASM only, not wp-now, and there is no WASM error.

It displays a different help screen with colors, and exit code 2. But working correctly. So it seems the WASM error arises from running a PHAR file using the PHP instance from wp-now, but not @php-wasm/node.

PHPUnit 9.6.17 by Sebastian Bergmann and contributors.

Usage:
  phpunit [options] UnitTest.php
  phpunit [options] <directory>

Code Coverage Options:
  --coverage-clover <file>    Generate code coverage report in Clover XML
                              format
  --coverage-cobertura <file> Generate code coverage report in Cobertura XML
                              format
  --coverage-crap4j <file>    Generate code coverage report in Crap4J XML
                              format
  --coverage-html <dir>       Generate code coverage report in HTML format
  --coverage-php <file>       Export PHP_CodeCoverage object to file
  --coverage-text=<file>      Generate code coverage report in text format
                              [default: standard output]
  --coverage-xml <dir>        Generate code coverage report in PHPUnit XML
                              format
  --coverage-cache <dir>      Cache static analysis results
  --warm-coverage-cache       Warm static analysis cache
  --coverage-filter <dir>     Include <dir> in code coverage analysis
  --path-coverage             Perform path coverage analysis
  --disable-coverage-ignore   Disable annotations for ignoring code coverage
  --no-coverage               Ignore code coverage configuration

Logging Options:
  --log-junit <file>          Log test execution in JUnit XML format to file
  --log-teamcity <file>       Log test execution in TeamCity format to file
  --testdox-html <file>       Write agile documentation in HTML format to file
  --testdox-text <file>       Write agile documentation in Text format to file
  --testdox-xml <file>        Write agile documentation in XML format to file
  --reverse-list              Print defects in reverse order
  --no-logging                Ignore logging configuration

Test Selection Options:
  --list-suites               List available test suites
  --testsuite <name>          Filter which testsuite to run
  --list-groups               List available test groups
  --group <name>              Only runs tests from the specified group(s)
  --exclude-group <name>      Exclude tests from the specified group(s)
  --covers <name>             Only runs tests annotated with "@covers <name>"
  --uses <name>               Only runs tests annotated with "@uses <name>"
  --list-tests                List available tests
  --list-tests-xml <file>     List available tests in XML format
  --filter <pattern>          Filter which tests to run
  --test-suffix <suffixes>    Only search for test in files with specified
                              suffix(es). Default: Test.php,.phpt

Test Execution Options:
  --dont-report-useless-tests Do not report tests that do not test anything
  --strict-coverage           Be strict about @covers annotation usage
  --strict-global-state       Be strict about changes to global state
  --disallow-test-output      Be strict about output during tests
  --disallow-resource-usage   Be strict about resource usage during small tests
  --enforce-time-limit        Enforce time limit based on test size
  --default-time-limit <sec>  Timeout in seconds for tests without @small,
                              @medium or @large
  --disallow-todo-tests       Disallow @todo-annotated tests

  --process-isolation         Run each test in a separate PHP process
  --globals-backup            Backup and restore $GLOBALS for each test
  --static-backup             Backup and restore static attributes for each
                              test

  --colors <flag>             Use colors in output ("never", "auto" or
                              "always")
  --columns <n>               Number of columns to use for progress output
  --columns max               Use maximum number of columns for progress output
  --stderr                    Write to STDERR instead of STDOUT
  --stop-on-defect            Stop execution upon first not-passed test
  --stop-on-error             Stop execution upon first error
  --stop-on-failure           Stop execution upon first error or failure
  --stop-on-warning           Stop execution upon first warning
  --stop-on-risky             Stop execution upon first risky test
  --stop-on-skipped           Stop execution upon first skipped test
  --stop-on-incomplete        Stop execution upon first incomplete test
  --fail-on-incomplete        Treat incomplete tests as failures
  --fail-on-risky             Treat risky tests as failures
  --fail-on-skipped           Treat skipped tests as failures
  --fail-on-warning           Treat tests with warnings as failures
  -v|--verbose                Output more verbose information
  --debug                     Display debugging information

  --repeat <times>            Runs the test(s) repeatedly
  --teamcity                  Report test execution progress in TeamCity format
  --testdox                   Report test execution progress in TestDox format
  --testdox-group             Only include tests from the specified group(s)
  --testdox-exclude-group     Exclude tests from the specified group(s)
  --no-interaction            Disable TestDox progress animation
  --printer <printer>         TestListener implementation to use

  --order-by <order>          Run tests in order:
                              default|defects|duration|no-depends|random|reverse|size
  --random-order-seed <N>     Use a specific random seed <N> for random order
  --cache-result              Write test results to cache file
  --do-not-cache-result       Do not write test results to cache file

Configuration Options:
  --prepend <file>            A PHP script that is included as early as
                              possible
  --bootstrap <file>          A PHP script that is included before the tests
                              run
  -c|--configuration <file>   Read configuration from XML file
  --no-configuration          Ignore default configuration file (phpunit.xml)
  --extensions <extensions>   A comma separated list of PHPUnit extensions to
                              load
  --no-extensions             Do not load PHPUnit extensions
  --include-path <path(s)>    Prepend PHP's include_path with given path(s)
  -d <key[=value]>            Sets a php.ini value
  --cache-result-file <file>  Specify result cache path and filename
  --generate-configuration    Generate configuration file with suggested
                              settings
  --migrate-configuration     Migrate configuration file to current format

PHAR Options:
  --manifest                  Print Software Bill of Materials (SBOM) in
                              plain-text format
  --sbom                      Print Software Bill of Materials (SBOM) in
                              CycloneDX XML format
  --composer-lock             Print composer.lock file used to build the PHAR

Miscellaneous Options:
  -h|--help                   Prints this usage information
  --version                   Prints the version and exits
  --atleast-version <min>     Checks that version is greater than min and exits
  --check-version             Checks whether PHPUnit is the latest version and
                              exits

ExitStatus: Program terminated with exit(2)
    at _proc_exit (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:8518:17)
    at exitJS (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:8525:5)
    at dynCall_vi (wasm://wasm/03636fe2:wasm-function[12812]:0x69952e)
    at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:10161:33)
    at runtime.asm.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at invoke_vi (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:10892:7)
    at run_cli (wasm://wasm/03636fe2:wasm-function[10402]:0x5b2dfa)
    at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:10161:33)
    at runtime.asm.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:71706:18)
    at Object.doRewind (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:10244:14) {
  status: 2,
  cause: Error
      at Asyncify.handleSleep (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:11150:45)
      at _js_popen_to_file (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:9529:21)
      at wasm_popen (wasm://wasm/03636fe2:wasm-function[2420]:0x192a5d)
      at zif_shell_exec (wasm://wasm/03636fe2:wasm-function[8190]:0x4f4731)
      at ZEND_DO_ICALL_SPEC_RETVAL_USED_HANDLER (wasm://wasm/03636fe2:wasm-function[14156]:0x6f99dc)
      at execute_ex (wasm://wasm/03636fe2:wasm-function[12618]:0x68fc43)
      at zend_execute (wasm://wasm/03636fe2:wasm-function[3444]:0x2303a5)
      at zend_execute_scripts (wasm://wasm/03636fe2:wasm-function[9274]:0x576a9d)
      at dynCall_iiiii (wasm://wasm/03636fe2:wasm-function[12817]:0x699a36)
      at ret.<computed> (~/wp-now-phpunit-example/node_modules/@php-wasm/node/index.cjs:10161:33)
}
eliot-akira commented 5 months ago

OK, I narrowed down the cause of the issue further. Created an even more minimal example (repo) using wp-now and a basically empty PHAR file. (Kind of fun side quest to figure out how to build a PHAR file using @php-wasm/node.)

git clone https://github.com/eliot-akira/wp-now-phar-example
cd wp-now-phar-example
npm install
npm run start

WASM ERROR
null function or function signature mismatch

So the issue is not related to PHPUnit, but simply running PHAR with wp-now.

The same PHAR file can be run using @php-wasm/node with no error.

npm run start:php-wasm

Hello from example.
adamziel commented 5 months ago

Fantastic reproduction case @eliot-akira, thank you! I'm marking as high priority crash. It seems like we're not getting all the stack trace functions somehow, similarly as in https://github.com/WordPress/wordpress-playground/pull/1273.

I wonder if, for Node.js, we could just switch to JSPI and solve all these problems in one go. They would still occur on the web, but hopefully to a lesser extent. I'll start a new issue.

mho22 commented 3 weeks ago

I tried to run a composer.phar file too and I had this runtime error :

RuntimeError: null function or function signature mismatch
    at php.wasm._php_stream_flush (wasm://wasm/php.wasm-038f5de6:wasm-function[1513]:0xe2a8f)
    at php.wasm._php_stream_cast (wasm://wasm/php.wasm-038f5de6:wasm-function[737]:0x5a905)
    at php.wasm.stream_array_from_fd_set (wasm://wasm/php.wasm-038f5de6:wasm-function[3842]:0x278d61)
    at php.wasm.zif_stream_select (wasm://wasm/php.wasm-038f5de6:wasm-function[8501]:0x53e12e)
    at php.wasm.ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED_HANDLER (wasm://wasm/php.wasm-038f5de6:wasm-function[15036]:0x7a78c1)
    ... 4 lines matching cause stack trace ...
    at ret.<computed> (file:///Users/mho/Work/Projects/Development/Web/Side/browser-php/package/node_modules/@php-wasm/node/index.js:14145:22) {
  cause: Error
      at Asyncify.handleSleep (file:///Users/mho/Work/Projects/Development/Web/Side/browser-php/package/node_modules/@php-wasm/node/index.js:14955:45)
      at _emscripten_sleep (file:///Users/mho/Work/Projects/Development/Web/Side/browser-php/package/node_modules/@php-wasm/node/index.js:12642:44)
      at php.wasm.__wrap_select (wasm://wasm/php.wasm-038f5de6:wasm-function[1392]:0xcf647)
      at php.wasm.zif_stream_select (wasm://wasm/php.wasm-038f5de6:wasm-function[8501]:0x53e02a)
      at php.wasm.ZEND_DO_FCALL_BY_NAME_SPEC_RETVAL_USED_HANDLER (wasm://wasm/php.wasm-038f5de6:wasm-function[15036]:0x7a78c1)
      at php.wasm.execute_ex (wasm://wasm/php.wasm-038f5de6:wasm-function[13394]:0x71fd7c)
      at php.wasm.zend_execute (wasm://wasm/php.wasm-038f5de6:wasm-function[3609]:0x24bb16)
      at php.wasm.zend_execute_scripts (wasm://wasm/php.wasm-038f5de6:wasm-function[9692]:0x5ca697)
      at php.wasm.dynCall_iiiii (wasm://wasm/php.wasm-038f5de6:wasm-function[13612]:0x73c00e)
      at ret.<computed> (file:///Users/mho/Work/Projects/Development/Web/Side/browser-php/package/node_modules/@php-wasm/node/index.js:14145:22)
}

The two missing functions where :

"zif_stream_select",\
"stream_array_from_fd_set",\

I added them in packages/php-wasm/compile/php/Dockerfile on 541 and 780, I compiled, built and I finally got this return when running Node's php.cli( [ 'php', 'vendor/bin/composer' ] ) :

Capture d’écran 2024-08-29 à 10 32 06

I am not sure if this will make every PHAR file run, but it can be a small step toward the solution.

@adamziel should I suggest a PR ?

adamziel commented 2 weeks ago

@mho22 a PR would be lovely, thank you!