xjs — Script Evaluation
Introduction
xjs evaluates JavaScript in two flavours — classic scripts (global code, no import/export) and ES modules (covered in module.md). This page focuses on the script path plus the shared job/GC machinery.
Check Syntax Only
bool xJSCheckScriptSyntax(xJSContextRef ctx, xJSStringRef script,
xJSStringRef sourceURL,
int startingLineNumber,
xJSValueRef *exception);
Compiles script with JS_EVAL_FLAG_COMPILE_ONLY and throws the compiled byte-code away. Use this to validate user input (e.g. an in-app script editor) without running any code. On failure the compile error is reported through *exception and the function returns false.
Evaluate a Script
xJSValueRef xJSEvaluateScript(xJSContextRef ctx,
xJSStringRef script,
xJSObjectRef thisObject,
xJSStringRef sourceURL,
int startingLineNumber,
xJSValueRef *exception);
script— source code (UTF-16 internally, transcoded to UTF-8 for the compiler).thisObject— bindsthisat top level. PassNULLto useglobalThis(JSC-equivalent default).sourceURL— shows up in stack traces. PassNULLfor the default placeholder<xjs>.startingLineNumber— currently accepted but ignored by the QuickJS backend; keep it at0or1for future compatibility.- Returns a fresh
xJSValueRef(release withxJSValueUnprotect) orNULLon throw (*exceptionis populated).
Example
xJSStringRef src = xJSStringCreateWithUTF8CString(
"const a = 2, b = 3;\n"
"a * b;");
xJSStringRef url = xJSStringCreateWithUTF8CString("calc.js");
xJSValueRef exc = NULL;
xJSValueRef r = xJSEvaluateScript(ctx, src, NULL, url, 1, &exc);
xJSStringRelease(src);
xJSStringRelease(url);
if (!r) {
// exc holds the thrown value
xJSValueUnprotect(ctx, exc);
} else {
printf("result = %g\n", xJSValueToNumber(ctx, r, NULL)); // 6
xJSValueUnprotect(ctx, r);
}
Binding this at top level
Host code sometimes wants scripts to run against a sandbox object:
xJSObjectRef sandbox = xJSObjectMake(ctx, NULL, NULL);
xJSStringRef hello = xJSStringCreateWithUTF8CString("hello");
xJSValueRef v = xJSValueMakeNumber(ctx, 42);
xJSObjectSetProperty(ctx, sandbox, hello, v, 0, NULL);
xJSValueUnprotect(ctx, v);
xJSStringRelease(hello);
// inside the script, `this.hello` is 42
xJSStringRef src = xJSStringCreateWithUTF8CString("this.hello + 1");
xJSValueRef r = xJSEvaluateScript(ctx, src, sandbox, NULL, 0, NULL);
xJSStringRelease(src);
Pumping Async Jobs
QuickJS queues Promise reactions and queueMicrotask callbacks on a runtime-level job list, and only executes them when the host explicitly pumps:
int xJSContextDrainPendingJobs(xJSContextRef ctx, xJSValueRef *exception);
bool xJSContextHasPendingJobs (xJSContextRef ctx);
Drain keeps executing jobs until either:
- the queue is empty — returns the number of jobs executed, or
- a job throws — returns the number of successfully executed jobs before the throw; writes the first exception to
*exceptionand stops.
Typical usage:
xJSValueRef e = NULL;
int ran = xJSContextDrainPendingJobs(ctx, &e);
if (e) {
// At least one microtask threw and was not caught by a .catch
// Report it or discard; draining is already halted.
xJSValueUnprotect(ctx, e);
}
When to call it
Call xJSContextDrainPendingJobs whenever host code has performed an action that may have scheduled a reaction:
- after calling
resolve()/reject()on a deferred Promise from host land, - after returning from a host-side async callback that woke JS up,
- before releasing the context if you want
finallyblocks on live Promises to run.
xJSAwaitPromise shortcut
When you already have a specific Promise and want to block until it settles, use xJSAwaitPromise() — it drains internally and returns the fulfilment value (or NULL + exception on reject).
Garbage Collection
void xJSGarbageCollect(xJSContextRef ctx);
Forces a full GC on the context's runtime. QuickJS already triggers collection automatically based on allocation pressure; this entry point is useful for:
- tests that want deterministic finalizer ordering,
- idle hooks in long-running hosts that can afford a pause,
- leak checks just before releasing the context.
Only values with zero xjs slot references (i.e. all xJSValueUnprotect calls are balanced) and no live JS-side references are reclaimable.
Best Practices
- Drain after every host→JS settle. Whenever host code resolves/rejects a deferred, returns from a native callback that might have woken a Promise, or completes async IO, call
xJSContextDrainPendingJobs. xjs does not drive an event loop — if you forget,.thenreactions simply never run. - Use
xJSAwaitPromisefor "block until this one settles". It drains internally and surfaces the fulfilment value or exception; you almost never need a hand-rolledwhile (HasPendingJobs) Drainloop against a specific Promise. - Validate with
xJSCheckScriptSyntaxbeforexJSEvaluateScript. For user-authored scripts (editor, REPL), checking syntax first gives you a clean error channel that cannot also execute side effects. xJSCheckScriptSyntaxdoesn't see module syntax. Branch onxJSDetectModulefirst; fall through toxJSEvaluateModuleif the source is a module.- Don't leak the throw value. On a
NULLreturn, alwaysxJSValueUnprotect(ctx, exc)in the error branch — forgetting is the most common xjs leak. - Call
xJSGarbageCollectsparingly. QuickJS already collects under pressure; forcing a GC is a multi-ms pause. Reserve it for tests, idle hooks, or pre-shutdown leak checks.
Caveats
startingLineNumberis currently a no-op on the QuickJS backend; stack-trace line numbers come from source positions alone.xJSCheckScriptSyntaxcompiles as a global script — it will not catch syntax errors that are only legal in module context (e.g. top-levelimport). UsexJSDetectModulefirst and branch between the two paths if needed (see module.md).- A runaway script (infinite loop with no job queue interaction) cannot be interrupted from another thread. Host code that embeds untrusted scripts should run them in a dedicated OS thread it can kill.