Search code examples
rustborrow-checkermutability

What should I do if I would like to pass a single mutable object into multiple parameters of a function?


I've written a program in Rust that plays music using stepper motors, and now I'd like to add some fake objects so that I can do automated testing. However, I don't know a good way to define those fake objects in such a way that my program can actually use them.

You can run my code on the Rust Playground.

The part that works

The program's main loop uses two trait objects. One object implements a trait called Timer, which represents a timer that can be used to make things happen at regular intervals. The other object implements a trait called Motor, and represents the stepper motor itself. The definitions of those traits are on my Rust Playground post.

The main loop simply waits 500 microseconds, pulls the "step" pin HIGH, waits another 500 microseconds, pulls the "step" pin LOW, and repeats for a total of 1,000 cycles. This causes the motor to step 1,000 times a second for one second.

fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
    timer.reset();
    for _ in 0..1000 {
        timer.wait_microseconds(500);
        motor.set_step_high();
        timer.wait_microseconds(500);
        motor.set_step_low();
    }
}

So far, everything works just great. I have (in my real code, not the Rust Playground post) a working implementation of Timer, and a working implementation of Motor, and the spin function makes the motor spin, and it sounds beautiful.

The problematic part

I want to be able to do automated testing, so I've written a "fake" object which implements Motor and Timer in a way that's useful for testing. The type itself is just a struct:

/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
    is_on: bool,
    time_high: u64,
}

The implementations of set_step_high and set_step_low just set is_on to true and false (respectively), and the implementation of wait_microseconds just checks whether or not is_on is true and adds the given amount of time to time_high if so. Those implementations are in my Rust Playground post.

Naively, I would expect to be able to pass a DummyTimerMotor object as both parameters of spin, and then look at time_high afterward, and see that it has a value of 500000. However, that is, of course, not allowed:

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false, time_high: 0
    };
    
    spin(&mut dummy, &mut dummy); // Oops, not allowed!
    
    print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}

This gives an error message: "cannot borrow dummy as mutable more than once at a time."

I know exactly why I'm getting that error message, and it makes sense. What's a good way to get the behavior I'm trying to get?

I only have one reasonable idea: change spin so that instead of taking an object which implements Timer, and another object which implements Motor, it takes a single object which implements both Timer and Motor. However, that seems inelegant to me (as a Rust newbie). Conceptually, a timer is one thing and a motor is another thing; having spin take a single object which is both a timer and a motor is quite unintuitive. It doesn't seem like I should change the way that spin is implemented merely to accommodate the details of how the timer and motor are implemented.


Full code listing

In case the Rust Playground ever goes down, below is the entire program that I have there, along with the entire error output.

/// A timer which can be used to make things happen at regular intervals.
trait Timer {
    /// Set the timer's reference time to the current time.
    fn reset(&mut self);
    /// Advance the timer's reference time by the given number of microseconds,
    /// then wait until the reference time.
    fn wait_microseconds(&mut self, duration: u64);
}

/// The interface to a stepper motor driver.
trait Motor {
    /// Pull the "step" pin HIGH, thereby asking the motor driver to move the
    /// motor by one step.
    fn set_step_high(&mut self);
    /// Pull the "step" pin LOW, in preparation for pulling it HIGH again.
    fn set_step_low(&mut self);
}

fn spin<T: Timer, M: Motor>(timer: &mut T, motor: &mut M) {
    timer.reset();
    for _ in 0..1000 {
        timer.wait_microseconds(500);
        motor.set_step_high();
        timer.wait_microseconds(500);
        motor.set_step_low();
    }
}

/// A mock timer and motor which simply tracks the amount of simulated time that
/// the motor driver has its "step" pin pulled HIGH.
struct DummyTimerMotor {
    is_on: bool,
    time_high: u64,
}

impl Timer for DummyTimerMotor {
    fn reset(&mut self) { }
    
    fn wait_microseconds(&mut self, duration: u64) {
        if self.is_on {
            self.time_high += duration;
        }
    }
}

impl Motor for DummyTimerMotor {
    fn set_step_high(&mut self) {
        self.is_on = true;
    }
    
    fn set_step_low(&mut self) {
        self.is_on = false;
    }
}

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false, time_high: 0
    };
    
    spin(&mut dummy, &mut dummy); // Oops, not allowed!
    
    print!("The 'step' pin was HIGH for {} microseconds", dummy.time_high);
}
error[E0499]: cannot borrow `dummy` as mutable more than once at a time
  --> src/main.rs:61:22
   |
61 |     spin(&mut dummy, &mut dummy); // Oops, not allowed!
   |     ---- ----------  ^^^^^^^^^^ second mutable borrow occurs here
   |     |    |
   |     |    first mutable borrow occurs here
   |     first borrow later used by call

Solution

  • You can have a distinct DummyTimer and DummyMotor who share state through a Rc<RefCell<State>>:

    struct State {
        is_on: bool,
        time_high: u64,
    }
    
    struct DummyTimer {
        state: Rc<RefCell<State>>,
    }
    
    impl Timer for DummyTimer {
        fn reset(&mut self) { }
        
        fn wait_microseconds(&mut self, duration: u64) {
            let mut t = self.state.borrow_mut();
            if t.is_on {
                t.time_high += duration;
            }
        }
    }
    
    struct DummyMotor {
        state: Rc<RefCell<State>>,
    }
    
    impl Motor for DummyMotor {
        fn set_step_high(&mut self) {
            self.state.borrow_mut().is_on = true;
        }
        
        fn set_step_low(&mut self) {
            self.state.borrow_mut().is_on = false;
        }
    }
    
    fn main() {
        let state = Rc::new (RefCell::new (State { is_on: false, time_high: 0, }));
        let mut motor = DummyMotor { state: Rc::clone (&state), };
        let mut timer = DummyTimer { state: Rc::clone (&state), };
    
        spin(&mut timer, &mut motor); // Now it's allowed
        
        print!("The 'step' pin was HIGH for {} microseconds", state.borrow().time_high);
    }
    

    Playground