Skip to content

Conversation

@paralin
Copy link
Contributor

@paralin paralin commented Jan 7, 2026

Add js_std_loop_once() and js_std_poll_io() functions to quickjs-libc for re-entrant event loop control. These enable embedding QuickJS in host environments where the host controls scheduling (browsers, Node.js, Deno, C programs).

js_std_loop_once() runs one iteration of the event loop:

  • Executes all pending promise jobs (microtasks)
  • Runs at most one expired timer callback
  • Returns: >0 (next timer ms), 0 (work pending), -1 (idle), -2 (error)

js_std_poll_io() polls for I/O and invokes read/write handlers:

  • Designed for hosts that know when I/O is available
  • Returns: 0 (success), -1 (error), -2 (exception in handler)

Add QJS_WASI_REACTOR cmake option that builds QuickJS as a WASI reactor module, exporting the quickjs.h and quickjs-libc.h library functions. Unlike the command model (which has _start and blocks), reactors export functions that can be called repeatedly by the host.

Build with:
cmake -B build -DCMAKE_TOOLCHAIN_FILE=.../wasi-sdk.cmake -DQJS_WASI_REACTOR=ON
cmake --build build --target qjs_wasi

Output: qjs.wasm (reactor module)

The reactor wasm will be included in releases as qjs-wasi-reactor.wasm.

@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

Rework of #1307 - this exposes quickjs-ng as a library, with all of the C symbols properly exported.

@paralin paralin force-pushed the wasi-reactor-libc branch 4 times, most recently from 53c1239 to f9b3d57 Compare January 7, 2026 09:12
@paralin
Copy link
Contributor Author

paralin commented Jan 7, 2026

@paralin paralin force-pushed the wasi-reactor-libc branch 2 times, most recently from e63c573 to cf50ed8 Compare January 8, 2026 02:04
@paralin paralin requested review from bnoordhuis and saghul January 8, 2026 02:09
@paralin
Copy link
Contributor Author

paralin commented Jan 8, 2026

I tried minimizing (removing) the init_argv routine but had to add it back eventually.

Setting up the module loader requires calling JS_SetModuleLoaderFunc2() with js_module_loader - a C function pointer that can't be obtained from the host side (Go, JavaScript, etc.). Without the module loader, import() fails.

The qjs_init_argv() function in the latest commit solves this by providing a thin wrapper that:

  • Sets up runtime/context with the module loader configured
  • Handles --std, -m, -e, -I flags (reusing existing qjs CLI semantics)
  • Returns immediately (no blocking event loop)

This lets hosts do:

qjs_init_argv(3, ["qjs", "--std", "/boot/script.js"])
while running:
    result = js_std_loop_once(qjs_get_context())
qjs_destroy()

Now the question is how to reduce duplicated code by re-using the arg parsing logic from qjs.c. I am looking into that.

Ok, I put a commit that does that as well. This is much more of a change than I had intended originally in #1307 - but this should at least be a good starting point to discuss what the "right" solution is.

@paralin paralin force-pushed the wasi-reactor-libc branch 4 times, most recently from 6cc707e to aa950c0 Compare January 8, 2026 06:43
@paralin
Copy link
Contributor Author

paralin commented Jan 8, 2026

This is quite a large change so I understand if you reject it outright, but for the sake of discussion...

aa950c0

This pulls out all of the common logic from main() like parsing args into a struct, etc, so we can re-use it. Overall cleaner result but the changeset appears large (Due to moving things around)

Copy link
Contributor

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM but I don't know about that last commit. @saghul?

@saghul
Copy link
Contributor

saghul commented Jan 12, 2026

Mostly LGTM but I don't know about that last commit. @saghul?

I'd leave it out for now. Let's have the WASI reactor build brew a bit and we can DRY it out afterwards.

@paralin
Copy link
Contributor Author

paralin commented Jan 12, 2026

@saghul fine by me - did you want me to force push with that commit removed?

@saghul
Copy link
Contributor

saghul commented Jan 12, 2026

@saghul fine by me - did you want me to force push with that commit removed?

Yes please.

Add js_std_loop_once() and js_std_poll_io() to quickjs-libc for embedding
QuickJS in host environments where the host controls the event loop (browsers,
Node.js, Deno, Bun, Go with wazero, etc).

The standard js_std_loop() blocks until all work is complete, which freezes the
host's event loop and prevents host callbacks from running. The new APIs enable
cooperative scheduling:

js_std_loop_once() - Run one iteration of the event loop (non-blocking)
  - Executes all pending promise jobs (microtasks)
  - Runs at most one expired timer callback
  - Returns >0: next timer fires in N ms, use setTimeout
  - Returns 0: more microtasks pending, call again immediately
  - Returns -1: idle, no pending work
  - Returns -2: error occurred

js_std_poll_io(timeout_ms) - Poll for I/O and invoke read/write handlers
  - Separate from loop_once so host can call it only when I/O is ready
  - Avoids unnecessary poll() syscalls when host knows data is available
  - Required because loop_once only handles timers/microtasks, not I/O
  - Returns 0: success, -1: error, -2: exception in handler

Add QJS_WASI_REACTOR cmake option that builds QuickJS as a WASI reactor module.
Unlike the command model (which has _start and blocks in js_std_loop), reactors
export library functions that can be called repeatedly by the host.

Build:
  cmake -B build -DCMAKE_TOOLCHAIN_FILE=.../wasi-sdk.cmake -DQJS_WASI_REACTOR=ON
  cmake --build build --target qjs_wasi

Output: qjs.wasm (reactor module with exported quickjs.h / quickjs-libc.h APIs)

The reactor wasm is included in releases as qjs-wasi-reactor.wasm.

Signed-off-by: Christian Stewart <[email protected]>
Address review feedback to eliminate code duplication between js_std_poll_io()
and js_os_poll(). Both functions now call a shared js_os_poll_internal() with
configurable behavior via flags:

  - JS_OS_POLL_RUN_TIMERS: process timer callbacks
  - JS_OS_POLL_WORKERS: include worker message pipes in poll
  - JS_OS_POLL_SIGNALS: check and dispatch pending signal handlers

js_os_poll() passes all flags for full event loop behavior.
js_std_poll_io() passes no flags to poll only I/O handlers, ensuring it
does not unexpectedly run timers, worker handlers, or signal handlers.

This reduces code by ~40 lines and ensures bug fixes apply to both paths.

Signed-off-by: Christian Stewart <[email protected]>
Replace the long list of -Wl,--export=<symbol> flags with -Wl,--export-dynamic
which automatically exports all symbols with default visibility. Since JS_EXTERN
is defined as __attribute__((visibility("default"))), all public API functions
are exported automatically.

Only libc memory functions (malloc, free, realloc, calloc) still need explicit
exports since they don't have default visibility in wasi-libc.

This addresses review feedback about keeping export lists in sync manually and
reduces the CMakeLists.txt WASI reactor section from ~80 lines to ~15 lines.

Signed-off-by: Christian Stewart <[email protected]>
The WASI reactor build exports raw QuickJS C APIs, but setting up the module
loader requires calling JS_SetModuleLoaderFunc2() with js_module_loader - a C
function pointer that cannot be obtained or called from the host side (Go,
JavaScript, etc). Without the module loader, dynamic import() fails.

Add qjs_init_argv() to qjs.c which initializes the reactor with CLI argument
parsing (like main() but without blocking in the event loop). This:

- Creates runtime and context
- Sets up the module loader via JS_SetModuleLoaderFunc2()
- Sets up the promise rejection tracker
- Parses CLI flags: --std, -m/--module, -e/--eval, -I/--include
- Loads and evaluates the initial script file

The functions are added to qjs.c (guarded by #ifdef QJS_WASI_REACTOR) rather
than quickjs-libc.c because they depend on static functions in qjs.c like
eval_buf(), eval_file(), parse_limit(), and JS_NewCustomContext().

Exported functions:
- qjs_init() - Initialize with default args
- qjs_init_argv(argc, argv) - Initialize with CLI args
- qjs_get_context() - Get JSContext* for use with js_std_loop_once etc
- qjs_destroy() - Cleanup runtime

Example usage from host:
  qjs_init_argv(3, ["qjs", "--std", "/boot/script.js"])
  while running:
    result = js_std_loop_once(qjs_get_context())
    // handle result
  qjs_destroy()

Signed-off-by: Christian Stewart <[email protected]>
nanosleep can return early with EINTR when interrupted by a signal.
Loop until the sleep completes or a new signal arrives, using
os_pending_signals to detect signal state changes.

Signed-off-by: Christian Stewart <[email protected]>
@paralin
Copy link
Contributor Author

paralin commented Jan 12, 2026

@saghul all done and added +1 commit to address the nanosleep PR comment

Copy link
Contributor

@bnoordhuis bnoordhuis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Want to give it one more quick look-over since it's a biggish change, @saghul?

Copy link
Contributor

@saghul saghul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, nice work!

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.

3 participants