Created
January 3, 2026 11:06
-
-
Save ssrlive/e0972b3ef71fccd8217df7f632dde879 to your computer and use it in GitHub Desktop.
Double Linked list with GC in Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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