Skip to content

Commit ffd7628

Browse files
committed
PHP.wasm: Explore WASMFS/OPFS
Trying to switch to WASMFS and lean on the native OPFS support see emscripten-core/emscripten#15949 Work in progress
1 parent b1b73f9 commit ffd7628

File tree

25 files changed

+154497
-131914
lines changed

25 files changed

+154497
-131914
lines changed

packages/php-wasm/compile/php/Dockerfile

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -332,29 +332,12 @@ RUN if [[ "${PHP_VERSION:0:1}" -le "7" && "${PHP_VERSION:2:1}" -le "3" ]]; then
332332
echo '#define HAVE_POSIX_READDIR_R 1' >> /root/php-src/main/php_config.h; \
333333
fi;
334334

335-
# Rename the original php_pollfd_for() implementation so that we can link our own version.
336-
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
337-
338-
RUN echo 'extern ssize_t wasm_read(int fd, void *buf, size_t count);' >> /root/php-src/main/php.h;
339-
RUN /root/replace.sh 's/ret = read/ret = wasm_read/g' /root/php-src/main/streams/plain_wrapper.c
340-
341335
# Provide a custom implementation of the php_exec() function that handles spawning
342336
# the process inside exec(), passthru(), system(), etc.
343337
# We effectively remove the php_exec() implementation from the build by renaming it
344338
# to an unused identifier "php_exec_old", and then we mark php_exec as extern.
345339
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
346340

347-
# Provide a custom implementation of the VCWD_POPEN() function that handles spawning
348-
# the process inside PHP_FUNCTION(popen).
349-
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
350-
RUN echo 'extern FILE *wasm_popen(const char *cmd, const char *mode);' >> /root/php-src/Zend/zend_virtual_cwd.h
351-
352-
# Provide a custom implementation of the shutdown() function.
353-
RUN perl -pi.bak -e $'s/(\s+)shutdown\(/$1 wasm_shutdown(/g' /root/php-src/sapi/cli/php_cli_server.c
354-
RUN perl -pi.bak -e $'s/(\s+)closesocket\(/$1 wasm_close(/g' /root/php-src/sapi/cli/php_cli_server.c
355-
RUN echo 'extern int wasm_shutdown(int fd, int how);' >> /root/php-src/main/php_config.h;
356-
RUN echo 'extern int wasm_close(int fd);' >> /root/php-src/main/php_config.h;
357-
358341
# Don't ship PHP_FUNCTION(proc_open) with the PHP build
359342
# so that we can ship a patched version with php_wasm.c
360343
RUN echo '' > /root/php-src/ext/standard/proc_open.h;
@@ -363,7 +346,7 @@ RUN echo '' > /root/php-src/ext/standard/proc_open.c;
363346
RUN source /root/emsdk/emsdk_env.sh && \
364347
# We're compiling PHP as emscripten's side module...
365348
export JSPI_FLAGS=$(if [ "$WITH_JSPI" = "yes" ]; then echo "-sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
366-
EMCC_FLAGS=" -sSIDE_MODULE -Dsetsockopt=wasm_setsockopt -Dphp_exec=wasm_php_exec $JSPI_FLAGS " \
349+
EMCC_FLAGS=" -sSIDE_MODULE -Dphp_exec=wasm_php_exec $JSPI_FLAGS " \
367350
# ...which means we must skip all the libraries - they will be provided in the final linking step.
368351
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" \
369352
emmake make -j1
@@ -927,7 +910,7 @@ RUN set -euxo pipefail; \
927910
mkdir -p /build/output; \
928911
source /root/emsdk/emsdk_env.sh; \
929912
if [ "$WITH_JSPI" = "yes" ]; then \
930-
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 "; \
913+
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 "; \
931914
echo '#define PLAYGROUND_JSPI 1' > /root/php_wasm_asyncify.h; \
932915
else \
933916
export ASYNCIFY_FLAGS=" -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 -s EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports $(cat /root/.emcc-php-asyncify-flags) "; \
@@ -939,6 +922,7 @@ RUN set -euxo pipefail; \
939922
"lengthBytesUTF8", \n\
940923
"FS", \n\
941924
"___wrap_select", \n\
925+
"_emscripten_stack_get_current", \n\
942926
"_wasm_set_sapi_name", \n\
943927
"_php_wasm_init", \n\
944928
"_emscripten_sleep", \n\
@@ -975,7 +959,6 @@ RUN set -euxo pipefail; \
975959
-I TSRM/ \
976960
-I /root/lib/include \
977961
-L/root/lib -L/root/lib/lib/ \
978-
-lproxyfs.js \
979962
$ASYNCIFY_FLAGS \
980963
$(cat /root/.emcc-php-wasm-flags) \
981964
-s EXPORTED_FUNCTIONS="$EXPORTED_FUNCTIONS" \
@@ -1034,21 +1017,21 @@ RUN set -euxo pipefail; \
10341017
# Emscripten produces an if that checks a stream.stream_ops.poll property. However,
10351018
# stream.stream_ops is sometimes undefined and the check fails. Let's adjust it to
10361019
# tolerate a null stream.stream_ops value.
1037-
/root/replace.sh "s/if\s*\(stream\.stream_ops\.poll\)/if (stream.stream_ops?.poll)/g" /root/output/php.js; \
1020+
# /root/replace.sh "s/if\s*\(stream\.stream_ops\.poll\)/if (stream.stream_ops?.poll)/g" /root/output/php.js; \
10381021
# Make Emscripten websockets configurable
10391022
# Emscripten makes the Websocket proxy connect to a fixed URL.
10401023
# This assumes the traffic is always forwarded to the same target.
10411024
# However, we want to support arbitrary targets, so we need to
10421025
# replace the hardcoded websocket target URL with a dynamic callback.
1043-
/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' \
1044-
/root/output/php.js; \
1026+
# /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' \
1027+
# /root/output/php.js; \
10451028
# Enable custom WebSocket constructors to support socket options.
1046-
/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 && \
1047-
if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
1048-
if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
1049-
/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; \
1050-
fi; \
1051-
fi; \
1029+
# /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 && \
1030+
# if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
1031+
# if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
1032+
# /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; \
1033+
# fi; \
1034+
# fi; \
10521035
# Add MSG_PEEK flag support in recvfrom
10531036
#
10541037
# Emscripten ignores the flags argument to ___syscall_recvfrom.
@@ -1060,11 +1043,11 @@ RUN set -euxo pipefail; \
10601043
# reading the remaining "TTP/1.1 200 OK" and not recognizing it as a valid
10611044
# status line.
10621045
# We need to patch the syscall to support the MSG_PEEK flag.
1063-
if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
1064-
/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; \
1065-
/root/replace.sh 's/recvmsg\(sock,\s*length\)\s*{/recvmsg(sock, length, flags) {/g' /root/output/php.js; \
1066-
/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; \
1067-
fi ; \
1046+
# if [ "$WITH_WS_NETWORKING_PROXY" = "yes" ]; then \
1047+
# /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; \
1048+
# /root/replace.sh 's/recvmsg\(sock,\s*length\)\s*{/recvmsg(sock, length, flags) {/g' /root/output/php.js; \
1049+
# /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; \
1050+
# fi ; \
10681051
# Replace the hardcoded ENVIRONMENT variable with a dynamic computation
10691052
#
10701053
# The JavaScript code of the web loader and web worker loader is identical,

packages/php-wasm/compile/php/php_wasm.c

Lines changed: 18 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
// Created by Dockerfile:
3131
#include "php_wasm_asyncify.h"
32+
#include <emscripten/wasmfs.h>
3233

3334
unsigned int wasm_sleep(unsigned int time)
3435
{
@@ -118,7 +119,6 @@ EM_JS(char*, js_popen_to_file, (const char *command, const char *mode, uint8_t *
118119
});
119120
});
120121

121-
122122
/**
123123
* Shims poll(2) functionallity for asynchronous websockets:
124124
* https://man7.org/linux/man-pages/man2/poll.2.html
@@ -247,112 +247,7 @@ EM_JS(int, wasm_poll_socket, (php_socket_t socketd, int events, int timeout), {
247247
});
248248
});
249249

250-
/**
251-
* Shims read(2) functionallity.
252-
* Enables reading from blocking pipes. By default, Emscripten
253-
* will throw an EWOULDBLOCK error when trying to read from a
254-
* blocking pipe. This function overrides that behavior and
255-
* instead waits for the pipe to become readable.
256-
*
257-
* @see https://github.com/WordPress/wordpress-playground/issues/951
258-
* @see https://github.com/emscripten-core/emscripten/issues/13214
259-
*/
260-
#ifdef PLAYGROUND_JSPI
261-
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), {
262-
const returnCallback = (resolver) => new Promise(resolver);
263-
#else
264-
EM_JS(__wasi_errno_t, js_fd_read, (__wasi_fd_t fd, const __wasi_iovec_t *iov, size_t iovcnt, __wasi_size_t *pnum), {
265-
const returnCallback = (resolver) => Asyncify.handleSleep(resolver);
266-
#endif
267-
if (Asyncify?.State?.Normal === undefined || Asyncify?.state === Asyncify?.State?.Normal) {
268-
var returnCode;
269-
var stream;
270-
let num = 0;
271-
try
272-
{
273-
stream = SYSCALLS.getStreamFromFD(fd);
274-
const num = doReadv(stream, iov, iovcnt);
275-
HEAPU32[pnum >> 2] = num;
276-
return 0;
277-
}
278-
catch (e)
279-
{
280-
// Rethrow any unexpected non-filesystem errors.
281-
if (typeof FS == "undefined" || !(e.name === "ErrnoError"))
282-
{
283-
throw e;
284-
}
285-
// Only return synchronously if this isn't an asynchronous pipe.
286-
// Error code 6 indicates EWOULDBLOCK – this is our signal to wait.
287-
// We also need to distinguish between a process pipe and a file pipe, otherwise
288-
// reading from an empty file would block until the timeout.
289-
if (e.errno !== 6 || !(stream?.fd in PHPWASM.child_proc_by_fd))
290-
{
291-
// On failure, yield 0 bytes read to indicate EOF.
292-
HEAPU32[pnum >> 2] = 0;
293-
return returnCode
294-
}
295-
}
296-
}
297-
298-
// At this point we know we have to poll.
299-
// You might wonder why we duplicate the code here instead of always using
300-
// Asyncify.handleSleep(). The reason is performance. Most of the time,
301-
// the read operation will work synchronously and won't require yielding
302-
// back to JS. In these cases we don't want to pay the Asyncify overhead,
303-
// save the stack, yield back to JS, restore the stack etc.
304-
return returnCallback((wakeUp) => {
305-
var retries = 0;
306-
var interval = 50;
307-
var timeout = 5000;
308-
// We poll for data and give up after a timeout.
309-
// We can't simply rely on PHP timeout here because we don't want
310-
// to, say, block the entire PHPUnit test suite without any visible
311-
// feedback.
312-
var maxRetries = timeout / interval;
313-
function poll() {
314-
var returnCode;
315-
var stream;
316-
let num;
317-
try {
318-
stream = SYSCALLS.getStreamFromFD(fd);
319-
num = doReadv(stream, iov, iovcnt);
320-
returnCode = 0;
321-
} catch (e) {
322-
if (
323-
typeof FS == 'undefined' ||
324-
!(e.name === 'ErrnoError')
325-
) {
326-
console.error(e);
327-
throw e;
328-
}
329-
returnCode = e.errno;
330-
}
331250

332-
const success = returnCode === 0;
333-
const failure = (
334-
++retries > maxRetries ||
335-
!(fd in PHPWASM.child_proc_by_fd) ||
336-
PHPWASM.child_proc_by_fd[fd]?.exited ||
337-
FS.isClosed(stream)
338-
);
339-
340-
if (success) {
341-
HEAPU32[pnum >> 2] = num;
342-
wakeUp(0);
343-
} else if (failure) {
344-
// On failure, yield 0 bytes read to indicate EOF.
345-
HEAPU32[pnum >> 2] = 0;
346-
// If the failure is due to a timeout, return 0 to indicate that we
347-
// reached EOF. Otherwise, propagate the error code.
348-
wakeUp(returnCode === 6 ? 0 : returnCode);
349-
} else {
350-
setTimeout(poll, interval);
351-
}
352-
}
353-
poll();
354-
})
355-
});
356251
extern int __wasi_syscall_ret(__wasi_errno_t code);
357252

358253
// Exit code of the last exited child process call.
@@ -1825,6 +1720,23 @@ static void wasm_sapi_log_message(char *message TSRMLS_DC)
18251720
*/
18261721
int php_wasm_init()
18271722
{
1723+
int err;
1724+
// backend_t memory = wasmfs_create_memory_backend();
1725+
// The /internal directory is required by the C module. It's where the
1726+
// stdout, stderr, and headers information are written for the JavaScript
1727+
// code to read later on.
1728+
// err = wasmfs_create_directory("/internal", 0777, memory);
1729+
// err = wasmfs_create_directory("/wordpress", 0777, memory);
1730+
// The files from the shared directory are shared between all the
1731+
// PHP processes managed by PHPProcessManager.
1732+
// FS.mkdir('/internal/shared');
1733+
// The files from the preload directory are preloaded using the
1734+
// auto_prepend_file php.ini directive.
1735+
// FS.mkdir('/internal/shared/preload');
1736+
1737+
backend_t opfs = wasmfs_create_opfs_backend();
1738+
err = wasmfs_create_directory("/internal", 0777, opfs);
1739+
18281740
wasm_server_context = malloc(sizeof(wasm_server_context_t));
18291741
wasm_init_server_context();
18301742

packages/php-wasm/compile/php/phpwasm-emscripten-library.js

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,6 @@ const LibraryExample = {
1616
// JavaScript library under the PHPWASM object:
1717
$PHPWASM: {
1818
init: function () {
19-
// The /internal directory is required by the C module. It's where the
20-
// stdout, stderr, and headers information are written for the JavaScript
21-
// code to read later on.
22-
FS.mkdir('/internal');
23-
// The files from the shared directory are shared between all the
24-
// PHP processes managed by PHPProcessManager.
25-
FS.mkdir('/internal/shared');
26-
// The files from the preload directory are preloaded using the
27-
// auto_prepend_file php.ini directive.
28-
FS.mkdir('/internal/shared/preload');
29-
3019
PHPWASM.EventEmitter = ENVIRONMENT_IS_NODE
3120
? require('events').EventEmitter
3221
: class EventEmitter {

0 commit comments

Comments
 (0)