From 7574711f51a93deb1580584cec60b8c23a6785ff Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 7 Jan 2026 00:36:41 -0800 Subject: [PATCH 1/5] Add WASI reactor build and re-entrant event loop APIs Add js_std_loop_once() and js_std_poll_io() to quickjs-libc for embedding QuickJS in host environments where the host controls the event loop (browsers, Node.js, Deno, Bun, Go with wazero, etc). The standard js_std_loop() blocks until all work is complete, which freezes the host's event loop and prevents host callbacks from running. The new APIs enable cooperative scheduling: js_std_loop_once() - Run one iteration of the event loop (non-blocking) - Executes all pending promise jobs (microtasks) - Runs at most one expired timer callback - Returns >0: next timer fires in N ms, use setTimeout - Returns 0: more microtasks pending, call again immediately - Returns -1: idle, no pending work - Returns -2: error occurred js_std_poll_io(timeout_ms) - Poll for I/O and invoke read/write handlers - Separate from loop_once so host can call it only when I/O is ready - Avoids unnecessary poll() syscalls when host knows data is available - Required because loop_once only handles timers/microtasks, not I/O - Returns 0: success, -1: error, -2: exception in handler Add QJS_WASI_REACTOR cmake option that builds QuickJS as a WASI reactor module. Unlike the command model (which has _start and blocks in js_std_loop), reactors export library functions that can be called repeatedly by the host. Build: cmake -B build -DCMAKE_TOOLCHAIN_FILE=.../wasi-sdk.cmake -DQJS_WASI_REACTOR=ON cmake --build build --target qjs_wasi Output: qjs.wasm (reactor module with exported quickjs.h / quickjs-libc.h APIs) The reactor wasm is included in releases as qjs-wasi-reactor.wasm. Signed-off-by: Christian Stewart --- .github/workflows/ci.yml | 5 ++ .github/workflows/release.yml | 9 ++- CMakeLists.txt | 96 +++++++++++++++++++++++ quickjs-libc.c | 142 ++++++++++++++++++++++++++++++++++ quickjs-libc.h | 2 + 5 files changed, 253 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7d897a1c..5f37978dd 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: build wasi reactor + run: | + cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON + make -C build qjs_wasi + ls -lh build/qjs.wasm cygwin: runs-on: windows-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c59aa163f..ec4d8ec86 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,11 +131,18 @@ 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 wasi reactor + run: | + cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON + make -C build qjs_wasi + mv build/qjs.wasm build/qjs-wasi-reactor.wasm - name: upload uses: actions/upload-artifact@v6 with: name: qjs-wasi - path: build/qjs-wasi.wasm + path: | + build/qjs-wasi.wasm + 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..5269ad28a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -335,6 +335,102 @@ target_link_libraries(qjs_exe qjs) if(NOT WIN32) set_target_properties(qjs_exe PROPERTIES ENABLE_EXPORTS TRUE) endif() + +# WASI Reactor +# + +if(CMAKE_SYSTEM_NAME STREQUAL "WASI") + option(QJS_WASI_REACTOR "Build WASI reactor (exports library functions, no _start)" OFF) + if(QJS_WASI_REACTOR) + add_executable(qjs_wasi + quickjs-libc.c + ) + set_target_properties(qjs_wasi PROPERTIES + OUTPUT_NAME "qjs" + SUFFIX ".wasm" + ) + target_compile_definitions(qjs_wasi PRIVATE ${qjs_defines}) + target_link_libraries(qjs_wasi qjs) + target_link_options(qjs_wasi PRIVATE + -mexec-model=reactor + # Memory management + -Wl,--export=malloc + -Wl,--export=free + -Wl,--export=realloc + -Wl,--export=calloc + # Core runtime (quickjs.h) + -Wl,--export=JS_NewRuntime + -Wl,--export=JS_FreeRuntime + -Wl,--export=JS_NewContext + -Wl,--export=JS_FreeContext + -Wl,--export=JS_GetRuntime + -Wl,--export=JS_SetMemoryLimit + -Wl,--export=JS_SetMaxStackSize + -Wl,--export=JS_SetGCThreshold + -Wl,--export=JS_RunGC + # Evaluation + -Wl,--export=JS_Eval + -Wl,--export=JS_EvalFunction + -Wl,--export=JS_Call + # Values (note: many JS_Is*/JS_New* are inline, so not exported) + -Wl,--export=JS_NewStringLen + -Wl,--export=JS_NewObject + -Wl,--export=JS_NewArray + -Wl,--export=JS_NewArrayBufferCopy + -Wl,--export=JS_ToBool + -Wl,--export=JS_ToInt32 + -Wl,--export=JS_ToInt64 + -Wl,--export=JS_ToFloat64 + -Wl,--export=JS_ToCStringLen2 + -Wl,--export=JS_FreeCString + -Wl,--export=JS_GetArrayBuffer + -Wl,--export=JS_DupValue + -Wl,--export=JS_FreeValue + # Properties + -Wl,--export=JS_GetPropertyStr + -Wl,--export=JS_GetPropertyUint32 + -Wl,--export=JS_SetPropertyStr + -Wl,--export=JS_SetPropertyUint32 + -Wl,--export=JS_HasProperty + -Wl,--export=JS_DeleteProperty + -Wl,--export=JS_GetGlobalObject + # Exceptions + -Wl,--export=JS_Throw + -Wl,--export=JS_GetException + -Wl,--export=JS_HasException + -Wl,--export=JS_IsError + # Promises + -Wl,--export=JS_PromiseState + -Wl,--export=JS_PromiseResult + # Jobs + -Wl,--export=JS_ExecutePendingJob + -Wl,--export=JS_IsJobPending + # Modules + -Wl,--export=JS_SetModuleLoaderFunc + -Wl,--export=JS_NewCModule + -Wl,--export=JS_AddModuleExport + -Wl,--export=JS_SetModuleExport + # Standard library (quickjs-libc.h) + -Wl,--export=js_init_module_std + -Wl,--export=js_init_module_os + -Wl,--export=js_init_module_bjson + -Wl,--export=js_std_init_handlers + -Wl,--export=js_std_free_handlers + -Wl,--export=js_std_add_helpers + -Wl,--export=js_std_loop + -Wl,--export=js_std_loop_once + -Wl,--export=js_std_poll_io + -Wl,--export=js_std_await + -Wl,--export=js_std_dump_error + -Wl,--export=js_std_eval_binary + -Wl,--export=js_load_file + -Wl,--export=js_module_set_import_meta + -Wl,--export=js_module_loader + -Wl,--export=js_std_promise_rejection_tracker + ) + endif() +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/quickjs-libc.c b/quickjs-libc.c index 9aa7e775e..920212087 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -2856,6 +2856,115 @@ static int js_os_poll(JSContext *ctx) } #endif // defined(_WIN32) +#if defined(_WIN32) +int js_std_poll_io(JSContext *ctx, int timeout_ms) +{ + JSRuntime *rt = JS_GetRuntime(ctx); + JSThreadState *ts = js_get_thread_state(rt); + int count; + JSOSRWHandler *rh; + struct list_head *el; + HANDLE handles[MAXIMUM_WAIT_OBJECTS]; + + /* Check if there are any I/O handlers registered */ + if (list_empty(&ts->os_rw_handlers)) + return 0; /* no handlers, nothing to do */ + + count = 0; + list_for_each(el, &ts->os_rw_handlers) { + rh = list_entry(el, JSOSRWHandler, link); + if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) + handles[count++] = (HANDLE)_get_osfhandle(rh->fd); + if (count == (int)countof(handles)) + break; + } + + if (count == 0) + return 0; /* no active handlers */ + + DWORD ret, timeout = (timeout_ms < 0) ? INFINITE : (DWORD)timeout_ms; + ret = WaitForMultipleObjects(count, handles, FALSE, timeout); + if (ret < (DWORD)count) { + list_for_each(el, &ts->os_rw_handlers) { + rh = list_entry(el, JSOSRWHandler, link); + if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) { + int r = call_handler(ctx, rh->rw_func[0]); + return (r < 0) ? -2 : 0; + } + } + } + return 0; +} +#else // !defined(_WIN32) +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 // defined(_WIN32) static JSValue make_obj_error(JSContext *ctx, JSValue obj, @@ -4660,6 +4769,39 @@ int js_std_loop(JSContext *ctx) return JS_HasException(ctx); } +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 */ +} + /* 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 58c2525bc..abfd6c2c4 100644 --- a/quickjs-libc.h +++ b/quickjs-libc.h @@ -48,6 +48,8 @@ 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); +JS_EXTERN int js_std_loop_once(JSContext *ctx); +JS_EXTERN int js_std_poll_io(JSContext *ctx, int timeout_ms); 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); From 516e2944523e1765190be19506fe065c682c5092 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 7 Jan 2026 16:28:37 -0800 Subject: [PATCH 2/5] Refactor js_std_poll_io to share code with js_os_poll Address review feedback to eliminate code duplication between js_std_poll_io() and js_os_poll(). Both functions now call a shared js_os_poll_internal() with configurable behavior via flags: - JS_OS_POLL_RUN_TIMERS: process timer callbacks - JS_OS_POLL_WORKERS: include worker message pipes in poll - JS_OS_POLL_SIGNALS: check and dispatch pending signal handlers js_os_poll() passes all flags for full event loop behavior. js_std_poll_io() passes no flags to poll only I/O handlers, ensuring it does not unexpectedly run timers, worker handlers, or signal handlers. This reduces code by ~40 lines and ensures bug fixes apply to both paths. Signed-off-by: Christian Stewart --- quickjs-libc.c | 250 ++++++++++++++++++++----------------------------- 1 file changed, 104 insertions(+), 146 deletions(-) diff --git a/quickjs-libc.c b/quickjs-libc.c index 920212087..c921f44ec 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -2673,8 +2673,13 @@ static int handle_posted_message(JSRuntime *rt, JSContext *ctx, #endif // USE_WORKER +/* flags for js_os_poll_internal */ +#define JS_OS_POLL_RUN_TIMERS (1 << 0) +#define JS_OS_POLL_WORKERS (1 << 1) +#define JS_OS_POLL_SIGNALS (1 << 2) + #if defined(_WIN32) -static int js_os_poll(JSContext *ctx) +static int js_os_poll_internal(JSContext *ctx, int timeout_ms, int flags) { JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = js_get_thread_state(rt); @@ -2685,13 +2690,17 @@ static int js_os_poll(JSContext *ctx) /* XXX: handle signals if useful */ - if (js_os_run_timers(rt, ctx, ts, &min_delay)) - return -1; - if (min_delay == 0) - return 0; // expired timer - if (min_delay < 0) - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->port_list)) - return -1; /* no more events */ + min_delay = timeout_ms; + + if (flags & JS_OS_POLL_RUN_TIMERS) { + if (js_os_run_timers(rt, ctx, ts, &min_delay)) + return -1; + if (min_delay == 0) + return 0; // expired timer + if (min_delay < 0) + if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->port_list)) + return -1; /* no more events */ + } count = 0; list_for_each(el, &ts->os_rw_handlers) { @@ -2702,13 +2711,15 @@ static int js_os_poll(JSContext *ctx) break; } - list_for_each(el, &ts->port_list) { - JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); - if (JS_IsNull(port->on_message_func)) - continue; - handles[count++] = port->recv_pipe->waker.handle; - if (count == (int)countof(handles)) - break; + if (flags & JS_OS_POLL_WORKERS) { + list_for_each(el, &ts->port_list) { + JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); + if (JS_IsNull(port->on_message_func)) + continue; + handles[count++] = port->recv_pipe->waker.handle; + if (count == (int)countof(handles)) + break; + } } if (count > 0) { @@ -2716,7 +2727,7 @@ static int js_os_poll(JSContext *ctx) if (min_delay != -1) timeout = min_delay; ret = WaitForMultipleObjects(count, handles, FALSE, timeout); - if (ret < count) { + if (ret < (DWORD)count) { list_for_each(el, &ts->os_rw_handlers) { rh = list_entry(el, JSOSRWHandler, link); if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) { @@ -2725,25 +2736,42 @@ static int js_os_poll(JSContext *ctx) } } - list_for_each(el, &ts->port_list) { - JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); - if (!JS_IsNull(port->on_message_func)) { - JSWorkerMessagePipe *ps = port->recv_pipe; - if (ps->waker.handle == handles[ret]) { - if (handle_posted_message(rt, ctx, port)) - goto done; + if (flags & JS_OS_POLL_WORKERS) { + list_for_each(el, &ts->port_list) { + JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); + if (!JS_IsNull(port->on_message_func)) { + JSWorkerMessagePipe *ps = port->recv_pipe; + if (ps->waker.handle == handles[ret]) { + if (handle_posted_message(rt, ctx, port)) + goto done; + } } } } } - } else { + } else if (min_delay > 0) { Sleep(min_delay); } done: return 0; } -#else // !defined(_WIN32) + static int js_os_poll(JSContext *ctx) +{ + return js_os_poll_internal(ctx, -1, + JS_OS_POLL_RUN_TIMERS | JS_OS_POLL_WORKERS | JS_OS_POLL_SIGNALS); +} + +int js_std_poll_io(JSContext *ctx, int timeout_ms) +{ + int ret = js_os_poll_internal(ctx, timeout_ms, 0); + /* map return codes: -1 on error stays -1, negative from handler becomes -2 */ + if (ret < -1) + return -2; + return ret; +} +#else // !defined(_WIN32) +static int js_os_poll_internal(JSContext *ctx, int timeout_ms, int flags) { JSRuntime *rt = JS_GetRuntime(ctx); JSThreadState *ts = js_get_thread_state(rt); @@ -2752,8 +2780,11 @@ static int js_os_poll(JSContext *ctx) struct list_head *el; struct pollfd *pfd, *pfds, pfds_local[64]; + min_delay = timeout_ms; + /* only check signals in the main thread */ - if (!ts->recv_pipe && + if ((flags & JS_OS_POLL_SIGNALS) && + !ts->recv_pipe && unlikely(os_pending_signals != 0)) { JSOSSignalHandler *sh; uint64_t mask; @@ -2768,13 +2799,15 @@ static int js_os_poll(JSContext *ctx) } } - if (js_os_run_timers(rt, ctx, ts, &min_delay)) - return -1; - if (min_delay == 0) - return 0; // expired timer - if (min_delay < 0) - if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->port_list)) - return -1; /* no more events */ + if (flags & JS_OS_POLL_RUN_TIMERS) { + if (js_os_run_timers(rt, ctx, ts, &min_delay)) + return -1; + if (min_delay == 0) + return 0; // expired timer + if (min_delay < 0) + if (list_empty(&ts->os_rw_handlers) && list_empty(&ts->port_list)) + return -1; /* no more events */ + } nfds = 0; list_for_each(el, &ts->os_rw_handlers) { @@ -2783,12 +2816,25 @@ static int js_os_poll(JSContext *ctx) } #ifdef USE_WORKER - list_for_each(el, &ts->port_list) { - JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); - nfds += !JS_IsNull(port->on_message_func); + if (flags & JS_OS_POLL_WORKERS) { + list_for_each(el, &ts->port_list) { + JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); + nfds += !JS_IsNull(port->on_message_func); + } } #endif // USE_WORKER + if (nfds == 0) { + if (min_delay > 0) { + struct timespec ts_sleep = { + .tv_sec = min_delay / 1000, + .tv_nsec = (min_delay % 1000) * 1000000L + }; + nanosleep(&ts_sleep, NULL); + } + return 0; + } + pfd = pfds = pfds_local; if (nfds > (int)countof(pfds_local)) { pfd = pfds = js_malloc(ctx, nfds * sizeof(*pfd)); @@ -2805,11 +2851,13 @@ static int js_os_poll(JSContext *ctx) } #ifdef USE_WORKER - list_for_each(el, &ts->port_list) { - JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); - if (!JS_IsNull(port->on_message_func)) { - JSWorkerMessagePipe *ps = port->recv_pipe; - *pfd++ = (struct pollfd){ps->waker.read_fd, POLLIN, 0}; + if (flags & JS_OS_POLL_WORKERS) { + list_for_each(el, &ts->port_list) { + JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); + if (!JS_IsNull(port->on_message_func)) { + JSWorkerMessagePipe *ps = port->recv_pipe; + *pfd++ = (struct pollfd){ps->waker.read_fd, POLLIN, 0}; + } } } #endif // USE_WORKER @@ -2819,6 +2867,10 @@ static int js_os_poll(JSContext *ctx) // i.e., it's probably good enough for now ret = 0; nfds = poll(pfds, nfds, min_delay); + if (nfds < 0) { + ret = -1; + goto done; + } for (pfd = pfds; nfds-- > 0; pfd++) { rh = find_rh(ts, pfd->fd); if (rh) { @@ -2834,8 +2886,9 @@ static int js_os_poll(JSContext *ctx) goto done; /* must stop because the list may have been modified */ } - } else { + } #ifdef USE_WORKER + else if (flags & JS_OS_POLL_WORKERS) { list_for_each(el, &ts->port_list) { JSWorkerMessageHandler *port = list_entry(el, JSWorkerMessageHandler, link); if (!JS_IsNull(port->on_message_func)) { @@ -2846,122 +2899,27 @@ static int js_os_poll(JSContext *ctx) } } } -#endif // USE_WORKER } +#endif // USE_WORKER } done: if (pfds != pfds_local) js_free(ctx, pfds); return ret; } -#endif // defined(_WIN32) -#if defined(_WIN32) -int js_std_poll_io(JSContext *ctx, int timeout_ms) +static int js_os_poll(JSContext *ctx) { - JSRuntime *rt = JS_GetRuntime(ctx); - JSThreadState *ts = js_get_thread_state(rt); - int count; - JSOSRWHandler *rh; - struct list_head *el; - HANDLE handles[MAXIMUM_WAIT_OBJECTS]; - - /* Check if there are any I/O handlers registered */ - if (list_empty(&ts->os_rw_handlers)) - return 0; /* no handlers, nothing to do */ - - count = 0; - list_for_each(el, &ts->os_rw_handlers) { - rh = list_entry(el, JSOSRWHandler, link); - if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) - handles[count++] = (HANDLE)_get_osfhandle(rh->fd); - if (count == (int)countof(handles)) - break; - } - - if (count == 0) - return 0; /* no active handlers */ - - DWORD ret, timeout = (timeout_ms < 0) ? INFINITE : (DWORD)timeout_ms; - ret = WaitForMultipleObjects(count, handles, FALSE, timeout); - if (ret < (DWORD)count) { - list_for_each(el, &ts->os_rw_handlers) { - rh = list_entry(el, JSOSRWHandler, link); - if (rh->fd == 0 && !JS_IsNull(rh->rw_func[0])) { - int r = call_handler(ctx, rh->rw_func[0]); - return (r < 0) ? -2 : 0; - } - } - } - return 0; + return js_os_poll_internal(ctx, -1, + JS_OS_POLL_RUN_TIMERS | JS_OS_POLL_WORKERS | JS_OS_POLL_SIGNALS); } -#else // !defined(_WIN32) + 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); + int ret = js_os_poll_internal(ctx, timeout_ms, 0); + /* map return codes: -1 on error stays -1, negative from handler becomes -2 */ + if (ret < -1) + return -2; return ret; } #endif // defined(_WIN32) From 4b13fcf7fdca34d7c1f6d51fdd9fd11870afa811 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 7 Jan 2026 18:07:07 -0800 Subject: [PATCH 3/5] Use --export-dynamic instead of manual export list for WASI reactor Replace the long list of -Wl,--export= flags with -Wl,--export-dynamic which automatically exports all symbols with default visibility. Since JS_EXTERN is defined as __attribute__((visibility("default"))), all public API functions are exported automatically. Only libc memory functions (malloc, free, realloc, calloc) still need explicit exports since they don't have default visibility in wasi-libc. This addresses review feedback about keeping export lists in sync manually and reduces the CMakeLists.txt WASI reactor section from ~80 lines to ~15 lines. Signed-off-by: Christian Stewart --- CMakeLists.txt | 73 +++----------------------------------------------- 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 5269ad28a..87e22c58f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -353,80 +353,13 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") target_link_libraries(qjs_wasi qjs) target_link_options(qjs_wasi PRIVATE -mexec-model=reactor - # Memory management + # Export all symbols with default visibility (JS_EXTERN functions) + -Wl,--export-dynamic + # Memory management (libc symbols need explicit export) -Wl,--export=malloc -Wl,--export=free -Wl,--export=realloc -Wl,--export=calloc - # Core runtime (quickjs.h) - -Wl,--export=JS_NewRuntime - -Wl,--export=JS_FreeRuntime - -Wl,--export=JS_NewContext - -Wl,--export=JS_FreeContext - -Wl,--export=JS_GetRuntime - -Wl,--export=JS_SetMemoryLimit - -Wl,--export=JS_SetMaxStackSize - -Wl,--export=JS_SetGCThreshold - -Wl,--export=JS_RunGC - # Evaluation - -Wl,--export=JS_Eval - -Wl,--export=JS_EvalFunction - -Wl,--export=JS_Call - # Values (note: many JS_Is*/JS_New* are inline, so not exported) - -Wl,--export=JS_NewStringLen - -Wl,--export=JS_NewObject - -Wl,--export=JS_NewArray - -Wl,--export=JS_NewArrayBufferCopy - -Wl,--export=JS_ToBool - -Wl,--export=JS_ToInt32 - -Wl,--export=JS_ToInt64 - -Wl,--export=JS_ToFloat64 - -Wl,--export=JS_ToCStringLen2 - -Wl,--export=JS_FreeCString - -Wl,--export=JS_GetArrayBuffer - -Wl,--export=JS_DupValue - -Wl,--export=JS_FreeValue - # Properties - -Wl,--export=JS_GetPropertyStr - -Wl,--export=JS_GetPropertyUint32 - -Wl,--export=JS_SetPropertyStr - -Wl,--export=JS_SetPropertyUint32 - -Wl,--export=JS_HasProperty - -Wl,--export=JS_DeleteProperty - -Wl,--export=JS_GetGlobalObject - # Exceptions - -Wl,--export=JS_Throw - -Wl,--export=JS_GetException - -Wl,--export=JS_HasException - -Wl,--export=JS_IsError - # Promises - -Wl,--export=JS_PromiseState - -Wl,--export=JS_PromiseResult - # Jobs - -Wl,--export=JS_ExecutePendingJob - -Wl,--export=JS_IsJobPending - # Modules - -Wl,--export=JS_SetModuleLoaderFunc - -Wl,--export=JS_NewCModule - -Wl,--export=JS_AddModuleExport - -Wl,--export=JS_SetModuleExport - # Standard library (quickjs-libc.h) - -Wl,--export=js_init_module_std - -Wl,--export=js_init_module_os - -Wl,--export=js_init_module_bjson - -Wl,--export=js_std_init_handlers - -Wl,--export=js_std_free_handlers - -Wl,--export=js_std_add_helpers - -Wl,--export=js_std_loop - -Wl,--export=js_std_loop_once - -Wl,--export=js_std_poll_io - -Wl,--export=js_std_await - -Wl,--export=js_std_dump_error - -Wl,--export=js_std_eval_binary - -Wl,--export=js_load_file - -Wl,--export=js_module_set_import_meta - -Wl,--export=js_module_loader - -Wl,--export=js_std_promise_rejection_tracker ) endif() endif() From 54f5f9d88a506200ea66a598f4651d5ebd3b788c Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Wed, 7 Jan 2026 21:55:37 -0800 Subject: [PATCH 4/5] Add reactor initialization functions for module loader setup The WASI reactor build exports raw QuickJS C APIs, but setting up the module loader requires calling JS_SetModuleLoaderFunc2() with js_module_loader - a C function pointer that cannot be obtained or called from the host side (Go, JavaScript, etc). Without the module loader, dynamic import() fails. Add qjs_init_argv() to qjs.c which initializes the reactor with CLI argument parsing (like main() but without blocking in the event loop). This: - Creates runtime and context - Sets up the module loader via JS_SetModuleLoaderFunc2() - Sets up the promise rejection tracker - Parses CLI flags: --std, -m/--module, -e/--eval, -I/--include - Loads and evaluates the initial script file The functions are added to qjs.c (guarded by #ifdef QJS_WASI_REACTOR) rather than quickjs-libc.c because they depend on static functions in qjs.c like eval_buf(), eval_file(), parse_limit(), and JS_NewCustomContext(). Exported functions: - qjs_init() - Initialize with default args - qjs_init_argv(argc, argv) - Initialize with CLI args - qjs_get_context() - Get JSContext* for use with js_std_loop_once etc - qjs_destroy() - Cleanup runtime Example usage from host: qjs_init_argv(3, ["qjs", "--std", "/boot/script.js"]) while running: result = js_std_loop_once(qjs_get_context()) // handle result qjs_destroy() Signed-off-by: Christian Stewart --- CMakeLists.txt | 1 + qjs-wasi-reactor.c | 208 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 qjs-wasi-reactor.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 87e22c58f..76ab2a519 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -344,6 +344,7 @@ if(CMAKE_SYSTEM_NAME STREQUAL "WASI") if(QJS_WASI_REACTOR) add_executable(qjs_wasi quickjs-libc.c + qjs-wasi-reactor.c ) set_target_properties(qjs_wasi PROPERTIES OUTPUT_NAME "qjs" diff --git a/qjs-wasi-reactor.c b/qjs-wasi-reactor.c new file mode 100644 index 000000000..91d74eb47 --- /dev/null +++ b/qjs-wasi-reactor.c @@ -0,0 +1,208 @@ +/* + * QuickJS WASI Reactor Mode + * + * In reactor mode, QuickJS exports functions that can be called repeatedly + * by the host instead of running main() once and blocking in the event loop. + * + * Copyright (c) 2017-2021 Fabrice Bellard + * Copyright (c) 2017-2021 Charlie Gordon + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +/* Include qjs.c to get access to static functions like eval_buf, eval_file, + * parse_limit, and JS_NewCustomContext */ +#include "qjs.c" + +static JSRuntime *reactor_rt = NULL; +static JSContext *reactor_ctx = NULL; + +int qjs_init_argv(int argc, char **argv); + +__attribute__((export_name("qjs_init"))) +int qjs_init(void) +{ + static char *empty_argv[] = { "qjs", NULL }; + return qjs_init_argv(1, empty_argv); +} + +__attribute__((export_name("qjs_init_argv"))) +int qjs_init_argv(int argc, char **argv) +{ + int optind = 1; + char *expr = NULL; + 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 */ + + /* Parse options (subset of main()) */ + 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 == '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 == 'm' || !strcmp(longopt, "module")) { + module = 1; + continue; + } + if (!strcmp(longopt, "std")) { + load_std = 1; + 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; + } + break; /* ignore unknown options */ + } + } + + 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); + + js_std_set_worker_new_context_func(JS_NewCustomContext); + js_std_init_handlers(reactor_rt); + + reactor_ctx = JS_NewCustomContext(reactor_rt); + if (!reactor_ctx) { + js_std_free_handlers(reactor_rt); + JS_FreeRuntime(reactor_rt); + reactor_rt = NULL; + return -1; + } + + JS_SetModuleLoaderFunc2(reactor_rt, NULL, js_module_loader, + js_module_check_attributes, NULL); + JS_SetHostPromiseRejectionTracker(reactor_rt, js_std_promise_rejection_tracker, NULL); + 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 (expr) { + if (eval_buf(reactor_ctx, expr, strlen(expr), "", + module == 1 ? JS_EVAL_TYPE_MODULE : 0)) + goto fail; + } else if (optind < argc) { + if (eval_file(reactor_ctx, argv[optind], module)) + goto fail; + } + + return 0; + +fail: + js_std_free_handlers(reactor_rt); + JS_FreeContext(reactor_ctx); + JS_FreeRuntime(reactor_rt); + reactor_rt = NULL; + reactor_ctx = NULL; + return -1; +} + +__attribute__((export_name("qjs_get_context"))) +JSContext *qjs_get_context(void) +{ + return reactor_ctx; +} + +__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; + } +} From 828729c890ccdd610b7d5812ce59ae8cbee26f11 Mon Sep 17 00:00:00 2001 From: Christian Stewart Date: Mon, 12 Jan 2026 14:22:32 -0800 Subject: [PATCH 5/5] Handle EINTR in nanosleep for signal-interrupted sleeps nanosleep can return early with EINTR when interrupted by a signal. Loop until the sleep completes or a new signal arrives, using os_pending_signals to detect signal state changes. Signed-off-by: Christian Stewart --- quickjs-libc.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/quickjs-libc.c b/quickjs-libc.c index c921f44ec..f81a75a7c 100644 --- a/quickjs-libc.c +++ b/quickjs-libc.c @@ -2830,7 +2830,10 @@ static int js_os_poll_internal(JSContext *ctx, int timeout_ms, int flags) .tv_sec = min_delay / 1000, .tv_nsec = (min_delay % 1000) * 1000000L }; - nanosleep(&ts_sleep, NULL); + uint64_t mask = os_pending_signals; + while (nanosleep(&ts_sleep, &ts_sleep) + && errno == EINTR + && mask == os_pending_signals); } return 0; }