Map

safe-gc

Toolbox toolboxrustgarbage-collectionmemory-safetyRustApache-2.0 OR MIT โ†ณ show in map Markdown
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, not Copy, Drop unroots. 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.