wiz21
wiz21

Reputation: 161

Idiomatic way to use multiple references shared by simple structs

I want to have a set of functions to operate on a given structure. Two CPUs need to access one single mutable memory. From what I've read, I need Rc<RefCell<_>>. This is akin to the problem of the representing a tree but it is simpler since my structs have relationships defined at compile time and don't need to be generic.

The code below works, but I wonder if it is idiomatic. Coming from OOP background, I find it quite problematic:

  1. Although the memory field is present in the Cpu struct, I have to apply a borrow_mut() on it each time I need to access it in the impl. Is this because Rust wants me to avoid two simultaneous mutable references?

  2. I have to pass the mem parameter to the do_stuff function. If I have dozens of nested functions I'll have to pass that parameter on each call. Is it possible to avoid that?

  3. If I pass that parameter around, then it'd be just as simple to not have the memory field in the Cpu struct, and just passing mut &Memory references around instead which questions the need for the Rc<RefCell<Memory>> construct...

Code sample:

use std::cell::{RefCell, RefMut};
use std::rc::Rc;

struct Cpu {
    memory: Rc<RefCell<Memory>>
}

impl Cpu {
    fn new(m : Rc<RefCell<Memory>>) -> Cpu {
        Cpu { memory: m } }

    fn run(&mut self) {
       self.do_stuff(self.memory.borrow_mut());
       // Show repetition of borrow_mut (point 1)
       self.do_stuff(self.memory.borrow_mut()); 
    }

    // Show the need for mem parameter (point 2)
    fn do_stuff(&self, mut mem: RefMut<Memory>) { 
       let i = mem.load(4) + 10;
       mem.set(4, i);
    }
}

struct Memory { pub mem: [u8; 5] }
impl Memory {
    fn new() -> Memory { Memory { mem: [0 as u8; 5] } }
    fn load( &self, ndx : usize) -> u8 { self.mem[ndx] }
    fn set( &mut self, ndx : usize, val: u8) { self.mem[ndx] = val }
}
    
fn main() {
    let memory: Rc<RefCell<_>> = Rc::new(RefCell::new(Memory::new()));
    let mut cpu1 = Cpu::new(memory.clone());
    let mut cpu2 = Cpu::new(memory.clone());
    cpu1.run();
    cpu2.run();
    println!("{}",memory.borrow().mem[4]);
}

Upvotes: 2

Views: 1732

Answers (2)

user4815162342
user4815162342

Reputation: 155186

  1. I have to pass the mem parameter to the do_stuff function. If I have dozens of nested functions I'll have to pass that parameter on each call. Is it possible to avoid that?

You can avoid that by introducing a type that both holds the mutable reference to the memory and defines the behavior:

struct CpuMem<'a> {
    mem: &'a mut Memory,
}

impl CpuMem<'_> {
    fn do_stuff(&mut self) {
        // can call other functions on self without passing an explicit `mem`
        let i = self.mem.load(4) + 10;
        self.mem.set(4, i);
    }
}

Your Cpu can define a utility function that obtains the memory:

fn mem(mem: &mut Memory) -> CpuMem {
    CpuMem { mem }
}

after which run() can look like this:

fn run(&mut self) {
    let mut borrow = self.memory.borrow_mut();
    let mut mem = Self::mem(borrow.deref_mut());
    mem.do_stuff();
    mem.do_stuff();
}

If do_stuff() and other need to access some fields from Cpu, you can put references to those other things in CpuMem as well. You can also put a reference to the Cpu itself there, but then you calling a function that borrows memory, such as run(), will simply panic at run-time.

Upvotes: 0

Kevin Reid
Kevin Reid

Reputation: 43842

I have to apply a borrow_mut() on it each time I need to access it in the impl. Is this because Rust wants me to avoid two simultaneous mutable references?

Yes. There's a tidy analogy with physical hardware, here: your two processors must have some mechanism to avoid conflicts resulting from accessing the memory bus at the same time. In this analogy, using RefCell is a bus arbiter*: it sees the requests and grants or denies them to ensure there's only one access at a time. The alternative you have identified, passing an &mut Memory to each call, is like there being a single clock which cycles through giving non-overlapping time-slices of the memory access to each CPU.

The second option is generally considered better and more idiomatic Rust, when it is practical. It is statically checked, avoiding the overhead of updating the borrow flags inside a RefCell. Its main disadvantage is that it requires the caller to pass in the right &mut reference — which is not a problem in straightforward and tightly-coupled situations like yours.

A third option, in this situation where you have a simple integer array, would be to simulate something closer to the way memory works from the perspective of the real CPU: store your memory as [Cell<u8>; 5] or [AtomicU8; 5], so that each addressable location is allowed to be independently modified at any time, even if you have a non-exclusive & reference to it rather than &mut. This allows more complex code structures than passing around &mut Memory, but does not require explicit borrowing/locking of the entire memory like using a RefCell does. It offers you the least static or dynamic checking against conflicting mutations, but it might be appropriate if the operations you intend to implement are not well aligned with that checking.


* I'm not a CPU hardware expert and may not be using the exact right terms.

Upvotes: 3

Related Questions