Skip to main content

Command Palette

Search for a command to run...

Beyond the Borrow Checker: Zero-Unsafe Garbage Collection in Rust

Updated
3 min read
K
Krun Dev. Skip the "Hello World" fluff. I write about keeping things alive in prod. Building krun.pro for devs who prefer raw code over polished stories. Less talk, more shipping

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/

2 views