Skip to Content
Course content

136: Pin and Unpin Explained

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

I've always found that the most confusing part of Rust isn't the borrow checker—it's the moment you hit Pin. For a long time, I just ignored it, treating it as "magic for async stuff," but that's a dangerous way to learn. Let's actually break something to see why it exists.

The danger of moving things

In Rust, moving a value is a bit-for-bit copy. It's fast and efficient. But what happens if a piece of data points to itself? Let's try to build a self-referential struct. I want a struct that holds a string and a pointer to that string's data.

struct SelfReferential {
    data: String,
    ptr: *const String,
}

impl SelfReferential {
    fn new(text: &str) -> Self {
        let mut s = SelfReferential {
            data: text.to_string(),
            ptr: std::ptr::null(),
        };
        s.ptr = &s.data;
        s
    }

    fn print_ptr(&self) {
        println!("Data: {}, Pointer points to: {}", self.data, unsafe { &*self.ptr });
    }
}

If I run this in a simple main function, it looks fine. I create it, I print the pointer, and it works. But here is the catch: I'm about to move this struct. In Rust, moving is the default behavior when you pass a value to a function or return it.

fn main() {
    let my_struct = SelfReferential::new("Hello Pin!");
    my_struct.print_ptr(); // Works fine.

    let moved_struct = my_struct; // The move happens here.
    moved_struct.print_ptr(); // Now we have a problem.
}

If I actually ran this with a debugger, I'd see that moved_struct.ptr is still pointing to the memory address where my_struct used to live. But my_struct is gone; its memory was shifted to a new location for moved_struct. The pointer is now dangling. It's pointing to a ghost. This is a classic memory safety violation, and it's exactly what Pin is designed to prevent.

Trying to lock it down

You might wonder why the compiler doesn't just stop me. The problem is that SelfReferential is just a struct. To the compiler, it's just bytes. It doesn't know that the ptr field has a semantic relationship with the data field.

This is where Pin comes in. Pin<P> is a wrapper around a pointer (like &mut T or Box<T>) that guarantees the data it points to will never be moved again.

But wait—if I just wrap my struct in Pin, can I still move it? Let's see. Most types in Rust are Unpin. This is a marker trait that basically says, "I don't have any self-referential pointers, so feel free to move me even if I'm pinned." Since my SelfReferential struct doesn't explicitly opt-out of Unpin, pinning it does absolutely nothing. It's like putting a "Do Not Move" sign on a piece of furniture that the movers are allowed to move anyway.

Forcing the Pin

To make Pin actually do its job, I need to tell Rust that my type is !Unpin (not Unpin). The easiest way to do this is by adding a PhantomPinned field. This is a zero-sized marker that tells the compiler: "Warning! Moving this type is dangerous."

use std::marker::PhantomPinned;
use std::pin::Pin;
use std::rc::Rc;

struct SelfReferential {
    data: String,
    ptr: *const String,
    _pin: PhantomPinned, // This makes the type !Unpin
}

Now, if I try to get a mut reference to the data inside a Pin, the compiler will stop me unless I use unsafe. It forces me to acknowledge that I'm dealing with something that cannot be moved. In a real-world scenario, like an async block, the compiler generates a state machine (a struct) that contains references to its own local variables. If that state machine moved in memory while it was awaiting a timer, those references would break. That's why Futures are pinned.

Wait, how do I actually use it?

In practice, you'll rarely implement Pin manually for your own structs unless you're writing low-level data structures. Most of the time, you'll use Box::pin(value). This puts the data on the heap and returns a Pin<Box<T>>. Because the data is on the heap, the Box itself can move around the stack, but the actual T stays put at one fixed heap address forever.

My rule of thumb: if you see Pin, someone is dealing with a pointer that depends on the object's address remaining constant. If you aren't doing that, you can safely ignore it. If you are, you're playing in the "unsafe" zone of Rust, and you'd better make sure your PhantomPinned is in place.




📋 Practical Task

Exercise: Fixing the Self-Referential Crash

Below is a buggy implementation of a PinnedBuffer. It attempts to store a value and a pointer to that value, but it ignores the Unpin trait and doesn't use Pin correctly, leading to a potential use-after-move crash.

Your Task:

  • Add std::marker::PhantomPinned to the PinnedBuffer struct to make it !Unpin.
  • Modify the main function to use Box::pin() to ensure the buffer is heap-allocated and pinned.
  • Ensure the print_value method correctly handles the pinned reference to print the value via the pointer.
use std::marker::PhantomPinned;
use std::pin::Pin;

struct PinnedBuffer {
    value: String,
    ptr: *const String,
    // TODO: Add marker here
}

impl PinnedBuffer {
    fn new(text: &str) -> Pin<Box<Self>> {
        let res = PinnedBuffer {
            value: text.to_string(),
            ptr: std::ptr::null(),
            // TODO: Initialize marker
        };
        
        // We need to pin it to the heap so we can safely set the pointer
        let mut boxed = Box::pin(res);
        
        // SAFETY: We are setting the pointer to the data within the pinned box
        let self_ptr: *const String = &boxed.value;
        unsafe {
            let mut_ref = boxed.as_mut().get_unchecked_mut();
            mut_ref.ptr = self_ptr;
        }
        
        boxed
    }

    fn print_value(self: Pin<&Self>) {
        println!("Value: {}, Pointer points to: {}", self.value, unsafe { &*self.ptr });
    }
}

fn main() {
    // TODO: Create a pinned buffer and call print_value
    // Ensure you aren't moving the buffer after it's pinned!
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.