Daniel Keast

Rust's ownership system

Programming, Rust

Rust is a systems programming language. It aims for there to be as little as possible between your code and the hardware running it. The reason for this is to give you full control of what the CPU is doing, and how your data structures are laid out in memory.

C is the systems programming language I am most familiar with, I’ve used it on and off for decades. Despite this (and the fact that it is a very small language), I have never felt like I know it well. C is hard.

This is partly because of the direct control of memory. You must make sure that you are:

  • Requesting the right amount of memory.
  • At the right time.
  • Never going out of it’s bounds.
  • Returning it to the operating system at the right time.
  • Only once.
  • No longer using it once returned.

When you forget to follow one of these rules, it is very easy to end up with a program that compiles and runs, but produces ‘undefined behaviour’. This could mean anything from crashing, executing arbitrary code elsewhere in memory, or corrupting files.

To make this easier for developers, most languages of the last 20+ years have included a garbage collector. When using one of these languages you don’t have to worry about memory management, it will all be handled by something running between your code and the operating system. Just create structures as you need them, and at some point after they can no longer be reached in code they will be cleaned up.

This means though that you can never be sure about your memory or performance characteristics. You don’t know when things will be freed, when more allocations will be requested from the operating system, or when the garbage collector will pause your entire application to analyse your heap.

I always thought these were direct trade offs. To have the efficiency and control of C, you had to accept that the training wheels were off and it was up to you to make sure you’re upright.

Rust is different though, it can protect you from making these kind of errors at compile time. The book describes it as having these three rules:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

This means the lifetime of the allocated memory is exactly the lifetime of the scope of it’s owner variable. You can’t reference freed memory, because it isn’t freed until your reference has gone.

Even in languages with garbage collectors you still really have the same problem you always had, just for different resources. You still have to manually manage the lifetimes of file handles, database connections, transactions, sockets, etc. There tend to be constructs specifically to help you manage the lifetimes of these objects.

With Python you get context managers:

with open('file') as f:
    print(f.read())

Java 7 introduced try-with-resources and AutoCloseable:

try (BufferedReader br = new BufferedReader(new FileReader("file"))) {
    return br.readLine();
}

Once you fall off the end of the construct the resource gets automatically closed by the runtime. This is great, it makes the scope of the resources clear, and it means not forgetting to close them when necessary.

In rust though, it uses the same ownership system to handle this. A file is closed automatically when the owner goes out of scope.

use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;

fn main() {
    let f = File::open("file")
        .expect("Error opening file");

    for line in BufReader::new(&f).lines() {
        println!("{}", line.unwrap());
    }
}

I don’t have to worry about calling a close function on the file handle, or wrapping it in something that will manage it for me. The same part of the language that ensures memory is handled correctly makes sure that it will be closed when the cpu falls off the end of the containing scope.