Search for help: cannot borrow `*self` as mutable more than once at a time

Hi, everyone. I am new to Rust. Recently, I am writing a GameBoy emulator with Rust. The code attached below cannot be compiled, which finds the current operation code and executes it.

use std::collections::HashMap; 

pub trait IOperationCode {
    fn step(&mut self, cpu: &mut GameBoyCPU) {
        // do something with cpu.registers, cpu.memories, ...
    }
}

pub struct GameBoyCPU {
    pub opcode_dict: HashMap<u16, Box<dyn IOperationCode>>
    // pub memory: ..., 
    // pub registers: ..., 
}


impl GameBoyCPU {
    pub fn new() -> GameBoyCPU {
        let mut ret = GameBoyCPU {
            opcode_dict: HashMap::new(),
        };
        
        // insert operation codes to opcode_dict 
        
        return ret;
    }
    
    pub fn current_op_mut(&mut self) -> &mut Box<dyn IOperationCode> {
        let current_op: u16 = 0;  // assume that the current opcode is 0.
        let current_op = self.opcode_dict.get_mut(&current_op);
        return current_op.expect("invalid opcode!");
    }
    
    pub fn step(&mut self) {
        let current_op = self.current_op_mut();
        current_op.step(self);
    }
    
}

The compilation error is:

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/lib.rs:35:25
   |
34 |         let current_op = self.current_op_mut();
   |                          --------------------- first mutable borrow occurs here
35 |         current_op.step(self);
   |                    ---- ^^^^ second mutable borrow occurs here
   |                    |
   |                    first borrow later used by call

Is there something improper with my structure design? Or the problem can be circumvented with some tricks? Any suggestion is appreciated.

This borrows self in a mutable way as long as current_op lives. I guess you could clone current_op instead of keeping it as reference.

Does the following work (adding .to_owned())?

        let current_op = self.current_op_mut().to_owned();

Either way, I'm mot sure if that's the best way to go.

1 Like

to_owned usually won’t work on trait objects like the Box<dyn IOperationCode> here.


@anti-destiny What the best approach(es) for solving your problem might be, depends a bit on the use-case here, I’d say. For example, it would be helpful to see some example implementations of what kinds of things/operations the fn step implementations want to do, so it’s easier to judge what modifications to its type signature would still facilitate such operations.

The problem here is that the current signature give them mutable access to self directly, and a second time indirectly via the &mut GameBoyCPU. Imagine the problems that would arise if cpu.opcode_dict.remove(i) was called, dropping the Box<dyn IOperationCode> in question, then self would become invalid, as there is no mechanism (like often garbage collection in other languages) that could keep self alive even if it was removed from the HashMap that used to own it.

Approaches to address this issue would need to get rid of the dual mutable access. E.g. if step doesn’t need any access to self, or doesn’t need access to the cpu.opcode_dict, there would be approaches to limit this access via a different type signature. Other things that could be expressed could even be access to both, but in sequence; or read-only access could be granted to both things (cpu and self) simultaneously, perhaps even combined with Rust’s “garbage collection” feature of Arc<…> (or Rc<…>) pointers, the access to cpu could still be mutable. And there might be even more alternatives I didn’t think of yet :wink:

2 Likes

If you look at this NES emulator in Rust, you can see that the top-level CPU struct is responsible for executing code and making changes to its own state rather than using a helper struct.

In my opinion, this is the idiomatic Rust way of doing something like this, otherwise you will be fighting the borrow checker more than you are writing code.

1 Like

Here is a case of a "jump" operation:

struct JPC3 {
    cycles: i32,
    counter: i32, 
}

impl JPC3 {
    fn new() -> Self {
        JPC3 {
            cycles: 16,
            counter: 0, 
        }
    }
}

impl IOperationCode for JPC3 {
    
    fn step(&mut self, cpu: &mut GameBoyCPU, memory: &mut GameBoyMemory, cycles_remain: &mut i32) -> bool {
        if *cycles_remain < self.cycles {
            return false;
        } 
        self.counter += 1; 
        let pc = cpu.registers.PC as usize;
        let jump_to_l: u8 = memory[pc + 1];
        let jump_to_h: u8 = memory[pc + 2];
        let jump_to = (jump_to_h as u16) << 8 | (jump_to_l as u16);
        cpu.registers.PC = jump_to;

        *cycles_remain -= self.cycles;
        return true;
    }
}

As shown by the example, I usually need to access cpu.registers in the step() function. Other fields of cpu, for example cpu.opcode_dict would be never visited in this function.

Then perhaps the easiest approach might be to pass not a mutable reference to the whole CPU but it the relevant field(s).

If, for such purposes, the CPU needs to be commonly split up in the same way, some helper structs for bundling the relevant fields together could be useful, either by subdividing the CPU and thus using such a struct in the definition of GameBoyCPU itself, or by defining a struct containing multiple references.

2 Likes

I just had a look at the implementation of the NES emulator. It seems to use a match keyword to process all kinds of CPU instructions. I am afraid of such implementation will lead to a lengthy function, which harms readability. In addition, I also wonder if the match-based methods (similar to switch in C?) run much slower than hashmap-based methods.

You don't have to inline everything into the match arms. You can refactor every instruction into a function and simply call it in the appropriate arm.

No, why would they? A hash map does a lot of processing and extra work (hashing, memory allocaion, and collision resolution, primarily), and it needs to accomodate fully dynamic keys. In contrast, a match statement can be directly optimized by the compiler, it has a statically known set of cases, and doesn't need to heap-allocate. Matches can be compiled to a decision tree or a lookup table, both of which are just a small number of jumps, so match is usually faster than a hash table lookup.

Although there can be exceptions, so if you want to know it for sure, you won't be able to spare yourself from benchmarking it.

2 Likes

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.