Rust Internal Mutability – Part 1 – Cell

Rust is known by some very strict rules when it comes to references and compile-time value integrity. This is the one of the main reasons why it was voted for the most loved language in the last 6 years by developers on StackOverflow (TODO: INSERT LINK HERE).

In fact, if you try to compile the following simple code in Rust:

struct Point {
    x: f32,
    y: f32
}

fn add(p: &Point, offset: f32) {
    p.x += offset;
    p.y += offset;
}

fn show(p: &Point) {
    println!("x: {}, y: {}", p.x, p.y);
}

fn main() {
    let mut p1 = Point {
        x: 5.0,
        y: 1.0
    };
    let p2 = &p1;
    show(&p2);
    add(&p1, 3.0);
    show(&p1);
}

… you will encounter this compile time error:

This shows the fundamental difference from references in C/C++. References in Rust can be mutable or immutable (just as variables can) and are immutable by default. Naturally, converting the parameter type to a mutable reference to Point fixed this compile error and prints out:

Now watch closely what happens if we only swap lines 21 and 23:

struct Point {
    x: f32,
    y: f32
}

fn add(p: &mut Point, offset: f32) {
    p.x += offset;
    p.y += offset;
}

fn show(p: &Point) {
    println!("x: {}, y: {}", p.x, p.y);
}

fn main() {
    let mut p1 = Point {
        x: 5.0,
        y: 1.0
    };
    let p2 = &p1;
    show(&p1);
    add(&mut p1, 3.0);
    show(&p2);
}

It was shocking to me to find out this code doesn’t compile in Rust! Luckily, I was curious enough to not let it go in rage and disbelief, but try to get to the bottom of this obviously sadistic global developer movement instead.

Borrowing and ownership

Apparently, what was known in C++ as “getting a reference to a variable” is called “borrowing” in Rust, and you must always choose whether you’ll borrow a mutable or an immutable reference to a variable.

Now, if you borrow a mutable reference to some variable, you are not allowed to have another reference to the same variable before this mutable borrow, and later use that same reference after the mutable borrow. Why? Simple – the mutable borrow might’ve changed it between the first and the second immutable borrow. Effectively, this could render your second immutable reference invalid… And that there, my friends, is what caused 95% of C and C++ memory leaks, crashes and security exploits in the world the last 40 years.

Interestingly, commenting out line 23 makes the program compile without any errors. Rust compiler is smart enough to understand that even though p2 has borrowed an immutable reference to p1 (line 20) we didn’t actually use p2 after borrowing the mutable reference to p1 (line 22), so no harm in borrowing it both mutably and immutably in the same scope. Compiler guarantees that, even though we almost did something risky, there is no case this code could result in an invalid reference. I don’t know about you, but this is the next level tooling for me – I never had a chance to work with such a smart compiler!

Smart pointers

This kind of restrictive referential integrity is sometimes not flexible enough, so Rust provides a set of wrapped generic types to handle various reference borrowing scenarios. They are usually referred to as “internal mutability types”. Some of these are:

  • Cell<>
  • Rc<>
  • Arc<>
  • Box<>
  • Mutex<>

Cell<>

Cell is a generic type holding a reference to a value. You are allowed to have multiple immutable references (aliases) to a Cell. You are also allowed to mutate the value inside of a Cell using set(&self, val: T) method, which at first can sound a bit counter intuitive. How can we mutate something inside of an immutable reference? The caveat is that the reference to a Cell is immutable, but the value the Cell manages is mutable.

let myCell = Cell::new(40);
myCell.set(20);
println!("{}", c);

This code is perfectly valid and would print the number 20.

You can also get a copy of the value inside the Cell, but not the reference to that value.

Cell is usually meant to be used in cases you:

  • work with simple types (i.e. numbers, booleans), which are cheap to copy
  • they need to be referenced from a lot of different places
  • want to be able to mutate them from anywhere you have a reference to them

Since we can mutate the value Cell holds you might think it’s not thread safe – and you’d be right:

use std::cell::Cell;
use std::sync::Arc;

struct Point {
    x: f32,
    y: f32
}

fn main() {
    let mut p = Cell::new(Point {
        x: 5.0,
        y: 3.0
    });
    
    let t1 = std::thread::spawn(|| {
        p.set(Point { 
            x: 1.0,
            y: 1.0
        })
    });
    
    let t2 = std::thread::spawn(|| {
        p.set(Point { 
            x: 2.0,
            y: 2.0
        })
    });
}

As we can see, Cell forbids implementing Sync trait, which effectively means you can’t pass reference to a Cell between threads in a safe way. Cell is great for a single-threaded scenarios where you need multiple references to a simple value you want to mutate.

If we wanted to pass a value around multiple threads we would have to wrap it into an Arc<Mutex<T>> type, which will be covered in the next blog post.

Leave a Comment