← See all issues

Shuttle Launchpad #4: Ownership, Lifetimes, and the big win!

Welcome to Shuttle Launchpad issue #4! In this issue, we want to take some more time to talk about ownership and lifetimes. In particular, we will work with an example that uses explicit lifetimes. All that is packed in a simple example that generates random numbers for the lottery. Try it out, and maybe you win the jackpot! Let's get started!

A Lotto Number Generator

We want to implement a simple lottery number generator API. As always, we use Shuttle to create a new project.

$ cargo shuttle init

Pick Axum for your framework, or choose the one you're most familiar with. Since we create random numbers, let's add the rand crate to our dependencies (with the small-rng feature as it is required for the bulk of this issue).

$ cargo add rand -F small-rng

Let's start with the Lotto struct. The struct contains a pot, a vector with all the lotto numbers from 1 to an upper range. We also need a random number generator, and we use the SmallRng generator from the rand package for that.

use rand::rngs::SmallRng;

struct Lotto {
    pot: Vec<u32>,
    rng: SmallRng,
}

In the implementation block, we create a constructor function new. We take an upper limit for the pot, and use the inclusive range (1..=pot_size) to collect numbers from 1 to the upper limit. We pass the random number generator as it is. Since we don't have any reference annotations, Lotto will take ownership over the random number generator.

The take method takes a mutable reference of the instance. Meaning that this method is only callable if the instance is mutable. We shuffle the pot, take the first amount of numbers, and return them as a vector. Since the lotto instance owns the vector, we need to clone the numbers, because we might want to reuse the struct again. Otherwise, we would move the vector out of the struct, and the struct would be empty. We don't want that (and thankfully, Rust will most likely complain).

impl Lotto {
    fn new(pot_size: u32, rng: SmallRng) -> Self {
        Self {
            pot: (1..=pot_size).collect(),
            rng,
        }
    }

    fn take(&mut self, amount: usize) -> Vec<u32> {
        self.pot.shuffle(self.rng);
        self.pot.iter()
            .take(amount)
            .map(|e| e.to_owned()).collect()
    }
}

That's already a big part of what we want to achieve today. But what if we don't want to create new random number generators for every time we create lotto numbers? What if we want to reuse a single small random number generator SmallRng over and over again? If we want to reuse the random number generator, we need to pass it as a mutable reference. But if we change the struct, Rust will complain!

// NOTE: THIS WON'T COMPILE!
struct Lotto {
    pot: Vec<u32>,
    rng: &mut SmallRng,
}

impl Lotto {
    fn new(pot_size: u32, rng: &mut SmallRng) -> Self {
        Self {
            pot: (1..=pot_size).collect(),
            rng,
        }
    }

    // ...
}

Rust tells us, that we need to have lifetime annotations. In Rust, lifetimes describe how long a certain value is kept in memory. The Rust borrow checker then figures out if references "live long enough". If they don't you get a compilation error. With that, Rust knows when to allocate memory and when to get rid of it.

Lifetime annotations are everywhere, but you usually don't have to write them. In that case, Rust needs a little help from you. It needs to be notified that there is a lifetime that we need to take care of, and you need to annotate the right field with it. You do that by using a generic lifetime parameter to the struct and field.

struct Lotto<'a> {
    pot: Vec<u32>,
    rng: &'a mut SmallRng,
}

And then we need to add the same lifetime annotation to the implementation block as well.

impl<'a> Lotto<'a> {
    fn new(pot_size: u32, rng: &'a mut SmallRng) -> Self {
        Self {
            pot: (1..=pot_size).collect(),
            rng,
        }
    }

    // ...
}

Rust now has all the information it needs. And we can create the lotto handler function which we wire up to Axum.

use axum::{extract::Path, response::IntoResponse, Json};

async fn handler_lotto(
    Path((pot_size, amount)): Path<(u32, usize)>
) -> impl IntoResponse {
    let mut rng = SmallRng::from_entropy();
    let mut lotto = Lotto::new(pot_size, &mut rng);
    let results = lotto.take(amount);
    Json(results)
}

Fantastic! But we still create a new random number generator for every call. Let's create one outside the router and add it as an extension to your router. We now have the problem that we need to share the random number generator between threads. To achieve that, we use a Mutex to guarantee safe writeable access from multiple threads. Random number generators need to be mutable as they start from a seed value and change the internal state with every call. Please make sure you use the tokio::sync::Mutex.

We also need to wrap the random number generator in an Arc. Arc is short for atomic reference counter. Since we have shared access to the random number generator, we need to make sure that we take care of all references in all threads. The Arc will take care of that for us. It counts up with every clone (which happens with every invocation), and then once the shared state goes out of scope, it counts down again. The original data stays intact and will only be discarded once the internal counter reaches zero. Basically a simple version of garbage collection.

use std::sync::Arc;
use tokio::sync::Mutex;
use axum::{Router, routing::get, Extension};

type SharedState = Arc<Mutex<SmallRng>>;

#[shuttle_runtime::main]
async fn axum() -> shuttle_axum::ShuttleAxum {
    let state =
         Arc::new(Mutex::new(SmallRng::from_entropy()));
    let router = Router::new()
        .route("/lotto/:pot/:amount", get(handler_lotto))
        .layer(Extension(state));
    Ok(router.into())
}

To wire our shared state up, we add an Extension extractor to our handler function.

async fn handler_lotto(
    Path((pot_size, amount)): Path<(u32, usize)>,
    Extension(state): Extension<SharedState>,
) -> impl IntoResponse {
    let mut rng = state.lock().await;
    let mut lotto = Lotto::new(pot_size, &mut rng);
    let results = lotto.take(amount);
    Json(results)
}

Fantastic! Shared random numbers for every call. Try it out by curling:

$ curl localhost:8000/50/5

Good luck playing!

Stretch goal: Make it generic!

But wait a second, we can do a little more. See this as a stretch goal for advanced developers. We can make our Lotto struct generic over the random number generator. That way we can use any random number generator we want. We just need to make sure that the random number generator implements the RngCore trait. That's a trait from the rand crate that is implemented for every random number generator.

Create a new generic type parameter RngCore and make sure it implements the RngCore trait. This is called a trait bound.

struct Lotto<'a, R: RngCore> {
    pot: Vec<u32>,
    rng: &'a mut R,
}

impl<'a, R: RngCore> Lotto<'a, R> {
    fn new(pot_size: u32, rng: &'a mut R) -> Self {
        Self {
            pot: (1..=pot_size).collect(),
            rng,
        }
    }

    //...
}

The only thing we need to change in our handler function is to annotate the Lotto struct with this new information.

💡 Question: Why do we need to annotate the Lotto struct? Come to our Discord and chat with us about it!

async fn handler_lotto(
    Path((pot_size, amount)): Path<(u32, usize)>,
    Extension(state): Extension<SharedState>,
) -> impl IntoResponse {
    let mut rng = state.lock().await;
    let mut lotto: Lotto<'_, SmallRng> =
        Lotto::new(pot_size, &mut rng);
    let results = lotto.take(amount);
    Json(results)
}

Now you can re-use Lotto for every possible random number generator from the rand crate!

Time for your feedback!

We want to tailor Shuttle Launchpad to your needs! Give us feedback on the most recent issue and your wishes here.

Join us!

Shuttle has a very active community. Join us on Discord, star us on GitHub, follow us on Twitter, and watch out for video content on YouTube.

If you have any questions regarding Launchpad, join the #launchpad channel on Shuttle's Discord.

Shuttle Beta: Shuttle reached Beta! Celebrate with us!

Launchpad Examples: Check out all Launchpad Examples on GitHub.

The Rust Borrow Checker - A Deep Dive: Nell Shamrell-Harrington explains the nitty gritty details of lifetimes and the borrow checker.

Bye!

That's it for today. Get in touch with us and let us know what you want to see!

-- Stefan and your friends from Shuttle