Rust, RAII as a service

Wat? Resource Acquisition Is Initialization or RAII, is a programming technique which states that any resource file,mutex lock, etc that is used in some way must be bound to the lifetime of an object. In english, this means when an object is goes out of scope or gets garbage collected, language dependant of course, the resources it contained are cleaned up. For example this take this python snippet, which uses a context manager, this is not the same as RAII but it should give you an idea of where we are headed.

with open("my_file.txt", "r") as my_file:
    my_data = my_file.read()

Similar to RAII, here the context manager takes care of cleaning up the resources, i.e. whilst in the context of the when block the file is open and readable (in this case) and when you exit the block the file is released.

This brings us to our good friend C++. C++ is a very powerful language and should be treated with care and respect. RAII in C++, as defined here is either left up to the user to implement or one can rely on the standard library. Here is an example of both good and bad RAII implementations.

#include <iostream>
#include <thread>
#include <mutex>

void f() {
  // I do bad things here
}

bool everything_ok() { return false; }

void bad(std::mutex *m) 
{
  m->lock();
  f();
  if(!everything_ok()) return;
  std::cout << "I never get here \n";
  m->unlock();
}

void good(std::mutex *m)
{
  std::lock_guard<std::mutex> lk(*m);
  f();
  if(!everything_ok()) return;
}

int main() {
  std::mutex m;
  good(&m);

  bad(&m);
  m.lock();
}

If this program is run with the following:

g++ -pthread -Wall -pedantic -Wextra -Werror test.cpp
./a.out

The extra options given to GNU Compiler Collection (g++), are here to show that its not an obvious problem and no warnings or errors will be shown and the code will hang forever. If you attempt to further debug the code:

g++ -pthread -Wall -pedantic -Wextra -Werror test.cpp
valgrind --tool=memcheck -s ./a.out

Again it will hang until you Control-c your way out, and then it will print the cause of issue:

As you can see the mutex is still locked and any attempts to lock it again, will result in programming hanging.

Many in the C++ community would accuse me of being a bad C++ dev, true but I have yet to meet C++ dev who can outsmart bullet, ie write perfect code. Given the simplicity of this code it is easy to imagine a much more serious issue like this arising inside a more complex code base.

Which brings us to Rust. A relatively new language given C++ seniority, Rust has a several features which help prevent issues like the above one from arising. For simplicities sake we are only going to discuss ownership and lifetimes here.

In Rust one can construct a similar situation:

use std::sync::{Arc, Mutex};
use std::thread;

// I am a bad programmer
fn bad(lock: Arc<Mutex<i64>>) {
    let _ = thread::spawn(move || -> () {
        let _guard = lock.lock().unwrap();
        panic!();
    })
    .join();
}

fn main() {
    let lock = Arc::new(Mutex::new(0));
    bad(lock);
    // Won't compile borrow value used after move
    lock.lock();
}

This code is very similar to our C++ code, we take a mutex lock and proceed to crash inside a thread. However here Rust is going to save us. Rust prevents the compilation of this program because of ownership, as bad “owns” lock.

cargo build

yields:

In Rust, variables are in charge of cleaning up after themselves. This means that variables can have only one owner. Bad gets ownership of lock and destroys it when it finished with it. Even if you try to pass lock as a reference that will also fail to compile:

use std::sync::{Arc, Mutex};
use std::thread;

// I am a bad programmer
fn bad(lock: &Arc<Mutex<i64>>) {
    // Thread will ask you for a lifetime associated with lock
    let _ = thread::spawn(move || -> () {
        let _guard = lock.lock().unwrap();
        panic!();
    })
    .join();
}

fn main() {
    let lock = Arc::new(Mutex::new(0));
    bad(&lock);
    // Won't compile borrow value used after move
    lock.lock();
}

This code will yield: error[E0621]: explicit lifetime required in the type of lock, as seen below:

In Rust lifetimes are used to ensure all borrows are valid. By using the lock inside the thread block we are breaking this rule, as Rust can’t figure out when to remove lock from the scope. Rust will try to ask for a static lifetime for lock, effectively extending lock to the global scope so it lasts the length of the program. This should already be ringing alarm bells, and helps remind us that maybe we are doing something wrong.

This is an example of the Rust compiler protecting you. Yes its annoying, and yes Rust is a difficult language to get to grips with because of all of these rules, but it will protect you from mistakes like this. Perhaps now, this will make more sense.

You can read more about RAII in Rust here.