xjs — Classes & Native Wrappers
Introduction
A class in xjs is a recipe for wrapping a C struct as a JavaScript object — the same role JSClassRef plays in JavaScriptCore. A class ties together:
- a class name (shows up in
Object.prototype.toString), - a finalizer that runs when the wrapped instance is garbage-collected,
- optional property callbacks (
hasProperty/getProperty/setProperty/deleteProperty/getPropertyNames) for exotic access patterns, - optional call / construct / hasInstance / convertToType hooks,
- static value and function tables installed on the prototype,
- an initializer invoked when new instances are created.
The Definition Struct
XDEF_STRUCT(xJSClassDefinition) {
int version; /* must be 0 */
xJSClassAttributes attributes; /* bitmask */
const char *className;
xJSClassRef parentClass;
const xJSStaticValue *staticValues; /* NULL-terminated */
const xJSStaticFunction *staticFunctions; /* NULL-terminated */
xJSObjectInitializeCallback initialize;
xJSObjectFinalizeCallback finalize;
xJSObjectHasPropertyCallback hasProperty;
xJSObjectGetPropertyCallback getProperty;
xJSObjectSetPropertyCallback setProperty;
xJSObjectDeletePropertyCallback deleteProperty;
xJSObjectGetPropertyNamesCallback getPropertyNames;
xJSObjectCallAsFunctionCallback callAsFunction;
xJSObjectCallAsConstructorCallback callAsConstructor;
xJSObjectHasInstanceCallback hasInstance;
xJSObjectConvertToTypeCallback convertToType;
};
Layout matches JSC's JSClassDefinition field-for-field. A zero-initialised helper is provided:
xJSClassDefinition def = kXJSClassDefinitionEmpty;
def.className = "Counter";
def.finalize = counter_finalize;
Lifecycle
xJSClassRef xJSClassCreate(const xJSClassDefinition *def);
xJSClassRef xJSClassRetain(xJSClassRef cls);
void xJSClassRelease(xJSClassRef cls);
xJSClassCreate is runtime-agnostic — it does not need an xJSContextRef. Callers typically build classes at module-init time and keep them in globals for the lifetime of the process. The first time an instance of the class is created or tested against (via xJSObjectMake, xJSObjectMakeConstructor, xJSValueIsObjectOfClass), xjs lazily registers the class against the context's runtime; subsequent uses on the same runtime are no-ops.
The same xJSClassRef can be shared across multiple runtimes in the same process — each runtime registers it once and allocates its own class-ID table.
Finalizer Contract
typedef void (*xJSObjectFinalizeCallback)(xJSObjectRef object);
Important constraints:
- Runs during GC, so the wrapped
xJSContextRefis not available — passingobjectto APIs that require a live context (anything that evaluates code, reads properties via scripted accessors, …) is undefined behaviour. - Safe operations:
xJSObjectGetPrivate(object)to retrieve thevoid *you stored atxJSObjectMaketime, so you can free it. - Finalizers may run in any order relative to other finalizers — do not rely on ordering between instances of different classes.
Full Example — a native Counter
typedef struct { long value; } Counter;
static void counter_finalize(xJSObjectRef obj) {
free(xJSObjectGetPrivate(obj));
}
static xJSValueRef counter_inc(xJSContextRef ctx, xJSObjectRef fn,
xJSObjectRef thiz, size_t argc,
const xJSValueRef argv[], xJSValueRef *exc) {
(void)fn; (void)argc; (void)argv; (void)exc;
Counter *c = (Counter *)xJSObjectGetPrivate(thiz);
c->value++;
return xJSValueMakeUndefined(ctx);
}
static xJSValueRef counter_get(xJSContextRef ctx, xJSObjectRef thiz,
xJSStringRef name, xJSValueRef *exc) {
(void)name; (void)exc;
Counter *c = (Counter *)xJSObjectGetPrivate(thiz);
return xJSValueMakeNumber(ctx, (double)c->value);
}
static xJSObjectRef counter_construct(xJSContextRef ctx, xJSObjectRef ctor,
size_t argc, const xJSValueRef argv[],
xJSValueRef *exc) {
(void)ctor; (void)argc; (void)argv; (void)exc;
Counter *c = calloc(1, sizeof(*c));
return xJSObjectMake(ctx, s_counter_class, c); // see below
}
static const xJSStaticFunction kFns[] = {
{ "inc", counter_inc, kXJSPropertyAttributeDontDelete },
{ NULL, NULL, 0 },
};
static const xJSStaticValue kVals[] = {
{ "value", counter_get, NULL, kXJSPropertyAttributeDontDelete | kXJSPropertyAttributeReadOnly },
{ NULL, NULL, NULL, 0 },
};
xJSClassRef s_counter_class;
void register_counter(xJSGlobalContextRef ctx) {
xJSClassDefinition def = kXJSClassDefinitionEmpty;
def.className = "Counter";
def.finalize = counter_finalize;
def.staticFunctions = kFns;
def.staticValues = kVals;
s_counter_class = xJSClassCreate(&def);
xJSObjectRef ctor = xJSObjectMakeConstructor(ctx, s_counter_class, counter_construct);
xJSStringRef name = xJSStringCreateWithUTF8CString("Counter");
xJSObjectRef g = xJSContextGetGlobalObject(ctx);
xJSObjectSetProperty(ctx, g, name, (xJSValueRef)ctor, 0, NULL);
xJSStringRelease(name);
xJSValueUnprotect(ctx, (xJSValueRef)g);
xJSValueUnprotect(ctx, (xJSValueRef)ctor);
}
Now JS code can do:
const c = new Counter();
c.inc(); c.inc(); c.inc();
console.log(c.value); // 3
Class Attributes
kXJSClassAttributeNone = 0
kXJSClassAttributeNoAutomaticPrototype = 1 << 1
NoAutomaticPrototype suppresses the auto-wired prototype chain — use it when parentClass is set and you need exact control over prototype linking.
Static Tables
xJSStaticValue
XDEF_STRUCT(xJSStaticValue) {
const char *name;
xJSObjectGetPropertyCallback getProperty;
xJSObjectSetPropertyCallback setProperty;
xJSPropertyAttributes attributes;
};
A NULL-terminated array installs one accessor per entry on the class prototype. Omit setProperty for a read-only property (pair it with kXJSPropertyAttributeReadOnly to keep the flag consistent).
xJSStaticFunction
XDEF_STRUCT(xJSStaticFunction) {
const char *name;
xJSObjectCallAsFunctionCallback callAsFunction;
xJSPropertyAttributes attributes;
};
Also NULL-terminated. Each entry becomes a prototype method.
Best Practices
- Build classes once, reuse forever.
xJSClassCreateis runtime-agnostic and the resultingxJSClassRefcan be shared across every context/group in the process. Stash it in astaticglobal at init time. - Keep static tables
static const. The class only shallow-copies the definition, so thestaticValues/staticFunctionsarrays and theclassNamestring must outlive the class.static constarrays satisfy this for free. - Free private data in
finalize, nowhere else. It is the only callback guaranteed to run exactly once per instance. Do not rely on explicit teardown from host code — the object may still be alive when the context is released. - Don't touch the context inside
finalize. The finalizer runs during GC with no live context; limit yourself toxJSObjectGetPrivate+free(or equivalent). - Prefer
xJSStaticFunction/xJSStaticValueover per-instance property installs. Static tables attach to the prototype once and cost nothing per instance; installing properties ininitializemultiplies memory and GC work by the instance count.
Caveats
xJSClassCreateonly makes a shallow copy of the definition —staticValues,staticFunctionsandclassNamepointers must stay alive for the class's lifetime (usestatic consttables as in the example).- The class holds no retain on
parentClass; you must keep it alive yourself. - Private data is a single
void *. For structured data, define a struct and store a pointer to it. xjs never touches the pointer other than to hand it back fromxJSObjectGetPrivate. hasInstanceandconvertToTypecallbacks are accepted in the definition for JSC parity but are not yet wired to QuickJS semantics. Avoid depending on them until the backend grows matching hooks.