The Developer’s Cry

Yet another blog by a hobbyist programmer

Rust In Practice - Dealing With Borrowck

Rust is a great language to code in, but as many would agree, it has a learning curve. Even if you don’t bother with traits, lifetimes, refcounted pointers, or any of that, you may still encounter difficulties with the borrow-checker from time to time. Rust demands that you structure the code in a certain way, and it may be hard putting a finger on what exactly is the right structure to keep our beloved borrow-checker happy.

Apples and oranges

Whenever I run into borrowck issues, my first intuitive reaction is to refactor the code. Sometimes it works, sometimes it doesn’t. What’s up with that? Let’s get to the bottom of this.

People online say that writing Rust code is not the same as writing in Java or C++. While I get their point, I’m not so convinced that it’s totally different.

As a concrete example, let’s have a look at a simple windowing system, that could be used for a GUI, or maybe a text-mode UI. The user interface has Windows that are drawn to the Screen. And thus we have:

struct Window {
    x: u16,
    y: u16,
    w: u16,
    h: u16,
    color: Color
}

struct Screen {
    w: u16,
    h: u16,
    stdout: Stdout
}

impl Screen {
    fn draw(&mut self, window: &Window) {
        ...
    }
}

The Screen does drawing operations, so it makes sense that Screen has a method that draws a Window, right?

From an object-oriented point of view, we might also say that the Window is the central object, and a Window knows how to draw itself onto the Screen.

[In fact, in C++ I would simply use a global variable screen and let Window::draw() use that. Since there is only one screen (AFAIC) anyway, I don’t have a problem with using globals like that. I do realize that technically, that means the C++ version relies on a side-effect. Sue me.]

In Rust mutable globals are a no-no, and therefore we write:

impl Window {
    fn draw(&self, screen: &mut Screen) {
        ...
    }
}

It’s apples and oranges whether it’s the screen doing the drawing, or the window drawing itself onto the screen. It doesn’t matter much. But let me get back to this later.

Brain stew

The window and screen have to be managed somewhere, and we can pass them down from main(), or we can have a nice WindowManager.

struct WindowManager {
    windows: Vec<Window>,
    screen: Screen
}

impl WindowManager {
    fn open(&mut self, window: Window) {
        self.windows.push(window);
        let window_ref = self.windows.last().unwrap();
                  ------------ immutable borrow occurs here
        self.draw(window_ref);
        |    |
        |    immutable borrow later used by call
        mutable borrow occurs here
    }

    fn draw(&mut self, window: &Window) {
        self.screen.draw(window);
    }
}

Ack! Borrowck kicks in, we can not code it like this. There is no harm done in Java nor C++, but Rust does not allow it.

Let’s try refactoring, and passing the screen separately:

fn open(&mut self, window: Window) {
    self.windows.push(window);
    let window_ref = self.windows.last().unwrap();
    self.draw(window_ref, &mut self.screen);
    ---- ----             ^^^^^^^^^^^^^^^^ mutable borrow occurs here
    |    |
    |    immutable borrow later used by call
    immutable borrow occurs here
}

fn draw(&self, window: &Window, screen: &mut Screen) {
    screen.draw(window);
}

Well, that was a futile attempt.

It can be frustrating when borrowck stops you. When you spell out all the bytes, there is nothing wrong with this code. The same thing works perfectly when written in Java or C++. There seriously is no bug here whatsoever—but this pattern doesn’t play by the borrowck rules. It’s a precaution mechanism for subtle, hard to find bugs that appear in Java, C++, and other unsafe languages.

Whenever I run into borrowck, I have to remind myself that a reference is not a pointer, it is a borrow that is handed out.

But if you can’t code it like this, then how do I …? This confusion causes many programmers to turn away from Rust. Imagine having 20, 30, 40 years of programming experience, and then this language just fails to compile? In order to make it work, we have to work with the borrow-checker, not against it.

Keep em separated

Let’s analyze it one more time, and see what we can do. We have our data structure:

struct WindowManager {
    windows: Vec<Window>,
    screen: Screen
}

We want to open a window, add it to the .windows list, and then draw that window to the screen.

What we are doing is calling a mutable method (ie. passing &mut self) while holding an immutable reference into self.windows. See the added annotations:

fn open(&mut self, window: Window) {
        ^ all of self is mutable

    // append window to our list of windows
    self.windows.push(window);

    let window_ref = self.windows.last().unwrap();
        ^ immutable reference into self.windows

    self.draw(window_ref);
         ^ call mutable method
}

fn draw(&mut self, window: &Window) {
        ^                  ^ immutable reference into self.windows
        ^ all of self is mutable

    self.screen.draw(window);
}

You can’t do this cross-reference of mutable and immutable data when calling the draw method. The compiler was pointing out the offending line all along:

    self.draw(window_ref);

There is no way for the Rust compiler to guarantee that self.draw() is not going to modify self.windows (that would be a data race!) and therefore it won’t compile.

We have a couple of options:

All of these solutions work;

1. change data layout

The Rust compiler doesn’t do code philosophy (it just looks at data ownerships and mutability), but we can easily argue that a WindowManager should not own a Screen. Therefore the screen does not belong in the struct. So let’s remove it, and pass it as a separate parameter:

struct WindowManager {
    windows: Vec<Window>,
}

impl WindowManager {
    fn open(&mut self, window: Window, screen: &mut Screen) {
        self.window.push(window);
        let window_ref = self.window.last().unwrap();
        screen.draw(window_ref);
    }
}

This shows that data design is more important in Rust than it is in other languages.

2. refactor code

Let’s keep the struct as it is.

We are having trouble calling a mutable method. So let’s try avoidance. One way of doing that is inlining:

impl WindowManager {
    fn open(&mut self, window: Window) {
        self.window.push(window);
        let window_ref = self.window.last().unwrap();
        self.screen.draw(window_ref);
    }
}

It may seem bananas that such trickery is necessary. However, we can now clearly see that self.window and self.screen are being used separately; there are no data races possible.

Another way of doing that is “outsourcing”:

impl WindowManager {
    fn open(&mut self, window: Window) {
        self.window.push(window);
        let window_ref = self.window.last().unwrap();
        Self::draw_window(window_ref, &mut self.screen);
    }

    fn draw_window(window: &Window, screen: &mut Screen) {
        screen.draw(window);
    }
}

Here draw_window() is a static method. It may also be a free function in the crate.

This code reveals that we can reverse the situation: instead of having the screen draw the window, let the window draw itself onto the screen. It’s apples and oranges, but sometimes the result is more logical. Notice how Window::draw() is an immutable method:

impl WindowManager {
    fn open(&mut self, window: Window) {
        self.window.push(window);
        let window_ref = self.window.last().unwrap();
        window_ref.draw(&mut self.screen);
    }
}

impl Window {
    fn draw(&self, screen: &mut Screen) {
        screen.draw(self);
    }
}

3. make a copy

We can prevent holding a reference into self.window by making a copy of the referenced element. A deep copy of an object is made with .clone().

impl WindowManager {
    fn open(&mut self, window: Window) {
        self.window.push(window);
        let window_clone = self.window.last().unwrap().clone();
        self.draw(&window_clone);
    }
}

Making a copy is considered bad practice because it’s a lazy solution. It takes memory and (a little) performance, while better options exist.

OOPsie daisy

Compositing structs is a common approach for data encapsulation and adding higher layers of abstraction. People on the internet are quick to blame OOP, but it’s not OOP that is the problem. It is easy to make connections that are criss-cross, inside-out, upside-down. Rust borrowck does not allow such design, because it wouldn’t be able to guarantee memory safety.

The key is to decouple things that should not be coupled in the first place. Use separation of concerns like your life depends on it. As a general rule, refrain from linking structures together, or excessively combining them into larger structures. When the data layout is sound, refactoring is usually possible.

One tip is spend more time in the design phase. I am not a fan of meticulously planning out everything up-front, however. I prefer the bottom-up approach, starting out small, and letting it come to fruition from there. Stick to the main strategy—a separation of concerns—and there should be few issues with borrowck.