Understanding Rust Ownership Model by Example - Rules that are foundation to Rust’s ownership Model

Over the last few months, I’ve been intrigued by the Rust programming language — especially its memory model and ownership.

At first, I thought it was just another systems programming language that I could learn in a few weeks by writing some sample applications. But then I hit numerous roadblocks while working with its ownership model.

It took me weeks to really understand the basic principles on which RUST is written. And now I can’t stop learning and trying more of it.

In this post, I’ll share some ownership principles using a simple example.

This example is part of an interactive product analytics framework I was trying to build to learn Rust and write a distributed system from scratch.

To get started refer to the data structure below:

In the data analytics world, an event is a data structure composed of key value pairs. These are data points sent to a data collection service. Events can usually be named for human readability. We’ll be working with this event structure throughout this tutorial and understand Rust’s ownership rule basics.

No Garbage Collector

Before we dive into the rules, it’s important to understand that Rust Doesn’t have a garbage collector like other high level programming languages. It also doesn’t have manual memory management like C / C++ for most of the parts. It is therefore a preferred choice when we desire performance as well as memory safety.

When it comes to memory management on the stack/heap, Rust has a few simple rules to manage objects and memory. Rust ensures that our program doesn’t make mistakes while managing memory — provided we adhere to certain rules.

It may seem very daunting to work with this memory model and ownership rules to start with, but once we get used to this operating model, it makes us better programmers and provides the best possible performance without having to manage low-level memory.

Below are the rules when working with Language objects and variables.

Every Variable Has an Owner

Consider the code block shown above. We initialize three variables namely event name client ID and the actual event object. The first rule is very simple-every variable has an owner. This means the three values that are defined above are owned by the three variables. These variables can only be accessed within this function and no access is permitted beyond the scope of this function. When I run this program, we see the expected output which is the event name that we configured.

Now let’s look at the other two rules.

Only One Owner Permitted

At any given point in time, Rust allows only one owner for variables. Typically, many programming languages have the concept of using multiple variables to refer to the same value. For instance, in JAVA one could write

String name = "Sam" ;   
String sam = name;   
System.out.println(name);   
System.out.println(sam);

This is perfectly fine in Java. It prints out both the values.

Now let’s try a similar thing in Rust.

We tried to copy the original reference and store it in another variable to use them both in a print statement. Guess what happens with this.

Below is the output when we try to run this.

What we did here was just use another variable name to refer to the same event and print them. But Rust doesn’t like this at all.

According to our second rule, there can only be one owner for a value. In our case, the event we first defined was owned by new_event variable.

Technically, when we try to create a new variable and refer to the original event, we intended to create two owners that point to the same data.

This is not permitted as per the first and second rules. To ensure that only one owner always exists, rust moves the value to this new variable denoted by new_event_copy_ref. So, as soon as we assign the old variable to a new event variable, this new variable points to the memory location of the original event.

From that point on we cannot use the original variable because it doesn’t exist. The data still exists but has a new owner. RUST drops the original variable immediately and cleans it up.

Thus, when we try to print the original event name using the variable new_event , we get an error message that clearly states Value borrowed after move. And all of these checks happen during compilation time. So, we are guaranteed that we don't have references that point to invalid memory locations when our program runs.

Now let’s look at the third rule.

Out of Scope Variables are Dropped Automatically

When a variable goes out of scope, rust immediately drops the variable and cleans it up. When we talked about Rust not having a garbage collector, this is what happens in the absence of it.

So, the only time one can use variables and refer to its memory locations is when a variable is in scope. Let’s take an example to understand this as well.

Let’s first understand a ‘scope block’. (The code above may seem a little different if you’re coming from another programming language) .

A scope is a block denoted by opening and closing braces. {}. It can be nested within a function, or it can be a new function call that has its own scope altogether. In the example above, we created an event within an inner scope.

The third rule specifies that as soon as the inner scope ends, the variable new_event and its data are dropped immediately.

By this logic and the example from our second rule, we would guess that the compiler would complain in this case. And we’re right this time. Below is the error that you get when this program is compiled.

error[E0425]: cannot find value `new_event` in this scope  
--> analytix/core/src/models.rs:49:71  
|  
49 |         println!("Original event name is {} . New Event Name is {}" , new_event.name , new_event_copy_ref.name)  
|                                                                       ^^^^^^^^^ not found in this scope

As expected, this variable wasn’t found in scope.

These three rules are the foundation of Rust’s ownership system and serve as a good starting point to understanding more complicated scenarios like shared states, concurrency, and memory management.

Memory management complexity is left to the language, and we as developers follow these rules to write clean, efficient code (this is not easy at the start, but it gets better).

By following these rules, rust not only guarantees memory safety for developers but also makes one a better programmer. Apart from this, it also gives programs a performance boost that is as close to that of C++ without the complexities of memory management.

Over the months of my experience using Rust, I’ve faced a lot of hurdles when dealing with these rules. Eventually, I’ve developed a way of really thinking of simplifying the memory model as well as code (Although I have a long way to go. ) I’d still say I’m a Rust newbie. But I’m loving it!