Explaining How Memory Management in Rust Works by Comparing with JavaScript

When I first started learning Rust, one of the more confusing concepts was the way it managed memory. Rust, unlike many other languages, has a unique system of handling memory called ownership. To help demonstrate it, we’re going to look at a code snippet and compare how it runs in Rust vs. JavaScript.

The Code and How It Runs

First, here’s our JavaScript snippet:

function hello(name) {
  console.log("Hello " + name + "!")
}

let myName = "Casey"

hello(myName)
hello(myName)

It’s pretty clear what this will do. We’re just declaring a string and then passing it to a function that will print out a little message with the string embedded within it. We call this function twice, so our output will be:

Hello Casey!
Hello Casey!

Simple enough. Let’s go ahead and write some code that will do the same thing in Rust. Here’s what that looks like:

fn hello(name: String) {
    println!("Hello {}!", name);
}

fn main() {
    let my_name = String::from("Casey");
    hello(my_name);
    hello(my_name);
}

This looks like it should do the same thing. We’re declaring a variable, which is of type String, and then we’re passing it into a function that will print out the same message.

Running this code, we end up not with nice greeting messages, but instead this:

error[E0382]: use of moved value: `my_name`
 --> src/main.rs:8:11
  |
6 |     let my_name: String = String::from("Casey");
  |         ------- move occurs because `my_name` has type `String`, which does not implement the `Copy` trait
7 |     hello(my_name);
  |           ------- value moved here
8 |     hello(my_name);
  |           ^^^^^^^ value used here after move

Well, this doesn’t look very nice. What’s interesting about this is if we look at where the error occurred, we see it happened on line 8, during the second call to hello(). As an experiment, let’s comment out the second call to hello() and run it again. Here’s the result:

Hello Casey!

The Explanation

Why does our code only fail when we call the function twice? This is because our variable is no longer in scope after the first function call. The core of the concept of ownership in Rust is that every value can only have one variable associated with it. This variable is the value’s owner. When a value’s owner goes out of scope, the value gets dropped and the memory gets cleared up.

When we first create our value on line 6, the value’s owner is my_name. On line 7, when we call hello(my_name), the value gets taken away from my_name and given to the name parameter in hello. name is now the value’s owner. When the function ends on line 3, name goes out of scope, and the value gets dropped. By the time we get to the second hello() call, my_name is no longer the owner. The value no longer exists, and so the function call fails.

Time for a curveball: I’m using a specific type of string, String, in these examples. String is a type whose size we may not know at runtime. Because of that, we need to make sure we allocate as much memory as it might need, and then we need to be able to handle the memory getting deallocated. This deallocation happens when the owner goes out of scope.

However, had I used a different type of string, &str, the above code actually would have worked. The compiler knows the amount of memory required for &str values at compile time, which means that they can’t grow in size the way a String can. So we can easily pass them around as references. This is also the case for other values whose size we know at compile time, like integers.

Put more simply, when we use &str, instead of the name parameter in hello taking ownership of the value, it merely borrows it. Go ahead and try out the Rust snippet, but change the parameter type of name to &str and change line 6 to let my_name = "your name here";.

Rust’s Unique System of Ownership

Rust’s system of ownership and memory management takes some getting used to. There’s no other language that I’m aware of that uses something similar. Once you get the hang of it, though, you’ll find that the code you write will be fast, efficient, and much freer of mystery memory errors that occur in other languages.

Conversation
  • niilz says:

    Hi Casey,

    very nice post. Ownership is indeed something one has to get used to. I still do.
    If I may I would like to mention though, that the reason, why the first Rust-function does not compile is not so much because `String` has an unknown size. In fact, `String` is `Sized`, otherwise the function would reject it.
    You could try to have a function of:
    “`
    fn takes_unsized(string_slice: str) { … }
    “`
    This would NOT compile because Rust-functions implicitly require their arguments to be `Sized` and `str` (not be mistaken with `&str`) does not implement `Sized`.

    On the other hand, if your function would take a `&String` you could also call the function twice, because the function only takes ownership of the reference (the pointer) to the underlying `String`.
    But it is indeed the idiomatic way to then take a reference to a string slice, which is `&str`, as you described.

    Why is String sized? Because it is actually a `Vec` of characters. And vectors in Rust live on the heap, so does `String` as a consequence. On the stack we then work with a type that merely points to that heap-value. And that type does not change it’s size.

    Nevertheless, if you hand over the type itself (`String`) and not as a reference (`&String` or &`str`) the value goes out of scope at the end of the function and can not be used again. Exactly as you described.

    I hope this is does make stuff more confusing.

    Looking forward to reading more posts. The comparison style is cool.

    Cheers and späters

    niilz

  • Comments are closed.