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.

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');
}

Giving Read Access to the Text

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

OK, now we need to add a method to TextEditor that will provide read only 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.

impl TextEditor {
  //Other methods omitted...

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

A String allocates memory on the heap for the text buffer. Creating a new String by cloning the original will allocate new heap memory for the text buffer and copy all the characters into that area. This option is easy to implement and might be acceptable for small amounts of text. But it will incur memory and CPU overhead when a large piece of text is involved. 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 member variable? Copying a String to another does create a new String but the internally held text buffer is not copied. It is simply transferred over to the new String. What if we could do this:

impl TextEditor {
  //Other methods omitted...

  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 text buffer will be shared by two String instances. Both will attempt to free it when they go out of scope. 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.

fn main() {
  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 the returned reference has a lifetime of a. Any variable that received it (my_text above) must have this lifetime or less. Our code ran afoul of this rule and failed to compile. Variable my_text outlives the editor variable.

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 the 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 text member variable is the original true owner of the String. The my_txt variable simply borrows a reference to it. In Rust you also borrow the parent if you borrow any of its member variables. Which means my_text has effectively borrowed a reference to editor. Now, at the point where you call editor.reset() a mutable reference to editor needs to be borrowed by the reset() 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. We are breaking that rule. So the code will not compile. Whew!

Consider another scenario:

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

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

    let my_txt = editor.get_text();

    println!("{}", my_txt);

    editor.add_char('c'); //Error!

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

Remember, my_text is an immutable &String reference. While this borrowing is active we can not mutate the String. An object can only be mutated if there are no other borrower of any kind mutable or immutable. This gives you a taste for how race conditions are avoided in concurrent programming.

The following is strangely legal.

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

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

    let my_txt = editor.get_text();

    println!("{}", my_txt);

    editor.add_char('c'); //Allowed!
}

Here, the compiler is able to deduce that my_text is no longer used after the println!() call. The borrowing goes away. Now we can safely mutate the String.

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.

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 )

Google photo

You are commenting using your Google 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 )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.