Rust Memory Management

In the light of my latest fascination with Rust programming language, I've started to make small presentation about Rust at my office, since I'm not the only one at our company who is interested in Rust. My first presentation in Feb was about a very general introduction to the language but at that time I had not yet really used the language for anything real myself so I was a complete novice myself and didn't have a very good idea of how memory management really works. While working on my gps-share project in my limited spare time, I came across quite a few issues related to memory management but I overcame all of them with help from kind folks at #rust-beginners IRC channel and the small but awesome Rust-GNOME community.

Having learnt some essentials of memory management, I thought I share my knowledge/experience with folks at the office. The talk was not well-attended due to conflicts with other meetings at office but the few folks who attended were very interested and asked some interesting and difficult questions (i-e the perfect audience). One of the questions was if I could put this up as a blog post so here I am. :)

Basics


Let's start with some basics: In Rust,

  1. stack allocation is preferred over the heap allocation and that's where everything is allocated by default.
  2. There is strict ownership semantics involved so each value can only and only have one owner at a particular time.
  3. When you pass a value to a function, you move the ownership of that value to the function argument and similarly, when you return a value from a function, you pass the ownership of the return value to the caller.

Now these rules make Rust very secure but at the same time if you had no way to allocate on the heap or be able to share data between different parts of your code and/or threads, you can't get very far with Rust. So we're provided with mechanisms to (kinda) work around these very strict rules, without compromising on safety these rules provide. Let's start with a simple code that will work fine in many other languages:

fn add_first_element(v1: Vec<i32>, v2: Vec<i32>) -> i32 {
    return v1[0] + v2[0];
}

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = vec![1, 2, 3];

    let answer = add_first_element(v1, v2);

    // We can use `v1` and `v2` here!
    println!("{} + {} = {}", v1[0], v2[0], answer);
}

This gives us an error from rustc:

error[E0382]: use of moved value: `v1`
  --> sample1.rs:13:30
   |
10 |     let answer = add_first_element(v1, v2);
   |                                    -- value moved here
...
13 |     println!("{} + {} = {}", v1[0], v2[0], answer);
   |                              ^^ value used here after move
   |
   = note: move occurs because `v1` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

error[E0382]: use of moved value: `v2`
  --> sample1.rs:13:37
   |
10 |     let answer = add_first_element(v1, v2);
   |                                        -- value moved here
...
13 |     println!("{} + {} = {}", v1[0], v2[0], answer);
   |                                     ^^ value used here after move
   |
   = note: move occurs because `v2` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait

What's happening is that we passed 'v1' and 'v2' to add_first_element() and hence we passed its ownership to add_first_element() as well and hence we can't use it afterwards. If Vec was a Copy type (like all primitive types), we won't get this error because Rust will copy the value for add_first_element and pass those copies to it. In this particular case the solution is easy:

Borrowing


fn add_first_element(v1: &Vec<i32>, v2: &Vec<i32>) -> i32 {
    return v1[0] + v2[0]; 
}

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = vec![1, 2, 3];

    let answer = add_first_element(&v1, &v2);

    // We can use `v1` and `v2` here!
    println!("{} + {} = {}", v1[0], v2[0], answer);
}                 

This one compiles and runs as expected. What we did was to convert the arguments into reference types. References are Rust's way of borrowing the ownership. So while add_first_element() is running, it owns 'v1' and 'v2' but not after it returns. Hence this code works.

While borrowing is very nice and very helpful, in the end it's temporary. The following code won't build:

struct Heli {
    reg: String
}

impl Heli {
    fn new(reg: String) -> Heli {
        Heli { reg: reg }
    }
    
    fn hover(& self) {
        println!("{} is hovering", self.reg);
    }
}

fn main() {
    let reg = "G-HONI".to_string();
    let heli = Heli::new(reg);

    println!("Registration {}", reg);
    heli.hover();
}

rustc says:

error[E0382]: use of moved value: `reg`
  --> sample3.rs:20:33
   |
18 |     let heli = Heli::new(reg);
   |                          --- value moved here
19 | 
20 |     println!("Registration {}", reg);
   |                                 ^^^ value used here after move
   |
   = note: move occurs because `reg` has type `std::string::String`, which does not implement the `Copy` 

If String had Copy trait implemented for it, this code would have compiled. But if efficiency is a concern at all for you (it is for Rust), you wouldn't want most values to be copied around all the time. We can't use a reference here as Heli::new() above needs to keep the passed 'reg'. Also note that the issue here is not that 'reg' was passed to Heli:new() and used afterwards by Heli::hover() afterwards but the fact that we tried to use 'reg' after we have given its ownership to Heli instance through Heli::new().

I realize that the above code doesn't make use of borrowing but if we were to make use of that, we'll have to declare lifetimes for the 'reg' field and the code still won't work because we want to keep the 'reg' in our Heli struct. There is a better solution here:

Rc


use std::rc::Rc;                                                                                         

struct Heli {
    reg: Rc<String>
}

impl Heli {
    fn new(reg: Rc<String>) -> Heli {
        Heli { reg: reg }
    }

    fn hover(& self) {
        println!("{} is hovering", self.reg);
    }
}

fn main() { 
    let reg = Rc::new("G-HONI".to_string());
    let heli = Heli::new(reg.clone());

    println!("Registration {}", reg);
    heli.hover();
}

This code builds and runs successfully. Rc stands for "Reference Counted" so by putting data into this generic container, adds reference counting to the data in question. Note that while you had to explicitly call clone() method of Rc to increment its refcount, you don't need to do anything to decrease the refcount. Each time an Rc reference goes out of scope, the reference is decremented automatically and when it reaches 0, the container Rc and its contained data are freed.

Cool, Rc is super easy to use so we can just use it in all situations where we need shared ownership? Not quite! You can't use Rc to share data between threads. So this code won't compile:

use std::rc::Rc;                                                                                         
use std::thread;

struct Heli {
    reg: Rc<String>
}

impl Heli {
    fn new(reg: Rc<String>) -> Heli {
        Heli { reg: reg }
    }

    fn hover(& self) {
        println!("{} is hovering", self.reg);
    }
}

fn main() { 
    let reg = Rc::new("G-HONI".to_string());
    let heli = Heli::new(reg.clone());
    
    let t = thread::spawn(move || {
        heli.hover();
    });
    println!("Registration {}", reg);

    t.join().unwrap();
}

It results in:

error[E0277]: the trait bound `std::rc::Rc<std::string::String>: std::marker::Send` is not satisfied in `[closure@sample5.rs:22:27: 24:6 heli:Heli]`
  --> sample5.rs:22:13
   |
22 |     let t = thread::spawn(move || {
   |             ^^^^^^^^^^^^^ within `[closure@sample5.rs:22:27: 24:6 heli:Heli]`, the trait `std::marker::Send` is not implemented for `std::rc::Rc<std::string::String>`
   |
   = note: `std::rc::Rc<std::string::String>` cannot be sent between threads safely
   = note: required because it appears within the type `Heli`
   = note: required because it appears within the type `[closure@sample5.rs:22:27: 24:6 heli:Heli]`
   = note: required by `std::thread::spawn`

The issue here is that to be able to share data between more than one threads, the data must be of a type that implements Send trait. However not only implementing Send for all types would be very impractical solution, there is also performance penalties associated with implementing Send (which is why Rc doesn't implement Send).

Introducing Arc


Arc stands for Atomic Reference Counting and it's the thread-safe sibling of Rc.

use std::sync::Arc;                                                                                      
use std::thread;

struct Heli {
    reg: Arc<String>
}

impl Heli {
    fn new(reg: Arc<String>) -> Heli {
        Heli { reg: reg }
    }

    fn hover(& self) {
        println!("{} is hovering", self.reg);
    }
}

fn main() {
    let reg = Arc::new("G-HONI".to_string());
    let heli = Heli::new(reg.clone());

    let t = thread::spawn(move || {
        heli.hover();
    });
    println!("Registration {}", reg);

    t.join().unwrap();
}

This one works and the only difference is that we used Arc instead of Rc. Cool, so now we have a very efficient by thread-unsafe way to share data between different parts of the code but also a thread-safe mechanism as well. We're done then? Not quite! This code won't work:

use std::sync::Arc;                                                                                      
use std::thread;

struct Heli {
    reg: Arc<String>,
    status: Arc<String>
}

impl Heli {
    fn new(reg: Arc<String>, status: Arc<String>) -> Heli {
        Heli { reg: reg,
               status: status }
    }

    fn hover(& self) {
        self.status.clear();
        self.status.push_str("hovering");
        println!("{} is {}", self.reg, self.status);
    }
}   

fn main() { 
    let reg = Arc::new("G-HONI".to_string());
    let status = Arc::new("".to_string());
    let mut heli = Heli::new(reg.clone(), status.clone());

    let t = thread::spawn(move || {
        heli.hover();
    });
    println!("main: {} is {}", reg, status);

    t.join().unwrap();
}

This gives us two errors:

error: cannot borrow immutable borrowed content as mutable
  --> sample7.rs:16:9
   |
16 |         self.status.clear();
   |         ^^^^^^^^^^^ cannot borrow as mutable

error: cannot borrow immutable borrowed content as mutable
  --> sample7.rs:17:9
   |
17 |         self.status.push_str("hovering");
   |         ^^^^^^^^^^^ cannot borrow as mutable

The issue is that Arc is unable to handle mutation of data from difference threads and hence doesn't give you mutable reference to contained data.

Mutex


For sharing mutable data between threads, you need another type in combination with Arc: Mutex. Let's make the above code work:

use std::sync::Arc;                                                                                      
use std::sync::Mutex;
use std::thread;

struct Heli {
    reg: Arc<String>,
    status: Arc<Mutex<String>>
}

impl Heli {
    fn new(reg: Arc<String>, status: Arc<Mutex<String>>) -> Heli {
        Heli { reg: reg,
               status: status }
    }

    fn hover(& self) {
        let mut status = self.status.lock().unwrap();
        status.clear();
        status.push_str("hovering");
        println!("thread: {} is {}", self.reg, status.as_str());
    }
}
    
fn main() {
    let reg = Arc::new("G-HONI".to_string());
    let status = Arc::new(Mutex::new("".to_string()));
    let heli = Heli::new(reg.clone(), status.clone());

    let t = thread::spawn(move || {
        heli.hover();
    });

    println!("main: {} is {}", reg, status.lock().unwrap().as_str());

    t.join().unwrap();
}

This code will work. Notice how you don't have to explicitly unlock the mutex after using. Rust is all about scopes. When the unlocked value goes out of the scope, mutex is automatically unlocked.

Other container types


Mutexes are rather expensive and sometimes you have shared date between threads but not all threads are mutating it (all the time) and that's where RwLock becomes useful. I won't go into details here but it's almost identical to Mutex, except that threads can take read-only locks and since it's possible to safely share non-mutable state between threads, it's a lot more efficient than threads locking other threads each time they access the data.

Another container types I didn't mention above, is Box. The basic use of Box is that it's a very generic and simple way of allocating data on the heap. It's typically used to turn an unsized type into a sized type. The module documentation has a simple example on that.

What about lifetimes


One of my colleagues who had had some experience with Rust was surprised that I didn't cover lifetimes in my talk. Firstly, I think it deserves a separate talk of it's own. Secondly, if you make clever use of the container types available to you and described above, most often you don't have to deal with lifetimes. Thirdly, lifetimes is Rust is something that I still struggle with, each time I have to deal with it so I feel a bit unqualified to teach others about how they work.

The end


I hope you find some of the information above useful. If you are looking for other resources on learning Rust, the Rust book is currently your best bet. I am still a newbie at Rust so if you see some mistakes in this post, please do let me know in the comments section.

Happy safe hacking!

Comments

Adrien Plazas said…
Thanks, this will be useful to me. :)
Paolo Bonzini said…
That's really clear, thanks. Just a question: when you wrote "let mut status = self.status.lock().unwrap();", I thought the mutex would get unlocked immediately after "unwrap()" finishes, because "lock()" returns a MutexGuard and the only reference to the MutexGuard is unreachable after "unwrap()" finishes.

Why am I wrong? And where is "unwrap()" defined on MutexGuard?
Anonymous said…
Is that a typo at the end of the "Mutex" section: "When the unlocked value goes out of the scope, mutex is automatically unlocked."

The first unlocked should probably read locked, or am I missing something?
zeenix said…
Paolo, Mutex::lock() returns a LockResult> rather than the MutexGuard and unwrap() is a method of Result (LockResult is just a specific type of Result). Related docs:

https://doc.rust-lang.org/std/sync/struct.Mutex.html#method.lock
https://doc.rust-lang.org/std/sync/type.LockResult.html
https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap
zeenix said…
Anonymous, thanks! Yes that's a typo.
Paolo Bonzini said…
Oh, I was missing the automatic deref (MutexGuard implements Deref so it can be treated as a String). The syntax/semantics of automatic deref is still a bit mysterious to me...

Popular posts from this blog

Welcome to the virtual world!

clutter-gst

zbus and Implementing Async Rust API