xjs — JavaScript Scripting Engine
Introduction
xjs is xKit's embeddable JavaScript engine. It runs modern ECMAScript (ES2020+) in-process, is implemented on top of QuickJS-ng, and exposes a C API that mirrors Apple's JavaScriptCore C API one-to-one (every JS/kJS/OpaqueJS prefix becomes xJS/kXJS/OpaqueXJS).
The mirror is deliberate — it keeps the public surface stable even if the engine backend is swapped — and it makes the API immediately familiar to anyone who has embedded JSC on macOS/iOS.
Design Philosophy
-
JSC-Shaped Public API — Every opaque handle, constant and function in
js.hhas a direct JSC counterpart. Callers who know JSC already know xjs; code originally written against JSC usually ports with a mechanicalJS→xJSrename. -
Backend Replaceable — QuickJS types (
JSValue,JSRuntime,JSContext, …) never leak throughjs.h. All QuickJS-specific plumbing lives in.cfiles andjs_private.h. Swapping to another engine only requires reimplementing those translation units. -
Host-Driven Async — xjs intentionally does not drive an event loop. The host is responsible for pumping pending microtasks (Promise reactions,
async/awaitcontinuations,queueMicrotaskjobs) viaxJSContextDrainPendingJobs()at appropriate yield points. Synchronous Promise waiting is provided byxJSAwaitPromise(). -
Explicit Value Lifetimes — Every
xJSValueRef/xJSObjectRefreturned by the API is reference-counted. The host balances its references withxJSValueUnprotect(); there is no "stack scope" to release values for you. This is a deliberate deviation from JSC's Protect/Unprotect-only model and is documented in detail in value.md. -
No Native Module Registration (yet) — ES modules can be loaded from host-supplied source strings via a loader callback, but xjs does not expose an API for registering a
JSModuleDefbacked by C callbacks. The recommended pattern is the "global hook + JS facade" idiom; seeexamples/xjs_native_module.cand module.md.
Architecture
graph TD
subgraph "Public API (js.h)"
CTX["Context Group / Global Context"]
VAL["Values & Objects"]
STR["Strings (UTF-16)"]
CLS["Classes (native wrappers)"]
EVAL["Eval / Drain / GC"]
MOD["ES Modules + Loader"]
end
subgraph "Internal (js_private.h)"
SLOT["Slot Arena<br/>(xJSValueRef pool)"]
TRAMP["Class/Function<br/>Trampolines"]
XCODE["UTF-8 ⇌ UTF-16<br/>Transcoder"]
end
subgraph "Backend"
QJS["QuickJS-ng<br/>JSRuntime / JSContext / JSValue"]
end
CTX --> SLOT
VAL --> SLOT
CLS --> TRAMP
MOD --> TRAMP
EVAL --> SLOT
STR --> XCODE
SLOT --> QJS
TRAMP --> QJS
XCODE --> QJS
style SLOT fill:#f5a623,color:#fff
style QJS fill:#50b86c,color:#fff
Sub-Module Overview
| File | Description | Doc |
|---|---|---|
js.h (Context group section) | Runtime / global context lifecycle, module loader install | context.md |
js.h (Value section) | Type queries, builders, conversions, JSON bridge, Protect/Unprotect | value.md |
js.h (Object section) | Object/Array/Date/Error/RegExp/Promise/Function construction, property access, call-as-function/constructor | object.md |
js.h (Class registration section) | xJSClassDefinition, xJSClassCreate, native finalizer contract | class.md |
js.h (String section) | UTF-16 storage, UTF-8 transcoding helpers, ref counting | string.md |
js.h (Script evaluation section) | xJSEvaluateScript, xJSCheckScriptSyntax, job draining, GC | eval.md |
js.h (ES modules section) | xJSEvaluateModule, xJSAwaitPromise, module loader callback | module.md |
Quick Start
The smallest useful program — evaluate a script and print the result.
#include <stdio.h>
#include <stdlib.h>
#include <xjs/js.h>
int main(void) {
xJSGlobalContextRef ctx = xJSGlobalContextCreate(NULL);
xJSStringRef src = xJSStringCreateWithUTF8CString("1 + 2 * 3");
xJSValueRef exc = NULL;
xJSValueRef r = xJSEvaluateScript(ctx, src, NULL, NULL, 0, &exc);
xJSStringRelease(src);
if (!r) {
xJSStringRef m = xJSValueToStringCopy(ctx, exc, NULL);
char buf[256];
xJSStringGetUTF8CString(m, buf, sizeof(buf));
fprintf(stderr, "error: %s\n", buf);
xJSStringRelease(m);
xJSValueUnprotect(ctx, exc);
xJSGlobalContextRelease(ctx);
return 1;
}
printf("= %g\n", xJSValueToNumber(ctx, r, NULL)); // = 7
xJSValueUnprotect(ctx, r);
xJSGlobalContextRelease(ctx);
return 0;
}
A fuller walk-through — ES modules, native hooks, and synchronous Promise await — lives in examples/xjs_native_module.c.
Relationship with Other Modules
- xbase — xjs depends on
xbase/base.hforXCAPI,XDEF_STRUCT, and error-code conventions. No event loop or IO integration is mandated: xjs stays runtime-agnostic. - xai (planned) — xjs is the intended substrate for letting agent/tool logic be authored in JavaScript instead of C; see the xai roadmap.
Backend Notes
- The runtime backend is QuickJS-ng. It is a
PRIVATECMake dependency ofxjs— nothing injs.hreferences a QuickJS type, so downstream targets never transitively seequickjs.h. - ES2020 features supported by QuickJS-ng (classes, async/await, optional chaining, BigInt, top-level
awaitin modules, …) are available to user scripts. - Thread-safety follows QuickJS-ng: a
xJSContextGroupRef(runtime) is single-threaded. Multiple runtimes can exist in the same process, but values and contexts from different groups must never be mixed.