Beyond the Borrow Checker: Zero-Unsafe Garbage Collection in Rust
Most Rust developers believe that building a Garbage Collector (GC) without unsafe is impossible. They assume the borrow checker is too rigid for the cyclic graphs and back-references that GCs must manage. As a result, many implementations hide a "dirty secret": unsafe blocks that bypass the very safety guarantees Rust is known for.
But there is a better way. By shifting from raw pointers to arena-based indices, you can build a high-performance, cyclic-friendly GC that is 100% verified Safe Rust.
The Architecture of Safety
The core of a zero-unsafe GC is the Arena. Instead of scattering objects across the heap with raw pointers, we store them in a Vec<Slot<T>>. Every "reference" is actually a u32 index.
Feature | Arc / Rc | Arena GC (Zero-Unsafe) |
Cycle Collection | No — leads to leaks | Yes — via mark-and-sweep |
Access Cost | 1 atomic deref | Bounds check + Gen check |
unsafe Usage | Common in internals | Zero |
Dangling Handles | Prevented by RAII | Handled via Generations |
Key Technical Pillars
1. Vec-Backed Arenas
By using a Vec to own all objects, we ensure that every allocation is bounds-checked by the Rust runtime. All access goes through heap.get(index), which returns an Option<&T>. This eliminates pointer arithmetic and "lifetime lies" entirely.
2. The Generation Counter (Solving ABA)
Index reuse creates the "ABA problem": if Object A at index 7 is collected and Object B is put in its place, an old handle to A might accidentally access B.
We solve this by adding a u32 generation stamp to every slot.
Every
Gc<T>handle stores the generation it was issued with.If the handle's generation doesn't match the slot's current version, the heap returns
None. It’s cheap insurance against type confusion.
3. RAII Rooting with Root
To prevent a live object from being collected, it must be "rooted." We use a Root<T> handle that implements Drop. When a Root is on the stack, the collector is structurally prevented from reclaiming the object. The 'heap lifetime ensures you can never hold a root longer than the heap itself.
4. Safe Mark-and-Sweep
The mark phase uses a Trace trait that objects implement to tell the collector which other handles they hold. The collector walks these edges using a simple Vec<u32> worklist. Since the marks are stored in a bitset or HashSet, the entire graph traversal happens in purely safe code.
The Performance Trade-off
There is no free lunch. On a Ryzen 9 7950X, pointer-based traversal runs at roughly 2.1 ns/op, while arena-indexed traversal takes 3.8 ns/op. You are trading a small amount of raw speed for absolute memory safety and the ability to handle complex, cyclic data structures like DOM trees or scripting VMs without fear.
Wanna see how to ship this without the unsafe bloat? Full deep dive and implementation specs are over at https://krun.pro/rust-garbage-collectio/
