What are rust lifetimes

Real world example of when we need lifetimes when we borrow data

Post coverPost cover
Quote logo
An investment in knowledge always pays the best interest. — Benjamin Franklin.
A lot of learning resources dedicated to Rust provide somewhat too trivial explanation to get the idea of why and when we need lifetimes. I personally read a couple of articles and thought "Now I know lifetimes!", but when I actually needed them I didn't have the full picture.
Generic simplest case is usually something like:
fn g<'a>(p: &'a i32) { ... }

let x = 10; 
g(&x);
When you read that you kinda understand that 'a is some constraint that requires reference to p to be alive during whole execution of g(). But if you get lost when you actually need lifetimes then let's take a look at real world example.

So you want to draw something in your app

Let's spare the details of the type of the app. It can be desktop application for any platform. Or can be cli tool that implements terminal UI and you want to draw UI there.
You will have your drawing method definition to be something like:
struct State {
    username: String,
    content: String,
}

fn draw_ui(state: State) { /* Arbitrary rawing code */ }
That is the simplest case. But you need to get your data from somewhere, right? So you will have some abstraction for it. It depends on the architecture you will pick, but it will be some form of:
struct ContentService {
    content: Mutex<String>
}
impl ContentService {
    fn observe_content(&self) -> impl AsyncStream<()> {
        /* Listens for content changes from websocket on background */ 
    }
}
So you have hypothetical service that listens for content changes from websocket and calls observation handler. Then you will update your UI like:
async fn main() {
    let user_service = UserService {};
    let content_service = ContentService {};

    for _ in content_service.observe_content().next() {
        // Aquiring a lock to synchronize access to content
        let synced_content = content_service.content.lock();

        let new_state = State {
            username: user_service.username.clone(),
            content: synced_content.clone(),
        };
        draw_ui(new_state);

        // Unlocks the lock (explicit for clarity)
        drop(synced_content);
    }
}
So following things happen here:
  • We listen for changes in ContentService
  • ContentService observes websocket on another thread
  • To synchronize data between threads we need Mutex
  • For each change we getting new content, creating State from it and drawing next frame
  • When we done, we unlocking the lock to let ContentService write new change when it has any
  • And it will work. No lifetimes here. We can do everything just by cloning data.

    But what if we need to be faster?

    In that example content is simple String, but what if it is some gigantic blob of data like bunch of images or video or text? In real world states are more like:
    struct State {
        username: String,
        awesome_4k_image_pack_in_your_feed: Vec<Image>,
    }
    
    It can easily weigh 200MB that you will ask your application to copy before rendering each frame.

    Let's go faster!

    Perhaps we can simply reference the data instead of owning it?
    fn draw_ui(state: &State) {}
    
    for _ in content_service.observe_content().next() {
        let synced_content = content_service.content.lock();
    
        let new_state = State {
            username: user_service.username.clone(),
            content: synced_content.clone(),
        };
        draw_ui(&new_state); // <== see, reference!
        
        drop(synced_content);
    }
    
    But that's not really what we need to do. We still clone all the data to create State itself. It means we need to have references within the State itself!
    struct State {
        username: String, // We still can clone username each time. It is tiny string
        big_4k_images: &[Image], // Now we *reference* images instead of cloning them
    }
    
    But we have a problem here. This reference to image is only valid while we aquired the lock to data. Once we finish the drawing, drop our lock we can't use the same reference anymore. The data will be updated by our service at any time. So we need some way to define a constraint on how long we can keep State alive. Alive? Lifetimes!
    struct State<'till_image_reference_is_safe_to_access> {
        username: String,
        big_4k_images: &'till_image_reference_is_safe_to_access [Image],
    }
    
    Now we have a constraint inside State. We can't use it anymore when reference to list of images no longer valid. Writing long lifetime names is not something Rust ecosystem tends to do so more often you'll see single-letter lifetime names.
    // That is exact quivalent of previous example
    struct State<'a> {
        username: String,
        big_4k_images: &'a [Image],
    }
    
    With these updates our UI updating code will look like:
    struct State<'till_image_reference_is_safe_to_access> {
        username: String,
        big_4k_images: &'till_image_reference_is_safe_to_access [Image],
    }
    
    fn draw_ui<'a>(state: State<'a>) {}
    
    for _ in content_service.observe_content().next() {
        let synced_content = content_service.content.lock();
        let images_ref: &'a [Image] = &synced_content; // Simply referencing the data, not copying it!
    
        // "Inherits" lifetime from `images_ref`
       let new_state: State<'a> = State {
            username: user_service.username.clone(),
            content: images_ref,
        };
        draw_ui(new_state);
        
        drop(synced_content);
    }
    
    So to somehow speed up our UI drawing code and connect it to the fact that actual data is hidden behind lock we had to introduce lifetime to our State. And this idea of specifying how long each object allowed to be alive and can be safely used is implemented deeply in Rust.

    Automatic lifetimes

    Often you don't even see lifetimes, but they still there! For example, you most likely will actually have lifetimes hidden in method signatures.
    struct State<'a> {}
    fn draw_ui(state: State) {}
    
    Lifetime 'a is still there, but you can omit it. During compilation Rust will automatically infer method signature to
    fn draw_ui<'1>(state: State<'1>) {}