Rust - Borrowing mutable references

Previously published

This article was previously published on len-learns-rust.com. A full index of these articles can be found here.

In addition to managing the lifetimes of references to variables, the Rust compiler’s borrow checker also deals with enforcing Rust’s guarantees about mutability and so helps to prevent data races.

Basically, you can have any number of immutable references to a variable, but only if there are no mutable references to it at the same time, and you can only ever have a single mutable reference. This piece on aliasing in Rust By Example covers this nicely.

Continuing the example from when I was exploring lifetimes, we can see that this restriction on mutability can start to cause problems for even simple code.

Suppose we wanted to add this to our log;

    fn log(&mut self, message: &str) {
        self.log_lines.push(message.to_string());

        println!("{}", message);
    }

It’s not unreasonable to expect the log to be able to log things. In this case, we have some simple hybrid log that prints a message and stores the message for later. To be able to store the message we need to have a mutable reference to self in log().

Let’s log the creation of our ThingThatLogs and also allow it to log when it does things that need logging.

impl<'a> ThingThatLogs<'a> {
    fn new(log: &'a Log) -> Self {
        log.log("created");
        ThingThatLogs { log }
    }

    fn do_thing(&mut self) {
        self.log.log("doing thing");
    }
}
Compiler says "no!"

We are, of course, already in the land of “no!”.

error[E0596]: cannot borrow `*log` as mutable, as it is behind a `&` reference
  --> src\main.rs:33:9
   |
32 |     fn new(log: &'a Log) -> Self {
   |                 ------- help: consider changing this to be a mutable reference: `&'a mut Log`
33 |         log.log("created");
   |         ^^^^^^^^^^^^^^^^^^ `log` is a `&` reference, so the data it refers to cannot be borrowed as mutable

Since the log needs to be mutable to call the log() method on it, we need to pass it in to the ThingThatLogs as mutable… Already warning bells are ringing, we’re chasing mutability up the callstack, and that can’t be good. We can actually end up with some simple example code here that works…

fn main() {
    let mut log = Log::new();

    {
        let mut thing1 = ThingThatLogs::new(&mut log);

        thing1.do_thing();

        let mut thing2 = ThingThatLogs::new(&mut log);

        thing2.do_thing();
   }

    println!("done");

    log.dump();
}

This surprises me, but I guess since it’s this simple the compiler can work out that actually it doesn’t matter that thing1 holds a mutable reference to log whilst thing2 also holds one as thing1 doesn’t actually get used again after thing2 is created… Adding any code that accesses thing1 after the creation of thing2 results in a compiler error.

Compiler says "no!"
error[E0499]: cannot borrow `log` as mutable more than once at a time
  --> src\main.rs:61:49
   |
56 |         let mut thing1 = ThingThatLogs::new(&mut log);
   |                                             -------- first mutable borrow occurs here
...
61 |             let mut thing2 = ThingThatLogs::new(&mut log);
   |                                                 ^^^^^^^^ second mutable borrow occurs here
...
66 |         thing1.do_other_thing();
   |         ----------------------- first borrow later used here

But it’s fairly obvious, from what we know about the rules for mutable references; the code above was doomed.

I think the thing to focus on here is that, at the point where we needed a mutable reference to be able to add a String to our Vec<> we were forced to expand the scope of the mutable reference. We shouldn’t need to do that. We want to be able to change the Vec<> in one place, not everywhere. In C++ we would simply not mark the method as const and then, as long as the object wasn’t const we could log to it - C++ is all kinda backwards here compared to Rust and I usually find myself chasing const up the callstack, and that’s generally a good thing…

So, we want a locally scoped mutable reference to the Vec<>. At this point we could go off on a wild-goose chase with unsafe code and std::cell::UnsafeCell<> in search of our own interior mutability, but I think that would be wrong. The fact that the name has “unsafe” in it should be enough to warn us off. Instead, we could use a std::sync::Mutex<>, that is, lock around the mutability so that we can’t introduce any data races when adding to the Vec<> and yet can scope the mutability requirement to the code that needs it.

We could end up with code like this in log():

    fn log(&self, message: &str) {
        self.log_lines
            .lock()
            .expect("failed to lock")
            .push(message.to_string());

        println!("{}", message);
    }

And our log looks like this:

struct Log {
    log_lines: std::sync::Mutex<Vec<String>>,
}

impl Log {
    fn new() -> Self {
        Log {
            log_lines: std::sync::Mutex::new(Vec::new()),
        }
    }

This has the advantage that the code is also thread safe. And, of course, if we ever find that the locking is a performance problem, we can profile it and then try and do something better and faster somehow…

Unlike before, when we worked out how to do with the lifetime requirements of the log, I think this time the implementation details, the Mutex<>, should stay on the inside and be encapsulated in the log. It’s the only way to ensure that the log can actually be used without needing to be mutable everywhere and so it seems an obvious choice. It might even be valid to use the Pimpl design we tried out with the lifetime issue.

Join in

The code can be found here on GitHub each step on the journey will have one or more separate directories of code, so this article’s code can be found here:

  1. Multiple Mutable References - we want to actually use the log
  2. Using a Mutex<> - reducing the scope of the mutable reference with a Mutex<>
  3. Putting this together with Rc<>

this allows for easy comparison of changes at each stage.

Of course, there may be a better way; leave comments if you’d like to help me learn.