Pin<T> in Rust: Unpin & Pin Projection Explained

Max WellsMax WellsFounder of Rustify

TL;DR: Pin<P> is a wrapper that prevents the value behind a pointer from being moved in memory. It exists because async state machines compiled from async fn are self-referential; they contain pointers into themselves. If such a type were moved, those internal pointers would become dangling. Pin guarantees the value stays at a stable memory address. You rarely use Pin directly unless implementing Future by hand or working with unsafe code.


Why Does Pin Exist?

Pin exists because Rust's async state machines are self-referential; they contain references into their own fields, and moving them after creation would invalidate those references.

When the compiler transforms an async fn into a state machine, it can generate a struct where one field holds a reference to another field:

// This simplified async function...
async fn read_and_log() {
    let buf = String::new();
    let future = fill_buffer(&buf); // borrows `buf`
    future.await;
}
 
// ...compiles to roughly this state machine:
struct ReadAndLog {
    buf: String,
    future: FillBuffer<'self>, // borrows from `buf` in the same struct!
}

If this struct were moved in memory, future's reference to buf would point to the old location (undefined behavior). Pin prevents the move.


What Does Pin Actually Do?

Pin<P> wraps a pointer type P (like &mut T or Box<T>) and removes the safe DerefMut access that would allow moving the value. If T: !Unpin, the only safe way to get &mut T is via unsafe code.

use std::pin::Pin;
 
fn needs_pinned(x: Pin<&mut String>) {
    // Safe: we can read through Pin
    println!("{}", x.as_ref().get_ref());
 
    // Safe mutation via get_mut(); only available if T: Unpin
    // For !Unpin types, you need unsafe or pin projection
}
 
fn main() {
    let mut s = String::from("hello");
    let pinned = Pin::new(&mut s); // String: Unpin, so this is fine
    needs_pinned(pinned);
}

What Is Unpin?

Unpin is an auto-trait that marks types which are safe to move even after pinning. Most types are Unpin. Only special types like async state machines and manual Future implementations are !Unpin.

use std::marker::PhantomPinned;
 
// This type explicitly opts out of Unpin
struct NotMovable {
    data: String,
    self_ref: *const String, // raw pointer to `data`
    _pin: PhantomPinned,     // marks this type as !Unpin
}

For normal types (String, Vec, i32, structs of them), Pin<&mut T> is effectively the same as &mut T; you can use get_mut() safely because they're Unpin.


When Do You Encounter Pin in Practice?

You encounter Pin at three boundaries: implementing Future by hand, using tokio::pin! macro for async code, and working with async_trait or self-referential data structures.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
 
// Manual Future implementation requires Pin
impl Future for MyFuture {
    type Output = i32;
 
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<i32> {
        // self is pinned; can safely access self-referential fields
        Poll::Ready(42)
    }
}

For everyday async code, the tokio::pin! macro pins a future to the stack:

use tokio::pin;
 
#[tokio::main]
async fn main() {
    let future = some_async_fn();
    pin!(future); // pins `future` to the current stack frame
 
    // Now `future` can be polled via select! or join!
    tokio::select! {
        _ = &mut future => println!("done"),
    }
}

What Is Pin Projection?

Pin projection is the technique for going from a pinned outer struct to pinned (or unpinned) inner fields safely. The pin-project crate generates this automatically.

[dependencies]
pin-project = "1"
use pin_project::pin_project;
use std::pin::Pin;
 
#[pin_project]
struct MyFuture<F> {
    #[pin]
    inner: F,    // projected as Pin<&mut F>
    counter: u32, // projected as &mut u32 (Unpin)
}

Without pin-project, projecting fields requires unsafe. The crate eliminates that unsafety.


Frequently Asked Questions

Not usually; async fn and .await handle pinning transparently. You need to understand Pin when implementing Future by hand, using select! with futures that aren't Unpin, or working with the pin-project crate for custom async types.

Because futures generated by async fn are !Unpin. Taking Pin<&mut Self> in poll ensures the future can never be moved between poll calls; this is a requirement for safe self-referential state machines.

Pin::new(value) only works for T: Unpin. Box::pin(value) heap-allocates the value and pins it; it works for any T, including !Unpin types. Use Box::pin when you need a Pin<Box<dyn Future>>.

No; Pin in Rust is about preventing moves within Rust's memory, not about preventing the OS from moving pages. It has nothing to do with memory-mapped I/O pinning or OS virtual memory.


Sources


  • Future: Future::poll takes Pin<&mut Self> (the reason Pin exists)
  • Async/Await: async state machines are !Unpin types that require pinning
  • Box: Box::pin(future) is the common way to create a heap-pinned future
  • Unsafe: Pin projection without pin-project requires unsafe code

Keep Reading

Ready to Land a $120k+ Rust Job in the US or Europe?