Sophie Swett
Sophie Swett

Reputation: 3398

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

Upvotes: 2

Views: 520

Answers (2)

Jmb
Jmb

Reputation: 23463

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

Upvotes: 2

Svetlin Zarev
Svetlin Zarev

Reputation: 15713

For obvious reasons you cannot have two mutable references to your DummyTimerMotor, but you can try some unsafe hackery to achieve something similar. This approach is inspired by slice::split_at_mut and tokio::io::split

Basically you can create two proxy objects, one implementing Timer and one implementing Motor:

impl DummyTimerMotor {
    pub fn split<'a>(&'a mut self) -> (impl Timer + 'a, impl Motor + 'a) {
        let ptr_is_on = &mut self.is_on as *mut bool;
        let ptr_time_high = &mut self.time_high as *mut u64;

        (
            TimerHalf::<()>::new(ptr_is_on, ptr_time_high),
            MotorHalf::<()>::new(ptr_is_on),
        )
    }
}

struct TimerHalf<'a, T: 'a> { // the dummy parameters are needed for the PhantomData
    is_on: *mut bool, // or *const bool instead
    time_high: *mut u64,
    // we need the phantom data in order to prevent someone from creating another 
    // borrow on the DummyTimerMotor and to track where it's safe to use this 
    // TimerHalf instance
    _phantom: PhantomData<&'a T>,
}

impl<'a, T> TimerHalf<'a, T> {
    fn new(is_on: *mut bool, time_high: *mut u64) -> TimerHalf<'a, ()> {
        TimerHalf {
            time_high,
            is_on,
            _phantom: PhantomData,
        }
    }
}

impl<'a, T> Timer for TimerHalf<'a, T> {
    fn reset(&mut self) {
        //
    }

    fn wait_microseconds(&mut self, duration: u64) {
        unsafe {
            if *self.is_on { // how safe is this ? 
                *self.time_high += duration;
            }
        }
    }
}

struct MotorHalf<'a, T: 'a> { // the dummy parameters are needed for the PhantomData
    is_on: *mut bool,
    // we need the phantom data in order to prevent someone from creating another 
    // borrow on the DummyTimerMotor and to track where it's safe to use this 
    // MotorHalf instance
    _phantom: PhantomData<&'a T>,
}

impl<'a, T> MotorHalf<'a, T> {
    fn new(is_on: *mut bool) -> MotorHalf<'a, ()> {
        MotorHalf {
            is_on,
            _phantom: PhantomData,
        }
    }
}

impl<'l, T> Motor for MotorHalf<'l, T> {
    fn set_step_high(&mut self) {
        unsafe { *self.is_on = true }
    }

    fn set_step_low(&mut self) {
        unsafe { *self.is_on = false }
    }
}

The rules around raw pointers are very vague and I could not find any authoritative document on whether you are allowed to dereference 2 mutable raw pointers to the same data. All I could find was that it's not allowe to dereference a mut raw pointer for as long as there is a mut reference, because that would violate the aliasing rules.

Well, in that case there are no references at all, so I guess that this is fine. I've run some experiments with MIRI and it didn't report any violations, but I'm not sure if that is any guarantee of UB-free behavior.

Then you can use it like that (playground):

fn main() {
    let mut dummy: DummyTimerMotor = DummyTimerMotor {
        is_on: false,
        time_high: 0,
    };

    let (mut t, mut m) = dummy.split();

    spin(&mut t, &mut m);


    // let (mut t, mut m) = dummy.split(); - not allowed - already borrowed (that's fine!)
    // unless you drop the timer/motor halves :)

    drop(t); 
    drop(m);

    
    print!(
        "The 'step' pin was HIGH for {} microseconds",
        dummy.time_high
    );
}

Relevant resources:

PS: @Jmb's solution IS better for your use-case, but as I've spent quite a lot of time researching this, I've decided to post it anyway in hopes of someone shedding some light on the unsafe aspects of my approach

Upvotes: 1

Related Questions