Rust’s Pin and Unpin traits let you create self-referential types, which are structures where a field points to another field within the same structure.

Let’s see this in action. Imagine a Box that holds some data, and we want to store a reference to that data within the Box itself.

use std::pin::Pin;
use std::marker::PhantomPinned;
use std::mem::MaybeUninit;

struct Data {
    value: i32,
    // This reference will point to 'value'
    self_ref: MaybeUninit<*const i32>,
    // This is a marker that prevents the struct from moving after pinning
    _pin: PhantomPinned,
}

impl Data {
    fn new(value: i32) -> Self {
        Data {
            value,
            self_ref: MaybeUninit::uninit(),
            _pin: PhantomPinned,
        }
    }

    // This method *must* be called on a pinned reference
    // because it writes to self_ref, which is part of the self-referential structure.
    fn init_self_ref(self: Pin<&mut Self>) {
        // Safety:
        // - We are guaranteed that `self` is pinned and will not move.
        // - `self_ref` is initialized only once.
        // - `value` is guaranteed to live as long as `self`.
        unsafe {
            let value_ptr: *const i32 = &self.value;
            self.self_ref.as_ptr().write(value_ptr);
        }
    }

    // This method also requires a pinned reference because it reads `self_ref`.
    // If `self` were not pinned, `self_ref` could point to invalid memory.
    fn get_value_via_ref(self: Pin<&Self>) -> i32 {
        // Safety:
        // - `self` is pinned, so `self_ref` is guaranteed to be valid.
        // - The pointer stored in `self_ref` points to `self.value`, which is valid.
        unsafe {
            let ptr: *const i32 = *self.self_ref.as_ptr();
            (*ptr)
        }
    }
}

fn main() {
    // Create the data.
    let mut data = Data::new(10);

    // Pin the data. This makes it impossible to move `data` in memory.
    let mut pinned_data = Pin::new(&mut data);

    // Initialize the self-referential pointer.
    pinned_data.as_mut().init_self_ref();

    // Now we can safely access the value through the self-referential pointer.
    // Note that we must use `as_ref()` because `get_value_via_ref` takes `Pin<&Self>`.
    let value = pinned_data.as_ref().get_value_via_ref();
    println!("Value accessed via self-ref: {}", value); // Output: 10

    // If we tried to move `data` after pinning, it would be a compile-time error.
    // For example, this would fail:
    // let mut data2 = data;
    // println!("{}", data2.value);

    // IMPORTANT: `PhantomPinned` ensures `Pin`'s safety guarantees.
    // If `Data` implemented `Unpin`, `Pin::new` would do nothing, and `init_self_ref`
    // could be called on a non-pinned `&mut Data`, leading to a dangling pointer.
}

The core problem Pin solves is the immutability of pointers to data that might move. When you have a struct like Data above, where self_ref points to value, if the Data struct itself were to be moved in memory (e.g., by being copied to a new location), the value field would be at a new address, but self_ref would still point to the old, now invalid, address. This is a classic dangling pointer scenario.

Pin guarantees that the memory location of the data it points to will not change for the lifetime of the Pin. This is achieved by making Pin a "nontransportable" type (it doesn’t implement Send or Sync in a way that allows its pointer to be moved across threads or even within the same thread). The PhantomPinned marker in Data is crucial; it tells the compiler that Data is a type that should not be moved once it’s pinned.

So, how do you actually make something pinned? You use Pin::new(&mut your_data). This gives you a Pin<&mut T>. You can then call methods that require Pin<&mut T> or Pin<&T> on this pinned reference.

The Unpin trait is the inverse. If a type T implements Unpin, it means that Pin::new(&mut T) is essentially a no-op. The type is "pin-safe" by default, meaning it’s safe to move its memory around even if it contains internal pointers. Types like Box<T>, Vec<T>, and String implement Unpin if their contents (T) also implement Unpin. This is because their internal pointers are managed in a way that they are either updated correctly upon moving, or they are not self-referential in a problematic way.

The most surprising true thing about Pin is that it doesn’t prevent movement itself; it prevents uninformed movement. If you have a type that can safely be moved even with internal pointers (like a Vec), it implements Unpin, and Pin doesn’t impose any restrictions. Pin’s power comes from its ability to opt-in to immutability of memory location, enabling safe self-referential structures.

When you’re working with Pin, particularly with self-referential types, the critical invariant to maintain is that any pointer stored within the struct must remain valid for the entire duration that the Pin is active. This means you can’t have methods that take &mut self (which could potentially move self) and then also try to dereference a self_ref pointer that was initialized in a Pin<&mut self> context. You must always access the self-referential data through a Pin<&Self> or Pin<&mut Self>.

The next step after mastering Pin and Unpin is understanding how they interact with futures and the async/await construct in Rust.

Want structured learning?

Take the full Rust course →