diff --git a/Dockerfile b/Dockerfile index 5efc045..bf5c01b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -46,7 +46,7 @@ RUN mkdir -p /zeroperl && chmod 777 /zeroperl FROM base AS native-perl ARG PERL_VERSION=5.42.0 -ARG EXIFTOOL_VERSION=13.42 +ARG EXIFTOOL_VERSION=13.58 ARG BUILD_EXIFTOOL=true ENV PERL_VERSION=${PERL_VERSION} \ @@ -59,7 +59,7 @@ COPY pipeline/build-native-perl.sh pipeline/build-exiftool.sh /build/repo/pipeli RUN chmod +x /build/repo/pipeline/*.sh RUN /build/repo/pipeline/build-native-perl.sh -RUN [ "${BUILD_EXIFTOOL}" = "true" ] && /build/repo/pipeline/build-exiftool.sh || true +RUN if [ "${BUILD_EXIFTOOL}" = "true" ]; then /build/repo/pipeline/build-exiftool.sh; fi FROM native-perl AS wasi-perl @@ -94,11 +94,13 @@ FROM wasi-perl AS final ARG STACK_SIZE=8388608 ARG INITIAL_MEMORY=33554432 ARG ASYNCIFY=true +ARG ENABLE_HOST_CALLBACKS=false ARG WASM_OPT_FLAGS="" ENV STACK_SIZE=${STACK_SIZE} \ INITIAL_MEMORY=${INITIAL_MEMORY} \ ASYNCIFY=${ASYNCIFY} \ + ENABLE_HOST_CALLBACKS=${ENABLE_HOST_CALLBACKS} \ WASM_OPT_FLAGS=${WASM_OPT_FLAGS} COPY stubs/ /build/repo/stubs/ @@ -106,7 +108,7 @@ COPY stubs/ /build/repo/stubs/ RUN /build/repo/pipeline/build-wasm.sh RUN mkdir -p /artifacts && \ - cp /build/wasm/config.h /build/wasm/zeroperl.wasm /build/wasm/zeroperl_reactor.wasm /artifacts/ && \ + cp /build/wasm/config.h /build/wasm/zeroperl.wasm /build/wasm/zeroperl_reactor.wasm /build/wasm/zeroperl_command.wasm /artifacts/ && \ cp -r /zeroperl /artifacts/perl-wasi-prefix && \ [ "${BUILD_EXIFTOOL}" = "true" ] && [ -f /build/repo/exiftool.min.pl ] && \ cp /build/repo/exiftool.min.pl /artifacts/ || true diff --git a/README.md b/README.md index edef5f8..06d6238 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ container run --rm -v $(pwd)/output:/output zeroperl cp -r /artifacts/. /output/ Output in `./output/`: - `zeroperl.wasm` — reactor with asyncify - `zeroperl_reactor.wasm` — reactor without asyncify +- `zeroperl_command.wasm` — command-compatible module with a returnable `_start` export - `perl-wasi-prefix/` — Perl library prefix - `exiftool.min.pl` — minified ExifTool (if enabled) @@ -44,15 +45,27 @@ container build --build-arg PERL_VERSION=5.42.0 --build-arg BUILD_EXIFTOOL=false | Arg | Default | | |-----|---------|--| | `PERL_VERSION` | `5.42.0` | Perl source version | -| `EXIFTOOL_VERSION` | `13.42` | ExifTool version | +| `EXIFTOOL_VERSION` | `13.58` | ExifTool version | | `BUILD_EXIFTOOL` | `true` | Include ExifTool | | `STACK_SIZE` | `8388608` | WASM stack (bytes) | | `INITIAL_MEMORY` | `33554432` | WASM initial memory (bytes) | | `ASYNCIFY` | `true` | Enable asyncify | +| `ENABLE_HOST_CALLBACKS` | `false` | Import `env.call_host_function` for host-registered Perl callbacks | | `TRIM` | `true` | Strip unused modules | +By default, the generated WebAssembly modules only require WASI imports. Use +`zeroperl.wasm` when embedding zeroperl through the exported `zeroperl_*` API. +Use `zeroperl_command.wasm` for loaders that call `instance.exports._start`, +such as drop-in replacements for the hosted `perl.objex.ai/zeroperl-1.0.0.wasm` +artifact. Its `_start` export reads WASI arguments but returns normally instead +of relying on `proc_exit`, which keeps browser loaders with simple WASI shims +from trapping after the Perl run completes. If your host uses +`zeroperl_register_function` or `zeroperl_register_method`, build with +`--build-arg ENABLE_HOST_CALLBACKS=true` and provide `env.call_host_function` +when instantiating the module. + ### Iterating on stubs/zeroperl.c Build from `final` stage to reuse cached wasi-perl: diff --git a/pipeline/build-exiftool.sh b/pipeline/build-exiftool.sh index 06ff432..10df3f2 100644 --- a/pipeline/build-exiftool.sh +++ b/pipeline/build-exiftool.sh @@ -1,7 +1,7 @@ #!/bin/sh set -e -EXIFTOOL_VERSION="${EXIFTOOL_VERSION:-13.42}" +EXIFTOOL_VERSION="${EXIFTOOL_VERSION:-13.58}" NATIVE_DIR="${NATIVE_DIR:-/build/native}" REPO_DIR="${REPO_DIR:-/build/repo}" diff --git a/pipeline/build-wasi-libs.sh b/pipeline/build-wasi-libs.sh index 97f75ee..874cad4 100755 --- a/pipeline/build-wasi-libs.sh +++ b/pipeline/build-wasi-libs.sh @@ -20,7 +20,7 @@ mkdir -p "$WORK" # --- zlib --- echo "Building zlib $ZLIB_VERSION for WASI..." cd "$WORK" -curl -fsSL "https://zlib.net/fossils/zlib-${ZLIB_VERSION}.tar.gz" | tar -xzf - +curl -fsSL "https://www.zlib.net/fossils/zlib-${ZLIB_VERSION}.tar.gz" | tar -xzf - cd "zlib-${ZLIB_VERSION}" CC="$CC" CFLAGS="$CFLAGS" AR="$AR" RANLIB="$RANLIB" \ diff --git a/pipeline/build-wasm.sh b/pipeline/build-wasm.sh index a94b3cc..2f95b0b 100644 --- a/pipeline/build-wasm.sh +++ b/pipeline/build-wasm.sh @@ -7,6 +7,15 @@ REPO_DIR="${REPO_DIR:-/build/repo}" STACK_SIZE="${STACK_SIZE:-8388608}" INITIAL_MEMORY="${INITIAL_MEMORY:-33554432}" ASYNCIFY="${ASYNCIFY:-true}" +ENABLE_HOST_CALLBACKS="${ENABLE_HOST_CALLBACKS:-false}" + +if [ "$ENABLE_HOST_CALLBACKS" = "true" ]; then + HOST_CALLBACKS_DEFINE=1 + ASYNCIFY_IMPORTS="wasi_snapshot_preview1.fd_read,env.call_host_function" +else + HOST_CALLBACKS_DEFINE=0 + ASYNCIFY_IMPORTS="wasi_snapshot_preview1.fd_read" +fi export PATH="$REPO_DIR/wasi-bin:$PATH" @@ -31,30 +40,28 @@ CFLAGS="-c -O3 -flto -DNO_MATHOMS -D_WASI_EMULATED_PROCESS_CLOCKS -D_WASI_EMULAT -D_GNU_SOURCE -D_POSIX_C_SOURCE -DBIG_TIME -Wno-implicit-function-declaration \ -Wno-null-pointer-arithmetic -Wno-incomplete-setjmp-declaration -Wno-incompatible-library-redeclaration \ -Wno-int-conversion -D_WASI_EMULATED_SIGNAL \ +-DZEROPERL_ENABLE_HOST_CALLBACKS=$HOST_CALLBACKS_DEFINE \ -include /opt/wasi-sdk/share/wasi-sysroot/include/wasm32-wasi/fcntl.h \ -I. -I$REPO_DIR/stubs -I$REPO_DIR/gen -cxx-isystem /opt/wasi-sdk/share/wasi-sysroot/include" wasic $CFLAGS zeroperl.c -o zeroperl.o +wasic $CFLAGS "$REPO_DIR/stubs/zeroperl_main.c" -o zeroperl_main.o wasic $CFLAGS "$REPO_DIR/stubs/stubs.c" -o stubs.o CFLAGS_DATA="-c -O0 -std=c23 \ -I. -I$REPO_DIR/stubs -I$REPO_DIR/gen -cxx-isystem /opt/wasi-sdk/share/wasi-sysroot/include" wasic $CFLAGS_DATA "$REPO_DIR/gen/zeroperl_data.c" -o zeroperl_data.o -wasic \ - -o zeroperl_reactor.wasm \ +link_zeroperl() { + OUTPUT="$1" + shift + + wasic \ + -o "$OUTPUT" \ -flto -g \ - -mexec-model=reactor \ + "$@" \ -z stack-size="$STACK_SIZE" -Wl,--initial-memory="$INITIAL_MEMORY" \ -static \ - -Wl,--no-entry \ - -Wl,--stack-first \ - -Wl,--export-dynamic \ - -Wl,--export=__stack_pointer \ - -Wl,--export=__memory_base \ - -Wl,--export=__table_base \ - -Wl,--export=malloc \ - -Wl,--export=free \ -DNO_MATHOMS \ -D_WASI_EMULATED_PROCESS_CLOCKS -lwasi-emulated-process-clocks \ -D_WASI_EMULATED_GETPID -lwasi-emulated-getpid \ @@ -63,7 +70,6 @@ wasic \ -D_WASI_EMULATED_SIGNAL -lwasi-emulated-signal \ -lwasi-emulated-mman \ -Wl,--strip-all \ - zeroperl.o stubs.o zeroperl_data.o \ -Wl,--whole-archive "$REPO_DIR/stubs/libasyncjmp.a" -Wl,--no-whole-archive \ -Wl,--whole-archive libperl.a -Wl,--no-whole-archive \ -Wl,--wrap=fopen -Wl,--wrap=open -Wl,--wrap=close -Wl,--wrap=read \ @@ -109,6 +115,25 @@ wasic \ -lm -lwasi-emulated-signal -lwasi-emulated-getpid \ -lwasi-emulated-process-clocks -lwasi-emulated-mman \ -ferror-limit=0 +} + +link_zeroperl zeroperl_reactor.wasm \ + -mexec-model=reactor \ + -Wl,--no-entry \ + -Wl,--stack-first \ + -Wl,--export-dynamic \ + -Wl,--export=__stack_pointer \ + -Wl,--export=__memory_base \ + -Wl,--export=__table_base \ + -Wl,--export=malloc \ + -Wl,--export=free \ + zeroperl.o stubs.o zeroperl_data.o + +link_zeroperl zeroperl_command_base.wasm \ + -mexec-model=reactor \ + -Wl,--no-entry \ + -Wl,--stack-first \ + zeroperl_main.o zeroperl.o stubs.o zeroperl_data.o # Restore real wasm-opt for asyncify pass mv /opt/binaryen/bin/wasm-opt-real /opt/binaryen/bin/wasm-opt @@ -116,25 +141,36 @@ mv /opt/binaryen/bin/wasm-opt-real /opt/binaryen/bin/wasm-opt if [ "$ASYNCIFY" = "true" ]; then wasm-opt zeroperl_reactor.wasm -O3 --strip-debug --enable-bulk-memory \ --enable-nontrapping-float-to-int --asyncify \ - --pass-arg=asyncify-imports@wasi_snapshot_preview1.fd_read,env.call_host_function \ + --pass-arg=asyncify-imports@$ASYNCIFY_IMPORTS \ ${WASM_OPT_FLAGS} \ -o zeroperl.wasm + wasm-opt zeroperl_command_base.wasm -O3 --strip-debug --enable-bulk-memory \ + --enable-nontrapping-float-to-int --asyncify \ + --pass-arg=asyncify-imports@$ASYNCIFY_IMPORTS \ + ${WASM_OPT_FLAGS} \ + -o zeroperl_command.wasm else wasm-opt zeroperl_reactor.wasm --strip-debug --enable-bulk-memory \ --enable-nontrapping-float-to-int --asyncify \ --pass-arg=asyncify-ignore-imports \ ${WASM_OPT_FLAGS} \ -o zeroperl.wasm + wasm-opt zeroperl_command_base.wasm --strip-debug --enable-bulk-memory \ + --enable-nontrapping-float-to-int --asyncify \ + --pass-arg=asyncify-ignore-imports \ + ${WASM_OPT_FLAGS} \ + -o zeroperl_command.wasm fi # Strip empty name section that wamrc cannot parse python3 -c " import sys -data = open('zeroperl.wasm', 'rb').read() # Custom name section: id=0x00, then LEB size, then 0x04 'name' marker = b'\x00\x05\x04name' -idx = data.rfind(marker) -if idx > 0 and idx + len(marker) == len(data): - open('zeroperl.wasm', 'wb').write(data[:idx]) - print(f'Stripped {len(marker)}-byte empty name section') +for filename in ('zeroperl.wasm', 'zeroperl_command.wasm'): + data = open(filename, 'rb').read() + idx = data.rfind(marker) + if idx > 0 and idx + len(marker) == len(data): + open(filename, 'wb').write(data[:idx]) + print(f'Stripped {len(marker)}-byte empty name section from {filename}') " diff --git a/pipeline/prepare-prefix.sh b/pipeline/prepare-prefix.sh index d7f378d..5e76b82 100644 --- a/pipeline/prepare-prefix.sh +++ b/pipeline/prepare-prefix.sh @@ -12,11 +12,19 @@ rm -rf /zeroperl/bin find /zeroperl -type f \( -name "*.so" -o -name "*.a" -o -name "*.ld" -o -name "*.pod" -o -name "*.h" -o -executable \) -delete if [ "$BUILD_EXIFTOOL" = "true" ]; then - SITE_PERL="$NATIVE_DIR/prefix/lib/perl5/site_perl/$PERL_VERSION" + SITE_PERL="$(find "$NATIVE_DIR/prefix" -type f -path '*/Image/ExifTool.pm' -exec dirname {} \; -quit)" + if [ -z "$SITE_PERL" ]; then + echo "ExifTool modules were not found under $NATIVE_DIR/prefix" >&2 + exit 1 + fi + SITE_PERL="$(dirname "$SITE_PERL")" + mkdir -p "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/File" mkdir -p "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/Image" - cp -R "$SITE_PERL/File/"* "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/File/" 2>/dev/null || true - cp -R "$SITE_PERL/Image/"* "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/Image/" + if [ -d "$SITE_PERL/File" ]; then + cp -R "$SITE_PERL/File/." "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/File/" + fi + cp -R "$SITE_PERL/Image/." "/zeroperl/lib/$PERL_VERSION/wasm32-wasi/Image/" fi node "$REPO_DIR/tools/delete.js" "$REPO_DIR/tools/delete.txt" /zeroperl "$PERL_VERSION" diff --git a/stubs/asyncify.h b/stubs/asyncify.h index c407316..8814a36 100644 --- a/stubs/asyncify.h +++ b/stubs/asyncify.h @@ -20,10 +20,21 @@ __attribute__((import_module("asyncify"), import_name("stop_unwind"))) void asyn } while (0) __attribute__((import_module("asyncify"), import_name("start_rewind"))) void asyncify_start_rewind(void *buf); +#define asyncify_start_rewind(buf) \ + do \ + { \ + extern int pl_asyncify_rewinding; \ + pl_asyncify_rewinding = 1; \ + asyncify_start_rewind((buf)); \ + } while (0) __attribute__((import_module("asyncify"), import_name("stop_rewind"))) void asyncify_stop_rewind(void); - -__attribute__((import_module("asyncify"), import_name("get_state"))) -int asyncify_get_state(void); +#define asyncify_stop_rewind() \ + do \ + { \ + extern int pl_asyncify_rewinding; \ + pl_asyncify_rewinding = 0; \ + asyncify_stop_rewind(); \ + } while (0) #endif diff --git a/stubs/setjmp.c b/stubs/setjmp.c index a7c6456..380b436 100644 --- a/stubs/setjmp.c +++ b/stubs/setjmp.c @@ -83,6 +83,7 @@ void async_buf_init(struct __asyncjmp_asyncify_jmp_buf *buf) // Global unwinding/rewinding jmpbuf state static asyncjmp_jmp_buf *_asyncjmp_active_jmpbuf; void *pl_asyncify_unwind_buf; +int pl_asyncify_rewinding; __attribute__((noinline)) int _asyncjmp_setjmp_internal(asyncjmp_jmp_buf *env) { diff --git a/stubs/zeroperl.c b/stubs/zeroperl.c index c32bf47..2b62b56 100644 --- a/stubs/zeroperl.c +++ b/stubs/zeroperl.c @@ -34,6 +34,19 @@ #define ZEROPERL_IMPORT(name) #endif +#ifndef ZEROPERL_ENABLE_HOST_CALLBACKS +#define ZEROPERL_ENABLE_HOST_CALLBACKS 0 +#endif + +extern int pl_asyncify_rewinding; + +// Replay is tracked by asyncify_start_rewind/stop_rewind wrappers. During +// replay, globals may already look initialized, but returning early would skip +// the matching asyncify_stop_rewind() and leave the module stuck rewinding. +static bool zeroperl_asyncify_is_rewinding(void) { + return pl_asyncify_rewinding != 0; +} + //! Writes the given string literal directly to STDERR via __wasi_fd_write //! Avoids calls to printf or other asyncified C library functions #define DEBUG_LOG_INTERNAL(msg) \ @@ -551,9 +564,11 @@ typedef struct { } zeroperl_context; //! Host-implemented function for calling back into the host environment +#if ZEROPERL_ENABLE_HOST_CALLBACKS ZEROPERL_IMPORT("call_host_function") zeroperl_value *host_call_function(int32_t func_id, int32_t argc, zeroperl_value **argv); +#endif //! Registry for host function IDs typedef struct { @@ -631,7 +646,11 @@ static XS(xs_host_dispatch) { } } +#if ZEROPERL_ENABLE_HOST_CALLBACKS zeroperl_value *result = host_call_function(func_id, items, argv); +#else + (void)func_id; +#endif if (argv) { for (int i = 0; i < items; i++) { @@ -641,6 +660,9 @@ static XS(xs_host_dispatch) { free(argv); } +#if !ZEROPERL_ENABLE_HOST_CALLBACKS + croak("Host callbacks are not enabled in this zeroperl build"); +#else if (!result || !result->sv) { if (result) { free(result); @@ -659,6 +681,7 @@ static XS(xs_host_dispatch) { free(result); ST(0) = sv_2mortal(sv); XSRETURN(1); +#endif } //! Internal callback for initialization @@ -942,7 +965,9 @@ static int zeroperl_reset_callback(int argc, char **argv) { //! Returns 0 on success, non-zero on error. ZEROPERL_API("zeroperl_init") int zeroperl_init(void) { - if (zero_perl) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if (zero_perl && !rewinding) { return 0; } @@ -960,7 +985,9 @@ int zeroperl_init(void) { //! Returns 0 on success, non-zero on error. ZEROPERL_API("zeroperl_init_with_args") int zeroperl_init_with_args(int argc, char **argv) { - if (zero_perl) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if (zero_perl && !rewinding) { return 0; } @@ -984,7 +1011,9 @@ int zeroperl_init_with_args(int argc, char **argv) { ZEROPERL_API("zeroperl_eval") int zeroperl_eval(const char *code, zeroperl_context_type context, int argc, char **argv) { - if (!zero_perl || !zero_perl_can_evaluate) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if ((!zero_perl || !zero_perl_can_evaluate) && !rewinding) { return -1; } @@ -1008,7 +1037,9 @@ int zeroperl_eval(const char *code, zeroperl_context_type context, int argc, //! Returns 0 on success, non-zero on error. ZEROPERL_API("zeroperl_run_file") int zeroperl_run_file(const char *filepath, int argc, char **argv) { - if (!zero_perl || !zero_perl_can_evaluate) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if ((!zero_perl || !zero_perl_can_evaluate) && !rewinding) { return 1; } @@ -1029,6 +1060,10 @@ int zeroperl_run_file(const char *filepath, int argc, char **argv) { //! After this, you can call zeroperl_init() again for a fresh interpreter. ZEROPERL_API("zeroperl_free_interpreter") void zeroperl_free_interpreter(void) { + if (zeroperl_asyncify_is_rewinding()) { + return; + } + if (zero_perl) { perl_destruct(zero_perl); perl_free(zero_perl); @@ -1043,6 +1078,10 @@ void zeroperl_free_interpreter(void) { //! Should be called only once at program exit. ZEROPERL_API("zeroperl_shutdown") void zeroperl_shutdown(void) { + if (zeroperl_asyncify_is_rewinding()) { + return; + } + zeroperl_free_interpreter(); if (zero_perl_system_initialized) { @@ -1068,7 +1107,9 @@ void zeroperl_clear_error(void) { //! Returns 0 on success, non-zero on error. ZEROPERL_API("zeroperl_reset") int zeroperl_reset(void) { - if (!zero_perl) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if (!zero_perl && !rewinding) { const char *err = "Interpreter not initialized"; strncpy(zero_perl_error_buf, err, sizeof(zero_perl_error_buf) - 1); zero_perl_error_buf[sizeof(zero_perl_error_buf) - 1] = '\0'; @@ -2152,7 +2193,13 @@ static int zeroperl_call_callback(int argc, char **argv) { ZEROPERL_API("zeroperl_call") zeroperl_result *zeroperl_call(const char *name, zeroperl_context_type context, int argc, zeroperl_value **argv) { - if (!zero_perl || !zero_perl_can_evaluate || !name) { + bool rewinding = zeroperl_asyncify_is_rewinding(); + + if ((!zero_perl || !zero_perl_can_evaluate) && !rewinding) { + return NULL; + } + + if (!name) { return NULL; } @@ -2285,4 +2332,4 @@ static void xs_init(pTHX) { newXS("Fcntl::bootstrap", boot_Fcntl, file); newXS("Opcode::bootstrap", boot_Opcode, file); newXS("Time::HiRes::bootstrap", boot_Time__HiRes, file); -} \ No newline at end of file +} diff --git a/stubs/zeroperl_main.c b/stubs/zeroperl_main.c new file mode 100644 index 0000000..24c3b5c --- /dev/null +++ b/stubs/zeroperl_main.c @@ -0,0 +1,61 @@ +#include +#include +#include + +#include + +#if defined(__WASI__) || defined(__wasi__) +#define ZEROPERL_COMMAND_EXPORT(name) \ + __attribute__((export_name(name))) __attribute__((visibility("default"))) +#else +#define ZEROPERL_COMMAND_EXPORT(name) __attribute__((visibility("default"))) +#endif + +int zeroperl_init_with_args(int argc, char **argv); + +static int run_zeroperl(int argc, char **argv) { + // Command modules are usually one-shot instances. Avoid running Perl global + // teardown from _start because some browser/WASI shims trap during cleanup + // after the script itself has completed. + int status = zeroperl_init_with_args(argc, argv); + return status; +} + +static int run_zeroperl_with_wasi_args(void) { + size_t argc = 0; + size_t argv_buf_size = 0; + + if (__wasi_args_sizes_get(&argc, &argv_buf_size) != __WASI_ERRNO_SUCCESS || + argc > INT_MAX) { + char *fallback_argv[] = {"zeroperl", "-e", "0", NULL}; + return run_zeroperl(3, fallback_argv); + } + + char **argv = (char **)calloc(argc + 1, sizeof(char *)); + char *argv_buf = (char *)malloc(argv_buf_size ? argv_buf_size : 1); + + if (!argv || !argv_buf) { + free(argv); + free(argv_buf); + return 1; + } + + if (__wasi_args_get((uint8_t **)argv, (uint8_t *)argv_buf) != + __WASI_ERRNO_SUCCESS) { + free(argv); + free(argv_buf); + return 1; + } + + int status = run_zeroperl((int)argc, argv); + free(argv); + free(argv_buf); + return status; +} + +ZEROPERL_COMMAND_EXPORT("_start") +void zeroperl_command_start(void) { + (void)run_zeroperl_with_wasi_args(); +} + +int main(int argc, char **argv) { return run_zeroperl(argc, argv); } diff --git a/tools/smoke-zeroperl-command.js b/tools/smoke-zeroperl-command.js new file mode 100644 index 0000000..852fe48 --- /dev/null +++ b/tools/smoke-zeroperl-command.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { WASI } = require("node:wasi"); + +function startReturnableCommand(wasi, instance) { + let initialized = false; + const wrappedExports = { + memory: instance.exports.memory, + _start() { + if (!initialized && typeof instance.exports._initialize === "function") { + initialized = true; + instance.exports._initialize(); + } + return instance.exports._start(); + }, + }; + + return wasi.start({ exports: wrappedExports }); +} + +async function main() { + const wasmPath = process.argv[2] || "output/zeroperl_command.wasm"; + const bytes = fs.readFileSync(wasmPath); + const mod = await WebAssembly.compile(bytes); + + const imports = WebAssembly.Module.imports(mod); + const envImports = imports.filter((entry) => entry.module === "env"); + if (envImports.length > 0) { + const names = envImports.map((entry) => entry.name).join(", "); + throw new Error(`Unexpected env imports in default command build: ${names}`); + } + + const exports = new Set(WebAssembly.Module.exports(mod).map((entry) => entry.name)); + if (!exports.has("_start")) { + throw new Error("zeroperl_command.wasm does not export _start"); + } + + const preopenRoot = fs.mkdtempSync(path.join(os.tmpdir(), "zeroperl-wasi-")); + fs.mkdirSync(path.join(preopenRoot, "dev"), { recursive: true }); + fs.writeFileSync(path.join(preopenRoot, "dev", "null"), ""); + + try { + const wasi = new WASI({ + version: "preview1", + args: [wasmPath, "-e", "print qq(zeroperl command smoke ok\\n);"], + env: { LANG: "C", LC_ALL: "C" }, + preopens: { "/": preopenRoot }, + }); + + const instance = await WebAssembly.instantiate(mod, { + wasi_snapshot_preview1: wasi.wasiImport, + }); + + if (typeof instance.exports._start !== "function") { + throw new Error("instance.exports._start is not callable"); + } + + startReturnableCommand(wasi, instance); + + if (typeof instance.exports.asyncify_get_state === "function") { + const state = instance.exports.asyncify_get_state(); + if (state !== 0) { + throw new Error(`Invalid async state ${state}, expected 0`); + } + } + } finally { + fs.rmSync(preopenRoot, { recursive: true, force: true }); + } + + console.log("zeroperl_command.wasm smoke test passed"); +} + +main().catch((error) => { + console.error(error && error.stack ? error.stack : error); + process.exit(1); +});