Skip to content

Instantly share code, notes, and snippets.

@jprochazk
Created April 20, 2025 22:50
Show Gist options
  • Save jprochazk/553ac6cddcae422c1af01a1ad78390f4 to your computer and use it in GitHub Desktop.
Save jprochazk/553ac6cddcae422c1af01a1ad78390f4 to your computer and use it in GitHub Desktop.
GC API

GC API

The following GC API has one main goal, which is to provide safe access to garbage-collected objects.

The definition of "safe access" here specifically means that the garbage collector would consider the object to be reachable if a garbage collection cycle were to take place while the reference to it is still live. As a consequence, it should also be safe to run garbage collection cycles while there are live references on the stack.

Any tracking required for the API to be safe must be as cheap as possible. We avoid heap allocation, and reference-counting schemes. A reference to a garbage collected object is a simple pointer stored on the stack.

It is impossible for the API to be as simple and ergonomic as regular heap allocation in Rust. That being said, ergonomics are still extremely important. The API

Object definitions

#[gc::object]
struct Foo {
    value: u32,
}

#[gc::object]
struct Bar {
    name: String,

    #[ref]
    foo: Gc<Foo>,
}

#[gc::object]
struct Baz {
    #[ref]
    bar: Gc<Bar>,
}

gc::object ensures that for a given T:

  1. Drop is not implemented for T.
  2. every field of T is either unmanaged, and not marked with #[ref], or managed and marked with #[ref].
  3. T is fully 'static.

(1) Adds an empty Drop impl which will conflict with any user Drop impls.
(2) Checks that for each field, if the type is a GcRef, it must be marked #[ref]. If the type is not a GcRef, it must not be marked #[ref]. This removes the need for every type in the universe to implement Trace1.
(3) Asserts that T: 'static via an unused fn.

On top of that, it implements:

  1. gc::Trace, for visiting all fields during the marking phase.
  2. An __object_init method, which is used to produce an initializer from its parts. This initializer may be passed to a root, which will construct the object in place, and allocate it on the GC heap.

It also outputs a macro_rules macro with the same name as the type, to produce its object initializer.

Rooting API

fn mutator(gc: &GcCtx) {
    // Roots are stack-pinned locations tracked by the GC.
    // An object may only be accessed if it is rooted.
    let_root!(in gc; bar = Bar! {
        name: "test".into(),
        foo: Foo! {
            value: 10,
        },
    });

    // Once an object is rooted, it can be safely accessed
    // at any point. The syntax is a little unwieldy, because
    // we must track accesses that go through the GC as borrowing
    // from the GC.
    println!("{}", bar.deref(gc).foo.deref(gc).value);

    // Parts of an object can be rooted separately to give them
    // their own lifetime:
    let_root!(in gc; foo);
    let foo = bar.deref(gc).foo.root(foo);

    // A rooted object can be used as a field in another object
    // during initialization.
    let_root!(in gc; baz = Baz! { bar });
}

Given that it is not possible to directly return roots, any function which wishes to return an already-rooted value must receive an uninitialized root as an argument. The root can be returned once it has been initialized.

fn producer<'root>(out: UninitRoot<'root, Foo>) -> Rooted<'root, Foo> {
    out.init(Foo! {
        value: 100,
    })
}

fn consumer(gc: &GcCtx) {
    let_root!(in gc; foo);
    let foo = caller(gc, foo);
}

Constructing roots is relatively cheap, and should be done with the smallest scope possible. It is not completely free however, so roots may also be declared mutable, which makes them "reusable":

// `GcVec` is a special kind of `Vec` that implements `Trace`, and
// may only hold gcrefs.
fn looping<'root>(gc: &GcCtx, list: GcRef<'root, GcVec<Foo>>) {
    let_root!(in gc; list);
    let_root!(in gc; mut item);
    for i in 0..list.len() {
        let item = list.get(i).root(&mut item);
        println!("{}", item.deref(gc).value);
    }
}

Footnotes

  1. We know which fields are gcrefs, so we only trace those. This is safe, because it is not possible to place a gcref onto the non-GC heap, only references to gcrefs, and references to roots of gcrefs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment