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..76ab2a519 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -335,6 +335,36 @@ 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
+ qjs-wasi-reactor.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
+ # 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
+ )
+ 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/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;
+ }
+}
diff --git a/quickjs-libc.c b/quickjs-libc.c
index 9aa7e775e..f81a75a7c 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,28 @@ 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
+ };
+ uint64_t mask = os_pending_signals;
+ while (nanosleep(&ts_sleep, &ts_sleep)
+ && errno == EINTR
+ && mask == os_pending_signals);
+ }
+ return 0;
+ }
+
pfd = pfds = pfds_local;
if (nfds > (int)countof(pfds_local)) {
pfd = pfds = js_malloc(ctx, nfds * sizeof(*pfd));
@@ -2805,11 +2854,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 +2870,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 +2889,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,16 +2902,30 @@ 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)
+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;
+}
+#endif // defined(_WIN32)
static JSValue make_obj_error(JSContext *ctx,
JSValue obj,
@@ -4660,6 +4730,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);