Skip to content
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

JSPI support #1339

Draft
wants to merge 14 commits into
base: trunk
Choose a base branch
from
Draft

JSPI support #1339

wants to merge 14 commits into from

Conversation

adamziel
Copy link
Collaborator

@adamziel adamziel commented Apr 26, 2024

What is this PR doing?

🚧 DO NOT MERGE – it would break the web and Node.js Playground for everyone 🚧

I got Playground to build with JSPI support!

This PR is based on the CURL PR by @mho22 so it has a bit more than we need, I'll clean it up later.

Testing

  • Use PHP 8.0 – this is the only version rebuilt with JSPI at the moment.

In the browser

  • Open the local web server and it should just work.
  • Feel free to turn on networking and kitchen sink or just try on the light build.

In Node.js

Run this on Node v22:

nx reset ; DEV=1 node --experimental-wasm-stack-switching ./cli.js ./test.php

Runtime support as of April 29th, 2024

JSPI is supported on:

  • βœ… Google Chrome with #enable-experimental-webassembly-jspi enabled at chrome://flags, or with sites where the JSPI origin trial is enabled.
  • βœ… Node.js v22+ with --experimental-wasm-stack-switching feature flag.
  • ❌ Safari, Firefox, etc.
  • ❌ Bun, Node.js <= 21
  • ❔ I'm Not sure about Chrome-based browsers

Debugging Asyncify issues with JSPI

  1. Clone this branch
  2. Add this code snippet somewhere in the relevant php.js module:
			global.asyncifyFunctions = new Set();
			const originalHandleSleep = Asyncify.handleSleep;
			Asyncify.handleSleep = function (fn) {
				const e = new Error();
				for (const elem of extractPHPFunctionsFromStack(e.stack)) {
					global.asyncifyFunctions.add(elem);
				}
				return originalHandleSleep(fn);
			};
  1. Run the Asyncify crash reproduction and log global.asyncifyFunctions (or log just e.stack in that snippet)
  2. Get all the call stack functions to append to ASYNCIFY_ONLY list
  3. Repeat for each PHP version 😭

Remaining work

  • Decide on a rollout strategy. We could ship the kitchen sink JSPI build to Chrome-based browsers to reduce the amount of errors, and still maintain the Asyncify build for all the other browsers. We'll likely have to do that anyway for the next year or two as I don't expect all the relevant desktop and mobile browsers to roll out JSPI support earlier than that.
  • Implement a wasm_free method to export here instead of _free as we can no longer use the _free reference.

Rant

Building for JSPI is extremely difficult.

I tried JSPI a year ago and gave up. These errors seemed opaque, there were no documentation or guidance on how to solve them, I'd have to debug emscripten which I wasn't willing to do as I didn't have enough confidence JSPI even worked.

This time I also nearly gave up, concluding Emscripten doesn't support this or that feature – there's a lot of discussions with conflicting information on the internet. Then I created a repository with a bunch of simpler builds just to verify the features PHP uses can actually work – and they all worked once I figured out the right flags.

There are cryptic errors behind every corner, there's no documentation, and even tracing the error message to the line that creates an error in v8 isn't very helpful as it's unclear how to proceed.

I only got it to work because I got lucky. I went down the wrong rabbit hole in the beginning, chasing a -fwasm-exceptions flag and a -sSUPPORT_LONGJMP=wasm flag that I thought was necessary to build. It wasn't, I just tried using libraries built without JSPI with an older Emscripten. But spending a few hours reading related issues allowed me to make the mental connections I needed to fix a few errors later on, like:

  • missing function: saveSetjmp (when a dependent library was build without -fwasm-exceptions -sSUPPORT_LONGJMP=wasm or with -fexceptions or with -sSUPPORT_LONGJMP=emscripten).
  • invalid suspender object for suspend (when the called function is not listen in the ASYNCIFY_EXPORT list)
  • trying to suspend JS frames (When the main program is built without -fwasm-exceptions -sSUPPORT_LONGJMP=wasm and an asynchronous call happens with a JavaScript invoke_iii function in a call stack, apparently JSPI doesn't do stack switching like Asyncify did but sets aside an entire wasm context. Because there is no stack rewinding in the same way as with Asyncify, there's no way to put the JS functions back in the call stack when the call is resumed later on.)

At one point I lucky-guessed an undocumented ASYNCIFY_EXPORT option exists.

At another I found someone on GitHub posing a JavaScript patch necessary to get the JavaScript module Emscripten produces to work with dynamic calls:

                if(!original.sig && x.startsWith("invoke_")) {
                    const l = "invoke_".length;
                    original.sig = x[l] + "i" + x.slice(l + 1);
                }

That patch solved the problem at hand, but later on it turned out my dependencies were built in a wrong way and I had to go back and rebuild everything.

It would be so useful to have documentation that explains:

  • How to build a larger codebase that uses dynamic calls, setjmp etc.
  • 20 most common issues and how to solve them.
  • What compilation flags even exist, what values they take, what this does with the binary.
  • Which flags matter for compilation, which flags matter for linking.
  • How to properly use those flags with programs shipping their own make pipeline when there is no direct access to emcc cc call (Playground patches the emcc script to both add and remove flags).

What I've learned

Consider the following example:

// main.c
int get_http_status_code() {
	char* url = get_url();
	int code = javascript_fetch_status_code(url);
	return code;
}
// handlers.js
const HandlersLibrary = {
    javascript_fetch_status_code(url) {
         return Asyncify.handleSleep( async wakeUp => {
                const response = await fetch(url);
                wakeUp( response.status ); // 200
         });
    }
}
mergeInto(LibraryManager.library, HandlersLibrary);

Emscripten offers two APIs for integrating asynchronous JavaScript code with the synchronous WASM code: Asyncify and JSPI. Both APIs would pause the C program when the asynchronous javascript_fetch_status_code() call is encountered, but they work in vastly different ways and it took me a while to figure out how to work with them.

Asyncify

The older API is called Asyncify and implements an idea called stack switching. When using Asyncify, the handleSleep() function saves ("unwinds") the current call stack and yields control back to JavaScript to perform the fetch() call. When the response comes back and wakeUp() is called, Asyncify "rewinds" the call stack by setting an internal Asyncify.state variable to Asyncify.State.Rewinding and calling all the functions on the call stack until it reaches the handleSleep() call. At that point, the wakeUp() argument (HTTP code 200) is returned from the javascript_fetch_status_code() function and the C (WASM) code execution continues.

The downside of stack switching is that every C and JavaScript function on the call stuck must be instrumented to support rewinding/unwinding. Why? Because our compiled get_http_status_code would we called twice: before the async call, and after the async call. However, the get_url(); function will only be called once, before the stack unwinding.

Asyncify automatically detects the C functions triggering asynchronous calls and instruments them at the compile time. JavaScript functions must be instrumented by the implementer, typically by adding a check like if( Asyncify.state === Asyncify.State.Normal ) { } to ensure the code before return handleSleep() only runs once before the stack unwinding and is not executed again by the rewinding process.

The compilation is simple:

emcc 
    --js-library handlers.js
    -s ASYNCIFY=1
    -s ASYNCIFY_IMPORTS=javascript_fetch_status_code
    main.c

However, the automated instrumentation added by Asyncify makes the build larger and slower.

Asyncify Overhead

For PHP.wasm, the automated Asyncify instrumentation made the build 2x larger and ~4x slower, meaning it took 4s-5s to boot. Would you use a CLI tool that makes you wait at least 5s for the output every time? I wouldn't, so I found an alternative approach.

Asyncify allows you to provide a list of specific functions to instrument via the -s ASYNCIFY_ONLY flag and skip all the automated detection. I did that and, today, the PHP.wasm build command lists around 300 PHP functions that may appear on the call stack in different asynchronous contexts. That's a huge improvement over the 70,000 functions that Asyncify would otherwise auto-instrument.

Debugging issues

Missing even a single function from the ASYNCIFY_ONLY list leads a fatal crash when that function is present on the call stack when the asynchronous call happens.

The message is typically a cryptic "unreachable" WASM instruction executed with no additional debugging information. You can't even check the stack trace because the way Emscripten optimizes against memory leaks gives you no useful .stack property on caught JavaScript errors.

WordPress Playground patches Emscripten to preserve the stack traces and report which C function wasn't properly instrumented, but it's not perfect and sometimes won't reveal the right function.

In those scenarios, we typically had to read the PHP (or curl, or openssl, or...) source code and try to figure out the right function to list – as you can imagine it was a difficult and tedious process.

Summary

  • It's supported in all the major browsers.
  • Both JavaScript and WebAssembly functions can be found on the call stack.
  • Asyncify uses stack switching and needs to know at the compile time about all the functions that will be on the call stack in at runtime when an asynchronous operation starts.

JSPI

JSPI is a newer API that enables async operations without instrumenting all the call stack functions.

Both Asyncify and JSPI perform stack switching, but Asyncify does it in JavaScript while JSPI does it in WebAssembly thanks to new browser APIs outlined in the spec proposal.

In practice, this means:

  • JSPI only instruments asynchronous WASM exports, not all the functions on the call stack.
  • JSPI doesn't support JavaScript functions on the call stack at the time of the asynchronous call (there are nuances I'll get to shortly).

While Asyncify played nicely with Emscripten-generated JavaScript wrappers for dynamic calls, e.g. invoke_i, invoke_viii, or dynCall_iiii, JSPI won't work when they're present. It will error out trying to suspend JS frames. You need to rebuild your WASM program and all the depedent libraries with -sSUPPORT_LONGJMP=wasm -fwasm-exceptions to get rid of those wrappers. Building with either no flags or -sSUPPORT_LONGJMP=emscripten -fexceptions will produce the JSPI-incompatible JavaScript wrappers.
Note these are compile-time flags, not linker flags – I lost a few hours figuring that out.

If you notice any invoke_* functions in the built JavaScript module, it's likely in there because one of your build dependencies was built with JavaScript instrumentation, not WASM instrumentation. You'll have to go back and rebuild it and all its dependencies until the entire build graph is JSPI-compatible.

Another thing – Asyncify supported asynchronous calls in functions exposed from a --js-library, but with JSPI you need to use the EM_ASYNC_JS C macro and specify those functions in C. I didn't figure out exactly why yet, as their JavaScript part is still included verbatim in the final JS module, but it didn't work for us otherwise.

Summary

  • JSPI is still experimental and only supported behind a feature flag / origin trial in Chrome and Node v22.
  • Only WebAssembly functions can be found on the call stack.
  • JSPI only need to know about the topmost WASM functions on the call stack (via ASYNCIFY_EXPORTS) and the asynchronous JS function they call (via ASYNCIFY_IMPORTS).

Related resources

These were helpful along the way:

cc @brandonpayton @mho22 @dmsnell @bgrgicak

MHO and others added 4 commits April 18, 2024 10:26
Got stuck at

```
RuntimeError: trying to suspend JS frames
```

Maybe I need to build with wasm flags
@mho22
Copy link
Contributor

mho22 commented Apr 27, 2024

@adamziel If I'm understanding this correctly, will JSPI ultimately replace Asyncify? I tried to run the code but I couldn't build php-wasm-node :

nx run php-wasm-node:build

> nx run php-wasm-node:"build:package-json"

Convert compiler options from json failed, Argument for '--lib' option must be: 'es5', 'es6', 'es2015', 'es7', ...

Needless to say, this still looks brilliant!

@adamziel
Copy link
Collaborator Author

@mho22 only web light 8.0 works for now, I'm figuring out a node build as we speak. JSPI will eventually replace Asyncify, yes, but I'm not sure about the timeline. I don't want to maintain two versions of PHP so maybe we'll wait for a better support in other runtimes and only use it to produce the ASYNCIFY_ONLY list for now.

@adamziel
Copy link
Collaborator Author

@mho22 I got it to work on Node.js with curl! It doesn't seem to receive any data yet, but it doesn't crash at least.

@adamziel
Copy link
Collaborator Author

Something's wrong with writing to the socket – the WS proxy seems to be connecting, but there's no data going either way.

$ DEV=1 node --experimental-wasm-stack-switching ./cli.js ./test.php                                                                                                                                    130 ↡
[WS Server] Binding the WebSockets server to 127.0.0.1:50123...
BEFORE*   Trying 172.29.1.0:443...
[WS Server]  127.0.0.1:  WebSocket connection from : 127.0.0.1 at URL /?host=wordpress.org&port=443
[WS Server]  127.0.0.1:  Version undefined, subprotocol: binary
[WS Server]  127.0.0.1:  resolving wordpress.org...
[WS Server]  127.0.0.1:  resolved wordpress.org -> 198.143.164.252
[WS Server]  127.0.0.1:  Opening a socket connection to 198.143.164.252:443
[WS Server]  127.0.0.1:  Connected to target
* Connection timed out after 5002 milliseconds
* Closing connection 0
AFTER
[WS Server]  127.0.0.1:  WebSocket client disconnected: 1005 []
[WS Server]  127.0.0.1:  target disconnected

@mho22
Copy link
Contributor

mho22 commented Apr 28, 2024

@adamziel I added an option in libcurl Dockerfile : --enable-debug to get more informations :

[WS Server] Binding the WebSockets server to 127.0.0.1:55872...
* STATE: INIT => CONNECT handle 0x10372c8; line 1619 (connection #-5000)
* Added connection 0. The cache now contains 1 members
*   Trying 172.29.1.0:443...
* STATE: CONNECT => WAITCONNECT handle 0x10372c8; line 1675 (connection #0)
[WS Server]  127.0.0.1:  WebSocket connection from : 127.0.0.1 at URL /?host=wordpress.org&port=443
[WS Server]  127.0.0.1:  Version undefined, subprotocol: binary
[WS Server]  127.0.0.1:  resolving wordpress.org...
[WS Server]  127.0.0.1:  resolved wordpress.org -> 198.143.164.252
[WS Server]  127.0.0.1:  Opening a socket connection to 198.143.164.252:443
[WS Server]  127.0.0.1:  Connected to target
* Connection timed out after 5000 milliseconds
* multi_done
* Closing connection 0
* The cache now contains 0 members
* Expire cleared (transfer 0x10372c8)
bool(false)
string(44) "Connection timed out after 5000 milliseconds"

What about that strange multi_done line ?

It seems that it correctly calls __wrap_select a million times until timeout but it doesn't call wasm_poll_socket inside it. Because none of the FD_ISSET return true:

if (FD_ISSET(i, read_fds))
{
	n += wasm_poll_socket(i, POLLIN | POLLOUT, timeoutms);
}
if (FD_ISSET(i, write_fds))
{
	n += wasm_poll_socket(i, POLLOUT, timeoutms);
}
if (FD_ISSET(i, except_fds))
{
	n += wasm_poll_socket(i, POLLERR, timeoutms);
	FD_CLR(i, except_fds);
}

Here are the two last printf I got :

MAX FD : 5
READ FDS : 48340
WRITE FDS : 48212
EXCEPT FDS : 48084
TIMEOUTTV : 0
MAX FD : 5
READ FDS : 49580
WRITE FDS : 49452
EXCEPT FDS : 49324
TIMEOUTTV : 1

After digging this, I found out the __wrap_select method is called from Curl_socket_check who is called in lib/connect.c Curl_is_connected on line 897 :

rc = SOCKET_WRITABLE(conn->tempsock[i], 0);

where SOCKET_WRITABLE is defined as follows :

#define SOCKET_WRITABLE(x,z) \
  Curl_socket_check(CURL_SOCKET_BAD, CURL_SOCKET_BAD, x, (time_t)z)

wasm_poll_socket is called ! Either one time in WRITE and in EXCEPT. But it seems the methods doesn't return 1. So I have to investigate further here.

@mho22
Copy link
Contributor

mho22 commented Apr 28, 2024

@adamziel Hourray : here we go ! Next step SENDPROTOCONNECT

⚑  nodecurl  DEV=1 node --experimental-wasm-stack-switching cli.js curl.php

[WS Server] Binding the WebSockets server to 127.0.0.1:58384...
* STATE: INIT => CONNECT handle 0x1037ab8; line 1619 (connection #-5000)
* Added connection 0. The cache now contains 1 members
*   Trying 172.29.1.0:443...
* STATE: CONNECT => WAITCONNECT handle 0x1037ab8; line 1675 (connection #0)
[WS Server]  127.0.0.1:  WebSocket connection from : 127.0.0.1 at URL /?host=wordpress.org&port=443
[WS Server]  127.0.0.1:  Version undefined, subprotocol: binary
[WS Server]  127.0.0.1:  resolving wordpress.org...
[WS Server]  127.0.0.1:  resolved wordpress.org -> 198.143.164.252
[WS Server]  127.0.0.1:  Opening a socket connection to 198.143.164.252:443
* Connected to wordpress.org (172.29.1.0) port 443 (#0)
* STATE: WAITCONNECT => SENDPROTOCONNECT handle 0x1037ab8; line 1795 (connection #0)
* Marked for [keep alive]: HTTP default
node:internal/process/promises:289
            triggerUncaughtException(err, true /* fromPromise */);
            ^

[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "#<ErrnoError>".] {
  code: 'ERR_UNHANDLED_REJECTION'
}

Node.js v22.0.0

To make this happen, I had to create a EM_ASYNC_JS method for wasm_poll_socket in php_wasm.c!

Wasm poll socket method code here
EM_ASYNC_JS(int, wasm_poll_socket, (php_socket_t socketd, int events, int timeout), {

    const POLLIN = 1;
    const POLLPRI = 2;
    const POLLOUT = 4;
    const POLLERR = 8;
    const POLLHUP = 16;
    const POLLNVAL = 32;

    return new Promise( async (wakeUp) =>
	{
      const polls = [];

	  if( socketd in PHPWASM.child_proc_by_fd )
	  {
        const procInfo = PHPWASM.child_proc_by_fd[socketd];
        if( procInfo.exited )
		{
          wakeUp(0);
          return 0;
        }
        polls.push(PHPWASM.awaitEvent(procInfo.stdout, "data"));
      }
	  else
	  {
        const sock = getSocketFromFD(socketd);
        if(!sock)
		{
          wakeUp(0);
          return 0;
        }

        const lookingFor = /* @__PURE__ */ new Set();
        if( events & POLLIN || events & POLLPRI )
		{
          if( sock.server )
		  {
            for( const client of sock.pending )
			{
              if( ( client.recv_queue || [] ).length > 0 )
			  {
                wakeUp(1);
                return 1;
              }
            }
          }
		  else if( ( sock.recv_queue || []).length > 0 )
		  {
            wakeUp(1);
            return 1;
          }
        }

        const webSockets = PHPWASM.getAllWebSockets(sock);
        if( !webSockets.length )
		{
          wakeUp(0);
          return 0;
        }

        for( const ws of webSockets )
		{
          if( events & POLLIN || events & POLLPRI )
		  {
            polls.push(PHPWASM.awaitData(ws));
            lookingFor.add("POLLIN");
          }

          if( events & POLLOUT )
		  {
            polls.push(PHPWASM.awaitConnection(ws));
            lookingFor.add("POLLOUT");
          }

          if( events & POLLHUP )
		  {
            polls.push(PHPWASM.awaitClose(ws));
            lookingFor.add("POLLHUP");
          }

          if( events & POLLERR || events & POLLNVAL )
		  {
            polls.push(PHPWASM.awaitError(ws));
            lookingFor.add("POLLERR");
          }
        }
      }
      if( polls.length === 0 )
	  {
        console.warn( "Unsupported poll event " + events + ", defaulting to setTimeout()." );

		setTimeout( function()
		{
          wakeUp(0);
          return 0;
        }, timeout );

        return 0;
      }

      const promises = polls.map(([promise]) => promise);
      const clearPolling = () => polls.forEach(([, clear]) => clear());
      let awaken = false;
      let timeoutId;

      Promise.race(promises).then( function(results)
	  {
        if( !awaken )
		{
          awaken = true;
          wakeUp(1);
          return 1;
          if( timeoutId )
		  {
			clearTimeout(timeoutId);
		  }
          clearPolling();
        }
      });

      if( timeout !== -1 )
	  {
        timeoutId = setTimeout( function()
		{
          if( !awaken )
		  {
            awaken = true;
            wakeUp(0);
            clearPolling();
            return 0;
          }
        }, timeout );
	  }
    } );
} );

@adamziel
Copy link
Collaborator Author

@mho22 It worked! I did what you said, moved a few more functions over to C, and added the } else if (FS.isSocket(stream.node.mode)) { check to wasm_poll_socket to avoid polling it like a socket if it isn't one. The output is abundant in information so here's just a part of it:

* Mark bundle as not supporting multiuse
* HTTP 1.1 or later with persistent connection
< HTTP/1.1 200 OK
< Server: nginx
< Date: Sun, 28 Apr 2024 18:00:18 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Vary: Accept-Encoding
< Strict-Transport-Security: max-age=3600
< X-Olaf: β›„
< Link: <https://wordpress.org/wp-json/>; rel="https://api.w.org/"
< Link: <https://wordpress.org/wp-json/wp/v2/pages/457>; rel="alternate"; type="application/json"
< Link: <https://w.org/>; rel=shortlink
< X-Frame-Options: SAMEORIGIN
< Alt-Svc: h3=":443"; ma=86400
< X-nc: HIT ord 2

@adamziel
Copy link
Collaborator Author

adamziel commented Apr 28, 2024

Multihandle requests also work:

function handle_for_url($url)
{
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_VERBOSE, 1);
    curl_setopt($ch, CURLOPT_TCP_NODELAY, 0);
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYSTATUS, 0);

    $streamVerboseHandle = fopen('php://stdout', 'w+');
    curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);
    return $ch;
}

$ch1 = handle_for_url('https://api.wordpress.org/stats/locale/1.0/');
$ch2 = handle_for_url('https://api.wordpress.org/stats/wordpress/1.0/');
$ch3 = handle_for_url('https://api.wordpress.org/stats/php/1.0/');

$mh = curl_multi_init();

curl_multi_add_handle($mh, $ch1);
curl_multi_add_handle($mh, $ch2);
curl_multi_add_handle($mh, $ch3);

$running = null;
do {
    curl_multi_exec($mh, $running);
} while ($running);

$output1 = curl_multi_getcontent($ch1);
$output2 = curl_multi_getcontent($ch2);
$output3 = curl_multi_getcontent($ch3);

curl_multi_remove_handle($mh, $ch1);
curl_multi_remove_handle($mh, $ch2);
curl_multi_remove_handle($mh, $ch3);
curl_multi_close($mh);

var_dump($output1);
var_dump($output2);
var_dump($output3);

@adamziel
Copy link
Collaborator Author

I replaced handleSleep with one that logs everything on the stack to hopefully get #1273 to work, here's the list that came up for a specific set of CURL options. I'm sure there's more poking around we can do to obtain more:

  'php.wasm.__wrap_select',
  'php.wasm.Curl_socket_check',
  'php.wasm.Curl_is_connected',
  'php.wasm.multi_runsingle',
  'php.wasm.curl_multi_perform',
  'php.wasm.easy_transfer',
  'php.wasm.Curl_poll',
  'php.wasm.Curl_multi_wait',
  'php.wasm.curl_multi_poll',
  'php.wasm.easy_perform',
  'php.wasm.RAND_poll',
  'php.wasm.rand_status',
  'php.wasm.RAND_status',
  'php.wasm.rand_enough',
  'php.wasm.Curl_ossl_seed',
  'php.wasm.zif_curl_multi_exec'
  "php.wasm.ossl_connect_common",
  "php.wasm.Curl_ossl_connect_nonblocking",
  "php.wasm.Curl_ssl_connect_nonblocking",
  "php.wasm.https_connecting",
  "php.wasm.Curl_readwrite",

@mho22
Copy link
Contributor

mho22 commented Apr 28, 2024

@adamziel I attempted to troubleshoot the issue on my end to understand the process, but encountered errors such as :

  • can't find function extractPHPFunctionsFromStack3
  • stack Error

There is something creating a extractPHPFunctionsFromStack3 instead of extractPHPFunctionsFromStack2 and the process.exit(0) after console.log('stack', stack) prevent the command to run correctly.

However, despite these issues, this improvement is quite exciting! I'm concerned that the 'ASYNCIFY_ONLY' list might already contain the methods you listed above, so it would be perfect to have the exact list from 'run_cli' to the end.

@adamziel
Copy link
Collaborator Author

@mho22 I must have made a typo before committing. Both extractPHPFunctionsFromStack calls in packages/php-wasm/compile/php/phpwasm-emscripten-library.js should have no numbers at the end. Oops!

I'm concerned that the 'ASYNCIFY_ONLY' list might already contain the methods you listed above,

Actually... I added them to the list, rebuilt PHP, and Curl worked :-) I'll update your PR shortly.

@mho22
Copy link
Contributor

mho22 commented Apr 28, 2024

@adamziel I hadn't seen the zif_curl_multi_exec function. You made a fantastic work!

@adamziel
Copy link
Collaborator Author

adamziel commented Apr 28, 2024

@mho22 it's teamwork :-) It wouldn't get this far without your perseverance. You've done brilliant work on curl support – thank you for that! πŸ™Œ

adamziel added a commit that referenced this pull request Apr 29, 2024
Ships the Node.js version of PHP built with `--with-libcurl` option to support the curl extension.

It also changes two nuances in the overall PHP build process:

* It replaces the `select(2)` function using `-Wl,--wrap=select` emcc
option instead of patching PHP source code – this enables supporting
asynchronous `select(2)` in curl without additional patches.
* Brings the `__wrap_select` implementation more in line with
`select(2)`, add support for `POLLERR`.
* Adds support for polling file descriptors that represent neither child
processes nor streams in `poll(2)` – that's because `libcurl` polls
`/dev/urandom`.

Builds on top of and supersedes
#1133

## Debugging Asyncify problems

The [typical way of resolving Asyncify
crashes](https://wordpress.github.io/wordpress-playground/architecture/wasm-asyncify/)
didn't work during the work on this PR. Functions didn't come up in the
error messages and even raw stack traces. The reasons are unclear.

[The JSPI build of
PHP](#1339) was
more helpful as it enabled logging the current stack trace in all the
asynchronous calls, which quickly revealed all the missing
`ASYNCIFY_ONLY` functions. This is the way to debug any future issues
until we fully migrate to JSPI.

## Testing Instructions

Confirm the CI checks pass. This PR ships a few new tests specifically
targeting networking with curl.


## Related resources

* #85
* #1093

---------

Co-authored-by: Adam ZieliΕ„ski <adam@adamziel.com>
Co-authored-by: MHO <yannick@chillpills.io>
@brandonpayton
Copy link
Member

A note in case it is valuable:
I'm able to reproduce a memory access out of bounds error from the Playground logs in WP.org Slack using this JSPI branch. It involves uploading a large GIF (~25MB) to the media page. It works OK on trunk though.

@brandonpayton
Copy link
Member

It looks like Firefox support for JSPI may be progressing well:

https://bugzilla.mozilla.org/show_bug.cgi?id=1850627#c11

@adamziel adamziel mentioned this pull request May 17, 2024
28 tasks
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.

None yet

3 participants