Two structs calling each other

I have two structs A and B that need to be able to call each other. A is supposed to be the owner of B, as in, A creates B and calls B.run() to start some concurrent loops. Occasionally, A calls b.start() to initiate some process in B. Similarly, occasionally, B calls a.append() to initiate some process in A.

I could get this setup working with the code below. However, is there a better way to structure my code to eliminate the cyclical Arc dependencies?

Link: Rust Playground

use std::sync::{Arc,Mutex};

struct A {
    b: Option<Arc<Mutex<B>>>,
}

struct B {
    a: Option<Arc<Mutex<A>>>,
}


impl A {
    fn start(&self){
        println!("start called in A...possibly accessing some fields...");
    }
    
    fn trigger(&self) {
        let inner_b = self.b.as_ref().unwrap();
        let b = inner_b.lock().unwrap();
        b.append();
    }
}

impl B {
    fn append(&self){
        println!("append called in B...possibly accessing some fields...");
    }
    
    fn trigger(&self) {
        let inner_a = self.a.as_ref().unwrap();
        let a = inner_a.lock().unwrap();
        a.start();
    }
}


#[tokio::main]
async fn main(){
    let mut a = Arc::new(Mutex::new(A {
        b: None,
    }));
    let mut b = Arc::new(Mutex::new(B {
        a: None,
    }));
    
    {
        let mut a = a.lock().unwrap();
        a.b = Some(b.clone());
    }
    {
        let mut b = b.lock().unwrap();
        b.a = Some(a.clone());
    }
    
    
    {
        let a = a.lock().unwrap();
        a.trigger();
    }
    {
        let b = b.lock().unwrap();
        b.trigger();
    }
    
}

It's a little hard to figure out the right solution because it will depend on the particular access patterns you need in the rest of your code; a less abstract description of the underlying problem might help. That said, one approach is to split A into two parts: the contained B and the other fields. Then, B only needs a reference to the other part of A:

use std::sync::{Arc,Mutex};


#[derive(Clone)]
struct A {
    data: Arc<Mutex<AData>>,
    b: Arc<Mutex<B>>,
}

struct AData {
    count: usize,
}

struct B {
    a_data: Arc<Mutex<AData>>
}


impl A {
    fn new()->Arc<Self> {
        let data = Arc::new(Mutex::new(AData { count: 0 }));
        Arc::new(A{
            b: Arc::new(Mutex::new(B { a_data: data.clone() })),
            data
        })
    }

    fn start(&self) {
        self.data.lock().unwrap().start();
    }
    
    fn trigger(&self) {
        self.b.lock().unwrap().append();
    }
}

impl AData {
    fn start(&self) {
        println!("start called in A...possibly accessing some fields...");
    }
}

impl B {
    fn append(&self){
        println!("append called in B...possibly accessing some fields...");
    }
    
    fn trigger(&self) {
        self.a_data.lock().unwrap().start();
    }
}


#[tokio::main]
async fn main(){
    let a = A::new();
    let b = a.b.clone();
    
    {
        a.trigger();
    }
    {
        let b = b.lock().unwrap();
        b.trigger();
    }
    
}

(Playground)

1 Like

Another option is to store a Weak inside B instead of Arc:

use std::sync::{Arc,Mutex,Weak};

struct A {
    b: Arc<Mutex<B>>,
}

struct B {
    a: Weak<Mutex<A>>,
}


impl A {
    fn new()->Arc<Mutex<Self>> {
        let b = Arc::new(Mutex::new(B { a: Weak::new() }));
        let a = Arc::new(Mutex::new(A { b: b.clone() }));
        b.lock().unwrap().a = Arc::downgrade(&a);
        a
    }
    
    fn start(&self){
        println!("start called in A...possibly accessing some fields...");
    }
    
    fn trigger(&self) {
        self.b.lock().unwrap().append();
    }
}

impl B {
    fn append(&self){
        println!("append called in B...possibly accessing some fields...");
    }
    
    fn trigger(&self) {
        self.a.upgrade().unwrap().lock().unwrap().start();
    }
}


#[tokio::main]
async fn main(){
    let a = A::new();
    let b = a.lock().unwrap().b.clone();
    {
        let a = a.lock().unwrap();
        a.trigger();
    }
    {
        let b = b.lock().unwrap();
        b.trigger();
    }
    
}

(Playground)

2 Likes

Thank you! This separation into AData and B was exactly what I was looking for.

What you are trying is pretty much straight against Rusts ownership model.

You can instead:

  • have superstruct owning both:
struct Common {
    a: A,
    b: B
}
impl Common {
    fn call_a(&mut self) {
        self.a(&mut self.b):
    }
    fn call_b(&mut self) {
        self.b(&mut self.a):
    }
}
  • merge them into one.
1 Like

In general A <-> B is a bad dependency graph to have. In particular in Rust that kind of structure hurts very much. Restructuring the original problem to change the graph of the program would probably be the better idea.