Skip to content

Commit 6bdf1df

Browse files
authored
Add WASI reactor build and re-entrant event loop APIs (#1308)
1 parent 97d23e5 commit 6bdf1df

File tree

6 files changed

+400
-45
lines changed

6 files changed

+400
-45
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,11 @@ jobs:
418418
wasmtime run build/qjs -qd
419419
echo "console.log('hello wasi!');" > t.js
420420
wasmtime run --dir . build/qjs t.js
421+
- name: build wasi reactor
422+
run: |
423+
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON
424+
make -C build qjs_wasi
425+
ls -lh build/qjs.wasm
421426
422427
cygwin:
423428
runs-on: windows-latest

.github/workflows/release.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,11 +131,18 @@ jobs:
131131
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake
132132
make -C build qjs_exe
133133
mv build/qjs build/qjs-wasi.wasm
134+
- name: build wasi reactor
135+
run: |
136+
cmake -B build -DCMAKE_TOOLCHAIN_FILE=/opt/wasi-sdk/share/cmake/wasi-sdk.cmake -DQJS_WASI_REACTOR=ON
137+
make -C build qjs_wasi
138+
mv build/qjs.wasm build/qjs-wasi-reactor.wasm
134139
- name: upload
135140
uses: actions/upload-artifact@v6
136141
with:
137142
name: qjs-wasi
138-
path: build/qjs-wasi.wasm
143+
path: |
144+
build/qjs-wasi.wasm
145+
build/qjs-wasi-reactor.wasm
139146
140147
upload-to-release:
141148
needs: [linux, macos, windows, wasi, check_meson_version]

CMakeLists.txt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,36 @@ target_link_libraries(qjs_exe qjs)
335335
if(NOT WIN32)
336336
set_target_properties(qjs_exe PROPERTIES ENABLE_EXPORTS TRUE)
337337
endif()
338+
339+
# WASI Reactor
340+
#
341+
342+
if(CMAKE_SYSTEM_NAME STREQUAL "WASI")
343+
option(QJS_WASI_REACTOR "Build WASI reactor (exports library functions, no _start)" OFF)
344+
if(QJS_WASI_REACTOR)
345+
add_executable(qjs_wasi
346+
quickjs-libc.c
347+
qjs-wasi-reactor.c
348+
)
349+
set_target_properties(qjs_wasi PROPERTIES
350+
OUTPUT_NAME "qjs"
351+
SUFFIX ".wasm"
352+
)
353+
target_compile_definitions(qjs_wasi PRIVATE ${qjs_defines})
354+
target_link_libraries(qjs_wasi qjs)
355+
target_link_options(qjs_wasi PRIVATE
356+
-mexec-model=reactor
357+
# Export all symbols with default visibility (JS_EXTERN functions)
358+
-Wl,--export-dynamic
359+
# Memory management (libc symbols need explicit export)
360+
-Wl,--export=malloc
361+
-Wl,--export=free
362+
-Wl,--export=realloc
363+
-Wl,--export=calloc
364+
)
365+
endif()
366+
endif()
367+
338368
if(QJS_BUILD_CLI_WITH_MIMALLOC OR QJS_BUILD_CLI_WITH_STATIC_MIMALLOC)
339369
find_package(mimalloc REQUIRED)
340370
# Upstream mimalloc doesn't provide a way to know if both libraries are supported.

qjs-wasi-reactor.c

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/*
2+
* QuickJS WASI Reactor Mode
3+
*
4+
* In reactor mode, QuickJS exports functions that can be called repeatedly
5+
* by the host instead of running main() once and blocking in the event loop.
6+
*
7+
* Copyright (c) 2017-2021 Fabrice Bellard
8+
* Copyright (c) 2017-2021 Charlie Gordon
9+
*
10+
* Permission is hereby granted, free of charge, to any person obtaining a copy
11+
* of this software and associated documentation files (the "Software"), to deal
12+
* in the Software without restriction, including without limitation the rights
13+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
* copies of the Software, and to permit persons to whom the Software is
15+
* furnished to do so, subject to the following conditions:
16+
*
17+
* The above copyright notice and this permission notice shall be included in
18+
* all copies or substantial portions of the Software.
19+
*
20+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
23+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26+
* THE SOFTWARE.
27+
*/
28+
29+
/* Include qjs.c to get access to static functions like eval_buf, eval_file,
30+
* parse_limit, and JS_NewCustomContext */
31+
#include "qjs.c"
32+
33+
static JSRuntime *reactor_rt = NULL;
34+
static JSContext *reactor_ctx = NULL;
35+
36+
int qjs_init_argv(int argc, char **argv);
37+
38+
__attribute__((export_name("qjs_init")))
39+
int qjs_init(void)
40+
{
41+
static char *empty_argv[] = { "qjs", NULL };
42+
return qjs_init_argv(1, empty_argv);
43+
}
44+
45+
__attribute__((export_name("qjs_init_argv")))
46+
int qjs_init_argv(int argc, char **argv)
47+
{
48+
int optind = 1;
49+
char *expr = NULL;
50+
int module = -1;
51+
int load_std = 0;
52+
char *include_list[32];
53+
int i, include_count = 0;
54+
int64_t memory_limit = -1;
55+
int64_t stack_size = -1;
56+
57+
if (reactor_rt)
58+
return -1; /* already initialized */
59+
60+
/* Parse options (subset of main()) */
61+
while (optind < argc && *argv[optind] == '-') {
62+
char *arg = argv[optind] + 1;
63+
const char *longopt = "";
64+
char *optarg = NULL;
65+
if (!*arg)
66+
break;
67+
optind++;
68+
if (*arg == '-') {
69+
longopt = arg + 1;
70+
optarg = strchr(longopt, '=');
71+
if (optarg)
72+
*optarg++ = '\0';
73+
arg += strlen(arg);
74+
if (!*longopt)
75+
break;
76+
}
77+
for (; *arg || *longopt; longopt = "") {
78+
char opt = *arg;
79+
if (opt) {
80+
arg++;
81+
if (!optarg && *arg)
82+
optarg = arg;
83+
}
84+
if (opt == 'e' || !strcmp(longopt, "eval")) {
85+
if (!optarg) {
86+
if (optind >= argc)
87+
return -1;
88+
optarg = argv[optind++];
89+
}
90+
expr = optarg;
91+
break;
92+
}
93+
if (opt == 'I' || !strcmp(longopt, "include")) {
94+
if (optind >= argc || include_count >= countof(include_list))
95+
return -1;
96+
include_list[include_count++] = argv[optind++];
97+
continue;
98+
}
99+
if (opt == 'm' || !strcmp(longopt, "module")) {
100+
module = 1;
101+
continue;
102+
}
103+
if (!strcmp(longopt, "std")) {
104+
load_std = 1;
105+
continue;
106+
}
107+
if (!strcmp(longopt, "memory-limit")) {
108+
if (!optarg) {
109+
if (optind >= argc)
110+
return -1;
111+
optarg = argv[optind++];
112+
}
113+
memory_limit = parse_limit(optarg);
114+
break;
115+
}
116+
if (!strcmp(longopt, "stack-size")) {
117+
if (!optarg) {
118+
if (optind >= argc)
119+
return -1;
120+
optarg = argv[optind++];
121+
}
122+
stack_size = parse_limit(optarg);
123+
break;
124+
}
125+
break; /* ignore unknown options */
126+
}
127+
}
128+
129+
reactor_rt = JS_NewRuntime();
130+
if (!reactor_rt)
131+
return -1;
132+
if (memory_limit >= 0)
133+
JS_SetMemoryLimit(reactor_rt, (size_t)memory_limit);
134+
if (stack_size >= 0)
135+
JS_SetMaxStackSize(reactor_rt, (size_t)stack_size);
136+
137+
js_std_set_worker_new_context_func(JS_NewCustomContext);
138+
js_std_init_handlers(reactor_rt);
139+
140+
reactor_ctx = JS_NewCustomContext(reactor_rt);
141+
if (!reactor_ctx) {
142+
js_std_free_handlers(reactor_rt);
143+
JS_FreeRuntime(reactor_rt);
144+
reactor_rt = NULL;
145+
return -1;
146+
}
147+
148+
JS_SetModuleLoaderFunc2(reactor_rt, NULL, js_module_loader,
149+
js_module_check_attributes, NULL);
150+
JS_SetHostPromiseRejectionTracker(reactor_rt, js_std_promise_rejection_tracker, NULL);
151+
js_std_add_helpers(reactor_ctx, argc - optind, argv + optind);
152+
153+
if (load_std) {
154+
const char *str =
155+
"import * as bjson from 'qjs:bjson';\n"
156+
"import * as std from 'qjs:std';\n"
157+
"import * as os from 'qjs:os';\n"
158+
"globalThis.bjson = bjson;\n"
159+
"globalThis.std = std;\n"
160+
"globalThis.os = os;\n";
161+
if (eval_buf(reactor_ctx, str, strlen(str), "<input>", JS_EVAL_TYPE_MODULE))
162+
goto fail;
163+
}
164+
165+
for (i = 0; i < include_count; i++) {
166+
if (eval_file(reactor_ctx, include_list[i], 0))
167+
goto fail;
168+
}
169+
170+
if (expr) {
171+
if (eval_buf(reactor_ctx, expr, strlen(expr), "<cmdline>",
172+
module == 1 ? JS_EVAL_TYPE_MODULE : 0))
173+
goto fail;
174+
} else if (optind < argc) {
175+
if (eval_file(reactor_ctx, argv[optind], module))
176+
goto fail;
177+
}
178+
179+
return 0;
180+
181+
fail:
182+
js_std_free_handlers(reactor_rt);
183+
JS_FreeContext(reactor_ctx);
184+
JS_FreeRuntime(reactor_rt);
185+
reactor_rt = NULL;
186+
reactor_ctx = NULL;
187+
return -1;
188+
}
189+
190+
__attribute__((export_name("qjs_get_context")))
191+
JSContext *qjs_get_context(void)
192+
{
193+
return reactor_ctx;
194+
}
195+
196+
__attribute__((export_name("qjs_destroy")))
197+
void qjs_destroy(void)
198+
{
199+
if (reactor_ctx) {
200+
js_std_free_handlers(reactor_rt);
201+
JS_FreeContext(reactor_ctx);
202+
reactor_ctx = NULL;
203+
}
204+
if (reactor_rt) {
205+
JS_FreeRuntime(reactor_rt);
206+
reactor_rt = NULL;
207+
}
208+
}

0 commit comments

Comments
 (0)