Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I'll be honest with you: Pin is one of those concepts in Rust that feels like a brick wall until you see exactly what it's preventing. Most of the time, the compiler handles this for you behind the scenes of an async block, but when you start implementing your own Future or dealing with low-level async primitives, you have to face the music.

The core issue is self-referential structs. In Rust, moving a value usually just means copying bytes to a new memory location. That's fine for an integer, but if your struct contains a pointer to one of its own fields, moving that struct makes the pointer point to the old, now-invalid memory address. This is exactly what happens inside the state machines generated by async blocks.

Creating a self-referential future

Let's build a small Future that manually tracks a piece of data. I want this future to hold a buffer and a pointer to that buffer. In a real-world scenario, this happens when an async function has a local variable and then takes a reference to it across an await point.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::marker::PhantomPinned;
use std::ptr;

struct MySelfReferential {
    data: String,
    pointer_to_data: *const String,
    _pin: PhantomPinned, // This tells Rust the type is !Unpin
}

impl MySelfReferential {
    fn new(text: &str) -> Self {
        Self {
            data: text.to_string(),
            pointer_to_data: ptr::null(),
            _pin: PhantomPinned,
        }
    }

    fn init(self: Pin<&mut Self>) {
        let this = unsafe { self.get_unchecked_mut() };
        this.pointer_to_data = &this.data;
    }
}

Notice the PhantomPinned. I added that because, by default, almost everything in Rust is Unpin (meaning it's safe to move). Since I'm intentionally creating a self-referential structure, I need to opt-out of that safety.

The mistake: Moving the Future after initialization

Here is where I usually trip up when I'm prototyping this. I'll initialize the pointer, and then I'll move the object—perhaps by pushing it into a Vec or passing it to another function—without realizing I've just invalidated my pointer.

// This is a conceptual example of the bug I often hit
let mut my_fut = MySelfReferential::new("Hello Pinning");

// We pin it to the stack to initialize it
let mut pinned_fut = unsafe { Pin::new_unchecked(&mut my_fut) };
pinned_fut.as_mut().init();

println!("Pointer before move: {:?}, Data: {:?}", pinned_fut.pointer_to_data, &pinned_fut.data);

// MISTAKE: If I were to move 'my_fut' now, 
// pointer_to_data would still point to the old address of 'my_fut'.
// The compiler stops us if we try to move a Pin, but if we bypass it 
// with unsafe or forget that we're dealing with raw pointers, we crash.

In a real async executor, if the executor moves your Future to a different thread or a different spot on the heap after it has already started polling (and thus initialized its internal references), your program would enter the realm of Undefined Behavior. That's why poll takes Pin&mut Self instead of just &mut Self.

Locking memory with Pin and Box

To actually use this in a way that's safe, we need to ensure the memory address never changes. The easiest way to do this is by putting the value on the heap using Box::pin. This pins the data to a specific heap location; even if the Pin wrapper itself is moved, the data it points to stays put.

impl Future for MySelfReferential {
    type Output = String;

    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        // We can't just use 'self' because we need to access the fields.
        // Since we know we are pinned, we can use get_unchecked_mut
        let this = unsafe { self.get_unchecked_mut() };
        
        if this.pointer_to_data.is_null() {
            // Initializing the self-reference on the first poll
            this.pointer_to_data = &this.data;
            Poll::Pending
        } else {
            // Now it's safe to dereference because the memory is pinned
            let value = unsafe { &*this.pointer_to_data };
            Poll::Ready(value.clone())
        }
    }
}

#[tokio::main]
async fn main() {
    // Box::pin puts the future on the heap and pins it there.
    let fut = Box::pin(MySelfReferential::new("Pinned and Safe"));
    
    let result = fut.await;
    println!("Result: {}", result);
}

By using Box::pin, we've guaranteed that MySelfReferential will never move again. The pointer we stored during the first poll call remains valid for the entire lifetime of the future. This is the "secret sauce" that allows Rust's async system to be so efficient—it can store references to local variables within the future's own state without risking memory corruption.




📋 Practical Task

Implement a Pinned Self-Referential Buffer

Your task is to create a struct called BufferFuture that simulates a data buffer. The struct should contain a Vec<u8> and a raw pointer ptr: *const u8 that points to the first element of that vector.

  • Implement the Future trait for BufferFuture.
  • In the first call to poll, the future should initialize the ptr to point to the start of the vector and return Poll::Pending.
  • In the second call to poll, the future should dereference the ptr to read the first byte and return it as Poll::Ready(u8).
  • Ensure the struct is !Unpin by using PhantomPinned.
  • In your main function, use Box::pin to execute the future and print the resulting byte.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.