diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7d897a1c..d0490b52a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -418,6 +418,11 @@ jobs: wasmtime run build/qjs -qd echo "console.log('hello wasi!');" > t.js wasmtime run --dir . build/qjs t.js + - name: test reactor + run: | + cmake -B build-reactor -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON + make -C build-reactor qjs_wasi_reactor + /opt/wasi-sdk/bin/llvm-nm build-reactor/qjs.wasm | grep -q " T qjs_init$" cygwin: runs-on: windows-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e97b23c7c..263041afb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,11 +131,21 @@ jobs: cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake make -C build qjs_exe mv build/qjs build/qjs-wasi.wasm + - name: build reactor + run: | + cmake -B build-reactor -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON + make -C build-reactor qjs_wasi_reactor + mv build-reactor/qjs.wasm build/qjs-wasi-reactor.wasm - name: upload uses: actions/upload-artifact@v6 with: name: qjs-wasi path: build/qjs-wasi.wasm + - name: upload reactor + uses: actions/upload-artifact@v6 + with: + name: qjs-wasi-reactor + path: build/qjs-wasi-reactor.wasm upload-to-release: needs: [linux, macos, windows, wasi, check_meson_version] diff --git a/CMakeLists.txt b/CMakeLists.txt index cf6bf1754..6598f0b08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,6 +129,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") -lwasi-emulated-process-clocks -lwasi-emulated-signal ) + option(QJS_WASI_REACTOR "Build WASI reactor (re-entrant, exports instead of _start)" OFF) endif() if(CMAKE_BUILD_TYPE MATCHES "Debug") @@ -335,6 +336,35 @@ target_link_libraries(qjs_exe qjs) if(NOT WIN32) set_target_properties(qjs_exe PROPERTIES ENABLE_EXPORTS TRUE) endif() + +# WASI Reactor build +# +if(QJS_WASI_REACTOR) + add_executable(qjs_wasi_reactor + gen/repl.c + gen/standalone.c + qjs.c + ) + add_qjs_libc_if_needed(qjs_wasi_reactor) + set_target_properties(qjs_wasi_reactor PROPERTIES + OUTPUT_NAME "qjs" + SUFFIX ".wasm" + ) + target_compile_definitions(qjs_wasi_reactor PRIVATE ${qjs_defines} QJS_WASI_REACTOR) + target_link_libraries(qjs_wasi_reactor qjs) + target_link_options(qjs_wasi_reactor PRIVATE + -mexec-model=reactor + -Wl,--export=qjs_init + -Wl,--export=qjs_init_argv + -Wl,--export=qjs_eval + -Wl,--export=qjs_loop_once + -Wl,--export=qjs_poll_io + -Wl,--export=qjs_destroy + -Wl,--export=malloc + -Wl,--export=free + ) +endif() + if(QJS_BUILD_CLI_WITH_MIMALLOC OR QJS_BUILD_CLI_WITH_STATIC_MIMALLOC) find_package(mimalloc REQUIRED) # Upstream mimalloc doesn't provide a way to know if both libraries are supported. diff --git a/QJS_WASI_REACTOR.md b/QJS_WASI_REACTOR.md new file mode 100644 index 000000000..3572563e0 --- /dev/null +++ b/QJS_WASI_REACTOR.md @@ -0,0 +1,315 @@ +# Design Document: `QJS_WASI_REACTOR` + +## 1. Context and Scope + +QuickJS-ng currently supports WASI (WebAssembly System Interface) as a build target, producing a `.wasm` binary that can run under WASI runtimes like `wasmtime`. This build uses the standard WASI "command" model where the binary has a `_start()` entry point, runs to completion, and exits. + +This design introduces a **WASI reactor** build variant that enables QuickJS to be embedded in JavaScript host environments (browsers, Node.js, Deno) with a **re-entrant execution model**. Instead of blocking in an event loop, the reactor yields control back to the JavaScript host after processing available work, allowing the host's event loop to run (handling DOM events, network I/O, etc.) before resuming QuickJS execution. + +### Background + +- **WASI command**: Has `_start()`, runs once, blocks in event loop, exits +- **WASI reactor**: Exports functions, no `_start()`, host controls execution flow + +The existing WASI command build works for CLI-style usage under `wasmtime`/`wasmer`, but cannot be embedded in browser/Node.js environments where: +1. Blocking is not allowed (would freeze the UI/event loop) +2. The host needs to interleave its own event processing with QuickJS execution +3. Timer scheduling should integrate with the host's `setTimeout`/`queueMicrotask` + +### Reference Implementation + +Go's WebAssembly support (`$GOROOT/lib/wasm/wasm_exec.js`) uses a similar pattern: +- WASM module exports `run()` and `resume()` functions +- Host provides imports for time, I/O, and scheduler integration +- `runtime.scheduleTimeoutEvent` import lets Go request wake-ups from the host + +## 2. Goals and Non-Goals + +### Goals + +- **Re-entrant execution**: QuickJS processes pending microtasks/timers and returns control to the host, rather than blocking +- **Host event loop integration**: Enable `queueMicrotask()` scheduling in the host to yield between QuickJS iterations +- **Timer integration**: Host can query when the next QuickJS timer fires and schedule a wake-up +- **Minimal changes**: Reuse existing WASI infrastructure; avoid duplicating code or adding new files where possible +- **Clean separation**: Use `#ifdef QJS_WASI_REACTOR` to clearly delineate reactor-specific code + +### Non-Goals + +- **Pure WASM (no WASI)**: We will continue using WASI for memory allocation, stdout/stderr, clock functions, and filesystem. A pure WASM build without WASI dependencies is not in scope. +- **Custom host imports for I/O**: Console output, file I/O, and time functions continue to use WASI. We do not add custom `qjs_host.*` imports. +- **Multiple simultaneous contexts**: The reactor exports operate on a single global runtime/context. Supporting multiple independent contexts via handles is not in scope. +- **Async I/O integration**: File and network I/O remain synchronous via WASI. Integrating with host async APIs (fetch, streams) is not in scope. +- **Worker thread support**: The `os.Worker` API is already disabled for WASI builds and remains so. + +## 3. Design Overview + +### System Context Diagram + +``` +┌─────────────────────────────────────────────────────────────┐ +│ JavaScript Host │ +│ (Browser / Node.js / Deno / Bun) │ +│ │ +│ ┌─────────────────────────────────-────────────────────┐ │ +│ │ Host JavaScript │ │ +│ │ - Loads WASM module │ │ +│ │ - Provides WASI imports (via polyfill or node:wasi) │ │ +│ │ - Calls qjs_run(), qjs_eval(), qjs_loop_once() │ │ +│ │ - Schedules next iteration via queueMicrotask() │ │ +│ │ - Schedules timer wake-ups via setTimeout() │ │ +│ └──────────────────────┬───────────-───────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────-───────────────────┐ │ +│ │ qjs.wasm (WASI Reactor) │ │ +│ │ │ │ +│ │ Exports: │ │ +│ │ - qjs_init() → i32 │ │ +│ │ - qjs_init_argv(argc, argv) → i32 │ │ +│ │ - qjs_eval(code, len, filename, is_module) → i32 │ │ +│ │ - qjs_loop_once() → i32 (timeout_ms or status) │ │ +│ │ - qjs_destroy() → void │ │ +│ │ - malloc(size) → ptr, free(ptr) → void │ │ +│ │ │ │ +│ │ Imports: wasi_snapshot_preview1.* │ │ +│ └─────────────────────────────────-────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### APIs + +#### New C API (quickjs-libc.h) + +```c +/* + * Run one iteration of the event loop (non-blocking). + * + * Executes all pending microtasks (promise jobs), then checks timers + * and runs at most one expired timer callback. + * + * Returns: + * > 0: Next timer fires in this many milliseconds; call again after delay + * 0: More work pending; call again immediately (via queueMicrotask) + * -1: No pending work; event loop is idle + * -2: An exception occurred; call js_std_dump_error() for details + */ +int js_std_loop_once(JSContext *ctx); +``` + +This single function replaces the need for separate "step" and "get timeout" functions by encoding both status and timeout in the return value. + +#### WASM Exports (qjs.c, under `#ifdef QJS_WASI_REACTOR`) + +| Export | Signature | Description | +|--------|-----------|-------------| +| `qjs_init` | `() → i32` | Initialize empty runtime (no script). Use `qjs_eval()` to run code. Returns 0 on success, -1 on error. | +| `qjs_init_argv` | `(argc: i32, argv: i32) → i32` | Initialize with CLI arguments. Same args as `qjs` CLI (e.g., `["qjs", "--std", "script.js"]`). Returns 0 on success, -1 on error. | +| `qjs_eval` | `(code: i32, len: i32, filename: i32, is_module: i32) → i32` | Evaluate JS code. `filename` used for errors and relative imports (0 for ``). Returns 0 on success, -1 on error. | +| `qjs_loop_once` | `() → i32` | Run one loop iteration. Returns timeout_ms (>0), 0 (pending), -1 (idle), or -2 (error). | +| `qjs_destroy` | `() → void` | Free runtime and context. | +| `malloc` | `(size: i32) → i32` | Allocate memory (for host to build argv array or code strings). | +| `free` | `(ptr: i32) → void` | Free memory. | + +#### Usage Example (JavaScript Host) + +**Simple usage with `qjs_init()` + `qjs_eval()`:** + +```javascript +// Initialize empty runtime +if (exports.qjs_init() !== 0) throw new Error("init failed"); + +// Evaluate code +const code = 'console.log("Hello from QuickJS!")'; +const codePtr = writeString(code); +exports.qjs_eval(codePtr, code.length, 0, 0); +free(codePtr); + +// Drive event loop +function loop() { + const result = exports.qjs_loop_once(); + if (result === 0) queueMicrotask(loop); + else if (result > 0) setTimeout(loop, result); +} +loop(); +``` + +**With CLI arguments using `qjs_init_argv()`:** + +```javascript +// Build argv in WASM memory: ["qjs", "--std", "/app/script.js"] +const args = ["qjs", "--std", "/app/script.js"]; +const argvPtrs = args.map(s => writeString(s)); +const argvPtr = malloc(argvPtrs.length * 4); +new Uint32Array(memory.buffer, argvPtr, argvPtrs.length).set(argvPtrs); + +// Initialize and load script (via WASI filesystem) +if (exports.qjs_init_argv(args.length, argvPtr) !== 0) { + throw new Error("init failed"); +} + +// Drive event loop (same as above) +function loop() { + const result = exports.qjs_loop_once(); + if (result === 0) queueMicrotask(loop); + else if (result > 0) setTimeout(loop, result); +} +loop(); +``` + +### Components and Interactions + +#### 1. `js_std_loop_once()` Implementation + +Added to `quickjs-libc.c`, approximately 35 lines: + +``` +js_std_loop_once(ctx) + │ + ├─► Execute all pending jobs via JS_ExecutePendingJob() + │ (loops until no more jobs) + │ + ├─► Check for expired timers via js_os_run_timers() + │ (runs at most one timer callback) + │ + ├─► If JS_IsJobPending() or timers exist: + │ └─► Return next timer delay (or 0 if jobs pending) + │ + └─► Else return -1 (idle) +``` + +#### 2. Reactor Entry Points in `qjs.c` + +At the end of `qjs.c`, guarded by `#if defined(__wasi__) && defined(QJS_WASI_REACTOR)`: + +- Static globals: `reactor_rt`, `reactor_ctx`, `reactor_ready` +- `qjs_init()`: Initialize empty runtime (calls `qjs_init_argv` with no args) +- `qjs_init_argv(argc, argv)`: Parses CLI args, creates runtime/context, loads script (same logic as `main()` but stops before event loop) +- `qjs_eval(code, len, filename, is_module)`: Evaluate JS code +- `qjs_loop_once()`: Delegates to `js_std_loop_once()` +- `qjs_destroy()`: Cleanup + +#### 3. Build Configuration + +In `CMakeLists.txt`, new option and target: + +```cmake +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + option(QJS_WASI_REACTOR "Build WASI reactor (re-entrant, no _start)" OFF) + + if(QJS_WASI_REACTOR) + add_executable(qjs_wasi_reactor ...) + target_compile_definitions(qjs_wasi_reactor PRIVATE QJS_WASI_REACTOR) + target_link_options(qjs_wasi_reactor PRIVATE + -mexec-model=reactor + -Wl,--export=qjs_init + -Wl,--export=qjs_init_argv + -Wl,--export=qjs_eval + -Wl,--export=qjs_loop_once + -Wl,--export=qjs_destroy + -Wl,--export=malloc + -Wl,--export=free + ) + endif() +endif() +``` + +### Data Storage + +No persistent data storage is involved. Runtime state exists in WASM linear memory managed by WASI libc. The single global `JSRuntime *rt` and `JSContext *ctx` are static variables in the WASM module. + +## 4. Alternatives Considered + +### Alternative A: Pure WASM (No WASI) + +**Description**: Build QuickJS targeting bare `wasm32` without WASI, providing all I/O via custom JavaScript imports. + +**Trade-offs**: +- (+) Smaller binary, no WASI overhead +- (+) Full control over all I/O +- (-) Must implement/import: `malloc`/`free`, `clock_gettime`, `write()`, `read()`, etc. +- (-) No filesystem support without significant effort +- (-) ~300+ lines of additional harness code to provide imports +- (-) Cannot reuse existing WASI-SDK toolchain and CI + +**Rejection reason**: Significantly more implementation effort for marginal benefits. WASI polyfills exist for browsers, making the WASI-based approach viable everywhere. + +### Alternative B: Emscripten-based Build + +**Description**: Use the existing Emscripten `qjs_wasm` target with modifications for re-entrancy. + +**Trade-offs**: +- (+) Emscripten has mature browser support +- (+) Automatic memory management, async support +- (-) Different toolchain from WASI (two WASM paths to maintain) +- (-) Emscripten adds runtime overhead (~100KB+) +- (-) Different API patterns (ccall/cwrap vs direct exports) + +**Rejection reason**: Maintaining two separate WASM toolchains increases complexity. WASI is the emerging standard, and polyfills bridge the browser gap. + +### Alternative C: Separate `qjs-wasm.c` File + +**Description**: Create a new C file for reactor entry points instead of adding `#ifdef` to `qjs.c`. + +**Trade-offs**: +- (+) No changes to existing `qjs.c` +- (+) Clear separation of concerns +- (-) Code duplication (context creation, module init) +- (-) Additional file to maintain +- (-) Harder to share helper functions + +**Rejection reason**: The reactor entry points share initialization logic with the CLI. Using `#ifdef` at the end of `qjs.c` keeps related code together. + +### Alternative D: Two New API Functions (step + get_timeout) + +**Description**: Expose `js_std_loop_step()` and `js_std_get_timeout()` as separate functions. + +**Trade-offs**: +- (+) More explicit API +- (+) Timeout query doesn't require running a step +- (-) Two functions instead of one +- (-) Two WASM exports instead of one +- (-) More API surface to document/maintain + +**Rejection reason**: A single `js_std_loop_once()` function that returns encoded status+timeout is simpler and sufficient for the use case. The harness always needs both pieces of information together. + +## 5. Cross-Cutting Concerns + +### Security + +**Sandbox boundaries**: QuickJS running in WASM inherits the WASM security model—memory isolation, no direct system access. All I/O goes through WASI imports which the host controls. + +**WASI capabilities**: The harness configures WASI with minimal capabilities: +- No filesystem access by default (can be enabled by host) +- stdout/stderr for console output +- Clock access for timers + +**Code evaluation**: The `qjs_eval` export allows arbitrary JS execution. This is intentional—the host is responsible for controlling what code is passed to the engine. The WASM sandbox prevents escape to the host system. + +### Privacy + +No privacy-specific concerns. QuickJS in WASM has no network access, no persistent storage, and no access to host APIs beyond what the harness explicitly provides. + +### Observability + +**Console output**: `console.log` in QuickJS writes to WASI stdout (fd 1). The harness can intercept this by providing a custom `fd_write` implementation or by capturing the WASI stdout buffer. + +**Errors**: Exceptions are printed to stderr via existing `js_std_dump_error()`. The `qjs_loop_once()` return value (-2) signals an error occurred. + +**Debugging**: The WASM module can be built with debug info. Browser DevTools and Node.js support WASM debugging with source maps. + +### Portability + +**WASI polyfills**: For browser environments, WASI polyfill libraries provide the required imports. Node.js has native support via `node:wasi`. + +**Feature detection**: The harness should check for `WebAssembly.instantiateStreaming` support and fall back to `instantiate` with `fetch().then(r => r.arrayBuffer())` for older environments. + +## Appendix: File Changes Summary + +| File | Change Type | ~Lines | Description | +|------|-------------|--------|-------------| +| `quickjs-libc.h` | Modify | +3 | Add `js_std_loop_once()` declaration | +| `quickjs-libc.c` | Modify | +40 | Add `js_std_loop_once()` implementation | +| `qjs.c` | Modify | +220 | Add `#ifdef QJS_WASI_REACTOR` section with `qjs_init`, `qjs_init_argv`, `qjs_eval`, `qjs_loop_once`, `qjs_destroy` | +| `CMakeLists.txt` | Modify | +25 | Add `QJS_WASI_REACTOR` option and `qjs_wasi_reactor` target | + +**Total: ~290 lines changed/added** diff --git a/qjs.c b/qjs.c index 4aed6837e..04d39f0bb 100644 --- a/qjs.c +++ b/qjs.c @@ -746,3 +746,338 @@ int main(int argc, char **argv) JS_FreeRuntime(rt); return 1; } + +#if defined(__wasi__) && defined(QJS_WASI_REACTOR) +/* + * WASI Reactor exports for embedding QuickJS in JavaScript hosts. + * + * Usage: call qjs_run() with argc/argv just like CLI, then call + * qjs_loop_once() repeatedly to drive the event loop. + */ + +static JSRuntime *reactor_rt; +static JSContext *reactor_ctx; +static int reactor_ready; + +int qjs_init_argv(int argc, char **argv); + +/* + * Initialize with no arguments (empty runtime, no script loaded). + * Use qjs_eval() to evaluate code after calling this. + * Returns 0 on success, -1 on error. + */ +__attribute__((export_name("qjs_init"))) +int qjs_init(void) +{ + static char *empty_argv[] = { "qjs", NULL }; + return qjs_init_argv(1, empty_argv); +} + +/* + * Initialize and run script (like main() but stops before event loop). + * Pass same arguments as CLI: e.g. ["qjs", "--std", "script.js"] + * Returns 0 on success, -1 on error. + */ +__attribute__((export_name("qjs_init_argv"))) +int qjs_init_argv(int argc, char **argv) +{ + struct trace_malloc_data trace_data = { NULL }; + int optind = 1; + char exebuf[JS__PATH_MAX]; + size_t exebuf_size = sizeof(exebuf); + char *expr = NULL; + char *dump_flags_str = NULL; + int standalone = 0; + int interactive = 0; + int dump_flags = 0; + int trace_memory = 0; + int empty_run = 0; + int module = -1; + int load_std = 0; + char *include_list[32]; + int i, include_count = 0; + int64_t memory_limit = -1; + int64_t stack_size = -1; + + if (reactor_rt) + return -1; /* already initialized */ + + qjs__argc = argc; + qjs__argv = argv; + + if (!js_exepath(exebuf, &exebuf_size) && is_standalone(exebuf)) { + standalone = 1; + goto start; + } + + dump_flags_str = getenv("QJS_DUMP_FLAGS"); + dump_flags = dump_flags_str ? strtol(dump_flags_str, NULL, 16) : 0; + + while (optind < argc && *argv[optind] == '-') { + char *arg = argv[optind] + 1; + const char *longopt = ""; + char *optarg = NULL; + if (!*arg) + break; + optind++; + if (*arg == '-') { + longopt = arg + 1; + optarg = strchr(longopt, '='); + if (optarg) + *optarg++ = '\0'; + arg += strlen(arg); + if (!*longopt) + break; + } + for (; *arg || *longopt; longopt = "") { + char opt = *arg; + if (opt) { + arg++; + if (!optarg && *arg) + optarg = arg; + } + if (opt == 'h' || opt == '?' || !strcmp(longopt, "help")) { + return -1; /* help not useful in reactor mode */ + } + if (opt == 'e' || !strcmp(longopt, "eval")) { + if (!optarg) { + if (optind >= argc) + return -1; + optarg = argv[optind++]; + } + expr = optarg; + break; + } + if (opt == 'I' || !strcmp(longopt, "include")) { + if (optind >= argc || include_count >= countof(include_list)) + return -1; + include_list[include_count++] = argv[optind++]; + continue; + } + if (opt == 'i' || !strcmp(longopt, "interactive")) { + interactive++; + continue; + } + if (opt == 'm' || !strcmp(longopt, "module")) { + module = 1; + continue; + } + if (opt == 'C' || !strcmp(longopt, "script")) { + module = 0; + continue; + } + if (opt == 'd' || !strcmp(longopt, "dump")) { + continue; /* ignore in reactor */ + } + if (opt == 'D' || !strcmp(longopt, "dump-flags")) { + dump_flags = optarg ? strtol(optarg, NULL, 16) : 0; + break; + } + if (opt == 'T' || !strcmp(longopt, "trace")) { + trace_memory++; + continue; + } + if (!strcmp(longopt, "std")) { + load_std = 1; + continue; + } + if (opt == 'q' || !strcmp(longopt, "quit")) { + empty_run++; + continue; + } + if (!strcmp(longopt, "memory-limit")) { + if (!optarg) { + if (optind >= argc) + return -1; + optarg = argv[optind++]; + } + memory_limit = parse_limit(optarg); + break; + } + if (!strcmp(longopt, "stack-size")) { + if (!optarg) { + if (optind >= argc) + return -1; + optarg = argv[optind++]; + } + stack_size = parse_limit(optarg); + break; + } + /* skip compile/out/exe options - not useful in reactor */ + if (opt == 'c' || !strcmp(longopt, "compile") || + opt == 'o' || !strcmp(longopt, "out") || + !strcmp(longopt, "exe")) { + return -1; + } + return -1; /* unknown option */ + } + } + +start: + if (trace_memory) { + js_trace_malloc_init(&trace_data); + reactor_rt = JS_NewRuntime2(&trace_mf, &trace_data); + } else { + reactor_rt = JS_NewRuntime(); + } + if (!reactor_rt) + return -1; + + if (memory_limit >= 0) + JS_SetMemoryLimit(reactor_rt, (size_t)memory_limit); + if (stack_size >= 0) + JS_SetMaxStackSize(reactor_rt, (size_t)stack_size); + if (dump_flags != 0) + JS_SetDumpFlags(reactor_rt, dump_flags); + + js_std_set_worker_new_context_func(JS_NewCustomContext); + js_std_init_handlers(reactor_rt); + reactor_ctx = JS_NewCustomContext(reactor_rt); + if (!reactor_ctx) + goto fail; + + JS_SetModuleLoaderFunc(reactor_rt, NULL, js_module_loader, NULL); + JS_SetHostPromiseRejectionTracker(reactor_rt, js_std_promise_rejection_tracker, NULL); + + if (!empty_run) { + js_std_add_helpers(reactor_ctx, argc - optind, argv + optind); + + if (load_std) { + const char *str = + "import * as bjson from 'qjs:bjson';\n" + "import * as std from 'qjs:std';\n" + "import * as os from 'qjs:os';\n" + "globalThis.bjson = bjson;\n" + "globalThis.std = std;\n" + "globalThis.os = os;\n"; + if (eval_buf(reactor_ctx, str, strlen(str), "", JS_EVAL_TYPE_MODULE)) + goto fail; + } + + for (i = 0; i < include_count; i++) { + if (eval_file(reactor_ctx, include_list[i], 0)) + goto fail; + } + + if (standalone) { + JSValue ns = load_standalone_module(reactor_ctx); + if (JS_IsException(ns)) + goto fail; + JSValue func = JS_GetPropertyStr(reactor_ctx, ns, "runStandalone"); + JS_FreeValue(reactor_ctx, ns); + if (JS_IsException(func)) + goto fail; + JSValue ret = JS_Call(reactor_ctx, func, JS_UNDEFINED, 0, NULL); + JS_FreeValue(reactor_ctx, func); + if (JS_IsException(ret)) { + JS_FreeValue(reactor_ctx, ret); + goto fail; + } + JS_FreeValue(reactor_ctx, ret); + } else if (expr) { + int flags = module ? JS_EVAL_TYPE_MODULE : 0; + if (eval_buf(reactor_ctx, expr, strlen(expr), "", flags)) + goto fail; + } else if (optind < argc) { + const char *filename = argv[optind]; + if (eval_file(reactor_ctx, filename, module)) + goto fail; + } else if (interactive) { + JS_SetHostPromiseRejectionTracker(reactor_rt, NULL, NULL); + js_std_eval_binary(reactor_ctx, qjsc_repl, qjsc_repl_size, 0); + } + } + + reactor_ready = 1; + return 0; + +fail: + if (reactor_ctx) { + js_std_free_handlers(reactor_rt); + JS_FreeContext(reactor_ctx); + reactor_ctx = NULL; + } + if (reactor_rt) { + JS_FreeRuntime(reactor_rt); + reactor_rt = NULL; + } + return -1; +} + +__attribute__((export_name("qjs_eval"))) +int qjs_eval(const char *code, int len, const char *filename, int is_module) +{ + if (!reactor_ready) + return -1; + + const char *fname = filename ? filename : ""; + int flags = is_module ? JS_EVAL_TYPE_MODULE : JS_EVAL_TYPE_GLOBAL; + JSValue val; + + if (is_module) { + val = JS_Eval(reactor_ctx, code, len, fname, + flags | JS_EVAL_FLAG_COMPILE_ONLY); + if (!JS_IsException(val)) { + js_module_set_import_meta(reactor_ctx, val, true, true); + val = JS_EvalFunction(reactor_ctx, val); + } + } else { + val = JS_Eval(reactor_ctx, code, len, fname, flags); + } + + if (JS_IsException(val)) { + JS_FreeValue(reactor_ctx, val); + return -1; + } + + JS_FreeValue(reactor_ctx, val); + return 0; +} + +__attribute__((export_name("qjs_loop_once"))) +int qjs_loop_once(void) +{ + if (!reactor_ready) + return -2; + return js_std_loop_once(reactor_ctx); +} + +/* + * Poll for I/O events and invoke registered read/write handlers. + * Call this when the host knows that stdin (or other fds) have data available. + * + * Parameters: + * timeout_ms: Poll timeout in milliseconds + * 0 = non-blocking (check and return immediately) + * >0 = wait up to timeout_ms for I/O events + * -1 = block indefinitely (not recommended) + * + * Returns: + * 0: Success (handler invoked or no handlers registered) + * -1: Error or no I/O handlers + * -2: Not initialized or exception in handler + */ +__attribute__((export_name("qjs_poll_io"))) +int qjs_poll_io(int timeout_ms) +{ + if (!reactor_ready) + return -2; + return js_std_poll_io(reactor_ctx, timeout_ms); +} + +__attribute__((export_name("qjs_destroy"))) +void qjs_destroy(void) +{ + if (reactor_ctx) { + js_std_free_handlers(reactor_rt); + JS_FreeContext(reactor_ctx); + reactor_ctx = NULL; + } + if (reactor_rt) { + JS_FreeRuntime(reactor_rt); + reactor_rt = NULL; + } + reactor_ready = 0; +} + +#endif /* __wasi__ && QJS_WASI_REACTOR */ diff --git a/quickjs-libc.c b/quickjs-libc.c index 20d61394f..a0995b0da 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -2753,6 +2753,99 @@ static int js_os_poll(JSContext *ctx) } #endif // defined(_WIN32) +#ifdef QJS_WASI_REACTOR +/* + * Poll for I/O events and invoke registered read/write handlers. + * This is specifically for the reactor model where the host controls the + * event loop and needs to trigger I/O handler callbacks when data is available. + * + * Unlike js_os_poll(), this function: + * - Does NOT run timers (js_std_loop_once handles that) + * - Uses a caller-specified timeout instead of timer-based min_delay + * - Returns immediately after invoking one handler (like js_os_poll) + * + * Parameters: + * ctx: The JavaScript context + * timeout_ms: Poll timeout in milliseconds + * 0 = non-blocking (return immediately) + * >0 = wait up to timeout_ms for I/O + * -1 = block indefinitely (not recommended for reactor) + * + * Returns: + * 0: Success (handler was invoked or no handlers registered) + * -1: Error or no I/O handlers registered + * -2: Exception occurred in handler + */ +int js_std_poll_io(JSContext *ctx, int timeout_ms) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = js_get_thread_state(rt); + int r, w, ret, nfds; + JSOSRWHandler *rh; + struct list_head *el; + struct pollfd *pfd, *pfds, pfds_local[64]; + + /* Check if there are any I/O handlers registered */ + if (list_empty(&ts->os_rw_handlers)) + return 0; /* no handlers, nothing to do */ + + nfds = 0; + list_for_each(el, &ts->os_rw_handlers) { + rh = list_entry(el, JSOSRWHandler, link); + nfds += (!JS_IsNull(rh->rw_func[0]) || !JS_IsNull(rh->rw_func[1])); + } + + if (nfds == 0) + return 0; /* no active handlers */ + + pfd = pfds = pfds_local; + if (nfds > (int)countof(pfds_local)) { + pfd = pfds = js_malloc(ctx, nfds * sizeof(*pfd)); + if (!pfd) + return -1; + } + + list_for_each(el, &ts->os_rw_handlers) { + rh = list_entry(el, JSOSRWHandler, link); + r = POLLIN * !JS_IsNull(rh->rw_func[0]); + w = POLLOUT * !JS_IsNull(rh->rw_func[1]); + if (r || w) + *pfd++ = (struct pollfd){rh->fd, r|w, 0}; + } + + ret = 0; + nfds = poll(pfds, nfds, timeout_ms); + if (nfds < 0) { + ret = -1; + goto done; + } + + for (pfd = pfds; nfds-- > 0; pfd++) { + rh = find_rh(ts, pfd->fd); + if (rh) { + r = (POLLERR|POLLHUP|POLLNVAL|POLLIN) * !JS_IsNull(rh->rw_func[0]); + w = (POLLERR|POLLHUP|POLLNVAL|POLLOUT) * !JS_IsNull(rh->rw_func[1]); + if (r & pfd->revents) { + ret = call_handler(ctx, rh->rw_func[0]); + if (ret < 0) + ret = -2; /* exception in handler */ + goto done; + } + if (w & pfd->revents) { + ret = call_handler(ctx, rh->rw_func[1]); + if (ret < 0) + ret = -2; /* exception in handler */ + goto done; + } + } + } + +done: + if (pfds != pfds_local) + js_free(ctx, pfds); + return ret; +} +#endif /* QJS_WASI_REACTOR */ static JSValue make_obj_error(JSContext *ctx, JSValue obj, @@ -4548,6 +4641,53 @@ int js_std_loop(JSContext *ctx) return JS_HasException(ctx); } +#ifdef QJS_WASI_REACTOR +/* + * Run one iteration of the event loop (non-blocking). + * + * Executes all pending microtasks (promise jobs), then checks timers + * and runs at most one expired timer callback. + * + * Returns: + * > 0: Next timer fires in this many milliseconds; call again after delay + * 0: More work pending; call again immediately (via queueMicrotask) + * -1: No pending work; event loop is idle + * -2: An exception occurred; call js_std_dump_error() for details + */ +int js_std_loop_once(JSContext *ctx) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = js_get_thread_state(rt); + JSContext *ctx1; + int err, min_delay; + + /* execute all pending jobs */ + for(;;) { + err = JS_ExecutePendingJob(rt, &ctx1); + if (err < 0) + return -2; /* error */ + if (err == 0) + break; + } + + /* run at most one expired timer */ + if (js_os_run_timers(rt, ctx, ts, &min_delay) < 0) + return -2; /* error in timer callback */ + + /* check if more work is pending */ + if (JS_IsJobPending(rt)) + return 0; /* more microtasks pending */ + + if (min_delay == 0) + return 0; /* timer ready to fire immediately */ + + if (min_delay > 0) + return min_delay; /* next timer delay in ms */ + + return -1; /* idle, no pending work */ +} +#endif /* QJS_WASI_REACTOR */ + /* Wait for a promise and execute pending jobs while waiting for it. Return the promise result or JS_EXCEPTION in case of promise rejection. */ diff --git a/quickjs-libc.h b/quickjs-libc.h index 8617700ef..7fc4e942a 100644 --- a/quickjs-libc.h +++ b/quickjs-libc.h @@ -48,6 +48,10 @@ JS_EXTERN JSModuleDef *js_init_module_bjson(JSContext *ctx, const char *module_name); JS_EXTERN void js_std_add_helpers(JSContext *ctx, int argc, char **argv); JS_EXTERN int js_std_loop(JSContext *ctx); +#ifdef QJS_WASI_REACTOR +JS_EXTERN int js_std_loop_once(JSContext *ctx); +JS_EXTERN int js_std_poll_io(JSContext *ctx, int timeout_ms); +#endif JS_EXTERN JSValue js_std_await(JSContext *ctx, JSValue obj); JS_EXTERN void js_std_init_handlers(JSRuntime *rt); JS_EXTERN void js_std_free_handlers(JSRuntime *rt);