Understanding rust lang - Ownership

May 27, 2020

Intro

One of the great features of rust is ownership. Let's start with some basics.

Programming languages manage computers memory. In particular during execution, a program may:

  • Request memory to use. This process is called allocation
  • Free no longer needed memory

There is also one more rule: system only gives back memory that is free. In lower level programming languages, programmer has to manage memory - explicitly allocate or free memory. Second approach to managing memory in programming languages is that some of them have garbage collector(GC). GC automatically manages memory, so programmer doesn't have to.

Rust language uses different approach memory is managed through a system of ownership with a set of rules that the compiler checks at compile time.

Rules of ownership

As we can read in rust book there are some rules:

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Stack and a Heap

In order describe how memory is allocated in Rust, we need to explain what stack, and a heap is. These terms are already explainted in rust book, but I created simple graphic to explain features of each of them in a layman's terms. I feel that, we don't need to dive deep into the bare metal. For now keep in mind that stack, and a heap are part of computers memory. Each of them have its own features that programmer has to recognize to make its programm efficient.

img

Examples

So since, only particular type of data can be put on stack or heap, lets see some examples:

//STACK - fixed size data
let stack_letter: char = 'l';
let stack_boolean: bool = true;
let stack_integer: i8 = 3;
let stack_float: f32 = 32.3;

//HEAP - data that can grow/shrink
let heap_vector: Vec<i8> = Vec::new();
let heap_string: String = String::from("hello");

We can explicitly move memory around stack and heap like this through Box

let stack_u8: u8 = 3; // here value is allocated on the stack
let boxed_u8: Box<u8> = Box::new(stack_u8); // here we explicity put this value in Box which is pointer type for heap allocation.

Owning memory

Let's see how to handle memory correctly knowing rules of ownership. In particular, we'll handle situation when memory goes out of the scope.

#[allow(unused_variables)] // place this so we don't get warning when running code
fn main() {
    let stack_u8: u8 = 3;
    let heap_u8: Box<u8> = Box::new(stack_u8);

    doing_stack_stuff(stack_u8);
    println!("{}", stack_u8);

    doing_heap_stuff(heap_u8); // 1. cargo: borrow of moved value: `heap_u8`: value borrowed here after move
    println!("{}", heap_u8);
}

fn doing_stack_stuff(mut param: u8) {
    // 0. we need to keep in mind to add mut when we want to mutate data
    // since rust assumes immutability by default
    param += 3;
    println!("{}", param);
}

fn doing_heap_stuff(param: Box<u8>) {
    println!("{}", param);
} // doing_heap_stuff function is owner of memory and when it goes out of the scope it drops heap_u8

Why is that happening? One of the rules of ownership is When the owner goes out of scope, the value will be dropped.. In this situation it means that when we pass heap_u8 to doing_heap_stuff, the doing_heap_stuff function becomes owner of this memory. The only job of that function is to print out its param and when its done doing so, it drops this memory. That's why we get compile error, when we try to print out this memory after calling doing_heap_stuff function.

How to fix that? From what I know there are two ways of fixing that:

  • copy heap memory
  • explicitly return memory from the doing_heap_stuff function
#[allow(unused_variables)]
fn main() {
    let stack_u8: u8 = 3;
    let mut heap_u8: Box<u8> = Box::new(stack_u8);

    doing_stack_stuff(stack_u8);
    println!("{}", stack_u8);

    heap_u8 = doing_heap_stuff(heap_u8);
    println!("{}", heap_u8);
}

fn doing_stack_stuff(mut param: u8) {
    param += 3;
    println!("{}", param);
}

fn doing_heap_stuff(param: Box<u8>) -> Box<u8> {
    // we added here return type annotation
    println!("{}", param);
    // the shortest way of returning data from function
    param
    // return param; will also work
}