Skip to content

Commit

Permalink
PHP.wasm: Explore WASMFS/OPFS
Browse files Browse the repository at this point in the history
Trying to switch to WASMFS and lean on the native OPFS support

see emscripten-core/emscripten#15949

Work in progress
  • Loading branch information
adamziel committed Oct 9, 2024
1 parent b1b73f9 commit ffd7628
Show file tree
Hide file tree
Showing 25 changed files with 154,497 additions and 131,914 deletions.
51 changes: 17 additions & 34 deletions packages/php-wasm/compile/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -332,29 +332,12 @@ RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then
echo '#define HAVE_POSIX_READDIR_R 1' >> /root/php-src/main/php_config.h; \
fi;

# Rename the original php_pollfd_for() implementation so that we can link our own version.
RUN /root/replace.sh 's/static inline int php_pollfd_for\(/int php_pollfd_for(php_socket_t fd, int events, struct timeval *timeouttv); static inline int __real_php_pollfd_for(/g' /root/php-src/main/php_network.h

RUN echo 'extern ssize_t wasm_read(int fd, void *buf, size_t count);' >> /root/php-src/main/php.h;
RUN /root/replace.sh 's/ret = read/ret = wasm_read/g' /root/php-src/main/streams/plain_wrapper.c

# Provide a custom implementation of the php_exec() function that handles spawning
# the process inside exec(), passthru(), system(), etc.
# We effectively remove the php_exec() implementation from the build by renaming it
# to an unused identifier "php_exec_old", and then we mark php_exec as extern.
RUN /root/replace.sh 's/PHPAPI int php_exec(.+)$/PHPAPI extern int php_exec\1; int php_exec_old\1/g' /root/php-src/ext/standard/exec.c

# Provide a custom implementation of the VCWD_POPEN() function that handles spawning
# the process inside PHP_FUNCTION(popen).
RUN /root/replace.sh 's/#define VCWD_POPEN.+/#define VCWD_POPEN(command, type) wasm_popen(command,type)/g' /root/php-src/Zend/zend_virtual_cwd.h
RUN echo 'extern FILE *wasm_popen(const char *cmd, const char *mode);' >> /root/php-src/Zend/zend_virtual_cwd.h

# Provide a custom implementation of the shutdown() function.
RUN perl -pi.bak -e $'s/(\s+)shutdown\(/$1 wasm_shutdown(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN perl -pi.bak -e $'s/(\s+)closesocket\(/$1 wasm_close(/g' /root/php-src/sapi/cli/php_cli_server.c
RUN echo 'extern int wasm_shutdown(int fd, int how);' >> /root/php-src/main/php_config.h;
RUN echo 'extern int wasm_close(int fd);' >> /root/php-src/main/php_config.h;

# Don't ship PHP_FUNCTION(proc_open) with the PHP build
# so that we can ship a patched version with php_wasm.c
RUN echo '' > /root/php-src/ext/standard/proc_open.h;
Expand All @@ -363,7 +346,7 @@ RUN echo '' > /root/php-src/ext/standard/proc_open.c;
RUN source /root/emsdk/emsdk_env.sh && \
# We're compiling PHP as emscripten's side module...
export JSPI_FLAGS=$(if [ "$WITH_JSPI" = "yes" ]; then echo "-sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dphp_exec=wasm_php_exec $JSPI_FLAGS " \
EMCC_FLAGS=" -sSIDE_MODULE -Dphp_exec=wasm_php_exec $JSPI_FLAGS " \
# ...which means we must skip all the libraries - they will be provided in the final linking step.
EMCC_SKIP="-lz -ledit -ldl -lncurses -lzip -lpng16 -lssl -lcrypto -lxml2 -lc -lm -lsqlite3 /root/lib/lib/libxml2.a /root/lib/lib/libsqlite3.so /root/lib/lib/libsqlite3.a /root/lib/lib/libsqlite3.a /root/lib/lib/libpng16.so /root/lib/lib/libwebp.a /root/lib/lib/libjpeg.a" \
emmake make -j1
Expand Down Expand Up @@ -927,7 +910,7 @@ RUN set -euxo pipefail; \
mkdir -p /build/output; \
source /root/emsdk/emsdk_env.sh; \
if [ "$WITH_JSPI" = "yes" ]; then \
export ASYNCIFY_FLAGS=" -s ASYNCIFY=2 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=js_open_process,js_waitpid,js_process_status,js_create_input_device,wasm_setsockopt,wasm_shutdown,wasm_close -sJSPI_EXPORTS=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 -s EXTRA_EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports,_malloc "; \
export ASYNCIFY_FLAGS=" -sWASMFS -DWASMFS_SETUP -lopfs.js -s ASYNCIFY=2 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=js_open_process,js_waitpid,js_process_status,js_create_input_device,wasm_setsockopt,wasm_shutdown,wasm_close -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 -s EXTRA_EXPORTED_RUNTIME_METHODS=ccall,wasmExports,_malloc "; \
echo '#define PLAYGROUND_JSPI 1' > /root/php_wasm_asyncify.h; \
else \
export ASYNCIFY_FLAGS=" -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 -s EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports $(cat /root/.emcc-php-asyncify-flags) "; \
Expand All @@ -939,6 +922,7 @@ RUN set -euxo pipefail; \
"lengthBytesUTF8", \n\
"FS", \n\
"___wrap_select", \n\
"_emscripten_stack_get_current", \n\
"_wasm_set_sapi_name", \n\
"_php_wasm_init", \n\
"_emscripten_sleep", \n\
Expand Down Expand Up @@ -975,7 +959,6 @@ RUN set -euxo pipefail; \
-I TSRM/ \
-I /root/lib/include \
-L/root/lib -L/root/lib/lib/ \
-lproxyfs.js \
$ASYNCIFY_FLAGS \
$(cat /root/.emcc-php-wasm-flags) \
-s EXPORTED_FUNCTIONS="$EXPORTED_FUNCTIONS" \
Expand Down Expand Up @@ -1034,21 +1017,21 @@ RUN set -euxo pipefail; \
# Emscripten produces an if that checks a stream.stream_ops.poll property. However,
# stream.stream_ops is sometimes undefined and the check fails. Let's adjust it to
# tolerate a null stream.stream_ops value.
/root/replace.sh "s/if\s*\(stream\.stream_ops\.poll\)/if (stream.stream_ops?.poll)/g" /root/output/php.js; \
# /root/replace.sh "s/if\s*\(stream\.stream_ops\.poll\)/if (stream.stream_ops?.poll)/g" /root/output/php.js; \
# Make Emscripten websockets configurable
# Emscripten makes the Websocket proxy connect to a fixed URL.
# This assumes the traffic is always forwarded to the same target.
# However, we want to support arbitrary targets, so we need to
# replace the hardcoded websocket target URL with a dynamic callback.
/root/replace.sh $'s/if\s*\(\s*["\']string["\']\s*===\s*typeof Module\[["\']websocket["\']\]\[["\']url["\']\]\s*\)/if("function"===typeof Module["websocket"]["url"]) {\nurl = Module["websocket"]["url"](...arguments);\n}else if ("string" === typeof Module["websocket"]["url"])/g' \
/root/output/php.js; \
# /root/replace.sh $'s/if\s*\(\s*["\']string["\']\s*===\s*typeof Module\[["\']websocket["\']\]\[["\']url["\']\]\s*\)/if("function"===typeof Module["websocket"]["url"]) {\nurl = Module["websocket"]["url"](...arguments);\n}else if ("string" === typeof Module["websocket"]["url"])/g' \
# /root/output/php.js; \
# Enable custom WebSocket constructors to support socket options.
/root/replace.sh "s/ws\s*=\s*new WebSocketConstructor/if (Module['websocket']['decorator']) {WebSocketConstructor = Module['websocket']['decorator'](WebSocketConstructor);}ws = new WebSocketConstructor/g" /root/output/php.js && \
if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
/root/replace.sh "s/sock\.server\s*=\s*new WebSocketServer/if (Module['websocket']['serverDecorator']) {WebSocketServer = Module['websocket']['serverDecorator'](WebSocketServer);}sock.server = new WebSocketServer/g" /root/output/php.js; \
fi; \
fi; \
# /root/replace.sh "s/ws\s*=\s*new WebSocketConstructor/if (Module['websocket']['decorator']) {WebSocketConstructor = Module['websocket']['decorator'](WebSocketConstructor);}ws = new WebSocketConstructor/g" /root/output/php.js && \
# if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
# if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
# /root/replace.sh "s/sock\.server\s*=\s*new WebSocketServer/if (Module['websocket']['serverDecorator']) {WebSocketServer = Module['websocket']['serverDecorator'](WebSocketServer);}sock.server = new WebSocketServer/g" /root/output/php.js; \
# fi; \
# fi; \
# Add MSG_PEEK flag support in recvfrom
#
# Emscripten ignores the flags argument to ___syscall_recvfrom.
Expand All @@ -1060,11 +1043,11 @@ RUN set -euxo pipefail; \
# reading the remaining "TTP/1.1 200 OK" and not recognizing it as a valid
# status line.
# We need to patch the syscall to support the MSG_PEEK flag.
if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
/root/replace.sh 's/sock\.sock_ops\.recvmsg\(sock,\s*len\);/sock.sock_ops.recvmsg(sock, len, typeof flags !== "undefined" ? flags : 0);/g' /root/output/php.js; \
/root/replace.sh 's/recvmsg\(sock,\s*length\)\s*{/recvmsg(sock, length, flags) {/g' /root/output/php.js; \
/root/replace.sh 's/if\s*\(sock\.type\s*===\s*1\s*&&\s*bytesRead\s*<\s*queuedLength\)/if (flags&2) {bytesRead = 0;} if (sock.type === 1 && bytesRead < queuedLength)/g' /root/output/php.js; \
fi ; \
# if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
# /root/replace.sh 's/sock\.sock_ops\.recvmsg\(sock,\s*len\);/sock.sock_ops.recvmsg(sock, len, typeof flags !== "undefined" ? flags : 0);/g' /root/output/php.js; \
# /root/replace.sh 's/recvmsg\(sock,\s*length\)\s*{/recvmsg(sock, length, flags) {/g' /root/output/php.js; \
# /root/replace.sh 's/if\s*\(sock\.type\s*===\s*1\s*&&\s*bytesRead\s*<\s*queuedLength\)/if (flags&2) {bytesRead = 0;} if (sock.type === 1 && bytesRead < queuedLength)/g' /root/output/php.js; \
# fi ; \
# Replace the hardcoded ENVIRONMENT variable with a dynamic computation
#
# The JavaScript code of the web loader and web worker loader is identical,
Expand Down
124 changes: 18 additions & 106 deletions packages/php-wasm/compile/php/php_wasm.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

// Created by Dockerfile:
#include "php_wasm_asyncify.h"
#include <emscripten/wasmfs.h>

unsigned int wasm_sleep(unsigned int time)
{
Expand Down Expand Up @@ -118,7 +119,6 @@ EM_JS(char*, js_popen_to_file, (const char *command, const char *mode, uint8_t *
});
});


/**
* Shims poll(2) functionallity for asynchronous websockets:
* https://man7.org/linux/man-pages/man2/poll.2.html
Expand Down Expand Up @@ -247,112 +247,7 @@ EM_JS(int, wasm_poll_socket, (php_socket_t socketd, int events, int timeout), {
});
});

/**
* Shims read(2) functionallity.
* Enables reading from blocking pipes. By default, Emscripten
* will throw an EWOULDBLOCK error when trying to read from a
* blocking pipe. This function overrides that behavior and
* instead waits for the pipe to become readable.
*
* @see https://github.com/WordPress/wordpress-playground/issues/951
* @see https://github.com/emscripten-core/emscripten/issues/13214
*/
#ifdef PLAYGROUND_JSPI
EM_ASYNC_JS(__wasi_errno_t, js_fd_read, (__wasi_fd_t fd, const __wasi_iovec_t *iov, size_t iovcnt, __wasi_size_t *pnum), {
const returnCallback = (resolver) => new Promise(resolver);
#else
EM_JS(__wasi_errno_t, js_fd_read, (__wasi_fd_t fd, const __wasi_iovec_t *iov, size_t iovcnt, __wasi_size_t *pnum), {
const returnCallback = (resolver) => Asyncify.handleSleep(resolver);
#endif
if (Asyncify?.State?.Normal === undefined || Asyncify?.state === Asyncify?.State?.Normal) {
var returnCode;
var stream;
let num = 0;
try
{
stream = SYSCALLS.getStreamFromFD(fd);
const num = doReadv(stream, iov, iovcnt);
HEAPU32[pnum >> 2] = num;
return 0;
}
catch (e)
{
// Rethrow any unexpected non-filesystem errors.
if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
{
throw e;
}
// Only return synchronously if this isn't an asynchronous pipe.
// Error code 6 indicates EWOULDBLOCK – this is our signal to wait.
// We also need to distinguish between a process pipe and a file pipe, otherwise
// reading from an empty file would block until the timeout.
if (e.errno !== 6 || !(stream?.fd in PHPWASM.child_proc_by_fd))
{
// On failure, yield 0 bytes read to indicate EOF.
HEAPU32[pnum >> 2] = 0;
return returnCode
}
}
}

// At this point we know we have to poll.
// You might wonder why we duplicate the code here instead of always using
// Asyncify.handleSleep(). The reason is performance. Most of the time,
// the read operation will work synchronously and won't require yielding
// back to JS. In these cases we don't want to pay the Asyncify overhead,
// save the stack, yield back to JS, restore the stack etc.
return returnCallback((wakeUp) => {
var retries = 0;
var interval = 50;
var timeout = 5000;
// We poll for data and give up after a timeout.
// We can't simply rely on PHP timeout here because we don't want
// to, say, block the entire PHPUnit test suite without any visible
// feedback.
var maxRetries = timeout / interval;
function poll() {
var returnCode;
var stream;
let num;
try {
stream = SYSCALLS.getStreamFromFD(fd);
num = doReadv(stream, iov, iovcnt);
returnCode = 0;
} catch (e) {
if (
typeof FS == 'undefined' ||
!(e.name === 'ErrnoError')
) {
console.error(e);
throw e;
}
returnCode = e.errno;
}

const success = returnCode === 0;
const failure = (
++retries > maxRetries ||
!(fd in PHPWASM.child_proc_by_fd) ||
PHPWASM.child_proc_by_fd[fd]?.exited ||
FS.isClosed(stream)
);

if (success) {
HEAPU32[pnum >> 2] = num;
wakeUp(0);
} else if (failure) {
// On failure, yield 0 bytes read to indicate EOF.
HEAPU32[pnum >> 2] = 0;
// If the failure is due to a timeout, return 0 to indicate that we
// reached EOF. Otherwise, propagate the error code.
wakeUp(returnCode === 6 ? 0 : returnCode);
} else {
setTimeout(poll, interval);
}
}
poll();
})
});
extern int __wasi_syscall_ret(__wasi_errno_t code);

// Exit code of the last exited child process call.
Expand Down Expand Up @@ -1825,6 +1720,23 @@ static void wasm_sapi_log_message(char *message TSRMLS_DC)
*/
int php_wasm_init()
{
int err;
// backend_t memory = wasmfs_create_memory_backend();
// The /internal directory is required by the C module. It's where the
// stdout, stderr, and headers information are written for the JavaScript
// code to read later on.
// err = wasmfs_create_directory("/internal", 0777, memory);
// err = wasmfs_create_directory("/wordpress", 0777, memory);
// The files from the shared directory are shared between all the
// PHP processes managed by PHPProcessManager.
// FS.mkdir('/internal/shared');
// The files from the preload directory are preloaded using the
// auto_prepend_file php.ini directive.
// FS.mkdir('/internal/shared/preload');

backend_t opfs = wasmfs_create_opfs_backend();
err = wasmfs_create_directory("/internal", 0777, opfs);

wasm_server_context = malloc(sizeof(wasm_server_context_t));
wasm_init_server_context();

Expand Down
11 changes: 0 additions & 11 deletions packages/php-wasm/compile/php/phpwasm-emscripten-library.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,6 @@ const LibraryExample = {
// JavaScript library under the PHPWASM object:
$PHPWASM: {
init: function () {
// The /internal directory is required by the C module. It's where the
// stdout, stderr, and headers information are written for the JavaScript
// code to read later on.
FS.mkdir('/internal');
// The files from the shared directory are shared between all the
// PHP processes managed by PHPProcessManager.
FS.mkdir('/internal/shared');
// The files from the preload directory are preloaded using the
// auto_prepend_file php.ini directive.
FS.mkdir('/internal/shared/preload');

PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE
? require('events').EventEmitter
: class EventEmitter {
Expand Down
Loading

0 comments on commit ffd7628

Please sign in to comment.