Ownership and Borrowing in Rust

The Ultimate Guide to Efficiently Using Ownership and Borrowing Concepts in Rust

Cheris Patel
Rustaceans
7 min readSep 28, 2023

--

One of Rust’s amazing feature is its ownership system, which ensures memory safety without needing a garbage collector. Ownership, combined with the concepts of borrowing, helps Rust prevent common programming errors such as null pointer dereferencing, buffer overflows, and data races in concurrent contexts.

Understanding Ownership

It enables Rust to make memory safety guarantees without needing a garbage collector. Ownership is a set of rules that govern how a Rust program manages memory.

Owner: The owner of value is a variable or data structure that holds it and is responsible for allocating and freeing the memory used to store that data.

Ownership is the state of “owning” and “controlling” legal possession of “something”. With that said, we must identify who the owner is and what the owner owns and controls.

In Rust, each value has a variable called its owner. To put it simply, a variable is an owner, and the value of a variable is what the owner owns and controls.

let x = 5;

Here x is owner of 5. With an ownership model, memory is automatically released (freed) once the variable that owns it goes out of scope.

Ownership Rules

  • Each value in Rust has an owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Scope: Range within program which is valid for variable access

  • Global Scope: Accessible throughout the whole program
  • Local Scope: Accessible only within function or in a block of code

Example:

{                      // s is not valid here, it’s not yet declared
let s = "hello world!"; // s is valid from this point forward

// do stuff with s
} // this scope is now over, and s is no longer valid
  • When s comes into scope, it is valid.
  • It remains valid until it goes out of scope.

Types of Memory for variable storage:

Stack Memory:

  • Last in, first out (LIFO) basis, where the most recently added item is the first to be removed. Each function call allocates a block of memory, called a “stack frame,” to store local variables and return addresses. When the function call completes, its stack frame is popped off.
  • All data stored in the stack must have fixed size i.e. primitive data types (like int, char, str, bool etc.)

Heap Memory:

Used for dynamic memory allocation, where blocks of memory are allocated and freed in an arbitrary order. Unlike stack memory, heap memory does not follow a LIFO order, allowing for more flexible management of data that needs to persist beyond the scope of individual function calls.

  • Data of no known belongs on heap
  • Allocating data on the heap will return the pointer to data store on heap which stored on stack
  • Allocating on heap is slower than pushing on stack
  • Accessing data on the heap is slower as it has to be access using pointer stored in stack

In Rust, there are two kinds of strings: String (heap allocated, and growable) and &str (fixed size, and can’t be mutated).

copy

“copy” means exact replica of one owner to another. Primitive types, which have a fixed size, can be stored in the stack and removed when their scope ends. They can also be easily duplicated when needed in a different part of the code. This ability to create exact replicas inexpensively is referred to as copy semantics. Essentially, it allows for the effortless creation of duplicates.

let x = "hello world!";
let y = x;
println!("{}", x) // hello world!
println!("{}", y) // hello world!

Move

“move” means the ownership of the memory is transferred to another owner. Consider the case of complex types that are stored on the heap.

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

When we assign s1 to s2, the String metadata is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. But what happens to s1 once it’s been assigned to s2? Rust no longer considers s1 to be valid.

Consider the assignment let s2 = s1. Imagine if Rust still considered s1 as usable after this assignment. When both s2 and s1 eventually go out of scope, they would both attempt to free the same area of memory. This would be a serious problem, known as a "double free error," and it's a type of memory safety bug. Releasing memory twice can lead to memory corruption and even pose security risks.

To ensure memory safety, Rust marks s1 as invalid after the line let s2 = s1. Consequently, when s1 is no longer within scope, Rust doesn't attempt to free its memory. If we were to try using s1 after creating s2, we would encounter issues.

clone

If we want to copy all data that stored in heap from one owner to another, not just the stack metadata then there is clone method.

let s1 = String::from("hello world!");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

How ownership moves

  • Return from function
  • Passing a reference
  • Passing a mutable reference
  • Assigning to variable (already discussed with the move interaction)

Passing value to a function

Here’s example of move:

fn main() {
let text = String::from("hello world!"); // text comes into scope

move_ownership(text); // text's value moves into the function...
// so it's no longer valid from this // point forward // primitive, so we use x afterward

} // Here, text goes out of scope. But because text's value was moved, nothing
// special happens.


fn move_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called.
// The occupied memory is freed.

If we tried to use textafter the call to takes_ownership, Rust would throw a compile-time error.

Here’s example of copy value:

fn main() {
let x = 5; // x comes into scope

makes_copy(x); // x would move into the function,
// but i32 is Copy, so it's okay to still
// use x afterward
} // Here, x goes out of scope.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

Returning from a function

fn main() {
let s1 = gives_ownership(); // gives_ownership moves its return
// value into s1

let s2 = String::from("hello world!"); // s2 comes into scope

let s3 = takes_and_gives_back(s2); // s2 is moved into
// takes_and_gives_back, which also
// moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
// happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {
let some_string = String::from("yours"); // some_string comes into scope

some_string // some_string is returned and
// moves out to calling function
}

// This function takes a String and returns it
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
// scope
a_string // a_string is returned and moves out to the calling function
}

Borrowing

Borrowing means getting something and promising to give it back. borrowing is a way of accessing value without claiming ownership of it.

Similar to C pointers, a reference is a pointer to memory that contains a value that owned by another variable. However, in rust reference can’t be null pointer.

let x = "hello world!";
let y = &x;

println!("Value y = {}", y);
println!("Address of y = {:p}", y);
println!("Deref of y = {}", *y);

Output will be:

Value y = hello world!
Address of y = 0x7fffe7f6c5c0
Deref of y = hello world!

Here’s x still owns the value while y just borrow value of x by assigning address of x.

Example of passing reference:

fn main() {
let s = "hello world!";
ref_pass(&s);
println!("{}", s); // can access s here as references can't move the value
}

fn ref_pass(s: &str) {
println!("Inside ref_pass function {:?}", s);
}

Output:

Inside print_vector function "hello world!"
hello world!

Mutable references

Just like a variable, references are immutable by default, but they can be made mutable using the mut keyword. However, the owner of the reference should be mutable, as ownership hasn’t changed.

fn main() {
let mut x = 2;
let y = &mut x;
*y = 10;
println!("Changed value: {}", x);
}

Output:

Changed value: 10

Borrowing rules

Borrowing a value in Rust is not always possible. Borrowing is only permitted in certain cases. That’s why we still required move semantics.

  • At any time either one mutable reference or any number of immutable references.
  • References must always be valid (not null).
fn main() {
let mut s = String::from("hello, ");

let r1 = &mut s;
r1.push_str("world");
let r2 = &mut s;
r2.push_str("!");

println!("{}",r2);
}

This will work because the compiler won’t see r1 after r2 is created. Even though there are multiple mutable references, since r1is not being used after another mutable reference is created, the compiler won’t throw an error.

Practice

practice rust provides many code snippets to explore and hands on practice for ownership concepts.

Summary

In world of Rust Programming, understanding ownership and borrowing is essential for writing safe and efficient code. Moreover, it is one of the most confusing concepts in Rust.

This blog provides comprehensive guide of ownership and borrowing along with its rules.

--

--