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

PHP.wasm: Explore WASMFS/OPFS #1878

Draft
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 17 additions & 35 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,14 +922,14 @@ 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\
"_wasm_sleep", \n\
"_wasm_set_phpini_path", \n\
"_wasm_add_SERVER_entry", \n\
"_wasm_add_ENV_entry", \n\
"_wasm_read", \n\
"_wasm_free", \n\
"_wasm_sapi_handle_request", \n\
"_wasm_sapi_request_shutdown", \n\
Expand Down Expand Up @@ -975,7 +958,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 +1016,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 +1042,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
183 changes: 29 additions & 154 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 @@ -548,29 +443,6 @@ static size_t handle_line(int type, zval *array, char *buf, size_t bufl)
return bufl;
}

/**
* 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
*/
EMSCRIPTEN_KEEPALIVE ssize_t wasm_read(int fd, void *buf, size_t count)
{
struct __wasi_iovec_t iov = {
.buf = buf,
.buf_len = count};
size_t num;
if (__wasi_syscall_ret(js_fd_read(fd, &iov, 1, &num)))
{
return -1;
}
return num;
}

/*
* If type==0, only last line of output is returned (exec)
* If type==1, all lines will be printed and last lined returned (system)
Expand Down Expand Up @@ -693,31 +565,6 @@ EMSCRIPTEN_KEEPALIVE int wasm_php_exec(int type, const char *cmd, zval *array, z

int wasm_socket_has_data(php_socket_t fd);

/* hybrid select(2)/poll(2) for a single descriptor.
* timeouttv follows same rules as select(2), but is reduced to millisecond accuracy.
* Returns 0 on timeout, -1 on error, or the event mask (ala poll(2)).
*/
EMSCRIPTEN_KEEPALIVE inline int php_pollfd_for(php_socket_t fd, int events, struct timeval *timeouttv)
{
php_pollfd p;
int n;

p.fd = fd;
p.events = events;
p.revents = 0;

// must yield back to JS event loop to get the network response:
wasm_poll_socket(fd, events, php_tvtoto(timeouttv));

n = php_poll2(&p, 1, php_tvtoto(timeouttv));

if (n > 0)
{
return p.revents;
}

return n;
}

ZEND_BEGIN_ARG_INFO_EX(arginfo_post_message_to_js, 0, 1, 1)
ZEND_ARG_INFO(0, data)
Expand Down Expand Up @@ -1825,6 +1672,34 @@ 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);

backend_t opfs2 = wasmfs_create_opfs_backend();
err = wasmfs_create_directory("/internal2", 0777, opfs2);

FILE *file = fopen("/internal2/hi.txt", "w");
if (file != NULL) {
fprintf(file, "Hello World");
fclose(file);
} else {
fprintf(stderr, "Error creating file in /internal2/hi.txt\n");
}

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
Loading