,

Rust, fighting the borrow checker

This post is about my experience battling the Rust borrow checker during compilation. This is a common issue, especially for those with a strong OOP background.

Spoiler alert: If you are struggling with the borrow checker, you are likely doing something wrong. Specifically, you might be thinking about your code structure in the wrong way.

I was working with the process API in Rust and encountered issues because I was thinking about mutability from an OOP perspective.

In Rust, you can only have a single mutable reference to an object at a time. There are still many things in Rust that I don’t fully understand, and that contributed to my problems.

During my journey to get the process API to work, I spent two weeks, including evenings and weekends, figuring out the right approach to design the library structure. It was a time-consuming and frustrating process, but what I learned was invaluable.

Before we dive in, let’s look at the basic premise of what I want to achieve.

We are creating a module system where I can define a process in JSON and execute sequenced steps. These steps access actions on modules registered with the process API. Modules can also call other modules on the process API, even actions on the same module.

The problem I faced was that modules might need some mutability. For example, I have a module that executes JS using the V8 engine. You first need to start the V8 engine, then you can run any number of JS actions on it. Once done, you need to close it. To prevent starting an already running engine, we need to mark a state on the module to indicate the engine is running. If the engine is running, just ignore the action and continue. The same goes for shutting down the engine.

When you start the engine, you want to set a flag like “running = true” on the module. This is where the battle with Rust begins. You need to get a mutable reference to the module, which means that all modules are mutable, whether they need to be or not. Read-only modules should remain read-only, but this also requires the process API to be mutable so that you can get a mutable reference to the module within it. This is neither a clean solution nor a workable one because it forces a double mutable borrow, which Rust does not allow.

This forced me to reconsider mutability. In an OOP environment, especially in languages like JS and Python, everything is mutable unless explicitly locked. Thinking about Rust mutability in the same context as standard OOP-based languages is a mistake. You need to think about your code structure in a Rust-centric way.

The solution I found was to use internal mutability with smart pointers. By default, everything is immutable. The process API is immutable, and the modules are immutable, which is the way it should be. Move the mutability problem to the module level so that the module can decide if it has mutable aspects. This concept might seem strange—saying that the module is immutable but has internal mutability using smart pointers.

Initially, I had the modules’ hash map wrapped as follows:

pub struct ProcessApi {
    modules: Arc<Mutex<HashMap<String, Box<dyn Module + 'static>>>>,
}

This solution worked to some extent until I encountered deadlock issues where a module was already locked, and I was attempting to lock it again. The updated approach now looks like this:

pub struct ProcessApi {
    modules: HashMap<String, Box<dyn Module + 'static>>,
}

Normally, the structure for modules is quite straightforward.

pub struct ConsoleModule;

However, if I want to manage internal mutability, I can add a state struct of my choosing and wrap it in an Arc and Mutex.

struct MutexModelState {
    pub from_value: i32,
    pub to_value: i32
}

pub struct MutexModel {
    state: Arc<Mutex<MutexModelState>>
}

What this buys you is a very short-term lock at the module level, precisely when and where you want to make changes to the module. This not only solves the deadlock issue but also ensures a clean approach, as the mutability is very granular.

{
    let mut state = self.state.lock().unwrap();
    state.from_value = from_value;
    state.to_value = to_value;
}

All the time I was fighting with the borrow checker, it was because I was trying to force my preconceived ideas onto Rust. Granted, there was a learning curve for me to start thinking more in a Rust-like way, but now that I see the running solution, I must agree with the borrow checker—this is much better.

Not only is it cleaner, but I also have multi-thread-safe mutability. I now realize that the borrow checker was trying to lead me down the right path, and the conflict I had with it was my fault.

Sorry, borrow checker. My bad.