To decode the concepts of Rust Ownership and Borrowing, let us start with an example:

fn main() {
    let greetingMsg = String::from("Hello, World");
    transfer_ownership(greetingMsg);
    println!("Main Function: {}", greetingMsg);
}

fn transfer_ownership(msg: String) {
    println!("Transfer Ownership Function: {}", msg);
}

Looks like a pretty harmless piece of code. After declaration and initialization, the greetingMsg variable is passed to the function transfer_ownership(). We display the message within the function. Also, once the function execution is over, we display the greetingMsg within the main() function.

Based on common knowledge, this tiny little Rust program should work.

However, it does not. When we run this program, we get a nasty error message.

error[E0382]: borrow of moved value: `greetingMsg`
 --> src/main.rs:4:35
  |
2 |     let greetingMsg = String::from("Hello, World");
  |         ----------- move occurs because `greetingMsg` has type `String`, which does not implement the `Copy` trait
3 |     transfer_ownership(greetingMsg);
  |                        ----------- value moved here
4 |     println!("Main Function: {}", greetingMsg);
  |                                   ^^^^^^^^^^^ value borrowed here after move

The error originates on line 4 when we are trying to print the greetingMsg. The error message seems to suggest that we are trying to use a variable that has been moved. And hence, this operation is invalid.

In reality, this happens because of the concept of Rust Ownership and Borrowing. In this post, we will dive deeper into these concepts.

However, if you are completely new to Rust, I will suggest you to first go through the Introduction to Rust Programming Language.

1 – The Role of Stack and Heap

All programming languages make use of the Stack and Heap. However, not all languages require you to think about them while writing application code. Much of the complexity of dealing with these data structures is handled by the language.

But, in a systems programming language like Rust, it is important to understand whether a value is on the stack or heap. This understanding can help us write more efficient code.

1.1 – Stack

The stack stores data in the order it arrives and removes it in the opposite order. Basically, stacks follows Last-In First-Out or LIFO approach. Think of a pile of plates at a buffet restaurant. You add plates to the top of the pile. When you need a plate, you take it from the top. The main requirement is that all data stored on the stack must have a known, fixed size at compile time. If the data size is unknown or subject to change at runtime, it must be stored on the heap.

1.2 – Heap

The heap is a less organized data structure when compared to stack. When placing data on the heap, we request a certain amount of space. The memory allocator finds an empty spot on the heap, marks it as being in use, and returns the pointer. Basically, the pointer is the address to the memory location.

See below illustration depicting stack and heap.

rust stack vs heap
Stack vs Heap

Pushing data to the stack is faster because the allocator does not have to search for empty space. On stack, new data is always placed on top of the stack. Accessing data from stack is also faster because we always get the data from the top of the stack.

In a heap, the allocator needs to perform more work to store the data. Also, accessing data from the heap is slower since we have to follow a pointer to reach the data.

Due to this distinction, it becomes important for programs to manage data on stack and heap. Since heap access is costly, it is vital to minimize the amount of duplicate data on the heap. Also, cleaning up unused data is an equally significant task so that the program does not run out of memory.

Rust Ownership and Borrowing seeks to address the problems with storing data on stack and heap. And knowing the concept of ownership is necessary for developers to write error-free code.

2 – Scope of Variable in Rust

Ownership in Rust is based on variable scoping. Therefore, it is important to understand how variables are scoped.

See below example:

{
   let greetingMsg = "Hello, World"; //greetingMsg scope beginning
   println!("{}", greetingMsg);
}                                    //greetingMsg scope end

The variable greetingMsg is a string literal. In other words, we hard-code the value of the string. From line number 2, the variable is valid or in-scope. At the end of the scope i.e. the second curly braces, the variable greetingMsg goes out-of-scope. Important point is that greetingMsg is immutable.

This is not so different from other programming languages.

However, let us take a different example.

{
   let mut greetingMsg = String::from("Hello, World");
   greetingMsg.push_str(", how are you?");
   println!("{}", greetingMsg);
}

Here, we are using the String type to declare the greetingMsg variable. Also, we mutate the string in line 3.

How String Literal Differs from String Type?

In the first case, we are using a string literal. Since we are hard-coding the text, the compiler can figure out the exact size of the literal. Due to this, they are fast and efficient. The downside is that string literals are immutable.

However, the same is not the case with String type. It supports a mutable piece of text that can change its size over time. Since the size is unknown, we need to allocate the String type on heap memory. In other words, the memory must be requested from the memory allocator at runtime. Also, if the String type is no longer in use, we also need to return the memory to the allocator.

Allocating of memory happens when we use String::from. However, for releasing the memory, Rust uses a different approach. In languages that support garbage collection, it is the job of the garbage collector to clean up memory that is no longer in use. In languages without garbage collection, it is the responsibility of the developer to clean up memory. This is not a trivial problem.

Rust takes a different approach. In Rust, the memory is automatically returned once the variable that owns it goes out of scope. For example, when greetingMsg goes out of scope, Rust will automatically call a special function known as drop that will clean up the memory associated with greetingMsg.

3 – Concept of Ownership in Rust

This brings us to understanding the concept of ownership in Rust.

To begin with, there are three ownership rules:

  • Each value in Rust has a variable. The variable is known as the owner.
  • There can only be one owner at a given point of time.
  • The value is dropped from memory when the owner goes out of scope.

Let us look at a simple example:

let greetingMsg = String::from("Hello, World");

Here, greetingMsg is the owner. The memory is assigned on the heap storage.

We can now extend the example:

let greetingMsg = String::from("Hello, World");
let anotherMsg = greetingMsg;
println!("{}", greetingMsg);

This will throw an error:

--> src/main.rs:4:20
  |
2 |     let greetingMsg = String::from("Hello, World");
  |         ----------- move occurs because `greetingMsg` has type `String`, which does not implement the `Copy` trait
3 |     let anotherMsg = greetingMsg;
  |                      ----------- value moved here
4 |     println!("{}", greetingMsg);
  |                    ^^^^^^^^^^^ value borrowed here after move

This is because once we assign greetingMsg to anotherMsg, Rust invalidates the variable greetingMsg. Basically, the data has moved from one variable to another. In other words, the ownership has been transferred.

See below illustration of how Rust treats the above operation.

rust ownership and borrowing

As you can see, the String type stores ptr, len & capacity. When we declare a new variable and assign the value to it, the connection with the previous variable is broken.

The advantage of this approach is that there are no chances of double free error where we might accidentally free the same memory twice. At any given point of time, there is only owner for the value.

Do note that the above concept is applicable for heap data. In other words, complex types that don’t have a fixed size in memory.

4 – Ownership in Rust with Functions

The concept of passing a value to a function is quite similar to assigning a value to a variable. If interested, you can read more about Functions in Rust.

However, for the purpose of ownership in Rust, when we pass a variable to a function, it will also move or copy similar to how assignment works.

This is where our initial example comes back to the picture.

fn main() {
    let greetingMsg = String::from("Hello, World");
    transfer_ownership(greetingMsg);
    println!("Main Function: {}", greetingMsg);
}

fn transfer_ownership(msg: String) {
    println!("Transfer Ownership Function: {}", msg);
}

With the knowledge from this post, we can now clearly understand what’s going on in the above example. Basically, when greetingMsg was passed to the function transfer_ownership, the value was moved. In other words, greetingMsg was no longer valid.

We can transfer scope and ownership by returning from the function as well. See below example:

fn main() {
    let greetingMsg = String::from("Hello, World");
    let returnedMsg = transfer_ownership_and_return(greetingMsg);
    println!("Main Function: {}", returnedMsg);
}

fn transfer_ownership_and_return(msg: String) -> String {
    println!("Transfer Ownership Function: {}", msg);
    return msg;
}

Here, the function transfer_ownership_and_return() takes the ownership of the message and returns it back to the main() function. Therefore, this program will work fine.

5 – Borrowing in Rust with References

While the above example of transferring ownership back from the function works, it is still a bit tedious.

For example, if we implement a function to calculate the length of the String type variable, we will have to pass both the length and the original string. See below:

fn main() {
    let greetingMsg = String::from("Hello, World");

    let (anotherMsg, len) = calculate_length(greetingMsg);

    println!("The length of '{}' is {}.", anotherMsg, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); 
    (s, length)
}

Here, the function calculate_length() has to return two values – the length and the value of the String type. If we don’t return the String type value, the main() function will lose access to the message and it will be removed from memory. However, this increases the complexity of the calculate_length() function. The function needs to make sure and return the original String value along with the length.

We can get around this issue by using the concept of Rust Ownership and Borrowing together. See below example:

fn main() {
    let greetingMsg = String::from("Hello, World");

    let length = calculate_length(&greetingMsg);

    println!("The length of '{}' is {}.", greetingMsg, length);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Basically, here we pass the reference to the greetingMsg.

In Rust, the ampersand represents a reference. It allows us to refer to a value without taking ownership of it. In other words, the calculate_length() function is simply borrowing the string value. It does not have to give back ownership since it never got the ownership. In this case, the value will not be dropped in the main() function when the call to calculate_length() is made.

Conclusion

Ownership and Borrowing in Rust are fundamental concepts that determine the structure of your code.

Whenever we are building any functionality, we should think about scoping of variables and whether you need to pass the ownership of a value. This will make our program more efficient in terms of memory usage.

Want to know more about borrowing references in Rust? Check out this detailed post on how Rust Borrow Mutable References.

If you have any comments or queries about this post, please feel free to mention them in the comments section below.

Categories: BlogRust

Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *