How to design a method that takes a struct and a reference to one or more fields?

Hello!

I'm having (plenty of) troubles trying to design a method that takes a reference to a struct, and references to fields of the struct.

The high level logic is fairly simple, and it's a model of the execution logic of a processor. There is a, say, Cpu struct, that represents the CPU state, including its registers.

The problem is that the dispatcher is written in generic form, and needs to pass both the Cpu instance, and one or more registers any given instruction works on:

    // extremely simplified example

    pub fn dispatcher(&mut self) {
        let register = &mut self.SP;
        self.execute_increment(register);
    }

    fn execute_increment(&mut self, register: &mut u16) {
        *register += 1;
        self.PC += 2;
    }

This is a big problem. The solution I colud come up with until now, is to convert the execute_instruction method to static:

    fn execute_increment(register: &mut u16, PC: &mut u16) {
        *register += 1;
        *PC += 2;
    }

While this works, it considerably increases the complexity, because the list of parameters passed can get significantly more complex, while it could be kept simple if each method had access to the Cpu instance and the reference of the registers selected by each instruction.

Is there anything I can do?

Thanks!

Unfortunately, there aren't any pretty solutions to this.

&mut self means exclusive borrow of all fields of self. The borrow checker intentionally verifies against function signatures, not function bodies. So if the signature says exclusive borrow of self, it's gonna be exclusive.

  • Static methods that get only fields they need is one solution

  • If you make methods take &self, then they can also take shared borrows of other fields of self. Shared mutability can be done via Cell, AtomicUn, or Mutex.

  • Sometimes it's possible to refactor a large struct into two smaller ones, which can be borrowed independently.

1 Like

Another option is to store the registers in some sort of map or vector, and pass in an index or key instead of a reference to the register.

(Even if you keep the registers in separate fields, you could pass in an enum to specify which register to get, and then have a function that translates that enum into a reference to the appropriate field.)

1 Like

Unfortunately, I think those strategies won't work.

The enum solution would be nice, but if I need two mutable registers, the function will need to mutably borrow the instance twice, which is not possible.

I think collections ultimately suffer from the same problem - I need to get multiple field mutable references, which will conflict with each other, for the same reason.

In this case, I think this won't work - given the generic nature, any instruction may work on any register, so I can't partition them in a predefined way.

However...

Bling! I think the Cell strategy may be the right track, thanks! :grin:

Here's a playground showing that you can use this pattern even with instructions that access multple registers:

This works because within the function you can borrow self mutably multiple times, as long as you don't keep two such borrows alive at the same time. In some cases, this might require calling get_mut each time you access a register, instead of just once.

2 Likes

Another option is to implement Index<Register> and IndexMut<Register> for a register file struct:

fn execute_pseudo_ldi_r1_r2(&mut self, dst_register: Register, src_register: Register) {
    let addr:usize = self.registers[src_register];
    let new_value = self.internal_ram[addr] as u16; // simplification
    self.registers[dst_register] = new_value;
    self.registers[src_register] += 1;
}
1 Like

Thanks everybody, it's been a very interesting discussion. :smile:

Ultimately, I find that using enums along with Index[Mut], yields a clean and readable solution.

My question about the scopes has been indirectly solved by rearranging the code as described by @2e71828's example.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.