Native plugins › 05 Memory and GC

Memory and GC

This is the chapter that prevents use-after-free. The rule is short: the GC cannot see your C variables, so every value you keep must be rooted.

1. Allocation APIs

Value vm_new_string(VM* vm, const char* s, int32_t len);
Value vm_new_instance(VM* vm, ClassMeta* cls);
Value vm_new_native(VM* vm, ClassMeta* cls, const ClassRef* cr);
Value vm_new_array(VM* vm, TypeDesc* elem, int32_t ndims, const int64_t* dims);
Value vm_new_view(VM* vm, ObjArray* parent, int64_t idx);
Obj*  vm_alloc(VM* vm, ClassMeta* cls, uint8_t kind, size_t payload);

Any of these may trigger a collection (the VM collects before allocating when its threshold is reached). That is why the next section matters.

2. The three ways to root

ToolLifetimeUse for
ctx->outthe callthe return value; write it as soon as you create it
vm_push_root / vm_pop_rootLIFO, until poppeda few intermediate values inside one call
gc_scan + vm_add_rootper collectionvalues stored in the handle across calls
/* One value: */
ctx->out = vm_new_string(ctx->vm, "hi", 2);

/* Several: */
Value a = vm_new_string(ctx->vm, "a", 1);
vm_push_root(ctx->vm, a);
Value b = vm_new_string(ctx->vm, "b", 1);   /* may collect; a is rooted */
vm_push_root(ctx->vm, b);
/* ... use a and b ... */
vm_pop_root(ctx->vm);
vm_pop_root(ctx->vm);

3. Values stored in the handle

A handle that keeps a Value between calls must declare it during each collection with gc_scan:

static void counter_scan(VM* vm, ObjNative* o) {
    Counter* c = (Counter*)o->handle;
    if (c && c->label.o) vm_add_root(vm, &c->label, 1);
}

4. Releasing C resources

static void counter_finalize(VM* vm, ObjNative* o) {
    (void)vm;
    free(o->handle);
    o->handle = NULL;
}

finalize runs when the object is swept. Release only C resources; do not call back into the VM. If the handle holds an array registered with vm_add_root, free that too (the roots themselves are per-collection and disappear on their own).

5. Value lifetimes in one table

Where the value livesSafe?
In ctx->out after assignmentyes, until the call returns
In a C local, across an allocationno — root it
In the handle, with gc_scanyes
In the handle, without gc_scanno — collected at the next GC
In a language field of the receiveryes — fields are scanned by the VM

6. The example

Counter.setLabel(String) stores the argument in the handle and label() returns it. Without the gc_scan callback the string could be collected while the counter still refers to it; with it, the value survives as long as the counter does.