Rust – What made it “click” for me (Ownership & memory internals)

This is aimed for people who are coming to Rust from garbage collected languages, such as Python or JavaScript and have trouble with the compiler throwing errors without reason. This guide assumes you already know some Rust.

For those like me that worked with non-GC languages (C, C++) the borrow checker still feels hard to understand at first, but the learning curve is quite less and the documentation and design makes quite a lot of sense.

So I want to add some basic context for those that never managed manually requesting and freeing memory such that the underlying design in Rust ownership system makes more sense and clicks. Because this is all it takes, once it clicks, everything seems to fall together. (If you already know C or C++, you’ll already know most of the stuff I’m going to talk about, and probably notice that I’m oversimplifying a lot; but it might be worth the read as it contains how Rust “clicked” for me)

First of all I want you to consider a small snippet of code in Rust:

let a: String = "Hello world".to_string();
let b: String = a;

Here’s the question: What is the final value for “a” and “b” variables?

Think about it.

Usually in GC languages assignment can do two things depending on the types involved. For basic types (i.e. numeric) the value inside ‘a’ is usually copied into ‘b’, so you end with two variables with the same content. For complex types it is common to instead of copying the data just make “b” point to “a”, so internally they share the same data.

But this is not the case for Rust. In the snippet above “a” is moved into “b”, and “a” is freed afterwards. It does not exist anymore and does not hold any value. Puzzled? So was I. But don’t worry, this will make sense at the end.

By default Rust will move the data instead of copying it. This means that it will copy byte by byte from one place to the new one and then it will remove the original copy, returning the memory to the operating system.

I know. This feels stupid, but it comes from the ownership rules in Rust. It also has its special cases, for example, for simple types it will copy the contents but the original variable will not be freed. I will cover this later on.

Basic overview of a program’s memory

As said earlier, C++ developers have an advantage on understanding Rust because they have a mental model of what the memory looks like, pointers and so on. We need to cover some ground here to get proper understanding, but I don’t want to get in-depth, so I’ll try to get it as simple as possible.

But let’s be fair: this is going to be more in-depth than most people would like to be. I’m sorry, but I think this is needed for later.

Memory pointers

Have you thought about how the values are actually stored in memory? How does it organize different things as integers, floating points and strings? How can the program differentiate between two variables?

Memory can be thought as a list of bytes of roughly 264 items:

let mem: Vec<u8> = vec![0; 18_000_000_000_000_000_000];

(Note: This Rust syntax just creates a Vector of unsigned numbers, 8 bit each, with a size of 18,000,000,000,000,000,000 elements, initialized with the value ‘0’ for all elements)

When you declare a variable, the program needs to decide where to put it in this list. The position of a variable in this list would represent the memory address. And of course you could store this index in the list in another variable, this is called a pointer and it’s represented by the ampersand (&). Memory addresses are usually represented in hexadecimal.

Let’s say we want the variable “a” to be stored at index 0x100, so we could create a pointer to “a”:

let p_a: &i32 = 0x100;

Now we could use the dereferencing operator (*) to read the contents of the memory at that address:

let a: i32 = *p_a;

In our imaginary example, “*p_a” will equate to “mem[p_a]” with a catch. Because memory is bytes and the variable is 4 bytes long, it needs to read all 4 indices, and would do something like this:

let a: i32 = mem[p_a] << 24 + mem[p_a + 1] << 16 
           + mem[p_a + 2] << 8 + mem[p_a + 3] << 0;

Notice that in this example we decided that the first byte will be mapped to the highest value of the integer while the last byte is the least value of it. This depends on the processor and for this case big-endian is assumed. Most consumer processors are little-endian and store the lower part of the value first.

All these nifty details are done under the hood by your programming language, including Rust, C and C++. Some of this is even done internally in your processor directly.

While we don’t need any of this to code Rust or C, a basic understanding here does help to understand the design decisions of programming languages, specially those without GC.

Now we have a variable of 4 bytes in memory. Where would we put another variable of 8 bytes? Consider this:

let p_a: &i32 = 0x100;  // original pointer
// ----
let p_b: &i64 = 0x102;
let p_c: &i64 = 0x0FA;
let p_d: &i64 = 0x106;

For these three positions (b,c,d), they all have problems as they’re meant to hold a 8 byte variable (i64 is 64bits long, which is 8 bytes). The previous variable actually spans from 0x100 to 0x103 (both included), so this means that p_b is overlapping two bytes. If this is done, changing “b” would change “a” and vice-versa, in a very strange manner.

The reverse happens for “c”, because it needs 8 bytes, it will span from 0x0FA to 0x101, also overlapping 2 bytes with “a”.

The last one, “d”, does not cause any overlap, and it would work. But the problem here is that it leaves a gap of a few bytes between “d” and “a”, which will be hard to fill later. Usually the compiler and the operating system want the values in memory packed together so they use less memory and don’t leave gaps, as gaps are almost impossible to fill.

Key points to remember:

  • Memory is a flat list of bytes. The compiler and processor takes this into account to be able to have anything bigger than a byte.
  • Pointers are used internally all over the place to make any program work.
  • Compilers must know how big is the data behind a pointer to memory in order to read/write it correctly.
  • Endianness (big or little) matters when handling memory manually byte by byte.

Virtual memory and initialization

Going back to the memory example:

let mem: Vec<u8> = vec![0; 18_000_000_000_000_000_000];

You might wonder why I decided to give this imaginary vector roughly 264 elements, almost 16 Exbibytes which is clearly above any quantity in RAM possible.

(Note: I keep saying “roughly” 264 bytes because some parts might be reserved)

Let me ask a different question. Do you think that the memory from other programs running would be in the same place? in such a way, a program would need to avoid colliding variables on the same memory as other programs to avoid corruption.

The answer is usually no, but it depends. Your program’s memory is isolated from other programs and you cannot see or touch their memory. In fact a pointer to 0x100 in different programs maps to different physical locations memory. This is because the Operating System provides a virtual memory layout. This virtual memory is usually 264 in size for 64 bit platforms, so each program technically has its own 16 EiBytes of memory available even on computers with 1 GiByte of RAM.

The “depends” part is because some OS/platforms do not provide this abstraction, and also because you might be running a program without an operating system at all. But since my guess is that you’re currently using a GC language, most likely you’re not interested at all in doing this, so we can assume that a program always works with virtual memory. So for now on we’ll be assuming that our program runs in virtual memory.

The next question to ask is the initial value in memory. Do you think it comes initialized at zero or some crap/past data? Actually it can be anything, this truly depends on the OS and its configuration. So never assume that the initial value is zero or that the initial value is crap data. Zeroing may be used to avoid leaking private information to other programs.

Most GC languages (like Go) will initialize memory at zero for you. And most non-GC languages (like C++ or Rust) will try to prevent you from reading uninitialized memory, which you have to do manually.

Rust can allow reading uninitialized memory, but only within “unsafe” blocks. This can be used to avoid initializing memory twice on complex algorithms. C, C++ and Rust trusts the programmer knows what they’re doing, but the difference with Rust is that regular Rust has all the safeguards in, whereas in C++ you need to be careful at all times. (I almost never use unsafe, and if you come from GC languages you should also avoid using it. All programs you’re used to do can be written without using unsafe)

Key points to remember:

  • Programs typically run in virtual memory, which is way larger than the installed RAM size.
  • Memory usually comes uninitialized, with random data on it. But some OS might initialize it for security reasons.
  • Programming languages either zero it for you or try to prevent you from reading uninitialized memory.

Memory allocation and deallocation

Last thing on memory and we’ll move to less theoretical topics. Let’s talk about allocating memory.

Before, we were assuming you could do something like this:

let p_a: &i32 = 0x100;
let a: i32 = *p_a;

This does not work in Rust. But the counterpart in C kind of does:

int &p_a = 0x100;
int a = *p_a;

Rust will not let us manipulate memory directly unless we’re using unsafe code. I’m no expert on unsafe, so I’m not even going to try that. The point here is that the equivalent C code, even if it compiles, doesn’t work.

The reason is that “p_a” points to unallocated memory, which is different from uninitialized memory. A program cannot access any point in memory unless it’s allocated by the OS. To do this it needs to call the OS to request memory. In C this is done by the alloc() function family, where the most known one is malloc().

When a program requests memory, it doesn’t ask for a particular point in memory, but for a specific size instead:

int &p_a = (int*) malloc(sizeof(int));

So the memory address is chosen by the operating system (or the allocator), and the program has no influence over it.

Now we can use that memory, read and write into it. Just remember that until you write on it, the contents are undefined and platform dependent.

When we finish with that memory and we no longer need it we should free it; basically we should tell the OS that we’re finished with it so they can reuse that chunk for other programs. If we keep allocating but we never free our program would have a memory leak and will keep growing in size.

In Rust we don’t need to worry about allocating and freeing memory as it’s managed for us. There’s no risk of a memory leak, except for recursive data structures (depending on implementation) for which Rust has the same risks as any GC language.

This directly contrasts with C where you need to manually allocate and free the memory properly.

In C freeing memory looks like this:

free(p_a);

And in Rust would be:

drop(a);

As said before, in Rust you don’t need to worry about this. But if you want to release memory early, you can. This is commonly used to invoke the destructor early rather than to actually free memory (it’s something useful when handling locks between threads).

Other common pitfalls in C with freeing memory are the use-after-free and double-free. The names are self explanatory: If you free something and then use it (read or write), it’s an error. If you free something twice, it’s an error.

Key points to remember:

  • Internally, memory needs to be allocated and freed from/to the operating system.
  • A program cannot choose which address will be allocated.
  • Freeing is important to avoid memory leaks, but this is handled by Rust.
  • Forget that unsafe exists in Rust. Regular Rust is enough for anything you can imagine in a GC language. Leave unsafe for the experts (which is definitely not me).

Stack and Heap

Over this whole section I addressed only dynamic memory allocation with manual malloc and free, which is for heap memory. There’s also the stack for which is managed even in C. For what I want to explain I don’t think we need to really understand what the stack or heap is or what are the differences. 

Rust extends on that approach of automatic allocation and deallocation based on scopes to avoid having a GC.

If you’re confused about stack and heap, and which should you use, let me say that you don’t need to care about this at all. If you’re interested, it’s a great topic, but same as you don’t care in Python or Go, you also don’t need to care in Rust.

Rust will place some stuff on the heap and the majority of variables into the stack. For example, Box<T> places stuff into the heap.

In simple terms, the stack refers to the variables that are tied to a specific code block (between some braces), while the heap refers to dynamically allocated memory.

Key points to remember:

  • Stop worrying about stack or heap and move on.

Composite types in memory

Now that I already gave you a headache with all that stupid stuff about memory internals that no one cares about, we can begin to understand how objects are layed out internally. This will come in handy in the next part.

What objects actually are

In C++ we could do something like this to create an object:

class Card {
  public:
    int number;
    int suit;
};
int main() {
  Card aceOfSpades;
  aceOfSpades.number = 1;
  aceOfSpades.suit = 4;
}

In Rust this would be:

pub struct Card {
   pub number: i64,
   pub suit: i64,
}
fn main() {
    let ace_of_spades = Card {
        number: 1,
        suit: 4,
    };
}

If you’re wondering why I’m using C++ as a reference and not PHP, Javascript or any other “simple” language, the reason is that C++ shares syntax with all those so you should be familiar enough to be able to read it. But those languages don’t map exactly into memory as C++ or Rust does, so to be as correct as possible, I prefer to use C++ as an example. And you usually need types to get something that can be mapped onto memory. 

So, if you come from Python and don’t have any other language to leverage, the best I can come out with is with typed Python:

class Card:
    number: int
    suit: int

ace_of_spades = Card()
ace_of_spades.number = 1
ace_of_spades.suit = 4

Anyway, notice how most languages let you instantiate the object without specifying the contents, so you can write to them later. But not Rust, as it forces us to define the actual content values to instantiate the object.

Remember before when we were talking about uninitialized data? What is happening here is that C++ and Python (and mostly all languages) initialize the contents to zero implicitly when the object is created. So this means that we’re writing twice in order to define a particular value.

The worst problem in all other languages is not performance but lack of exhaustiveness: If you forgot to define the value of any member it will be silently set to zero, no warning. When adding a new field down the line it can be very hard to track all the places where it’s created and some bugs might appear because we forgot to set this new field to something. This simply can’t happen in Rust.

Back to the original topic. How is this layed out internally in memory? Simple, it uses 16 bytes where the first 8 are used for “number” and the latter 8 are used for “suit”. We get something like this:

(Note: I’m using big-endian above. Most computers are little-endian and would write 0x0100000000000000 and 0x0400000000000000 instead. Unless you want to manipulate bytes manually, you don’t need to worry about endianness.)

So far so good, right? This object has a known size at compile time of 16 bytes. If the compiler has an address to a Card, it knows it’s 16 bytes long and knows that the suit is 8 bytes to the right.

This means that:

&ace_of_spades.suit == (&ace_of_spades + 8)

(Note: Be aware that some compilers, specially Rust, has the right to reorder the fields in memory so it’s not guaranteed that the second field will appear after the first one)

Now let’s talk about strings. How big is a String type? If the compiler has a memory address to a string, how many bytes it has to read?

The problem here is that strings can be of any size. From empty strings to full books inside a single variable. It’s not possible to always know the size of a string at compile time. How does it store them?

In C, strings were basically unspecified in length but terminated by 0x00 (or ‘\0’) so all functions had to keep reading until they found this character. This has an obvious downside: if your string contains the ASCII character ‘\0’ in the middle, you cannot read it completely.

In Rust, the String type is basically a pointer to somewhere else and a length:

pub struct String {
  buf: *mut u8,
  len: usize,
}

(Note: Actually, String in Rust is just a Vec<u8>, but Vec itself is using something similar as above; also here I left out the capacity which would add another 8 bytes to the space used)

This makes the String type sized, and in 64 bit platforms this will be 16 bytes long. Regardless of the string contents, the String object is always 16 bytes.

It doesn’t mean that all strings would use only 16 bytes. Obviously the memory required to hold the text is still used, but it’s allocated elsewhere. Compared to C strings, they don’t have any of their downsides, but the problem with this approach is that instead of wasting 1 byte per string, it wastes 16 bytes.

What about the methods? Are they in memory as well? Yes, code is also in memory, but this code is only once in memory and not copied per each object instantiation. It doesn’t matter how many methods a type has, the object has the same size.

Key points to remember:

  • Objects are usually laid out in memory by just concatenating the fields.
  • Objects may contain memory addresses (pointers).
  • Methods do not use space in the object.

Ownership

The most important Rust concept is probably ownership, what does this mean?

Imagine we’re passing data through several functions. Where should the data be allocated and where it should be freed? A garbage collector will wait until no part of code has access or pointers to that data and then proceed to freeing it. This is done while the program is running and does cost cycles. Go is known for having “pauses” where the GC runs.

But C and C++ have a cool way of handling this by leveraging the stack and the scopes. For example, in this code:

int main() {
  Card aceOfSpades;
  aceOfSpades.number = 1;
  aceOfSpades.suit = 4;
}

The variable aceOfSpades is allocated by C++ automatically and when it exits the scope, it is freed also automatically. Cool, right? For such simple cases the memory is managed for us.

It would be nice if this system could be extended to all other cases, when data is passed over other functions and methods, because sometimes data needs to be dropped in an inner function. It would be awesome if a function could know beforehand if it should free after using the data; but the problem is, if a function frees some data and some caller expects that this data still exists, we would get a use-after-free error.

It is hard in C++ to follow this semantic properly across the project; some might use some naming schema for functions to convey this, others might just workaround this by copying/cloning the data instead of passing pointers to avoid the risk.

In Rust, this is enforced by the compiler as it tracks “who owns the data”, and the owner has the duty of freeing the data. Obviously there can be only one owner at any point in time, because if not you’ll get double-free errors.

When a function creates some data, it becomes the owner of this data:

fn main() {
    let ace_of_spades = Card {
        number: 1,
        suit: 4,
    };
}

In here, main owns ace_of_spades and it’s responsible for freeing this data at the end. Therefore, there’s an implicit “drop(ace_of_spades)” at the end of the code block.

So far this is the same. But here’s the cool part in Rust: Ownership can be transferred:

fn main() {
   let ace_of_spades = Card {
       number: 1,
       suit: 1,
   };
   print_card(ace_of_spades);
}
fn print_card(card: Card) {
   println!("Number: {} Suit: {}", card.number, card.suit);
}

Now in the above code print_card receives the ownership of Card and will drop its contents at the end of the function. This means that main() now does not free the memory for ace_of_spades anymore.

But wait, how does Rust know that print_card will drop the data at the end? Because it takes the type “Card” instead of “&Card” or “&mut Card”. Whenever you see a full type in Rust without the ampersand, the value is owned.

For &Card and &mut Card what your function owns is the pointer to the memory address, but not the contents. In the same fashion, for things like Rc<T> or Box<T> the function owns the outer Rc or Box, and the behavior on the inner value depends on the actual type used.

What if we don’t want the type to be dropped? There are two solutions: one is to change print_card to receive a borrowed object (similar to a pointer), while the other way would be to copy the data before sending. The latter is what C++ does under the hood:

   print_card(ace_of_spades.clone());

In Rust we would need to implement how cloning works, but we could also just use “#[derive(Clone)]” on our struct to add the default implementation that just copies everything as-is.

So we can see that Rust by default “moves” the data, while C++ by default copies it.

If we wanted to change the print_card function to avoid freeing inside, it would be just adding an ampersand:

fn print_card(card: &Card) {

That tells the compiler that this function will not own that data and cannot free it. “card” is then treated as a pointer to a “Card” address and dropping the variable will drop only the pointer, not the underlying data.

The remaining function code doesn’t need any change. In C++ you’ll need to change the dot with an arrow to dereference the pointer, while Rust is smart enough to do this for you. Nice! you don’t need to think about dereferencing pointers in Rust.

(Note: “dereferencing” is to apply the asterisk operator “*ptr” where we signal that we want to operate on the contents of what the pointer is pointing to, instead of trying to work on the pointer itself as a variable)

But there is another line that needs to be changed, the call to print_card. As we want to retain ownership, we need to signal this to the compiler as well by adding an ampersand:

   print_card(&ace_of_spades);

By these simple rules Rust can always make a clear cut decision on how memory is allocated and freed, so we don’t have to worry about it. Instead, we need to worry about ownership and borrowing, but these are checked by the compiler and there’s no way to fool it. It is guaranteed that a program that compiles is memory safe and sound.

Could we instead just return the Card object to avoid freeing it? Sure! Let’s see:

fn main() {
    let mut ace_of_spades = Card {
        number: 1,
        suit: 1,
    };
    ace_of_spades = print_card(ace_of_spades);
}

fn print_card(card: Card) -> Card {
    println!("Number: {} Suit: {}", card.number, card.suit);
    return card;
}

This does the trick, “card” is no longer freed on the print_card function while retaining ownership. But this is not a good idea. First, we depend on the compiler to be smart enough to avoid moving the data twice. And second, this is almost an anti-pattern on Rust, it tends to cause more trouble than it solves. As a rule of thumb, if you don’t want the function to consume the data (to free it), don’t ask for ownership.

Ownership and borrowing can be thought as a permission system:

  • The owner has the highest permission. It can do everything it pleases with the data, because it’s their data. If I have my own car, I can do what I please with it, including disposing of it. There is always an owner, as the car needs to be registered to someone.
  • The &mut borrow comes next. It can change the contents of the data because the owner allowed them to do so. On a car, this is the mechanic where they can add/remove stuff from it, but they definitely can’t dispose of it. The car can only be on one mechanic at a time, and while the car is on the mechanic the owner cannot use it. In the same sense, while a &mut exists, the owner must wait until it finishes to be able to use it.
  • Finally the & borrow is shared read-only. It’s like when you allow your friends to see your car and take photos of it. There can be many people doing this at the same time, but while this happens you cannot send the car to the mechanic or dispose of it.

One important thing to remember is that others can make copies of the borrowed data. So for example, if in a function that it uses a shared borrow (&Card), if it needs to change the data it could copy it and then do the pertinent modifications.

For example, consider this function:

fn next_card(card: &Card) -> Card {
    let mut next = card.clone();
    next.number += 1;
    return next;
}

This function receives a borrowed card, and cannot change it. But we want to be able to add one to the number. What do we do? We clone it, then we change our copy. We can return the new copy afterwards.

We could have avoided the copy by having a “&mut Card” instead, then we could have mutated the same data in-place.

The beauty of this system is that the developer knows right away if a function will change the contents of the data we’re passing or not. A function receiving “&Card” will never change its contents, and the caller can continue using it for other stuff assuming it never changed.

Key points to remember:

  • Regular types in Rust are always Owned unless “&” or “&mut” is written before the type.
  • Ownership means that memory will be freed when it exits the scope.
  • There is always one owner. Not less, not more. One.
  • Cloning or copying the data is what other languages implicitly do. Don’t be afraid to do it.
  • Borrowing is the right tool to share data between functions when we don’t want the data to be freed at the end.
  • When creating functions, try to use the least permissive borrow that works.
  • Even if you need to change the data, remember that you can always change a copy of the data. Your function might not require changing the original data.

Copyable types

In Rust, copying has a special meaning. You might have noticed that we talk about cloning and copying as if they were two different things.

Well, because they are. Copying in Rust strictly means implicit byte by byte copying, while cloning is customizable and explicit.

Let’s forget about cloning for now and focus on just copying. Remember the byte representation of a Card struct we discussed before:

Copying this would mean that our program reads the bytes in memory and writes them elsewhere. For a second, it forgets that this is a “Card”. It just knows that it has 16 bytes of data so it does a copy-paste elsewhere.

The new data will have its owner which might be different from the old one. And as discussed before, this copy might happen from read only borrows. You don’t need ownership to be able to copy data.

Now, a question: Would this copying always work? Will the resulting data be correct?

Think about this for a second.

For regular values such as numbers and characters, it does work, no problem. 

But what about memory addresses? What would happen if it contained a pointer to somewhere else?

If the pointer is a shared read-only borrow, this will work out no problem. As there can be as many readers as we like, copying it works. When copying the pointer’s address, it still points to the same position so it must work.

For mutable borrows the only problem is that we would break the rules as there will be more than one pointer to the same address. Other than that, this would work: the pointer is still valid. But Rust will not let you copy a struct containing a &mut pointer to prevent you from breaking the rules.

Therefore there is data that can be copied and data that cannot be copied.

But wait! There is another possibility. The pointer might be something that is actually owned by the struct!

Remember the String implementation from before?

pub struct String {
  buf: *mut u8,
  len: usize,
}

This “buf” is actually owned by the String: when you create a new string, memory is also reserved for the buffer; and when it’s dropped, the contents buffer must be freed as well.

You might be asking now, is this some kind of trickery that only the Rust internals can do? Can we do the same in our Rust structs?

Yes! This is done by the type Box<T>. This stores a pointer to another datatype (or the same if you want to do something recursive) and the underlying data is owned by the same struct. For example:

pub struct HandOfCards {
    card1: Box<Card>,
    card2: Box<Card>,
    card3: Box<Card>,
    card4: Box<Card>,
}

This would make HandOfCards contain 4 pointers to 4 different cards. (Be aware that this implementation does not make sense in real code. I wrote this just to show Box<T>, but in this case it just wastes memory with no benefit.)

In this case, the memory of those “Card” needs to be allocated before creating HandOfCards, but it will be freed automatically when it exits the scope, as usual.

If we wanted to store an indeterminate amount of cards we could use Vec<Card> instead. Vec is similar to Box in the sense that stores a pointer to somewhere else, and when Vec is freed, the contents of that pointer are dropped as well.

Back to copying. If we try to copy those structs byte by byte the problem is that we will copy the pointer, but not the internal data; therefore that data will have now two owners, and not only this breaks the rules, it also will create a double free at some point; because once all the copies are dropped, the internal Box<T> will be freed more than once.

And this is the real reason why not all types can be copied byte by byte. In some cases it will create a double free error.

Which types are copyable is something defined by the Copy Trait. If the struct implements Copy, it is copyable. As easy as this:

impl Copy for Card {}

And now our struct Card is copyable. No need to explain Rust how to do this, or implement any method, as there is only one way of doing this. Usually we implement copy using the derive macro instead; this is just convenience, the macro just writes the code above:

#[derive(Copy)]

But as I said, not all types can be Copy. For example if we try the same for HandOfCards, this happens:

error[E0204]: the trait `Copy` may not be implemented for this type
   --> src/main.rs:234:6
    |
228 |     card1: Box<Card>,
    |     ---------------- this field does not implement `Copy`
...
234 | impl Copy for HandOfCards {}
    |      ^^^^

Because Box<T> does not implement Copy, a struct containing Box<T> can’t implement Copy either.

Turns out that it’s much easier to work with types that implement Copy than with types that don’t. For example, the following code does work only if Card implements Copy:

fn main() {
    let ace_of_spades = Card {
        number: 1,
        suit: 1,
    };
    print_card(ace_of_spades);
    print_card(ace_of_spades);
}

If it doesn’t implement Copy, the first print_card consumes ace_of_spades and it no longer exists when the second call is done. This program would not compile unless the Copy trait is implemented. When this is the case, ace_of_spades is copied for each of the calls, similar to what C++ does.

Key points to remember:

  • Copy in Rust means a byte by byte copying, without understanding the contents of the type.
  • Memory addresses can prevent Copy from working correctly, therefore some of their uses in a struct will forbid it from being copyable.
  • Shared read-only borrows (&var) are fine for copy but mutable ones (&mut) are not.
  • Box<T> can be used to have a pointer to owned data, but this also prevents the struct from implementing Copy.
  • Remember to implement Copy if possible. This will make your life easier.

Exceptions on copying and cloning everything

I know, I said to copy or clone everything and forget. But there are a few gotchas that we should cover about this. I lied a bit to make things easier.

First and foremost, C++ does not always clone stuff. Cloning is kind of a deep copy. For simple types it will copy them by default, but for complex ones it depends on the implementation. So take that with a pinch of salt.

Cloning too much has its drawbacks, obviously this will waste cycles. For small things it will not make a difference, but if you clone big values of course it will take time. And of course this also depends on how many times your program does this clone. (i.e. it’s not the same to do 10 clones than doing a single clone in a for loop of 1 million iterations)

Same applies to implicit copies. It’s a bit harder to get a lot of data copied than cloned, but it’s definitely possible (for example with arrays [T]).

Passing borrowed values to functions (&T or &mut T) instead of the value (T) will help preventing unnecessary copies.

Always copying/cloning can also lead to disappearing changes: you might accidentally write to a different copy than the one you intended to. Using a &mut reference or a Rc<T> could help.

Finally, implementing Copy on some types might be a bad idea as it could end with unintended behavior. For example, Iterators are not expected to implement Copy, as you could end with different copies of the iterators instead of consistently using the first one created. Deliberately not implementing Copy is a way for the author to convey how the type is intended to be used.

The main reason for me to insist on cloning being “good” is that I had a hard time when I started on Rc<T> types (kinda like garbage collected). Once I got the hang of cloning, it was much easier, and it turned out that Rc<T> (and other similar types) is meant to be cloned. Cloning is not bad unless there’s a lot of data to clone, and even if there is, it can be still fine when used in non performance critical parts of the program.

Move semantics

In Rust, all types are “move”, which means that creating a copy of the data must be valid as long as the initial version is destroyed. So, in this fashion I should be able to get any type and change its memory address from 0x100 to 0x200 by copying the data and removing the original and it should work.

This, of course, only works if there are no pointers to the initial data; this means that there are no borrows, either immutable or mutable. In the end, what this tells us is that ownership is required to move the data.

With one exception. Rust has std::mem::swap which accepts two &mut pointers and this moves the data. Because the contents are switched with another instance of the same type, this must be valid as well.

Now, does this work for every data type? Can we put anything we want in a struct and this trick still works?

Almost. There is one case where this fails completely. (I kind of hate that there are so many exceptions to the rule in programming)

If you build a self-referential struct, this fails. Let me explain.

Imagine you want a struct where it owns some data, but exposes a read-only buffer to it via a member; the point of this could be just preventing others from modifying it without going through the controlled methods:

struct MyData {
    buf: Vec<u8>,
    pub buffer: &Vec<u8>, // This must always point to &buf
}

This struct will break when moved, because *buffer will point to the old place whenever this moves and changes the memory address of MyData. Remember, “MyData.buf” address is just “&MyData+0”, so it depends on where the struct is placed in memory.

Because of this reason, Rust will not allow you to safely build self-referential structs. It is forbidden via the lifetime of the borrow, as you’ll need it to exist for the lifetime of the struct, and it’s not possible to tie these two in this way.

(The solution for the above code is to return the borrowed pointer in a method, in this way you don’t need it to be stored; But in real code this happens in very contrived scenarios that might not have an easy solution)

In my way of learning Rust I tried several times to create a self-referential one without noticing and I ended fighting lifetimes with the borrow checker for hours until I gave up. It’s directly impossible, because this breaks move semantics but Rust will blame the lifetimes because it doesn’t understand that this is self-referential. And it doesn’t understand these because it can’t support them without unsafe code.

Did I say unsafe? Can this be built on unsafe code? Oh, yes, it can. But it will break in crazy ways every time it is used. Rust will move the contents in memory often without warning as it is implicit. It would be really hard to use them without making a mess.

So, are they really impossible to do in Rust in a correct way? Not quite. There’s something called Pin<T> for these purposes.

Let’s think a bit. If moving is only possible with ownership or a &mut reference, if we hide the variable under a type that doesn’t allow these, the inner variable is guaranteed to not move in memory. The outer type can expose its own memory address that can be moved freely, but the underlying pointer is fixed in memory. This basically means that only &T borrows are allowed.

This is exactly what Pin<T> does. But to use Pin<T> to effectively make self-referential structs still requires unsafe. This is because, again, Rust does not have tooling to explain this to the compiler.

(Note: Docs might also point to a different reason, Pin<T> will not allow creation of Pin<T> where T is Unpin)

Ideally, you don’t want to make a self-referential struct in Rust. Instead you should leverage other  types to accomplish the same thing; for example Rc<T> can be used for things like linked-lists or trees. Also, have a look for libraries that might do this work for you, as these are very easy to do wrong.

It’s possible to encounter Pin<T> in Rust, just unlikely. The most common place is async programming where the future trait requires Pinning to implement. Pin<T> can be created without unsafe as long as the data follows move semantics; so for regular programming Pin<T> is just something that you might need to create or access. A bit of a burden, but that’s it.

As a side note, let me add that Rust does not guarantee that memory does not leak. Correct Rust programs will not leak, but it’s actually quite easy to make a program in Rust that leaks memory without unsafe. As with GC languages, if you have Rc<T> values in a cyclic reference, they will fail to free memory. And also there’s std::mem::forget which will remove a variable from scope, making it unreachable, but will not call destructors and will not free it; therefore causing a leak.

Key points to remember:

  • All types in Rust are “Move”, meaning they can change their memory address without problems.
  • mem::swap can be used with two &mut references to swap the contents of two variables. This is also considered a move.
  • Self-referential data structures cannot be made in Rust without unsafe, because they can’t be moved safely.
  • Self-referential data with unsafe still must use Pin<T> to ensure the data does not move around.
  • The recommendation is to avoid them entirely and use Rc<T> instead. If possible, use libraries and don’t implement trees or linked lists yourself.
  • You might have to work with Pin<T> when doing async programming.

Back to the beginning

Remember we got puzzled by this simple code? Have a look again:

let a: String = "Hello world".to_string();
let b: String = a;

Now if I ask you what’s happening here, it should be easier to reason. First this creates a new String type and stores it into “a”. Then, because String can’t be Copy (it must contain an owned reference to a buffer for the text), it can only be moved. Therefore Rust copies the contents of “a” into “b” and drops the memory for “a”. There’s no other way around to make this possible, so now it does make sense.

(Note: This case is too simplistic and it doesn’t make sense for Rust to move the data. It will instead point “b” to the address of “a”, and forget “a”. This is one of many optimizations inside Rust that will come into play to reduce the code produced to the absolute minimum)

You wanted to have two different copies? Sure!

let b: String = a.clone();

You wanted to have two variables pointing to the same thing, so it doesn’t use twice the memory? Sure!

let b = &a;

Once we understand what’s happening under the hood, the behavior becomes self evident, right?

If instead of a string we had a number, these are copyable, so in this case Rust will copy and not drop:

let a: i64 = 123;
let b: i64 = a;
dbg!(a,b); // This will now print both variables

Because “a” doesn’t need to be dropped, as it still is consistent, it remains valid after assigning, and we end with two variables with the same content.

I hope this helps understanding why Rust acts in the way it does. I know that lifetimes are still missing, but this was a bit too much already; maybe later I’ll write something about them. In the meantime, please let me know if it helped or any questions!

2 thoughts on “Rust – What made it “click” for me (Ownership & memory internals)”

  1. Good article!
    Some notes:
    1) int& in C++ is not a pointer, but a reference. If you do int& i = 8, you assign 8 to i and create a reference to it, not assigning 8 to a pointer. Similarly, int &p = (int*)malloc(sizeof(int)` is invalid. int* is a pointer (int* p = (int*)malloc(sizeof(int)), although we of course prefer new for C++.
    2) > What is happening here is that C++ and Python (and mostly all languages) initialize the contents to zero implicitly when the object is created.
    Wrong. C++ does not zero memory (except for static duration storage). Instead, it leaves uninitialized objects in an undefined them, where reading an uninitialized memory is an instant Undefined Behavior.
    Other languages also does “zero” memory in the simple meaning of the sentence. In JS uninitialized variables (or object properties) are defined to be `undefined`. In Go, primitives are zeroed and other values are initialized to `nil`. In Python, variables are initialized to `None` and object properties are created on-demand, so it’s an error to access them if they’re not initialized. Most of these languages will define the default value as zero (I think it’s even mentioned explicitly on the Go standard), but it’s more as an optimization, because it’s easier to zero memory than to set it to other value (for example, you can request an already-zeroed memory from most OSes).
    3) > There is always one owner. Not less, not more. One.
    Actually, there is a very interested case of zero owners: leaked memory. For example, one can use `Box::leak()` to create a mutable reference to a memory with zero owners. However, in the general sense, you’re right.
    4) > (for example with arrays [T])
    Pedantic note: arrays in Rust are `[T; N]`. `[T]` is a slice. Also, array _are_ `Copy` if their underlying type is. Slices are not (nor they are `Clone`, however they can be cloned into a `Vec`).
    5) > Always copying/cloning can also lead to disappearing changes: you might accidentally write to a different copy than the one you intended to. Using a &mut reference **or a Rc** could help.
    Another pedantic note: `Rc` alone will not help: it is a shared smart pointer to read-only memory. You need a `Rc<RefCell>`.
    6) > With one exception. Rust has std::mem::swap which accepts two &mut pointers and this moves the data. Because the contents are switched with another instance of the same type, this must be valid as well.
    _std::mem::swap()_ does not allow swapping if borrows exist, because you need `&mut` references for it to work, and you cannot get them if there are borrows. In fact, it relies on that: it would be very bad if this was allowed. Think about iterators, for instance: what if you’ll have an iterator to the middle of an 10,000 elements slice, and you replace it with an empty slice? You’ll reference an invalid address!
    7) > (Note: Docs might also point to a different reason, Pin will not allow creation of Pin where T is Unpin)
    Exactly the opposite! For types that are `Unpin`, you can create and manage `Pin` _completely safely_. OTOH, types that are `!Unpin` requires `unsafe` code.
    8) > for example Rc can be used for things like linked-lists or trees.
    `Rc` is not useful, neither for singly linked lists nor for trees. They don’t have cycles. It _can_ be useful for doubly linked lists and graphs, however that will require a lot of `std::rc::Weak`s and slow code. **`Rc` is for shared ownership, not for cycles.** Use crates for that.
    9) > It’s possible to encounter Pin in Rust, just unlikely. The most common place is async programming where the future trait requires Pinning to implement. Pin can be created without unsafe as long as the data follows move semantics; so for regular programming Pin is just something that you might need to create or access. A bit of a burden, but that’s it.
    It’s very unlikely that you’ll meet `Pin`, even in async code, unless you’re writing a low-level async library (so low-level that you’ll have to `impl Future` manually). Even then, there are crates like pin-project to help you with that.
    10) Last note: you said at least twice in the article that GCed languages may have data structures with cycles unrecycled. Although reference-counted languages definitely can, it’s in a great doubt if they even deserve being called “GCed”. Tracing GCs handle cycles just fine.
    After all of the notes, still, great work! Continue writing good articles like that!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s