@Jujumba

Your Missed Rust Class

There are two topics I struggled to grasp back when I was learning Rust:

These topics may sound scary for some people, but they are actually quite straightforward once explained. Here, I want to explain them in layman’s terms.

Pins§

Pinning in Rust ensures that an object has a stable location in memory, meaning it won’t be moved

You might wonder why this matters and how does the Pin type achieve this?

These are excellent questions. To answer them, we need to understand what Future<T> is hiding from us.

Anatomy of a Future<T>§

Future<T> is a type for representing asynchronous jobs, somewhat similar to Promise in JavaScript. The interesting part here is the internals of a Future<T>. How does it determine when it’s completed, and how is its state stored?

Before answering this, let’s take a step back and look at how to obtain a Future<T> in the first place.

You may have heard that the async keyword is a function decorator in Rust. It transforms a function in an interesting way. Suppose we have this:

1
2
3
async fn hello() -> String {
    String::from("Hello world")
}

What does async do here? It takes your function, and wraps the return type in a Future and moves body into an async block, like this:

1
2
3
4
5
fn hello() -> impl Future<Output = String> {
    async move {
        String::from("Hello world")
    }
}

The async block here generates an “anonymous structure” implementing the Future trait which is defined as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pub trait Future {
    type Output;

    // Ignore `Pin` for now
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}

The poll function drives the Future<T> forward. If the future is ready (i.e., the function has returned), it returns Poll::Ready(Output), informing the runtime that the future has completed. Otherwise, it returns Poll::Pending, which the runtime receives. In this case, the runtime may then poll other futures to allow them to progress.

In the example above, our future will return Poll::Ready immediately, as there are no await points. Let’s see an example of a more complicated future:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
use tokio::{io::AsyncReadExt, fs::File}; // feature flags: ['fs', 'io-util']

async fn foo() -> [u8; 64] {
    let mut src = File::open("foo.txt")
        .await
        .expect("damn would you create `foo.txt` for me 🥺😭??");
    let mut dst = [0u8; 64];
    let _ = src.read(&mut dst).await;
    dst
}

Let’s try to reason about it. The runtime will execute our future until it hits the first await point (opening the file). This operation won’t return the value immediately, so the runtime polls other futures. However, after some time the runtime polls our foo future again, and this time the file has been opened, so the execution continues up until the second .await point. After the read operation completed, we return our dst array.

This seems straightforward, doesn’t it? But have you noticed a potential issue? If the read operation returns early, the dst array, being a stack variable, would be deallocated, and we would return garbage data.

Futures are stateful§

To solve this problem, futures are designed as “functions with state”. For any variable that crosses a .await point, the generated future stores it in its internal state.

Let’s have a look at the actual future generated by the compiler for the example above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
print-type-size type: `{async fn body of foo()}`: 200 bytes, alignment: 8 bytes
print-type-size     discriminant: 1 bytes
print-type-size     variant `Unresumed`: 0 bytes
print-type-size     variant `Suspend0`: 87 bytes
print-type-size         padding: 7 bytes
print-type-size         local `.__awaitee`: 80 bytes, alignment: 8 bytes, type: {async fn body of tokio::fs::File::open<&str>()}
print-type-size     variant `Suspend1`: 199 bytes
print-type-size         padding: 7 bytes
print-type-size         local `.src`: 104 bytes, alignment: 8 bytes
print-type-size         local `.__awaitee`: 24 bytes, type: tokio::io::util::read::Read<'_, tokio::fs::File>
print-type-size         local `.dst`: 64 bytes
print-type-size     variant `Returned`: 0 bytes
print-type-size     variant `Panicked`: 0 bytes

To generate this for yourself, run this:

1
$ cargo +nightly rustc -- -Zprint-type-sizes

Note: You can also use the #[rustc_layout(debug) attribute in conjuction with #[define_opaque(T)] to see the layout of the future. More

There will be lots of output, search for something like {async block ..} or {async fn body of ..}.

Back to the topic. The generated future seems to be an enum with 5 variants. Only two of them are of interest. Translating it to Rust code we get this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
enum FooFuture {
    Unresumed,
    // the first .await point
    Suspend0 {
        // the `FileFuture` type is made up, but you get the idea
        file: tokio::fs::FileFuture
    },
    // the second .await point
    Suspend1 {
        src: tokio::fs::File,
        dst: [u8; 64],
        __awaitee: tokio::io::util::read::Read<'_, tokio::fs::File>
        // Pay attention to the lifetime!
    },
    Returned,
    Panicked
}

The first one (Suspend0) is a variant for our first .await. It’s pretty simple and only contains the future generated by tokio::fs::File::open::<T>().

The next variant, though, is much more interesting. Two fields are already familiar to us, as they are the variables we created: src, representing tokio::fs::File (notable 104 bytes, quite large) and the dst array of 64 bytes.

The .__awaitee field is particularly interesting. It represents the future from the read operation, and its internals are what matter: it’s 24 bytes on my x86_64 machine (where width of a poitnter is 8 bytes). It also contains two references: one to src and one to dst (a fat pointer, hence occupying 16 bytes)

Let’s make sure that this’s true by looking at the definition if the tokio::io::util::read::Read<'_, R> structure:

1
2
3
4
5
6
7
8
// in our case `R = tokio::fs::File`
pub struct Read<'a, R: ?Sized> {
    reader: &'a mut R,
    buf: &'a mut [u8],
    // Make this future `!Unpin` for compatibility with async trait methods.
    #[pin]
    _pin: PhantomPinned,
}

Yeap, in our future reader and buf fields are indeed self-references. So the lifetime in tokio::io::util::read::Read<'_, tokio::fs::File> has to be something like 'self, but as of now there is no way to express self-references with lifetimes.

All of this leads to the question of what can go wrong here and how can the Pin<P> type address the issue?

Self-referential types are subtle§

You see, self-references add a bit of spiciness: it becomes unsafe to move any structure containing them.

Let’s forget about futures for now and have a look at another self-referential struct (that’s easier to reason about):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct SelfReferential {
    s: String,
    // `p` should point to `s`
    p: *const String
}

impl SelfReferential {
    pub fn new(s: String) -> Self {
        Self { s, p: std::ptr::null() }
    }
    fn init_self_ref(&mut self) {
        let self_ref = &raw const self.s;
        self.p = self_ref;
    }
}

So, why is it unsafe to move such a structure? Well, I think an example here illustrates it better:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let mut s1 = SelfReferential::new(String::from("Fizz"));
    s1.init_self_ref();
    let mut s2 = SelfReferential::new(String::from("Buzz"));
    s2.init_self_ref();

    std::mem::swap(&mut s1, &mut s2);

    println!("{}", unsafe { &*s1.p })
}

Playground link.

What is the output? Pay attention, as we moved (swapped) these objects.

«YOUR INTENSIVE THINKING HERE»

Obviously enough, it’s Fizz. Wait, what? We swapped these structures, isn’t it supposed to be Buzz?

Nope. You see, in both structures, p contains address of the s field, and when we swap this is what happens:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
For simplicity, pretend that addr_of(s1.s) = 0xA and addr_of(s2.s) = 0xB;

Before swap:
+--------------------------+
|s1                        |
+--------------------------+
|s = "Fizz", address = 0xA |
|p -> 0xA                  |
+--------------------------+

+--------------------------+
|s2                        |
+--------------------------+
|s = "Buzz", address = 0xB |
|p -> 0xB                  |
+--------------------------+

After swap:
+--------------------------+
|s1                        |
+--------------------------+
|s = "Buzz", address = 0xA |
|p -> 0xB                  |
+--------------------------+

+--------------------------+
|s2                        |
+--------------------------+
|s = "Fizz", address = 0xB |
|p -> 0xA                  |
+--------------------------+

After the swap, ps point to the address of the string before the swap happened. Surely enough, this is unsound as it just broke our invariant: p is not a self reference anymore. If s2 dies, s1.p is a dangling reference, but we still assume that it’s a self-reference with the lifetime of s1!

One simple swap (move) can break all of this.

So the question is: how to guarantee a stable location in memory of a self-referential object?

Pin<P> and Unpin§

To solve the problem of fragility of self-referential types, a new smart pointer and an auto trait were added: Pin<P> and Unpin. The former is a type that wraps a pointer with some restrictions depending on the pointee (we’ll discuss it below), and the latter is a trait the compiler implements for every type if it’s save to move.

But why is it a smart pointer? The reason for this is pretty simple: if Pin<T> held the object directly, we would need to ensure Pin<T> itself isn’t moved, which doesn’t solve the problem. Such a type would make little sense, and as we all know

All problems in computer science can be solved by another level of indirection

So P in Pin<P> stands for pointer.

To achieve its ambitious goal, Pin<P> has the following idea in mind:

Let’s look at an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
use std::{marker::PhantomPinned, pin::Pin};

struct YouCantMoveMe {
    // `PhantomPinned` is a marker structure
    // that doesn't implement `Unpin`
    _ph: PhantomPinned
}

impl YouCantMoveMe {
    fn new() -> Self {
        Self {
            _ph: PhantomPinned
        }
    }
}

fn main() {
    let x = 42;
    let pin = Pin::new(&mut x);
    // Pin::set works for any pointee types,
    // regardless of whether or not it's okay
    // to move. The reason for this is quiet
    // simple: we destroy the previous value
    // and assign a new one, so the value
    // that's unsafe to move wasn't moved
    pin.set(43);

    let youcantmoveme = YouCantMoveMe::new();
    let pin = Pin::new(&mut youcantmoveme);
}

If you try to compile this, you should get a pretty bizzare (on the first glance) compilation error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
error[E0277]: `PhantomPinned` cannot be unpinned
    --> src/main.rs:21:24
     |
21   |     let pin = Pin::new(&mut youcantmoveme);
     |               -------- ^^^^^^^^^^^^^^^^^^ within `YouCantMoveMe`, the trait `Unpin` is not implemented for `PhantomPinned`
     |               |
     |               required by a bound introduced by this call
     |
     = note: consider using the `pin!` macro
             consider using `Box::pin` if you need to access the pinned value outside of the current scope
note: required because it appears within the type `YouCantMoveMe`

If you recall the contract of Pin<P>, it explicitly states that if we wish to pin something that isn’t Unpin, it must be dropped with the Pin<P>. Regular references allow to bypass this rule, as Pin<P> will live less than a the object. Consider this:

1
2
3
4
5
6
7
8
9
fn main() {
    let youcantmoveme = YouCantMoveMe::new();
    {
        let pin = Pin::new(&mut youcantmoveme);
        // Although `pin` dies here
        // `youcantmoveme` will be dropped later.
        // `pin` is not happy.
    }
}

If this was allowed, the contract of the Pin<P> would be violated.

Okay, we got this, but why does the compiler suggest using Box::pin? You see, Box<T> is an owning pointer: the pointee is being dropped with the box. By extension, it’ll be dropped with the pin:

1
2
3
4
5
6
7
8
9
fn main() {
    let youcantmoveme = YouCantMoveMe::new();
    {
        let pin: Pin<Box<YouCantMoveMe>> = Box::pin(youcantmoveme);
        // `youcantmoveme` was actually moved,
        // but that's fine since we didn't pin it yet
        // and now it dies with the pin itself
    }
}

In this example, the pinned variable will die when Pin will drop the box, so the contract is upheld and everybody is happy. However, have you noticed another suggestion from the compiler?

1
note: consider using the `pin!` macro

Looking at the documentation, we see this:

Constructs a Pin<&mut T>, by pinning a value: T locally.

What, local pinning? That’s some new terminology.

Let’s look at the definition of the macro, maybe it’s going to be more insightful:

1
2
3
4
pub macro pin($value:expr $(,)?) {
    // <a large interesting comment goes here, read it youself...>
    $crate::pin::Pin::<&mut _> { __pointer: &mut { $value } }
}

The crucial part are braces around $value. Shortly, these braces move $value to the scope of the pin, making it inaccessible for everyone else. This works since now no one except the created pin will have access to the value, and the value’ll die alongside the pin.

If the paragraph above isn’t convincing, I encourage you to read the large comment I ommited, it has a lot of text for such a short macro :)

Conclusion§

Hopefully this wasn’t hard and even eye-opening for someone. Although the pinning topic in Rust is pretty straighforward, a lot of people struggle understanding it. With this foundation, you can explore more on your own, as I didn’t cover some topics (like pin projection). I advise you by starting from a brilliant video from Jon, the chapter on pinning from the async book (or even better, spend some time and read the whole book). The documentation of the pin module is also quite comprehensive, give it a read!

Subtyping§

Subtyping and variance in Rust are all about lifetimes and making our lives easier. There are three “types” of variance (for types Child and Parent, where Parent is the parent of Child):

Let’s start with covariance and its definition, but before, a bit of notation:

‘a defines a region of code.

’long <: ‘short means that ’long defines a region of code that completely contains ‘short.

This is taken from then excellent chapter from The Rustonomicon which the rest of section on variance will follow. The Nomicon’s explanation, though, can be quite challenging for beginners — I know students who didn’t understand it at all

Covariance§

Immutable references are covariant over their lifetime. This means that we can “downcast” &'long T to &'short T. Here is a small example from the nomicon to make this clear:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Note: debug expects two parameters with the *same* lifetime
fn debug<'a>(a: &'a str, b: &'a str) {
    println!("a = {a:?} b = {b:?}");
}

fn main() {
    let hello: &'static str = "hello";
    {   // 'world begins here
        let world = String::from("world");
        let world = &world; // 'world has a shorter lifetime than 'static
        debug(hello, world);
    }
}

If there were no covariance, this code wouldn’t have compiled: &'static str is not of the same type as &'world str. However, it compiles perfectly well without any warning, because the hello string was “downcasted” to a shorter lifetime. This absolutely makes sense, if we think logically: if an immutable reference lives for some long lifetime, we can downcast it to some shorter lifetime, as the variable will still be around.

Pretty simple, huh? And that’s it. But did you notice my emphasis on immutability? Why can’t this rule be extended for mutable references as well?

Invariance§

Here is the interesting part. Mutable references are invariant over their lifetime, meaning that there is no subtyping relationship. Before explaining anything, let’s assume that a covariance relationship exists, meaning that if there is &'long mut T, we can assume it’s similar to &'short mut T. The code is again taken from the nomicon chapter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn assign<T>(input: &mut T, val: T) { // (1)
    *input = val;
}

fn main() {
    let mut hello: &'static str = "hello";
    {   // 'world begins here
        let world = String::from("world");
        assign(&mut hello, &world); // (2)
    }
    println!("{hello}"); // (3)
}

What does happen here? Let’s first pay attention to the function signature of assign (1). It accepts a mutable reference to an object of type T, and an object of that type. So, types here must be absolutely equal. Now, at (2), they differ: we have hello which lives longer than world, so we downcast &'static str to &'world str to have two arguments of the same type (T = &'world str, I know it’s a bit confusing).

That’s exactly where the catastrophy happens! Shortening lifetime of a mutable reference allows us to write a value which doesn’t live long enough. In our example, world string is dropped at the end of its scope, though hello lives and points to it. Now, at (3) we end up with a dangling reference which is dereferenced to print the string…

This’s it, the entire reason why mutable references are invariant. Not so hard, isn’t it?

Contravariance§

This is my favorite part. The only source of contravariance in Rust are function pointers. Specifically, arguments of function pointers.

What does that mean? Well, let’s see. Assume we have this function

1
2
3
fn print_static_str<'a>(input: &'static str) {
    println!("{}", input);
}

Its function pointer type is fn(&'static str). So, this is a function pointer that accepts a string slice of any lifetime. Now, why is this contravariant? If you recall, “child type” of an immutable reference is a reference that lives shorter. This means that we are free to substitute something like fn(&'short str) instead of fn(&'long str) (if 'long overlives 'short, of course). WHY?

Again, let’s have a look at the code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
fn print_str<'a>(input: &'a str) {
    println!("{}", input);
}
fn print_static_str(input: &'static str) {
    println!("{}", input);
}

fn main() {
    let mut x: fn(&'static str) = print_static_str; // (1)

    x("hello!")

    x = print_str; // (2) Compiles

    // Type of `x` is still fn(&'static str)
}

We have two functions, one takes a string slice of any lifetime, and the other one only static string slices. Variable x is of type fn(&'static str). Then we call it and reinitialize at (2)

What happens with print_str, is that it gets “upcasted”, from

1
for<'a> fn(&'a str) // `for<'a>` means for any lifetime

to

1
fn(&'static str)

Reasoning about this should eventually lead to the conclusion that this is okay, as print_str is intended to be used with string slices of any lifetimes (with short lifetimes, in particular). However, x is of type fn(&'static str), so we know a static string slice will always be passed to it. Therefore, print_str will always have a 'static str, and as we know 'static outlives any other lifetime, or using the nerdy notation we defined: for<'a> 'static <: 'a.

This rule is tightly coupled with the covariance property of immutable lifetimes.

There is more to function pointers§

If you read the nomicon chapter I referenced earlier, there is a small section about covariance of function pointers. Wait, aren’t they contravariant? Well, again, function pointers are contravariant over input arguments. Covariance here, though, is about return types. The same stuff in essence, but just flipped :)

Let’s assume we have the following functions:

1
2
3
4
fn get_static() -> &'static str;
// and
fn get_str<'a>() -> &'a str;
// pretend that we can write such a signature...

And here is the example code:

1
2
3
4
5
fn main() {
    let mut x: for <'a> fn() -> &'a str = get_str;

    x = get_static;
}

This is fine because the static string slice returned from get_static will be treated as &'a str. After all, we already concluded that “downcasting” immutable references of a long lifetime to a shorter one is okay (and, again, &'static outlives anything).

Conclusion§

This section may seem like a breather at first glance, but it’s quite interesting in practice. I highly recommend reading the chapter from the nomicon, as it covered a bit more that I did here.