The QuickJS C API on the Odin runtime (src/engine/jscapi)
Wren's glue (native/js/odin, package wj and the domains above it) is written against the QuickJS‑ng
C API: JSValue words, JS_DupValue/JS_FreeValue, classes with an opaque pointer and a finalizer,
JS_NewCFunction2, JS_Call… jscapi implements that API on src/engine/jsrt, so the glue runs on the
Odin engine without touching a domain file. It is the only engine now (WJ_JSRT :: true, wj/backend.odin): the
QuickJS-ng C engine is gone since 0.6.0.
domain files (web_url.odin, dom_core.odin, …) unchanged
wj.JS_Call, wj.JS_NewObject … the layer's names
wj/backend_jsrt.odin aliases of src/engine/jscapi
src/engine/jscapi (167 procedures, "c") the QuickJS contracts
src/engine/jsrt the engineThe API types (Context, Runtime, Class_Def, Native, Property_Descriptor, …) are defined once, in
jscapi/types.odin, and wj aliases them.
Contracts kept
JSValueis the 64‑bit NaN‑boxed word of QuickJS on wasm32 (types.odin: tags-9 … 8, floats offset by0x7ff8000A << 32, heap values a 32‑bit pointer). The glue's own bit operations (wj.tag_of,is_string,int_of, …) keep working. A 64‑bit host must keep the engine below 4 GiB (the judge's test reserves an arena at a low address); wasm32 has 32‑bit pointers anyway.- Counted references on a tracing collector. A reference the caller owns (every
JS_New*,JS_Get*,JS_To*,JS_Dup*result) adds one to theextcount in the cell header, which makes the cell a collector root;JS_FreeValueremoves it. Values the callee consumes (JS_SetPropertyStr's value,JS_Throw's,JS_DefinePropertyValue*'s,JS_SetModuleExport's …) are released for the caller. Borrowed values (a native'sargv,this) carry no count. Natives return an owned value; the bridge releases it as the engine takes it over. Freeing a value that has no count is harmless. - Cycles through opaque data are collected.
Class_Def.gc_markreports the values the opaque data holds (JS_MarkValue); the collector subtracts them from the host counts before marking, so an object that holds a counted reference to a closure that captures the object dies (checked byjudge capi: 1000 such cycles are all finalized by oneJS_RunGC). Finalizers run before any cell of the collection is freed. - Classes (
JS_NewClassID,JS_NewClass,JS_NewObjectProtoClass,JS_GetOpaque/SetOpaque, and QuickJS-ng'sJS_GetAnyOpaque: the class and the opaque data in one call, what the glue reads of every platform object) with all sevenJSClassExoticMethods(get/set/has/delete/define/own‑names/own‑property); an exoticget_propertyreplaces the prototype walk after the own shape lookup, as in QuickJS. Wren's ownJS_NewObjectProtoClassPayload(ctx, proto, class, size)makes the opaque datasizezeroed bytes of the engine's heap in the block of the object's host part, freed with the object (the glue's wrappers: one allocation from the engine's size classes instead of two, and nothing for the finalizer to free). Such an object is made unmarked: the collector does not call its class'sgc_markfor it until Wren'sJS_SetOpaqueMarked(ctx, obj, true), which the glue calls before the data takes a value (a group, a listener, a [SameObject] value, an impl); a collection then skips the thousands of node wrappers that carry nothing. One the host forgot to mark keeps what its data counts as roots (a leak, not an early free). - Native functions:
JS_NewCFunction2/3,JS_NewCFunctionData/2, with prototypesgeneric,generic_magic,constructor,constructor_or_func(and_magic),getter,setter. - Errors: the
JS_Throw*Errorfamily takesprintfarguments (%s %d %u %x %c %.*s %ld); a C caller passes them as wasm32 varargs, which the exported versions read (cexport.odin). - Entry points bracket their work with
host_enter/host_exit, so the temporaries of an API call made outside any JavaScript (a host event) are released when it returns. - Hooks: interrupt handler, GC prologue, rejection tracker, compile‑strings hook, dynamic‑import hook,
module normalize/loader/attribute check,
JS_SetCanBlock,JS_SetMemoryLimit, GC threshold (and floor). Wren's ownJS_SetGCEpilogueis called at the end of every collection, after its finalizers (the glue unpins the nodes of the wrappers they finalized in one call to the browser). - Strings read in place: Wren's own
JS_ToStringView(ctx, &len, val)gives the bytes of a string value that is all ASCII (a rope flattened first), with no copy and nothing to free, nil for anything else: not NUL-terminated, valid while the value lives (the glue'sSTR_VIEW: the generated bindings' string arguments, read by the import before the native returns). - Modules:
JS_NewCModule+JS_AddModuleExport+JS_SetModuleExport(init run before the first import),JS_LoadModule(WithFuncs),JS_ResolveModule,JS_GetModuleRequest*,JS_GetImportMeta,EVAL_FLAG_WREN_NO_RESOLVE. - Buffers:
JS_NewArrayBufferadopts host memory (released throughrealloc_func(…, 0)on collection), copies,JS_NewUint8Array*,JS_GetArrayBuffer,JS_GetTypedArrayBuffer, detach, immutable. JS_WriteObject/JS_ReadObjectof bytecode: a function compiled withCOMPILE_ONLYkeeps its image (Compile_Request.keep_image), so the realm snapshot ofqjs_realm.odin/prelude.odinworks.
With -define:JSCAPI_EXPORT=true every procedure is also exported under its QuickJS name, for C that calls the API.
The build has no such C any more.
Numbers (when the API was written)
Distinct JS_* / js_* names the glue references: 176.
| count | |
|---|---|
provided by jscapi | 167 |
static inline of quickjs.h reimplemented in wj (unchanged) | 4 |
helper names of the glue that are not API (js_str, js_cstr …) | 5 |
| missing | 0 |
The API itself is tested natively by judge capi (61 checks); the glue runs on it in every build.
What is approximated or absent (by weight)
- Atoms are permanent (
JS_FreeAtomis a no‑op): a page that builds millions of distinct property names keeps them. QuickJS frees them. JS_WriteObject/JS_ReadObjectof data (the structured clone format withREFERENCE): absent;web_clone.odinreads with it (READ_OBJ_REFERENCE) — the clone path must use its own walk.JS_DefineHostAutoInitPropertymakes the value at definition time (correct, not lazy: IndexedDB's interfaces are built in every realm that defines them).- Direct
evalfrom the API (EVAL_TYPE_DIRECT) behaves as an indirect eval (the scripteval(…)operator is unaffected). JS_LoadModulereturns the namespace promise through the dynamic‑import job; the layer's ownwrenmodule graph fetcher is exercised byLoadModuleWithFuncsonly structurally.JS_IsUncatchableErrorlooks at the last created runtime (no context argument).JS_ComputeMemoryUsagefills the totals only (malloc_size, counts).- Class
callhooks (callable platform objects) are not supported. - Performance: each API call enters/leaves the host bracket and copies the Odin context (the engine's own speed is in runtime.md).
- Memory: a tracing heap holds garbage until the next collection (1.6× QuickJS on allocation‑heavy
scripts);
JS_SetGCThresholdFloorraises the threshold now and is not re‑applied after a collection (the engine keepsmax(2 × live, live + 4 MiB)). - The ownership model is the most likely source of defects (a missing
JS_FreeValueleaks a root, an extra one lets a value die early): a debug counter ofextper cell is easy to add inext_ref/ext_unref.
Testing
judge capi (src/engine/jsrt/judge/capi_test.odin) runs the API natively on a heap below 4 GiB: values,
strings, properties, native functions with data and magic, formatted exceptions, classes with opaque data,
cycles through gc_mark, counted references as roots, the GC epilogue (once a collection, after its finalizers),
strings read in place (ASCII and concatenations; none of Latin-1, UTF-16 or other values), opaque data in the object's block (zeroed, one per object,
cycles through it collected, 10 000 made and dropped without the heap keeping them; JS_GetAnyOpaque of it, of a built-in object, of a number; gc_mark not called until JS_SetOpaqueMarked, the counted values roots until then), exotic objects, promises and jobs, adopted buffers,
a native module imported by a script module, a compiled function written out and read back, and a soak of
20 000 temporaries that must be collected.