Fil-C on Nix

Filnix wiki

boehm-on-fugc.md

Boehm GC on FUGC

pkgsFilc.boehmgc is not the Boehm-Demers-Weiser collector. It is the libgc API (headers, libgc, libcord, libgccpp, libgctba, bdw-gc.pc) implemented on Fil-C's own garbage collector. The code is in ports/boehmgc/, and ports/overlay.nix wires it in.

Why not port Boehm itself

Boehm scans thread stacks, registers and data segments conservatively and treats any word that looks like a heap address as a pointer. Under Fil-C:

None of this is needed. Every Fil-C allocation is already collected by FUGC, which is precise, concurrent and parallel, and free is optional. A Boehm client only needs its API calls to do the right thing on FUGC. djb's Filian does the same for simple clients (data/libgc/subst/gc.c: GC_malloc is malloc). This implementation covers the whole libgc 8.2 ABI, including finalization and weak references, and it keeps upstream's headers, so clients compile unchanged.

Mapping

libgc FUGC implementation
GC_malloc, _atomic, _ignore_off_page, typed, gcj, kinds zgc_alloc (zeroed, like libgc)
GC_malloc_uncollectable zgc_alloc, kept alive by a strong zexact_ptrtable. Membership is in a zweak_map, so GC_realloc keeps the kind
GC_realloc zgc_realloc
GC_free zgc_free (also drops the uncollectable hold and any finalizer). Later accesses trap
GC_base, GC_size zgetlower, zgetupper - zgetlower
GC_gcollect zgc_request_and_wait, then process the cycle (below)
finalizers (all GC_register_finalizer* variants) see below
disappearing and long links see below
GC_HIDE_POINTER / GC_REVEAL_POINTER a weak zexact_ptrtable (header patch below)
GC_generic_malloc_many, GC_malloc_many a list of separate objects linked through their first word
roots, displacements, exclusions, incremental mode, heap expansion, mark bits, GC_push_* accepted, no-ops
GC_pthread_*, GC_dlopen pass-through
GC_register_my_thread and other thread and stack calls bookkeeping only. FUGC knows every thread. GC_get_stack_base returns zstack_top()

Finalizers

FUGC's finalizer queues (zgc_finq) apply to objects that were allocated with zgc_finq_alloc. libgc lets you attach a finalizer to any existing object. The bridge:

Re-registering with fn == 0 cancels the finalizer and reports the old one through ofn/ocd. Registering again from inside a finalizer works, which is the guardian pattern. GC_finalize_all runs every registered finalizer. GC_finalized_malloc (gc_disclaim.h) is built on the same mechanism.

GC_general_register_disappearing_link(link, obj) records a zweak to the link's containing object and a zweak to obj. The record goes in a hash table keyed by the link's address. The library sweeps the table whenever it sees that a GC cycle has completed. Dead targets get *link = NULL. Records whose link object died are dropped. GC_move_* and GC_unregister_* behave as in libgc.

Hidden pointers

~(GC_word)p loses p's capability. include/gc.h is patched (hide-pointer.patch) so that under __FILC__, GC_HIDE_POINTER(p) is ~zexact_ptrtable_encode(weak_table, p) and GC_REVEAL_POINTER(h) decodes it. The encoding is still the complemented address, so hidden values look like libgc's. Revealing a live object's pointer gives back a usable pointer. The table is weak, so hiding does not keep objects alive, and the usual "hidden pointer plus disappearing link" weak-reference idiom works.

FUGC clears weak references before it revives finalizable objects. libgc, by contrast, keeps a hidden pointer to an unreachable object that has a finalizer valid until the object is reclaimed. Two things bridge the difference. When a revived object is queued, the library hides it again. And if a reveal fails while finalizers exist, the library waits for the running cycle to finish queueing, drains the queue, and retries.

Disclaim procedures and mark bits

GC_register_disclaim_proc runs the procedure as a finalizer on objects of that kind. If it returns nonzero, the object is kept and the procedure is asked again the next time the object is unreachable. GC_is_marked and GC_set_mark_bit only mean something inside a finalizer or disclaim procedure. There, the object being finalized counts as marked if the client called GC_set_mark_bit on it after the collection that found it unreachable started, which is how libgc clients rescue such objects. Every other object counts as marked.

When cycle effects become visible

FUGC collects on its own threads. A client observes a finished cycle (GC_gc_no, cleared links, queued finalizers, start/collection-event callbacks) the next time it allocates or calls into the library. The check is one zgc_completed_cycle() comparison. GC_call_with_alloc_lock runs the check before it takes the lock. So code that reveals links under the lock sees them cleared, as it would with libgc. libgc also only acts during allocation, so the observable behaviour is similar.

Semantic gaps

Validation

Consumers

Tested so far (nixos-26.05 campaign blockers):