Crate mutcy

Source
Expand description

§Mutable Cyclic Borrows

Safe mutable cyclic borrows using borrow relinquishing.

This crate enables objects to mutably reference each other in cycles while adhering to Rust’s borrowing rules.

Rust does not permit call chains as A -> B -> A on objects A and B using &mut self because that mutably aliases A which is immediate undefined behavior.

Mutcy provides an API to work around this constraint by making you relinquish all borrows to A when you call B. Thus, at the point B calls A (B -> A), there are no live references to A. When B returns, A’s borrow is reacquired.

§Note

This crate currently requires nightly Rust.

Note that arbitrary self types fn name(self: &mut Mut<T>) are not yet stable. These can be used on nightly using #![feature(arbitrary_self_types)].

While it is possible to use this crate without arbitrary_self_types, it makes working with this crate impractical.

§Example

#![feature(arbitrary_self_types)]
use mutcy::{Assoc, Mut, Res};

// Create a new association for our data. Every `Res` is associated with some `Assoc`.
let mut assoc = Assoc::new();

// Create two associated objects.
let a = Res::new_in(A { b: None, x: 0 }, &assoc);
let b = Res::new_in(B { a: a.clone(), y: 0 }, &assoc);

// Enter the association. This gives us a `Mut` which allows us to convert `Res` into `Mut`.
assoc.enter(|x: &mut Mut<()>| {
    // Before we can mutate `a`, we need to call `mutate().via(x)`. This relinquishes all existing
    // borrows acquired via `x`'s Deref/DerefMut implementation.
    a.mutate().via(x).b = Some(b);

    // Call a method on `A`
    a.mutate().via(x).call_a(5);

    // You can store the created `Mut` from `via` onto the stack.
    let mut a_mut: Mut<A> = a.mutate().via(x);

    // Note, the following fails to compile because of `a_mut`'s dependency on `x`. This prevents
    // Rust's requirements for references from being violated.

    // let _: () = **x;

    // error[E0502]: cannot borrow `*x` as immutable because it is also borrowed as mutable
    //   --> src/lib.rs:38:18
    //    |
    // 26 |     let mut a_mut: Mut<A> = a.mutate().via(x);
    //    |                                    - mutable borrow occurs here
    // ...
    // 31 |     let _: () = **x;
    //    |                  ^^ immutable borrow occurs here
    // ...
    // 46 |     a_mut.my_method();
    //    |     ----- mutable borrow later used here

    // Because this `Mut` (`a_mut`) still exists.
    a_mut.my_method();
});

struct A {
    b: Option<Res<B>>,
    x: i32,
}

impl A {
    fn call_a(self: &mut Mut<Self>, count: usize) {
        // Mutate through &mut Self.
        self.x += 1;

        println!(">>> A: {}", count);
        self.b
            .as_ref()
            .unwrap()
            .mutate()
            .via(self)
            .call_b(count - 1);
        println!("<<< A: {}", count);

        // Mutate again, this is valid after the call to `b` which has called back into here
        // because we reborrow &mut Self here.
        self.x -= 1;
    }

    fn remove_b(self: &mut Mut<Self>) {
        // This does not drop `B` yet even though it's the only direct `Res<B>` object referring
        // to the underlying data.
        // The call stack will still refer to this `B` after this call finishes.
        // Because `b` was invoked on as `self.b.as_ref().unwrap().mutate().via(self)`, we have
        // created a clone of the `Res<B>` at multiple locations on the call stack.
        // This ensures that `B` will exist as long as some part of the call stack is still
        // using it.
        self.b = None;
    }

    fn my_method(self: &mut Mut<Self>) {
        self.x += 1;
    }
}

impl Drop for A {
    fn drop(&mut self) {
        println!("A dropped");
    }
}

struct B {
    a: Res<A>,
    y: u64,
}

impl B {
    fn call_b(self: &mut Mut<Self>, count: usize) {
        self.y += 1;

        println!(">>> B: {}", count);
        let mut a = self.a.mutate().via(self);
        if count > 1 {
            a.call_a(count - 1);
        } else {
            a.remove_b();
        }
        println!("<<< B: {}", count);

        self.y -= 1;
    }
}

impl Drop for B {
    fn drop(&mut self) {
        println!("B dropped");
    }
}

§Drop guarantees

The system maintains object validity through two invariants:

  1. An object T will only be dropped when:
    • All Res<T> handles pointing to it have been dropped
    • All active Mut<T> borrow guards to it have been dropped

This prevents dangling references in call chains like:

A → B → C → B // Last B access can remove C, into which we return. Will stay valid.

§Example Scenario

struct Node {
    name: String,
    child: Option<Res<Node>>,
    parent: Option<Res<Node>>,
}

fn traverse(node: &mut Mut<Node>) {
    if let Some(child) = &node.child {
        child.mutate().via(node).parent = None; // Would invalidate parent
        // Without guarantees, this could access freed memory:
        println!("Parent data: {:?}", node.name);
    }
}

The guarantees ensure:

  • Original node (parent) persists until its first traverse call completes.
  • parent = None only marks the relationship, doesn’t drop immediately.
  • Final println safely accesses still-allocated parent.

Dropping T occurs immediately when all Res<T> and Mut<T> objects for that instance of T have been dropped.

Deallocation of memory backing that T occurs immediately if T was dropped and no WeakRes instances to that T exist.

Structs§

Assoc
Association of pointers.
Mut
Mutable borrow guard providing access to associated data.
Res
Associated reference-counted handle to data.
WeakRes
Non-owning version of Res similar to Weak.

Traits§

Associated
Sealed trait to allow Res::new_in and Res::new_cyclic_in to use Assoc, Mut, or Res as an association source.