The Problem: Self-Referential Futures

Async blocks compile into state machine structs. When a reference crosses an .await boundary, both the referent and the reference must be stored as fields in the generated struct:

async fn foo() {
    let s = String::from("hello");
    let r = &s;
    some_async_op().await;
    println!("{}", r);
}

The compiler generates roughly:

enum FooFuture {
    State0,
    State1 {
        s: String,
        r: *const String  // r points to field s
    },
    Done,
}

Field r points to field s—a self-referential struct. If the struct moves to a different address, r becomes a dangling pointer.

What “Move” Means in Rust

A move is both ownership transfer and a memcpy to a new stack location:

let a = String::from("hello");  // String struct at 0x1000
let b = a;                       // bytes copied to 0x2000, a invalidated

For most types this is fine—internal heap pointers remain valid. The String struct itself (pointer, len, capacity — 24 bytes) moves from one stack address to another, but the heap data stays at its original location.

Why Self-References Break

struct Bad {
    s: String,
    r: *const String,  // points to self.s
}
 
let a = Bad {
    s: String::from("hello"),  // s at 0x1000
    r: 0x1000 as *const String,
};
 
let b = a;  // bytes copied to 0x2000
// b.s is now at 0x2000
// b.r still contains 0x1000 — dangling pointer

The struct moved, but the internal pointer still holds the old address.

Pin as a Type-Level Contract

Pin<P> wraps a pointer type and restricts API access to prevent moves:

pub struct Pin<P> {
    pointer: P,
}

For Pin<&mut T> where T: !Unpin, there’s no safe way to get &mut T back—so you can’t call mem::swap or mem::replace. The value is stuck in place.

The Future Trait Signature

trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
    //      ^^^^^^^^^^^^^^^^^^ not &mut Self
}

This signature prevents a common footgun. Without Pin<&mut Self>, nothing would stop:

let mut fut = some_async();
fut.poll(cx);       // self-references created
let fut2 = fut;     // moved
fut2.poll(cx);      // 💥 dangling pointer

With the signature requiring Pin<&mut Self>, safe code cannot call poll at all on a raw &mut Future. You must first obtain a Pin<&mut T>.

Creating Pinned Pointers

MethodSafety
Box::pin(fut)Safe—heap allocation, address stable
pin!() macroSafe—shadows binding to prevent access
Pin::new(x) where T: UnpinSafe—type opts out of guarantees
Pin::new_unchecked(&mut x)Unsafe—caller asserts stability

Pin::new_unchecked does nothing at runtime. It’s a type cast. The unsafe means you’re claiming the address is stable—the compiler takes your word.

Safe Pinning Examples

// Heap pinning - Box guarantees stable address
let fut = Box::pin(async {
    foo().await;
    bar().await;
});
 
// Stack pinning - macro shadows the binding
let fut = async { /* ... */ };
pin_mut!(fut);  // fut is now Pin<&mut Future>

How Executors Handle Pinning

When you spawn a task:

tokio::spawn(async {
    foo().await;
    bar().await;
});

The executor pins the root future once:

fn block_on<F: Future>(fut: F) -> F::Output {
    let mut fut = fut;
    let mut pinned = unsafe { Pin::new_unchecked(&mut fut) };
    // fut lives here until block_on returns, never moves
 
    loop {
        match pinned.as_mut().poll(&mut cx) {
            Poll::Ready(val) => return val,
            Poll::Pending => park_thread_until_woken(),
        }
    }
}

Structural Pinning

Nested futures are fields inside the root struct. If the root is at address 0x1000 and never moves, field my_inner_future at 0x1008 also never moves.

MainFuture at 0x1000 (pinned by executor, NEVER MOVES)
└── my_func_future at 0x1008 (field, NEVER MOVES because parent doesn't)

The address 0x1008 is stable not because Pin enforces it mechanically, but because the parent was pinned and the field is part of the same memory block.

Why Pin::new_unchecked on Every Poll

The generated poll implementation looks like:

impl Future for MainFuture {
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        let inner: &mut MyFuncFuture = &mut self.my_func_future;
        let pinned = unsafe { Pin::new_unchecked(inner) };
        pinned.poll(cx)
    }
}

This seems redundant—why wrap on every poll? Because poll requires Pin<&mut Self>, not &mut Self. You cannot call it otherwise. The struct stores MyFuncFuture directly (no pointer), so there’s no persistent Pin to store.

Important

Each poll creates an ephemeral Pin<&mut T> to satisfy the type signature. The address 0x1008 is stable not because of the repeated Pin::new_unchecked calls, but because the root was pinned and never moves.

Why Not Store Pin<&mut T>

You cannot store Pin<&mut T> because it’s a reference:

struct MainFuture<'a> {
    my_func_future: MyFuncFuture,
    pinned: Pin<&'a mut MyFuncFuture>,  // points to field above?
}

This creates a self-referential struct—the exact problem Pin exists to handle. References need lifetimes and point to something elsewhere.

If you want to own the future and keep it pinned, use Pin<Box<T>>:

struct MainFuture {
    my_func_future: Pin<Box<MyFuncFuture>>,  // heap allocated, owned, pinned
}

This works but requires heap allocation for every nested future—expensive. The generated code avoids this by storing T directly and creating Pin<&mut T> on demand (zero-cost at runtime).

Executor Timeline Example

Consider this code:

#[tokio::main]
async fn main() {
    my_func().await;
}
 
async fn my_func() {
    read_with_epoll().await;  // wraps epoll, returns after 30s
}

T=0: Program starts

// #[tokio::main] expands to:
fn main() {
    let rt = Runtime::new();
    rt.block_on(async_main());  // PIN HAPPENS HERE
}

T=0: First poll

block_on polls MainFuture
  └─> MainFuture::poll uses Pin::new_unchecked on my_func_future field
        └─> MyFuncFuture::poll uses Pin::new_unchecked on read_future field
              └─> ReadWithEpollFuture::poll
                    - registers fd with epoll
                    - stores waker in reactor
                    - returns Poll::Pending

Thread parks until woken.

T=30s: Data arrives, waker fires

T=30s: Second poll

block_on polls MainFuture (same Pin, never moved)
  └─> MainFuture::poll
        └─> MyFuncFuture::poll
              └─> ReadWithEpollFuture::poll
                    - reads data from fd
                    - returns Poll::Ready(vec![...])

The root future is pinned once at the start. Each nested poll creates temporary Pin<&mut T> wrappers, but the underlying addresses never change.

The Contract is Social, Not Mechanical

Pin prevents accidents in safe code. It does not prevent someone from writing broken unsafe code:

let mut fut = some_async();
let pinned = unsafe { Pin::new_unchecked(&mut fut) };
pinned.poll(cx);
 
// Nothing stops you from doing this in unsafe code:
let fut2 = fut;  // moved — you broke your promise

The guarantees come from:

  • Safe APIs like Box::pin that structurally guarantee stability
  • Executor authors correctly using unsafe once in the core runtime
  • Generated async code relying on structural pinning (fields of a pinned struct are also pinned)

99% of Rust users never touch Pin directly. They write async/await, the compiler generates state machines, and executors handle pinning. The type-level contract exists so different executors can implement the same Future trait with consistent safety guarantees.

Tip

Pin is a compile-time gate. The Future trait says “give me proof you won’t move me” and Pin<&mut T> is that proof. Whether the proof is valid is either guaranteed by safe APIs or becomes your responsibility through unsafe.