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.
Reblogged this on leunggamciu.
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! This explained the weird annotation syntax in a way that shows how the multiple references to the lifetime works. Now it’s clear to me. Kudos.
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!)
Thanks for this! To me the lifetimes section in the Rust Book seems rushed… This should be helpful 🙂
What the difference with reference counting in rust and reference counting in objective-c?
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.
What happens when you do editor.add_char(‘d’); after printing my_txt
Awesome information in this post. Do you happen to
have an RSS feed? (Yes, some people still rely on RSS)
Thank you. The RSS2 feed is: https://mobiarch.wordpress.com/feed/