Skip to content

Instantly share code, notes, and snippets.

@ssrlive
Created January 3, 2026 11:06
Show Gist options
  • Select an option

  • Save ssrlive/e0972b3ef71fccd8217df7f632dde879 to your computer and use it in GitHub Desktop.

Select an option

Save ssrlive/e0972b3ef71fccd8217df7f632dde879 to your computer and use it in GitHub Desktop.
Double Linked list with GC in Rust
//
// https://github.com/kyren/gc-arena/blob/master/examples/linked_list.rs
//
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
// We define a node of a doubly-linked list data structure.
//
// `Collect` is derived procedurally, meaning that we can't mess up and forget
// to trace our inner `prev`, `next`, or `value`.
#[derive(Copy, Clone, Collect)]
// For safety, we agree to not implement `Drop`. We could also use
// `#[collect(unsafe_drop)]` or `#[collect(require_static)]` (if our type were
// 'static) here instead.
#[collect(unsafe_drop)]
struct Node<'gc, T: 'gc> {
// The representation of the `prev` and `next` fields is a plain machine
// pointer that might be NULL.
//
// Thanks, niche optimization!
prev: Option<NodePtr<'gc, T>>,
next: Option<NodePtr<'gc, T>>,
value: T,
}
// By default, `Collect` types (other than 'static types) cannot have interior
// mutability. In order to provide safe mutation, we need to use `gc-arena`
// specific types to provide it which guarantee that write barriers are invoked.
//
// We use `RefLock` here as an alternative to `RefCell`.
type NodePtr<'gc, T> = Gc<'gc, RefLock<Node<'gc, T>>>;
// A value type that prints when dropped so we can observe collection.
#[derive(Collect)]
#[collect(require_static)]
struct Droppable(i32);
impl Drop for Droppable {
fn drop(&mut self) {
println!("Dropping {}", self.0);
}
}
// Create a new `Node` and return a pointer to it.
//
// We need to pass the `&Mutation<'gc>` context here because we are mutating the
// object graph (by creating a new "object" with `Gc::new`).
fn new_node<'gc, T: Collect<'gc>>(mc: &Mutation<'gc>, value: T) -> NodePtr<'gc, T> {
Gc::new(
mc,
RefLock::new(Node {
prev: None,
next: None,
value,
}),
)
}
// Join two nodes together, setting the `left` node's `next` field to `right`,
// and the `right` node's `prev` field to `left`.
//
// Again, we are mutating the object graph, so we must pass in the
// `&Mutation<'gc>` context.
fn node_join<'gc, T>(mc: &Mutation<'gc>, left: NodePtr<'gc, T>, right: NodePtr<'gc, T>) {
// This is `Gc<RefLock<_>>::borrow_mut`, which takes the mutation context as
// a parameter. Write barriers will always be invoked on the target pointer,
// so we know it is safe to mutate the value behind the pointer.
left.borrow_mut(mc).next = Some(right);
right.borrow_mut(mc).prev = Some(left);
}
// Use a `NodePtr` as a cursor, move forward through a linked list by following
// `next` pointers.
//
// Returns `true` if there was a `next` pointer and the target node has been
// changed.
fn node_rotate_right<'gc, T>(node: &mut NodePtr<'gc, T>) -> bool {
if let Some(next) = node.borrow().next {
*node = next;
true
} else {
false
}
}
// Use a `NodePtr` as a cursor, move backward through a linked list by following
// `prev` pointers.
//
// Returns `true` if there was a `prev` pointer and the target node has been
// changed.
fn node_rotate_left<'gc, T>(node: &mut NodePtr<'gc, T>) -> bool {
if let Some(prev) = node.borrow().prev {
*node = prev;
true
} else {
false
}
}
fn main() {
// Create a new arena with a single `NodePtr<'_, i32>` as the root type.
//
// We can't refer to some *particular* `NodePtr<'gc, i32>`, what we need to
// be able to refer to is a set of `NodePtr<'_, i32>` for any possible '_
// that we might pick. We use gc-arena's `Rootable!` macro for this.
let mut arena =
Arena::<Rootable![(NodePtr<'_, Droppable>, NodePtr<'_, Droppable>)]>::new(|mc| {
// Create a simple linked list with three links.
//
// 1 <-> 2 <-> 3 <-> 4
let one = new_node(mc, Droppable(1));
let two = new_node(mc, Droppable(2));
let three = new_node(mc, Droppable(3));
let four = new_node(mc, Droppable(4));
node_join(mc, one, two);
node_join(mc, two, three);
node_join(mc, three, four);
// We return the pointer to 1 as our root and save it as initial
(one, one)
});
// Outside of a call to `Arena::new` or `Arena::mutate`, we have no access
// to anything *inside* the arena. We have to *visit* the arena with one of
// the mutation methods in order to access its interior.
arena.mutate_root(|_, root| {
// We can examine the root type and see that our linked list is still
// [1, 2, 3, 4]
for i in 1..=4 {
assert_eq!(root.0.borrow().value.0, i);
node_rotate_right(&mut root.0);
}
});
arena.mutate_root(|_, root| {
// Also, all of the reverse links work too.
for i in (1..=4).rev() {
assert_eq!(root.0.borrow().value.0, i);
node_rotate_left(&mut root.0);
}
});
arena.mutate(|mc, root| {
// Make the list circular! We sever the connection to 4 and link 3 back
// to 1 making a list like this...
//
// +-> 1 <-> 2 <-> 3 <-+
// | |
// +-------------------+
let one = root.0;
let two = one.borrow().next.unwrap();
let three = two.borrow().next.unwrap();
node_join(mc, three, one);
});
// The node for 4 is now unreachable!
//
// It can be freed during collection, but collection does not happen
// automatically. We have to trigger collection *outside* of a mutation
// method.
//
// The `Arena::finish_cycle` method runs a full collection cycle, but this
// is not the only way to trigger collection.
//
// `gc-arena` is an incremental collector, and so keeps track of "debt"
// during the GC cycle, pacing the collector based on the rate and size of
// new allocations.
//
// We can also call `Arena::collect_debt` to do a *bit* of collection at a
// time, based on the current collector debt.
//
// Since the collector has not yet started its marking phase, calling this
// will fully mark the arena and collect all the garbage, so this method
// will always free the 4 node.
arena.finish_cycle();
// Rotate the root once to demonstrate mutation then restore it.
arena.mutate_root(|_, root| {
node_rotate_right(&mut root.0);
assert_eq!(root.0.borrow().value.0, 2);
});
// Restore root to the initial node (1).
arena.mutate_root(|_, root| {
root.0 = root.1;
assert_eq!(root.0.borrow().value.0, 1);
});
arena.mutate_root(|_, root| {
// Now we can see that if we rotate through our circular list, we will
// get:
//
// 1 -> 2 -> 3 -> 1 -> 2 -> 3
for _ in 0..2 {
for i in 1..=3 {
assert_eq!(root.0.borrow().value.0, i);
node_rotate_right(&mut root.0);
}
}
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment