-
Notifications
You must be signed in to change notification settings - Fork 239
Add QJS_WASI_REACTOR build target #1307
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Added a WASI reactor build variant for QuickJS-ng that enables re-entrant
execution in JavaScript host environments (browsers, Node.js, Deno, Bun).
The standard WASI "command" model has a _start() entry point that blocks in an
event loop until completion. This freezes the host's event loop, preventing
queueMicrotask, setTimeout, DOM events, etc. from running.
The reactor model instead exports functions that the host calls:
- qjs_init() - Initialize empty runtime, returns 0 on success
- qjs_init_argv(argc, argv) - Initialize with e.g. ["qjs", "--std", "script.js"]
- qjs_eval(code, len, filename, is_module) - Evaluate JS code
- qjs_loop_once() - Run one iteration of the event loop (non-blocking)
- Returns >0: next timer fires in N ms
- Returns 0: more microtasks pending, call again immediately
- Returns -1: idle, no pending work
- Returns -2: error occurred
- qjs_poll_io(timeout_ms) - Poll for I/O and invoke read/write handlers
- Separate from qjs_loop_once() so the host can call it only when I/O is ready
- This avoids unnecessary poll() syscalls - the host knows when data is available
- Required because qjs_loop_once() only handles timers/microtasks, not I/O
- Returns 0: success, -1: error, -2: exception in handler
- qjs_destroy() - Cleanup runtime
- malloc/free - For host to allocate memory for argv/code strings
The host controls scheduling by calling qjs_loop_once() and using setTimeout or
queueMicrotask based on the return value. When stdin has data, the host calls
qjs_poll_io() to trigger os.setReadHandler callbacks.
Build:
cmake -B build-reactor \
-DCMAKE_TOOLCHAIN_FILE=/path/to/wasi-sdk/share/cmake/wasi-sdk.cmake \
-DQJS_WASI_REACTOR=ON
make -C build-reactor qjs_wasi_reactor
Output: build-reactor/qjs.wasm
See QJS_WASI_REACTOR.md for full design document.
Signed-off-by: Christian Stewart <christian@aperture.us>
|
This was quite some engineering effort required here w/ various solutions tried to build a quickjs-wasi version which can correctly process events from the JavaScript host environment. I'm using it in a few applications now. I'm sending this PR because I think this approach will prove useful for a lot of types of applications where we run Wasi within JavaScript and still want to be able to have JavaScript handling the I/O outside the Wasm context. For example, with this version, we can use MessagePort and/or BroadcastChannel to communicate with other Workers in the browser. There is also a performance improvement since the host environment knows when I/O is available and can defer processing that I/O until then. |
|
Interesting! Where can I read some more about this WASM reactor? As for this PR, looks like only the libc bits are necessary and then you could have some quickjs-reactor library of your own which embeds quickjs and exposes the necessary functions. Any reason why those would need to be part of this project? |
|
Hi @saghul I think this has everything - examples, design doc, etc:
This seems to be a good article on the terminology for "command" vs "reactor" module: https://dylibso.com/blog/wasi-command-reactor/ Here's the Go development log describing reactors: https://go.dev/blog/wasmexport#building-a-wasi-reactor The main difference is instead of calling _start we call functions inside the module. I sent this as a PR to this library because using quickjs-ng as a wasi library (not a program) actually seems to be a legitimate use case on a wider level than just my project. Technically yes it could be an external lib but since you already ship a wasi .wasm artifact on the GitHub releases for quickjs-ng it seems like a good idea to also add a -reactor.wasm variant. |
|
Were you thinking we could instead just expose the entire library on a libc level as a reactor and then have a third-party library that implements the higher-level SDK I added in this PR? |
Added a WASI reactor build variant for QuickJS-ng that enables re-entrant execution in JavaScript host environments (browsers, Node.js, Deno, Bun).
The standard WASI "command" model has a _start() entry point that blocks in an event loop until completion. This freezes the host's event loop, preventing queueMicrotask, setTimeout, DOM events, etc. from running.
The reactor model instead exports functions that the host calls:
The host controls scheduling by calling qjs_loop_once() and using setTimeout or queueMicrotask based on the return value. When stdin has data, the host calls qjs_poll_io() to trigger os.setReadHandler callbacks.
Build:
Output: build-reactor/qjs.wasm
See QJS_WASI_REACTOR.md for full design document.
The goal of this PR in general is to allow executing quickjs within an environment where we need to run JavaScript callbacks as well as WebAssembly. Since JS is single-threaded, if we run in a traditional wasi environment, it's stuck in there, and never goes back out to JavaScript, so JS-land never has a chance to execute.
A Go library consuming this variant is available with a proof of concept of usage: https://github.com/aperturerobotics/go-quickjs-wasi-reactor
A Js library consuming the variant with a Js-based harness: https://github.com/aperturerobotics/js-quickjs-wasi-reactor