Skip to content

contrib.host: Duktape bidirectional API, QuickJS as a host - #3487

Merged
paul-hammant merged 4 commits into
mainfrom
feat/js-hosts-bidirectional
Oct 31, 2026
Merged

paul-hammant merged 4 commits into
mainfrom
feat/js-hosts-bidirectional

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

Two JavaScript-hosting changes, drafted by Jules (paul-hammant#45, merged here unchanged) and shaped in the commit on top.

Duktape gets Lua's bidirectional embedding API (#904): a persistent VM, Aether callbacks the script calls by dotted name (vm_register(vm, "app.math.add", fn)), typed arguments and returns, global setters, and typed eval results with Lua's type tags. contrib.host.js (its alias) gets the same. vm_set_instruction_limit honestly reports the limit unavailable: the system libduktape this host loads is built without DUK_USE_EXEC_TIMEOUT_CHECK (checked against Debian's 2.7.0 duk_config.h).

QuickJS joins the contrib/host family. contrib.host.quickjs wraps contrib.quickjs with the surface every host shares: run, run_sandboxed, run_sandboxed_with_map, init, finalize. The engine has no file, network or OS access of its own, so grants decide only what the Aether host functions a script calls may do. contrib.quickjs's API is unchanged (sae depends on it).

What the shaping commit fixes

Duktape:

  • raise_error threw at once, longjmp-ing out of the Aether callback past its defers. It now returns a sentinel and the trampoline throws after the callback has returned.
  • The trampoline passed the callback's return straight to Duktape, where 2+ is invalid and a negative value is an error code. It now returns 0 or 1.
  • vm_result_int / arg_int cast NaN or out-of-range numbers to int64_t, which is undefined behaviour. They now read 0 or saturate.
  • throw "x" read as a string result; it now reads as an error.
  • vm_register silently truncated names of 256+ characters; it now refuses them.
  • The spec's string-list global was built as an int array, so its assertion failed. The integration script SKIPped on a machine with libduktape installed (pkg-config prints no flags for /usr/include), and turned a failed harness build into a SKIP; both fixed.

QuickJS:

  • A program importing only contrib.host.quickjs failed to link, the new example included. The module now imports contrib.quickjs, whose @source compiles the engine in. (check-examples only type-checks host examples, so it didn't notice.)
  • The C used qjs_global / qjs_set undeclared, an error under Apple clang and GCC 14.
  • The test named "run_sandboxed enforces sandbox grants" never called run_sandboxed. It's renamed for what it checks, and a new case checks that both wrappers leave the grant stack as they found it, also when the script throws.

Docs, the QuickJS README's claim about grants, and the changelog fragments are corrected.

Checked

  • Every fix above was undone once and its test failed (five Duktape fixes, two missing grant pops, the trampoline in the integration script); restored, all pass.
  • Duktape spec 7 passing, QuickJS spec 6 passing, both valgrind-clean (0 errors, 0 definitely lost). The Duktape integration script passes.
  • make contrib-host-check: aether 6, duktape 7, lua 5, perl 5, python 5, quickjs 6.
  • ae fmt, check-docs, check-changelog, check-tests, check-warnings, check-raw-calls, check-extern-widths, check-standalone, check-crypto-wipe, check-archive-exports-nozlib pass; check-examples passes apart from a local untracked build artifact.
  • Both host C files compile clean under -Wall -Wextra -Werror (QuickJS) and with no implicit declarations (Duktape).

For the maintainers

contrib.host.js still aliases Duktape (ES5, system library). Whether it should point at QuickJS (ES2023, compiled in, no ambient OS access, the engine sae chose) is left open.

🤖 Generated with Claude Code

google-labs-jules Bot and others added 4 commits October 30, 2026 20:51
- Part A: Add bidirectional embedding API to contrib/host/duktape and contrib/host/js (persistent VM, dotted callbacks, typed values, global setters, and execution limit check).
- Part B: Add contrib/host/quickjs as a member of the contrib/host family with shared run / run_sandboxed / run_sandboxed_with_map surface.

Co-authored-by: paul-hammant <82182+paul-hammant@users.noreply.github.com>
- Part A: Add bidirectional embedding API to contrib/host/duktape and contrib/host/js (persistent VM, dotted callbacks, typed values, global setters, and execution limit check).
- Part B: Add contrib/host/quickjs as a member of the contrib/host family with shared run / run_sandboxed / run_sandboxed_with_map surface.

Co-authored-by: paul-hammant <82182+paul-hammant@users.noreply.github.com>
…ckJS links alone

Duktape:
- duktape_raise_error pushed an Error and threw at once, longjmp-ing out of
  the Aether callback past its defers. It now returns a sentinel and the
  trampoline throws once the callback has returned.
- The trampoline handed the callback's return to Duktape unchanged; 2 or
  more is an invalid return there and a negative one a thrown error code.
  It now returns 1 for "the stack top" or 0.
- vm_result_int / arg_int cast a JS number straight to int64_t, undefined
  behaviour for NaN or an out-of-range value. Non-numbers and NaN now read
  as 0 and out-of-range values saturate.
- A thrown non-Error (`throw "x"`) read as a string result; it is now
  wrapped in an Error, so vm_result_type says DUKTAPE_TERR.
- vm_register truncated a name of 256 characters or more to the buffer and
  registered the wrong name; it now refuses it.
- The spec's `let keys: *StringList = [...]` compiled to an int array, so
  the strlist global never held strings and that assertion failed; it now
  builds a std.list. New cases cover each fix above, with 64-bit compares
  (spec.assert_eq takes int and truncated INT64_MIN to 0).
- The integration script skipped where libduktape is installed with its
  header in /usr/include (pkg-config prints no flags there), and turned a
  failed harness build into a SKIP. It now detects headers separately and
  fails a broken build.

QuickJS:
- A program importing only contrib.host.quickjs failed to link (qjs_*
  undefined), the example included: the module now imports contrib.quickjs,
  whose @source compiles the engine in. The spec passed only because it
  imported both.
- The C used qjs_global and qjs_set undeclared (an error under Apple clang
  and GCC 14) and declared a qjs_set_global that doesn't exist.
- "run_sandboxed enforces sandbox grants" never called run_sandboxed; it is
  renamed for what it checks (host functions under sandbox.enforce), and a
  new case checks both wrappers leave the grant stack as they found it,
  also when the script throws.

Docs: the README for QuickJS no longer claims run_sandboxed checks host
functions it doesn't have; Duktape's raise_error row says when the throw
happens; containment-sandbox.md lists contrib.host.js again. Changelog
fragments rewritten.

Every fix was undone once and its test failed; all restored pass. Duktape
7 and QuickJS 6 spec cases pass, valgrind-clean; contrib-host-check and
every static gate pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit 185a030 into main Oct 31, 2026
73 of 74 checks passed
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.

1 participant