safe-gc
- title
- safe-gc
- type
- toolbox
- summary
- Rust crate offering a garbage-collected heap with zero unsafe code, in the API or internally
- tags
- rust, garbage-collection, memory-safety
- language
- Rust
- license
- Apache-2.0 OR MIT
- created
- 2026-04-22
- updated
- 2026-04-22
A Rust crate providing a garbage-collected heap with zero unsafe code โ neither in the public API nor internally. Written by Nick Fitzgerald (Wasmtime, Cranelift) as a demonstration that careful API design can replace unsafe even in a domain that is historically one of its last refuges.
How it works
The heap is a HashMap<TypeId, Box<dyn ArenaObject>>, one arena per concrete type. Each arena is a Vec of slots plus a free list. References come in two forms:
Gc<T>โCopy, cheap, does not root. Safe across code that won't trigger collection.Root<T>โ rooted, notCopy,Dropunroots. Required for references held across allocations.
You can't deref either type. To read an object you index into the heap:
let h: Heap = Heap::new();
let r: Root<Foo> = h.alloc(Foo { ... });
let v: &Foo = &h[&r];
This is the whole safety story. Because access goes through Index/IndexMut on the heap, the returned reference's lifetime is tied to the borrow of the heap โ Rust's normal rules apply, no unsafe required to express it.
The collector is mark-and-sweep. Fitzgerald tried copying first and abandoned it โ forwarding-pointer updates couldn't be expressed without either unsafe or borrowing conflicts he couldn't resolve in a heterogeneous heap. Mark-and-sweep fell out in about half an hour because nothing has to move.
Basic usage
use safe_gc::{Heap, Trace, Collector, Gc, Root};
struct Node {
value: i32,
next: Option<Gc<Node>>,
}
impl Trace for Node {
fn trace(&self, collector: &mut Collector) {
if let Some(next) = &self.next {
collector.edge(next);
}
}
}
let mut heap = Heap::new();
let tail: Root<Node> = heap.alloc(Node { value: 2, next: None });
let head: Root<Node> = heap.alloc(Node {
value: 1,
next: Some(tail.unrooted()),
});
Trace is a safe trait โ getting it wrong produces logical bugs (objects freed prematurely) but not memory unsafety, because dangling Gc<T> accesses either hit a still-alive slot, panic, or observe ABA reuse.
Limitations
- Not a high-performance collector. Arenas, free lists, stop-the-world mark-and-sweep.
- Per-type arenas mean heterogeneous graphs pay a dispatch cost per edge.
- No generational or incremental collection.
- The indexing API is more verbose than
*gc_ptrโ every read passes through the heap.
When to reach for it
Interpreters, DSL runtimes, or embedded scripting where you want garbage collection semantics, want to run in #![forbid(unsafe_code)] environments, and don't need production-JVM throughput. The write-up is also worth reading as a general exercise in API-design-as-safety-proof, independent of whether you ever use the crate.
Repo
github.com/fitzgen/safe-gc โ Apache-2.0 OR MIT. See the write-up summary for the design rationale.