Rust - Part 4

Written on
This article is for those who already have some experience with rust, and want to refresh the concepts.
All the articles from this series are condensed versions of Rust official book, this post covers Chapter 4 from the book.

 

4. Ownership

This is what enables rust to work without Garbage collection, or so they say, let's dive in.

4.1 What is Ownership

Heap and Stack:

Stack:

Remember stackoverflow, not the website, but the actual stackoverflow error - what it means is we don't have space to store another stack(function context) in the memory (probably due to recursion), hence stack-overflow error.  That's the stack we are referring to - it's the place where we store the function context, primitive data, and references to the data in the heap.  Stack is called a stack because it follows last in first out principle, meaning the last function we entered will be the first function we get out from, which kinda makes sense.  It's more orderly compared to the heap, we know the size of all the things we will be in initializing in size and more.  When we say we are allocating space we usually talk about allocating in heap, because as far as the stack is concerned we will know the sizes beforehand. 

Heap:

Remember all OutOfMemoryError Java heap space errors - that's the one we are talking about.  On the heap we put all the non-primitive data, it could be a class object or any complex data.  Allocating space in heap is slower than the stack because we first need to find a space, assign it, and give the stack the pointer to the data we've assigned.   All the -Xms -Xms is what we use to set space for the heap in Java.  Garbage collection only runs on the heap and not on the stack because in the stack as soon as the frame is dropped, we are good.  Though GC scans the stack to check what all the heap objects are not being used and destroys them. So as you can guess heap is not that efficient, and rust ownership aims to solve these problems.

 

Ownership:

I'm quoting these three rules from the website because I don't wanna interpret them yet, but these look straight forward and it makes sense.

 

String type:

We are going to be discussing ownership with String because other data types we have seen so far are primitive and will be handled in the stack, string will be handled in the heap, which makes it ideal for this discussion, but note, these ownership concepts will apply for all other complex data types.

//String literal, size is known at compile time. (I'm not sure if this goes into stack or heap, for now, will come back to this) - okay it looks like these will be hardcoded in the final executable
let s = "hello"; 

//String 'object' in java terms
let mut s = String::from("hello"); 
s.push_str(", world!");

 

Let's say we need to get a username from user, we won't know the size of the username that the user is going to give, so we can't put some empty space in the final executable, that's where this whole heap, dynamic allocation comes into part, usually, we create a new Object using the new keyword or similar keywords.  But we never worry about the deallocating in most languages, that's because that's exactly the job of the garbage collection.

So in a scope, if we create a variable and get to the end curly braces, Rust automatically calls a drop method, and that's where the memory is cleared.

 

//Here y will get a new copy, because it's primitive
let x = 5;
let y = x;

//Read below
let s1 = String::from("hello");
let s2 = s1;

While assigning s2 = s1, what I would expect is, s2 just receives a pointer to s1's data, and nothing more, but in reality, it's a little more complicated.

A string is made of three parts - 

Length and capacity are the same in this context but apparently, they are both different, and we'll discover about those later.

So what happens, all the 3 items are copied - not the actual data is copied, but the pointer to the data in the heap is copied.

So now we have 2 strings, pointing to the same memory location in the heap, which is okay, but when we reach the scope's end, we will try to clear the memory, and since both the string's point to the same heap location, we'll try to clear them twice, which is a problem and it has a name for it - double free error.

 

In order to prevent this, as soon as we assign s1 to s2, rust is gonna consider s1 to be invalid. Wait, what?!?!?!

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);//Error during compile time

This reassigning sounds like a shallow copy, but since s1 is not valid after that point, this should be seen as a move.  If we want to make a deep copy, we can use .clone method.

 

Remember how for primitive types, we didn't clone, but we still ended up with two copies, that's because of Copy trait(more about it later), and Copy trait cannot be used if we want to implement Drop trait as well, or in other words, if you want something special needs to be done, when a variable goes out of scope, you can't implement copy trait.

As a thumb rule, if it's primitive, it will have copy trait, and hence new copies will be created.

 

Functions and Returns:

Passing a value to a function is the same as assigning it to some other variable, so if you call a function with some variable, then you won't own that variable anymore, and hence can't use it.

Same goes for return value, ownership will be transferred once we return it.

fn takes_ownership(some_string: String) {
    println!("{}", some_string);
} //de initialized here or 'drop' method is called

fn makes_copy(some_integer: i32) {
    println!("{}", some_integer);
} // drop method called

fn takes_and_gives_back(a_string: String) -> String {
    a_string 
} //nothing to be dropped, because a_string ownership has been moved

As you can guess, every time we need to do some operations on a variable, we can't keep moving to the function and taking the ownership, it quickly becomes super tedious, that's where references come into play.

To be continued!!




Tags · Rust, Tech, Blog