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);