Understanding Lifetime in Rust – Part II

In Part I we discussed the motivation behind lifetime management in Rust and how it works from a function. In this installment we will explore how lifetime helps us model containment relationship (that is, when an object contains a reference to another object).

The Business Requirement

We will design a Person type. A person may own a Car. A person should be able to buy and sell cars. Two persons should be able to exchange (or trade) their cars.

Design the Types

The Car type is easy. We will keep it simple.

struct Car {
    model : String
}

How should the Person type contain a Car? A full containment will look like this:

struct Person {
    car : Option<Car>
}

This is easy from a memory management point of view. But it has several problems. A car is not an integral part of a person. Buying and trading in cars will create copies of cars. For performance reasons we will like to avoid that. Instead, we will like Person to store an optional reference to a Car. But that’s when things begin get complicated.

Containing a Reference Pointer

In Rust a reference must point to a valid memory area. Which means that the container object must not live longer than the object that it holds a reference to. Rust requires us to model this rule using lifetime parameters. Our Person type will look like this.

struct Person<'a> {
    car:Option<&'a Car>
}

In Person<'a> we are saying that there is a lifitime named a.

In Option<&'a Car> we are saying that the Car object has a lifetime of a.

After this, the compiler will ensure that the Person object has a lifetime of a or less. This will be done to avoid a Person object from containing an invalid Car reference pointer.

What we did here is basic common sense. Unfortunately, the Rust compiler does not let us elide the lifetime parameters in this situation. At least not right now.

Write the Implementation

Now that our type is designed, we can go ahead an implement the methods.

impl <'a> Person<'a> {
    fn new() -> Person<'a> {
        Person{
            car: None
        }
    }

    fn buy_car(&mut self, c : &'a Car) {
        self.car = Some(c);
    }

    fn sell_car(&mut self) {
        self.car = None;
    }    
}

This has everything we need except for trading cars between persons. We will develop that later.

As you have caught on by now, lifetime parameters are built into the data type using the same syntax as generics (the full type name is: Person<'a>). That is why we start the implementation using impl <'a> Person<'a> just like we would if the Person type used generics. Technically this lets us write different implementations for different types of lifetimes. But this concept is beyond my understanding for now.

Using the Contained Reference

Alright. Let’s take our code for a spin. This will compile just fine.

fn main() {
  let car = Car{model: "Honda Civic".to_string()};
  let mut bob = Person::new();

  bob.buy_car(&car);

  println!("{}", bob.car.unwrap().model);
}

The order in which you create bob and car does not matter. This will also work.

fn main() {
  let mut bob = Person::new();
  let car = Car{model: "Honda Civic".to_string()};

  bob.buy_car(&car); //OK!

  println!("{}", bob.car.unwrap().model);
}

In the old days this would have caused compile error because compiler thought the car got destroyed before bob. We can try to enforce that order using a scope. Now the code will run afoul.

fn main() {
    let mut bob = Person::new();

    {
        let car = Car{model: "Honda Civic".to_string()};

        bob.buy_car(&car); //Error!
    }

    println!("{}", bob.car.unwrap().model);
}

The Car object does not live as long as the Person. This violates our lifetime rule.

The compiler is good but there are cases where it can not deterministically asses lifetime and errs on the side of safety. Consider the example below.

fn main() {
  let ghibli = Car{model: "Maserati Ghibli".to_string()};
  let mut bob = Person::new();

  { 
    let civic = Car{model: "Honda Civic".to_string()};

    bob.buy_car(&civic); //Error!
    bob.buy_car(&ghibli);
  }

  println!("{}", bob.car.unwrap().model);    
}

The code does not compile because the civic variable doesn’t live as long as bob. This is unfortunate because clearly the code is perfectly safe from a human developer’s point of view. At the end of the inner scope bob no longer has any reference to civic. The system is strictly going by the way we declared the buy_car(&mut self, c : &'a Car) method. All it knows is that the supplied Car has a lifetime of a and the container object must not outlive it.

Recall, we can have multiple immutable borrowers as long as there are no other mutable borrowers. This makes it possible for two people to own the same car at the same time.

fn main() {
    let mut bob = Person::new();
    let mut alice = Person::new();
    let civic = Car{model: "Honda Civic".to_string()};

    bob.buy_car(&civic);
    alice.buy_car(&civic);

    println!("Bob has: {}", bob.car.unwrap().model);
    println!("Alice has: {}", alice.car.unwrap().model);
}

Having two owners of a car is a possible violation of our nation’s legal construct. Compiler should not enforce such rules. If you must lean on the compiler to deny sharing of a car then read on. We discuss that case later in this article.

Implement Trading

Trading will involve swapping cars between two Person instances. We add this method to the implementation:

impl <'a> Person<'a> {
  //Other methods omitted...

  fn trade_with(&mut self, other : &mut Person<'a>) {
    let tmp = other.car;

    other.car = self.car;
    self.car = tmp;
  }
} 

Nothing special here. Except to note that the lifetime is a part of the data type. The other variable has a full data type of &mut Person<'a>.

We can now use the trade_with() method like this.

fn main() {
    let civic = Car{model: "Honda Civic".to_string()};
    let ghibli = Car{model: "Maserati Ghibli".to_string()};

    let mut bob = Person::new();
    let mut alice = Person::new();

    bob.buy_car(&civic);
    alice.buy_car(&ghibli);

    bob.trade_with(&mut alice);

    println!("Bob has: {}", bob.car.unwrap().model);
    println!("Alice has: {}", alice.car.unwrap().model);
}

These days the order in which Car and Person objects are created makes no difference. The compiler is smart enough to deduce their lifetimes.

You can throw a really devious case at the compiler. It will catch the problem and won’t compile.

fn main() {
    let mut bob = Person::new();
    let civic = Car{model: "Honda Civic".to_string()};
    
    {
        let ghibli = Car{model: "Maserati Ghibli".to_string()};
        let mut alice = Person::new();

        bob.buy_car(&civic);
        alice.buy_car(&ghibli); //Error!

        bob.trade_with(&mut alice);
  
        println!("Alice has: {}", alice.car.unwrap().model);
    }

    println!("Bob has: {}", bob.car.unwrap().model);
}

The compiler correctly deduces that bob becomes the new owner of the Maserati Ghibli and Ghibli doesn’t live as long as bob does.

Borrow Rules Apply

When an object holds a reference to another it borrows the reference and all standard borrowing rules apply. This is expected. I mention them here just to reinforce the concepts.

You can have as many immutable borrows as you want as long as there are no mutable borrows.

let ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();

bob.buy_car(&ghibli); //bob borrows ghibli immutably

let p1 = &ghibli; //More immutable borrows are OK
let p2 = &ghibli;

A mutable borrow is exclusive and is only permitted if there is no other borrow of any kind.

let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();

bob.buy_car(&ghibli); //bob borrows ghibli immutably

let p1 = &mut ghibli; //Can't do this.

You can not move an object while someone has borrowed it’s reference. Because doing so will make the borrowed reference point to an invalid memory area.

let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();

bob.buy_car(&ghibli); //bob borrows ghibli

let g = ghibli; //Can't move

This is, however, somewhat unexpected.

let civic = Car{model: "Honda Civic".to_string()};
let mut ghibli = Car{model: "Maserati Ghibli".to_string()};
let mut bob = Person::new();

bob.buy_car(&ghibli);
bob.buy_car(&civic);

let p1 = &mut ghibli; //Still Can't do this

From a human developer’s point of view the code above is safe. bob no longer borrows any reference to ghibli at a time when we are trying to borrow ghibli mutably. But the compiler is unable to deduce that.

Prevent Sharing of a Reference

Earlier we have shown that two Person objects can share the same Car reference. In some situations this may not be desired. For example this prevents a Person from tinkering with (mutating) the car in any way. We can solve this issue by giving a Person a mutable reference to a Car. Remember a mutable reference creates an exclusive access. Nobody else can have any kind of reference to the same object. Here is the full code.

struct Car {
    model : String
}

struct Person<'a> {
    //Hold a mutable reference
    car:Option<&'a mut Car>
}

impl <'a> Person<'a> {
    fn new() -> Person<'a> {
        Person{
            car: None
        }
    }

    fn buy_car(&mut self, c : &'a mut Car) {
        self.car = Some(c);
    }

    fn sell_car(&mut self) {
        self.car = None;
    }    

    fn trade_with<'b>(&mut self, other : &'b mut Person<'a>) {

        let tmp = other.car.take();
    
        other.car = self.car.take();
        self.car = tmp;
    } 
}

fn main() {
    let mut civic = Car{model: "Honda Civic".to_string()};
    let mut ghibli = Car{model: "Maserati Ghibli".to_string()};

    let mut bob = Person::new();
    let mut alice = Person::new();

    bob.buy_car(&mut civic);
    alice.buy_car(&mut ghibli);

    bob.trade_with(&mut alice);

    println!("Bob has: {}", bob.car.unwrap().model);
    println!("Alice has: {}", alice.car.unwrap().model);
}

That will work fine. Notice in the trade_with() method we had to introduce a new lifetime for the other variable. The reasoning gets complicated. Because the cars are switching hands they may not outlive either one of the persons. Compiler needs a little bit of help here. We are now saying that other may have a different lifetime than &self.

Sharing is now disallowed.

fn main() {
    let mut civic = Car{model: "Honda Civic".to_string()};

    let mut bob = Person::new();
    let mut alice = Person::new();

    bob.buy_car(&mut civic);
    alice.buy_car(&mut civic); //Error!

    println!("Bob has: {}", bob.car.unwrap().model);
    println!("Alice has: {}", alice.car.unwrap().model);
}

Not All is Well

Well, as we have seen above it’s not that hard to model containment of reference pointers. The bad news is that this model does not always work. Consider this function. It will not compile.

fn shop_for_car(p : &mut Person) {
    let car = Car{model: "Mercedes GLK350".to_string()};

    p.buy_car(&car); //Error! car doesn't live long enough
}

That is because the car object simply doesn’t live as long as the Person buying it. So how can we keep reference to an object that is created in an inner scope like a function? The answer lies in heap allocation which in Rust is achieved via Box::new. We will explore that in Part III.

Understanding Lifetime in Rust – Part I

Lifetime is where the true strength of Rust lies and makes it unique from other languages with similar syntax like C++ and Java. Unfortunately, it is also the area that takes the longest to understand. The effort put in learning this is well worth it. Remember that lifetime solves two very nagging problems in programming:

  • Memory management – Rust achieves automatic memory management without the need for GC or reference counting. The compiler is able to deterministically predict lifetime of objects and inject memory free code at the correct places. Rust also eliminates common memory safety errors like double free and use after free as we shall see here.
  • Race condition – Rust enforces strict ownership rules. While multiple threads can have read-only access to an object only one thread can own it for modification. This makes concurrent code error free and in many cases you can do lock free concurrent programming.

As I learn more about lifetime I plan on rolling out more articles in this series. In Part I we will explore one of the simplest use cases of lifetime.

Continue reading

Rust Using Visual Studio Code

I am getting decent productivity using MS Visual Studio Code to code Rust. This is how I have it setup.

Use a Good Font

VS Code has built in support for Rust syntax coloring. But a good font always helps. My favorite is Source Code Pro. Download and install it from Google Fonts.

In VS Code open preferences using the menu Code > Preferences > User Settings. The right hand pane has your personal settings. Enter the font settings as follows.

// Place your settings in this file to overwrite the default settings
{
    "editor.fontFamily": "SourceCodePro-Medium",
    "editor.fontSize": 18
}

image

Save changes.

Setup Build Tasks

With this setup you will be able to compile, run and test right from VS Code.

In VS Code press Command+Shift+P to open the available menu items.

image

Type task to filter the list.

Select Tasks: Configure Task Runner.

Enter this JSON in the editor. Courtesy various posters in this Reddit post.

{
    "version": "0.1.0",
    "command": "cargo",
    "isShellCommand": true,
    "tasks": [
        {
            "taskName": "build",
            "isBuildCommand": true,
            "showOutput": "always",
            "problemMatcher": {
                "owner": "rust",
                "fileLocation": [
                    "relative",
                    "${workspaceRoot}"
                ],
                "pattern": {
                    "regexp": "^(.*):(\d+):(\d+):\s+(\d+):(\d+)\s+(warning|error):\s+(.*)$",
                    "file": 1,
                    "line": 2,
                    "column": 3,
                    "endLine": 4,
                    "endColumn": 5,
                    "severity": 6,
                    "message": 7
                }
            }
        },
        {
            "taskName": "clean",
            "showOutput": "always"
        },
        {
            "taskName": "run",
            "showOutput": "always"
        },
        {
            "taskName": "test",
            "showOutput": "always",
            "isTestCommand": true,
            "problemMatcher": [
                {
                    "owner": "rust",
                    "fileLocation": [
                        "relative",
                        "${workspaceRoot}"
                    ],
                    "pattern": {
                        "regexp": "^(.*):(\d+):(\d+):\s+(\d+):(\d+)\s+(warning|error):\s+(.*)$",
                        "file": 1,
                        "line": 2,
                        "column": 3,
                        "endLine": 4,
                        "endColumn": 5,
                        "severity": 6,
                        "message": 7
                    }
                },
                {
                    "owner": "rust",
                    "fileLocation": [
                        "relative",
                        "${workspaceRoot}"
                    ],
                    "severity": "error",
                    "pattern": {
                        "regexp": "^.*panicked\s+at\s+'(.*)',\s+(.*):(\d+)$",
                        "message": 1,
                        "file": 2,
                        "line": 3
                    }
                }
            ]
        }
    ]
}

Save changes.

Configure Keyboard

VS Code already comes with keyboard shortcut for tasks like build (Command+Shift+B) and test (Command+Shift+t). But we will now setup a keyboard shortcut to run any task from the list of available tasks.

From VS Code menu select Code > Preferences > Keyboard Shortcuts.

In the right hand side pane enter:

// Place your key bindings in this file to overwrite the defaults
[
    { "key": "shift+cmd+r", "command": "workbench.action.tasks.runTask" }
]

Save changes.

Development Workflow

The tasks we have setup above works with Cargo based projects. From VS Code menu select File > Open. Select the root folder of your Cargo based project (the folder that contains Cargo.toml).

To do a build use Command+Shift+B. Errors and warnings will be highlighted in the editor.

image

You can also move the mouse over the error squiggly line and view the error message right there.

To test the crate press Command+Shift+t.

If you are working on a binary crate and want to run it press Command+Shift+r. This will open the list of all tasks.

image

Select run from the list.

Change Background Color

I use the dark theme (View > Theme > Dark theme). But the background is semi transparent which I find hugely distracting. To change the background, locate the file native.main.css within VS Code distribution. Open the file. Change the background to full black like this:

.monaco-editor.vs-dark .zone-widget .monaco-editor {
    color: #BBB;
    background: #000000;
}

Save changes.