ARTICLE

From Rust in Action by Tim Mcnamara

___________________________________________________________________

Save 37% on Rust in Action. Just enter fccmcnamara into the discount code box at checkout at manning.com.
___________________________________________________________________

Our strategy for this article is to use an example that compiles, then make a minor change that triggers an error which appears to emerge without any adjustment to the program’s flow. Working through the fixes to those issues should make the concepts more complete.

“Implementing” a Mock CubeSat Ground Station

The learning example for the article is a CubeSat constellation. CubeSats are miniature artificial satellites which are increasingly used to increase the accessibility of space research. With around 1.3kg of mass, they’re significantly cheaper to send into orbit than conventional satellites. A ground station is an intermediary between the operators and the satellites. It’s listening on the radio, checking on the status of every satellite in the constellation, and transmitting messages to and fro.

Image for post
Image for post

In Figure 1 we’ve three CubeSats. To model this, we’ll create a variable for each of them. They can happily be integers for the moment. We don’t need to model the ground station explicitly, and we’ll omit it.

let sat_a = 0;  
let sat_b = 1;
let sat_c = 2;

To check on the status of each of our satellites, we’ll use a stub function and an enum to represent potential error messages.

#[derive(Debug)]  
enum StatusMessage {
Ok, ❶
}
fn check_status(sat_id: u64) -> StatusMessage {
StatusMessage::Ok ❶
}

❶ For now, all of our CubeSats function perfectly all of the time

The check_status() function is extremely complicated in a production system. For our purposes though, returning the same value every time is perfectly sufficient. Pulling these two snippets into a whole program that “checks” our satellites twice, we end up with something like this:

Listing 1. Checking the status of our integer-based CubeSats (ch4/ch4-check-sats-1.rs)

#![allow(unused_variables)]

#[derive(Debug)]
enum StatusMessage {
Ok,
}

fn check_status(sat_id: u64) -> StatusMessage {
StatusMessage::Ok
}

fn main () {
let sat_a = 0;
let sat_b = 1;
let sat_c = 2;

let a_status = check_status(sat_a);
let b_status = check_status(sat_b);
let c_status = check_status(sat_c);
println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);

// "waiting" ...
let a_status = check_status(sat_a);
let b_status = check_status(sat_b);
let c_status = check_status(sat_c);
println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

Running Listing 1 should be fairly uneventful. The code compiles begrudgingly. We encounter the following output from our program:

Listing 2. Output of Listing 1

a: Ok, b: Ok, c: Ok  
a: Ok, b: Ok, c: Ok

Encountering our first lifetime issue

Let’s move closer to idiomatic Rust by introducing type safety. Instead of integers, let’s create a type to model our satellites. A real implementation of a CubeSat type would probably include lots of information about its position, its RF frequency band and more. We’ll stick with only recording an identifier.

Listing 3. Modelling a CubeSat as its own Type

#[derive(Debug)]  
struct CubeSat {
id: u64;
}

Now that we have a struct definition, let’s inject it into our code. Listing 4 will not compile. Understanding the details of why not is the task of much of this chapter.

Listing 4. Checking the status of our integer-based CubeSats (ch4/ch4-check-sats-2.rs)

#[derive(Debug)]                                       ❶
struct CubeSat {
id: u64,
}

#[derive(Debug)]
enum StatusMessage {
Ok,
}

fn check_status(sat_id: CubeSat) -> StatusMessage { ❷
StatusMessage::Ok
}

fn main() {
let sat_a = CubeSat { id: 0 }; ❸
let sat_b = CubeSat { id: 1 }; ❸
let sat_c = CubeSat { id: 2 }; ❸

let a_status = check_status(sat_a);
let b_status = check_status(sat_b);
let c_status = check_status(sat_c);
println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);

// "waiting" ...
let a_status = check_status(sat_a);
let b_status = check_status(sat_b);
let c_status = check_status(sat_c);
println!("a: {:?}, b: {:?}, c: {:?}", a_status, b_status, c_status);
}

❶ Modification 1: Add definition

❷ Modification 2: Use the new type within check_status()

❸ Modification 3: Create three new instances

When you attempt to compile the code within Listing 4 , you will receive a message similar to the following (which has been edited for brevity):

Listing 5. Error Message Produced When Attempting to Compile Listing 4

error[E0382]: use of moved value: `sat_a`
--> code/ch4-check-sats-2.rs:26:31
|
20 | let a_status = check_status(sat_a);
| ----- value moved here
...
26 | let a_status = check_status(sat_a);
| ^^^^^ value used here after move
|
= note: move occurs because `sat_a` has type `CubeSat`,
= which does not implement the `Copy` trait

...

error: aborting due to 3 previous errors

To trained eyes, the compiler’s message is helpful. It tells us exactly where the problem is and provides us with a recommendation on how to fix it. To less experienced eyes, it’s significantly less useful. We’re using a “moved” value and are fully advised to implement the Copy trait on CubeSat. Huh? Turns out, although it’s written in English, the term “move” means something specific within Rust. Nothing physically moves.

Movement within Rust code refers to movement of ownership, rather than movement of data. Ownership is a term used within the Rust community to refer to the compile-time process that checks that every use of a value is valid and that every value is destroyed cleanly.

Every value in rust is owned. Listing 1 and Listing 4 , sat_a, sat_b and sat_c own the data that they refer to. When calls to check_status() are made, ownership of the data moves from the variables in the scope of main() to the sat_id variable within the function. The significant difference is that the second example places that integer within a CubeSat struct. This type change alters the semantics of how the program behaves.

Here’s a stripped down version of the main() function from Listing 4 focusing on sat_a and places where ownership moves:

Listing 6. Extract of <<check-sats2>, focusing on the main() function

fn main() {    
let sat_a = CubeSat { id: 0 }; ❶
let a_status = check_status(sat_a); ❷ let a_status = check_status(sat_a); ❸
}

❶ Ownership originates here at the creation of the CubeSat object

❷ Ownership of the object moves to check_status(), but is not returned to main()

❸ At this point, sat_a is no longer owner of the object, making access invalid

Figure 2 provides a visual walk-through of the interrelated processes of control flow, ownership and lifetimes. During the call to check_status(sat_a), ownership moves to the check_status() function. When check_status() returns a StatusMessage, it drops the sat_a value and the lifetime of sat_a ends. Sadly for main() though, sat_a remains in local scope. A second attempt to access sat_a during the second call to check_status() fails. We discuss strategies to overcome this type of issue later in the article.

Image for post
Image for post

Special behavior of primitive types

Before carrying on, it may be wise to explain why the first code snippet, which is Listing 1 functioned at all. As it happens, primitive types in Rust have special behavior. They implement Copy. This means owned objects are duplicated when accessed at times that’d otherwise be illegal. This provides some convenience day-to-day, at the expense of inconsistency within the language. Formally, primitive types are said to possess copy semantics, whereas all other types have move semantics. Unfortunately for learners of Rust, that special case looks like the default case because they typically encounter primitive types first.

Ownership is an inherently compile-time construct. In both code examples, the data owned by sat_a, sat_b and sat_c has the same physical layout: they’re represented in memory as integers (0, 1 and 2). Ownership is checked by the compiler and imposes no runtime cost.

What is an Owner? Does it Have any Responsibilities?

In the world of Rust, the notion of ownership is rather limited. Unlike the Lockean notion of property, ownership doesn’t imply control or sovereignty. In fact, the “owners” of values don’t have special access to their owned data, nor do they have an ability to restrict others from trespassing. The responsibility of an owner is to clean up.

When variables assume control (ownership) of an object (data), memory for that object is retained for as long as the variable is alive. When variables go out of scope, their destructors are called. (The compiler injects code into the resulting binary that isn’t in the source code.) Owned data is then freed.

To provide a destructor for a type, implement Drop. Drop has one method, drop(&mut self) that you can use to conduct any necessary wind up activities before memory is freed.

Why use the term “ownership”, if a variable is referent object isn’t its property? The term has been in use for a few decades now and the language is somewhat fixed. It definitely emphasizes that there’s a single place where responsibility is kept at all times.

How Ownership Moves

You can hift ownership from one variable to another within a Rust program using two main ways. The first is through assignment. The second is by passing data through a function barrier, either as an argument or a return value.

Revisiting our original code from Listing 4, we can see that sat_a starts its life with ownership over a CubeSat object.

fn main() {    
let sat_a = CubeSat { id: 0 };
...

The CubeSat object is then passed into check_status() as an argument, moving ownership to the local variable sat_id.

fn main() {    
let sat_a = CubeSat { id: 0 };
...
let a_status = check_status(sat_a);
...

Another possibility could have been that sat_a relinquishes its ownership within main() to another variable. That would look something like this:

fn main() {    
let sat_a = CubeSat { id: 0 };
...
let new_sat_a = sat_a;
...

Lastly, were there to be a change in the check_status() function signature, it too could pass ownership of the CubeSat to a variable within the calling scope. Here’s our original function,

fn check_status(sat_id: CubeSat) -> StatusMessage {    
StatusMessage::Ok
}

and here is an adjusted function that achieves its message notification through a side-effect.

fn check_status(sat_id: CubeSat) -> CubeSat {    
println!("{:?}: {:?}", sat_id, StatusMessage::Ok); ❶
sat_id ❷
}

❶ Use the Debug formatting syntax as our types have use #[derive(Debug)]

❷ Return a value by omitting the semi-colon at the end of the last line

When used in conjunction with a new main(), it’s possible to see ownership of the CubeSat objects back to their original variables. The new code is

Listing 7. Returning ownership of objects back to their original variables via functions’ return values (ch4/ch4-check-sats-3.rs)

#![allow(unused_variables)]

#[derive(Debug)]
struct CubeSat {
id: u64,
}

#[derive(Debug)]
enum StatusMessage {
Ok,
}

fn check_status(sat_id: CubeSat) -> StatusMessage {
println!("{:?}: {:?}", sat_id, StatusMessage::Ok);
StatusMessage::Ok
}

fn main () {
let sat_a = CubeSat { id: 0 };
let sat_b = CubeSat { id: 1 };
let sat_c = CubeSat { id: 2 };

let sat_a = check_status(sat_a); ❶
let sat_b = check_status(sat_b);
let sat_c = check_status(sat_c);

// "waiting" ...
let sat_a = check_status(sat_a);
let sat_b = check_status(sat_b);
let sat_c = check_status(sat_c);
}

❶ Now that the return value of check_status() is the original sat_a, the new let binding is “reset”

Printing to the console changes, this responsibility has been pushed into check_status() The output from the new main() function looks like this:

Listing 8. Output of Listing 7

CubeSat { id: 0 }: Ok  
CubeSat { id: 1 }: Ok
CubeSat { id: 2 }: Ok
CubeSat { id: 0 }: Ok
CubeSat { id: 1 }: Ok
CubeSat { id: 2 }: Ok

A visual overview of the ownership movements within Listing 7 is provided below.

Image for post
Image for post

And that’s all for now. If you want to learn more about the book, check it out on liveBook here and see this slide deck.

About the author:
Tim McNamara is an experienced programmer with a deep interest in natural language processing, text mining, and wider forms of machine learning and artificial intelligence. He is very active in open source communities including the New Zealand Open Source Society.

Originally published at freecontent.manning.com.

Written by

Follow Manning Publications on Medium for free content and exclusive discounts.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store