1

I'm trying to convert some object oriented code into Rust. It was going okay until I ran into this situation.

struct A {
    root: Rc<RefCell<B>>,
}

struct B {
    parent: Weak<RefCell<B>>,
    c_lst: Vec<C>
    value: u32
}

struct C {
    parent: Weak<RefCell<B>>,
    b_lst: Vec<Rc<RefCell<B>>>,
    end: bool
}

I have three structs. I want to have a main struct (A here) hold a root of struct B. B and C need to access parent values as read only, but where the first Rc occurs they need to be mutable.

At start this function is called:

impl A {
    fn register(&mut self) {
        for c in self.root.borrow_mut().c_lst {
            c.parent = Rc::downgrade(&self.root);
        }
    }
}

Then I use an update function on A:

impl A {
    fn update(&mut self) {
        self.root.borrow_mut().update();
    }
}

impl B {
    fn update(&mut self) {
        for c in &mut self.c_lst {
            c.update();
        }
    }
}

impl C {
    fn update(&mut self) {
        match self.parent.upgrade() {
            Some(parent) => {
                // Fail because parent is already borrowed to call this update                       
                if parent.try_borrow().unwrap().value == 0 {
                    // Do stuff
                }
            },
            None => Panic!("Parent was freed")
        }
        if some_condition {
            self.spawn_b();
        }
        for b in &self.b_lst {
            b.borrow_mut().update();
        }
    }

    // This infinite loop in this state, but it's for the example
    fn spawn_b(&mut self) {
        let b = Rc::new(RefCell::new(B::new()));
        b.borrow_mut().parent = self.parent.clone();
        if !self.end {
            b.borrow_mut().c_lst = vec![C::new(Rc::downgrade(&b)), C::new(Rc::downgrade(&b))];
            for c in b.borrow_mut().c {
                c.spawn_b();
            }
        }
        self.b_lst.push(b);
    }
}

As you see in the code, I can't access my parent state. Any suggestions?

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Victor D
  • 15
  • 6

3 Answers3

1

The Rc/Weak tree in your example is a straightforward way to implement trees with parent references in Rust. The only issue is that your update function requires mutable access to the node its updating, which locks out the lower nodes from accessing the parent.

You're going to have to make an architectural change to solve this. A couple ways of doing it:

  • Split up the data into different RefCells or even Cells. Lock the main data while your own node is updating, then unlock it before recursing through the children, letting them have access.
  • Process the tree in two phases: one to gather the changes into a list, using only immutable borrows, then another to apply the stored changes, immutably and locally.
  • Move to an adjacency list representation, where the relations of each node are not stored in the node object itself but in a separate mapping object, that can be locked as needed.
Colonel Thirty Two
  • 23,953
  • 8
  • 45
  • 85
0

Try avoiding writing "object oriented" code in Rust. Prefer composition over inheritance. If you are using Rcs to represent a tree-like structure be very wary of how you point to "parents". If a parent were to be also a Rc you will have a memory leak, because that would create a cycle. To break those you could use Weak references. Maybe reading this section of the book will help you.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Aleksander Krauze
  • 3,115
  • 7
  • 18
  • Well I'm not trying to keep the same architecture, for the conversion i made before, i did it in rust way, but for this situation i couldn't figure it out another way. I have tried but the result wasn't good so i just wanted to try the Rc way. For leaks yes, i'm aware, i should use Weak for `B `and `C`, and keep RC for `C.b_lst`, right ? – Victor D Oct 13 '22 at 16:13
-1

I ended up using raw ptr. And by definition this solution is using unsafe.

struct A {
    root: B,
}

struct B {
    parent: *const B,
    c_lst: Vec<C>
    value: u32
}

struct C {
    parent: *const B,
    b_lst: Vec<B>,
    end: bool
}

impl C {
    fn spawn_b(&mut self) {
        let new_b = B::new();
        // We push our b to Vec, to get his final memory address on the heap,
        // then we access it through mutable reference
        self.b_lst.push(new_b);
        let b: &mut B = self.b_lst.get_mut(self.b_lst.len() - 1).unwrap();
        b.parent = self.parent.clone();
        if !self.end {
            b.c_lst = vec![C::new(b * as const Particle), C::new(b as * const Particle)];
            for c in b.c {
                c.spawn_b();
            }
        }
    }
}

It is important to notice that when a new B is pushed to C.b_lst, his memory address do NOT have to change, if you only push, it will work, but using remove(), of swap_remove() will change the location of B's and their memory address, children parent will point to a bad B, or panic.

To solve that, either create your implementation over Vec to keep B at there created place, or when a B address change, iterates over all his children to reassign the new address to parent.

I'm checking this as the answer, but if someone find a better solution with RC i will change.

Edit: When creating the Vec<B> use a fixed size with Vec::with_capacity(), or some other crate. It is important that the vector is not getting reallocated. Or once again iterates over all children to apply the new parent address.

Victor D
  • 15
  • 6
  • This is unsafe. If you push beyond a vector's capacity, it will be reallocated with more space and existing objects will be moved. – Colonel Thirty Two Oct 15 '22 at 21:29
  • You also have to be careful with the reference aliasing rules - it would be undefined behavior to de-reference the parent pointer if its borrowed mutably up the stack somewhere (which is what your program is doing). – Colonel Thirty Two Oct 15 '22 at 21:43