Understanding Lifetime in Rust – Part I

I estimate that bulk of the time you invest in learning Rust will go into learning lifetime management. If we can keep aside Foreign Function Interface for a second, the rest of the language is just as simple to learn as Swift.

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.

The Business Requirement

We will develop a text editor type called TextEditor. This type will contain a member variable of type String. This variable will be made private. Users of the type should be able to read the text entered in the editor but not be able to directly modify it by calling any String methods.

Get Started

Let’s develop the basic code.

pub struct TextEditor {
text: String //Private member variable
}

impl TextEditor {
pub fn new() -> TextEditor {
TextEditor{text: String::new()}
}

//Modify text
pub fn add_char(&mut self, ch : char) {
self.text.push(ch);
}
}

fn main() {
let mut editor = TextEditor::new();

editor.add_char('a');
editor.add_char('b');
editor.add_char('c');
}

This should compile. It has everything we need except we do not provide read access to the text yet.

Giving Read Access to the Text

OK, now we need to add a method to TextEditor that will provide read access to the text member variable. We have a few options to choose from:

Option 1 – Cloning

Clone the text variable and return the clone.

pub fn get_text_clone(&self) -> String {
return self.text.clone();
}

Cloning allocates new memory for the text and copies all the characters in to that area. This option is easy to implement but will incur memory and CPU overhead to copy a large piece of text. In fact this is surely the approach a Java class will take. But we are in the Rust world. Maximum performance should be our goal.

Option 2 – Copying

The second option is not really an option because Rust simply wont allow it. I will discuss this for the sake of completeness. What if we returned a copy of the text variable. Copying a String does not allocate new memory for the text and is cheap. What if we could do this:

pub fn get_text_copy(&self) -> String {
return self.text;
}

//Call it
let my_txt = editor.get_text_copy();

Rust will not allow this. Because what this does is moves the ownership of the text member variable to the my_txt variable that receives the result. Since TextEditor methods will no longer be able to access the data using self.text this will be a terrible situation. Rust moves ownership this way to solve among other things the “Double free” problem. Without this, the String will be freed twice, once when my_txt goes out of scope and again when the editor variable goes out of scope.

Option 3 – Return a Reference

In this option we intend to return a reference pointer to the text member variable. This will be lightning fast. The reference will be immutable so that the caller has read only access. This is a perfect situation.

impl TextEditor {
//Other methods omitted ...

pub fn get_text(&self) -> &String {
return &self.text;
}
}

//Use the method
let mut editor = TextEditor::new();

editor.add_char('a');
editor.add_char('b');
editor.add_char('c');

let my_txt = editor.get_text();

println!("{}", my_txt);

This code looks easy to understand. But much is going on behind the scene. You are using the lifetime feature and you don’t even know it.

There’s a huge problem with the way we are returning a reference to the text variable. What happens if the editor variable goes out of scope and gets destroyed while the my_txt variable still lives on. Now my_txt will point to a String that has been destroyed leading to the infamous “Use after free” problem.

This is where Rust’s lifetime system comes to the rescue. Without our knowledge the compiler has injected lifetime annotations in a way that the following will be enforced – The editor variable must outlive the my_txt variable so that “use after free” is not possible.

Below we try to create a “use after free” situation. But the compiler will not let it pass.

let my_txt;

{
let mut editor = TextEditor::new();

editor.add_char('a');
editor.add_char('b');
editor.add_char('c');

my_txt = editor.get_text();
} //Variable editor gets destroyed.

println!("{}", my_txt); //Use after free. Not possible.

Lifetime Annotation

The compiler is able to inject this lifetime annotation for us because the use case is trivial. It can easily infer that the returned &String can only come from &self. Hence &self must outlive any variable that stores a reference to the returned &String. This automatic injection of lifetime annotation is called lifetime elision. The Rust compiler is able to do this in this and a few other trivial but commonly occurring situations. If we are to do this manually, the method will look like this.

impl TextEditor {
//Other methods omitted ...

pub fn get_text<'a>(&'a self) -> &'a String {
return &self.text;
}
}

Here a represents a duration or lifetime. It can be anything like b or my_grandma. Most of the time we use a, b, c etc. as lifetime names.

In get_text<‘a> we are saying that there is a lifetime named a.

In (&’a self) we are saying that &self has a lifetime of a.

In &’a String we are saying that any variable that stores the returned reference must have a lifetime of a or less.

The Borrow Checker

Believe it or not use after free is still possible even after lifetime is enforced by the compiler. This can happen if the TextEditor type itself decides to destroy the String pointed to by text member variable while my_txt is still pointing to it. Let’s add a new method to TextEditor like this:

impl TextEditor {
//Other methods omitted ...

pub fn reset(&mut self) {
self.text = String::new();
}
}

Then try to do something foolish like this:

let mut editor = TextEditor::new();

editor.add_char('a');
editor.add_char('b');
editor.add_char('c');

let my_txt = editor.get_text();

editor.reset();

println!("{}", my_txt); //Use after free. Not possible.

After reset() is called the String pointed to by my_txt is no longer valid. Fortunately, the borrow checker comes to the rescue and saves the day for us. It works as follows.

The my_txt variable borrows a reference to the text member variable. This indirectly causes a reference to the editor variable to be borrowed. (You borrow the parent if you borrow any member variable). Now, at the point where you call editor.reset() a mutable reference to editor needs to be borrowed by the method. This violates one of the borrowing rules – To be able to borrow a mutable reference there may not be any other variable borrowing that reference either mutably or immutably. So the code will not compile. Whew!

Other Languages

If you are coming from languages like Java or Objective-C you might be wondering why do you have to deal with lifetime in Rust. There are very good reasons. In Java the use after free problem is solved through the use of Garbage Collection. GC will not destroy the String as long as any variable points to it. Objective-C will employ reference counting to solve this problem. In our example there will be two references – one by the text member variable and the other by my_txt. The String will be destroyed only when the reference count goes to 0. In other words when both text and my_txt stops existing.

GC requires a heavy runtime and may incur awkward pauses. Reference counting has extra overhead and simply not reliable in all cases. Rust’s lifetime provides an elegant solution with zero runtime overhead.

Part II

Read part II here.

Advertisements

13 thoughts on “Understanding Lifetime in Rust – Part I

  1. Very good post! Being a beginner in Rust, I was having a lot of trouble understanding the concept, but this post has made it much more clear, better than the rust manual, in my opinion.

      • I absolutely agree too! You showed the simple and real example that I stays confused when someone ask me “why lifetime exists?” or “where apply lifetime?”
        Waiting for more explains about Rust world 🙂

        (One suggestion, post after/before code on gist in other posts… I love read code!)

    • Hi, reference lifetime management of Rust doesn’t use reference counting. Neither does it use garbage collection. That’s the magic of Rust.

      Rust also has reference count based management which I do not discuss here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s