Native plugins › 05 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.
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.
| Tool | Lifetime | Use for |
|---|---|---|
ctx->out | the call | the return value; write it as soon as you create it |
vm_push_root / vm_pop_root | LIFO, until popped | a few intermediate values inside one call |
gc_scan + vm_add_root | per collection | values 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);
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);
}
vm_add_root takes a pointer to a persistent array of
values. Never pass a stack array.gc_scan again next time. You do not remove them
yourself.gc_scan is called for every native object on the heap,
including unreachable ones, and it may allocate.gc_scan.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).
| Where the value lives | Safe? |
|---|---|
In ctx->out after assignment | yes, until the call returns |
| In a C local, across an allocation | no — root it |
In the handle, with gc_scan | yes |
In the handle, without gc_scan | no — collected at the next GC |
| In a language field of the receiver | yes — fields are scanned by the VM |
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.