Skip to content

Conversation

@paralin
Copy link
Contributor

@paralin paralin commented Jan 7, 2026

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.

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

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>
@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

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.

@saghul
Copy link
Contributor

saghul commented Jan 7, 2026

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?

@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

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.

@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

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?

@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

@saghul I have implemented it on the libc level as an alternative PR #1308 as you suggested. It seems like the "correct" way to do it, actually. This exposes all the functions including manipulating js objects, etc. Closing this one in favor of that one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants