The collector boundary
The collector boundary
The collector is stop-the-world and sliding mark-compact. It stops every thread, marks what is reachable, computes each live object's new address, rewrites every reference, then slides the objects down to close the gaps.
The consequence that governs everything here: objects move.
The four rules
1. A raw pointer into the managed heap dies at the next collection.
It is valid for as long as nothing can collect. Once something can, the address may point at a different object, or into the middle of one. Reads return another object's bytes and writes corrupt it silently.
2. A synchronous, non-allocating C call is safe without pinning.
This is why most of the standard library's headers pass managed buffers directly:
the call reads its input and returns before anything can collect. The whole point
of leaving p_gcsafe off is to declare this.
3. To hold a managed object across time, use a handle.
A handle is a small integer naming an object. The collector treats live handles as roots and updates the table when objects move, so the integer stays valid even though the address does not.
pgc_handle h = pgc_handle_create(object);
/* ... later, possibly after a collection ... */
void *current = pgc_handle_get(h);
pgc_handle_release(h);Always re-resolve through pgc_handle_get immediately before use. Caching what
it returned defeats the purpose.
4. What is not traced is not kept alive.
opaque, cstr, references, and raw pointers are all unmanaged. An object
reachable only through one of them is invisible to the collector and will be
reclaimed while you still hold the address. If C needs to keep an object alive,
it needs a handle, not a pointer.
Structures that must not move
Anything the OS or a third-party library tracks by address has to live outside
the managed heap. The standard library allocates all of these with malloc and
hands PekoScript an opaque:
- Socket handles, because the OS tracks the descriptor.
- TLS contexts, because they contain internal self-pointers.
- Thread bookkeeping, because the collector must not shift it mid-run.
The pattern is: allocate with malloc, return p_opaque, and give the
PekoScript side an explicit free() to call.
What a violation looks like
A stale pointer usually does not fault where it is used. The object that moved gets overwritten by a later allocation, and the crash arrives when something reads what it believes is a type header and finds someone else's data. A fault inside a method dispatch on an object that has nothing to do with your change is the classic signature.
Two environment variables make this reproducible instead of intermittent:
| Variable | Effect |
|---|---|
PEKO_GC_STRESS=N |
force a collection every N allocations |
PEKO_GC_VERIFY |
cross-check mark completeness after marking |
Running a suspect workload under PEKO_GC_STRESS=1 turns a rare corruption into
a deterministic one.
Pinning
pgc_pin holds an object still so a raw pointer to it stays valid, and
pgc_unpin releases it. Pins nest.
Pinned objects fragment the heap, so pins should be brief. In practice the standard library never uses them: every case is covered by rule 2 or rule 3. Treat pinning as a last resort, and note it is not exercised by the shipped code.