Skip to content
advanced Phase 13 · Async Rust with Tokio

Pin and Unpin Explained

See why self-referential futures need pinned memory locations to poll safely.

55m
0 problems
Topic Progress 0%

Why a Future That Points Into Itself Cannot Move After First Poll

Ordinary Rust structs cannot point into themselves. let s = String::from("hi"); let r = &s[..]; is fine, but move s and r dangles — the borrow checker exists precisely to stop that. Compiler-generated futures do it anyway, and understanding why is the whole topic.

async fn summarize(body: String) -> usize {
    let head = &body[..64];      // reference INTO the state machine's own field
    let score = rate(head).await; // `head` must stay valid across this await
    score + head.len()
}

body and head are both live across the .await, so the lowering stores them as fields of the future itself, and head is a pointer to a sibling field. The moment the first poll runs, the future's address becomes part of its own state. Move it — mem::swap, a Vec reallocation, passing it by value — and head still points at the old location: use-after-free, in safe-looking code.

The standard library documents the lifecycle explicitly (std::pin):

Phase Address rule
After calling the async fn, before first poll Freely movable — only arguments were moved in
First poll The value starts depending on its address not changing
Every later poll Internal unsafe code assumes the address is still the original
Drop Must run before the memory is reused (the Drop guarantee)

And it names our case outright: "A key example of such self-referential types are the state machines generated by the compiler to implement Future for async fns."

This is why the trait you have been calling since async fn and await looks odd:

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

self: Pin<&mut Self> — not &mut self — encodes the promise you may poll me, but you may not move me out of this slot while I am suspended. Safe code cannot manufacture that move either: Pin<&mut T> deliberately withholds DerefMut when T: !Unpin, so mem::swap(&mut *pinned, &mut other) fails to compile — you never get the &mut T needed to swap, replace, or take the value out.

A concrete picture helps. Before the first poll, the future is a struct of loose parts:

future { body: String, head: Option<&str>, score: u32 }   <- movable: nothing points inward
        |
     first poll
        v
future { body: String, head: &str --> points into `body`, score: u32 }
        <- moving this struct now invalidates `head`

The historical detail interviewers enjoy: Pin shipped in Rust 1.33 (March 2019) and async/.await followed in 1.39 — pinning landed first, because without it the compiler's self-referential state machines could not be exposed through a safe poll.

Two clarifications interviews ask for:

Why not just heap-allocate every future? Box it and the address is stable — but composition nests futures several layers deep, and one allocation per layer (per combinator, per await chain) is exactly what Rust refused to pay. Pin is a type-level contract with zero runtime cost: no vtable, no flag, no check on poll.

Is it the compiler's magic? No. Pinning is a library contract between unsafe code and its users; the compiler does not track addresses. Safe code cannot violate it simply because every API that could move the value requires unsafe or an Unpin bound. Getting that boundary wrong is unsafe code's job — which is where The unsafe Keyword comes in.

Reading the Pin API: What the Wrapper Promises and What Unpin Gives Back

Pin<Ptr> wraps a pointer; it pins the pointee, never itself. Pin<Box<T>> can be moved, cloned into a variable, passed across threads — the box moves, the T inside does not. That distinction resolves most of the confusion.

let mut boxed: Pin<Box<Unmovable>> = Box::pin(Unmovable::new());
let mut local = Unmovable::new();          // still movable: not pinned yet
let pinned: Pin<&mut Unmovable> = std::pin::pin!(local);  // stack pin (stable 1.68)

Unpin is the escape hatch: an auto-trait meaning this type is fine with moving, so Pin imposes nothing. When T: Unpin, Pin<Box<T>> behaves exactly like Box<T> and Pin<&mut T> like &mut T. Most types — String, Vec, Box<T>, references, plain structs — are Unpin, which is why pinning disappears from daily code until you touch futures.

Value Unpin? May the pointee move?
String, Vec<T>, Box<T> yes yes — no restrictions
Pin<Box<T>> where T: !Unpin yes (the pointer is movable) no
A self-referential async future, polled no no, until dropped
A struct containing PhantomPinned no (opted out) no

Three ways to get a pin, and their costs:

let a: Pin<Box<F>> = Box::pin(fut);        // heap: stable for the future's whole life
let b = std::pin::pin!(fut);               // stack: pinned until the scope ends
let c = tokio::pin!(fut);                  // stack pin used inside tokio macros

Access rules follow from Deref/DerefMut:

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
    let this = self.get_mut();      // OK only because Countdown: Unpin
    if this.remaining == 0 { return Poll::Ready(0); }
    this.remaining -= 1;
    Poll::Pending
}
  • as_mut() — reborrows as Pin<&mut T>, cheap, used to poll repeatedly.
  • get_mut() — yields &mut T, requires T: Unpin; otherwise compile error: the trait bound is not satisfied for Pin<&mut T> to implement DerefMut.
  • get_unchecked_mut() / Pin::new_unchecked — the unsafe door, where you promise not to move the value. You will write these only when implementing address-sensitive types.
  • into_inner() — consumes the pin, requires Unpin, returns the value for normal use.

Making a normal type refuse to move takes three lines:

use std::marker::PhantomPinned;

struct Session {
    id: u64,
    _pin: PhantomPinned,      // opts the type OUT of Unpin
}

With that field, Pin::new(&mut s) and mem::swap both stop compiling for Session, and any API promising stability can be trusted. One ergonomic concession survives: Pin::set(&mut pinned, new_value) is allowed — it runs the old value's destructor and then writes in place, so the address never changes and the Drop guarantee holds. Assigning through a pin is fine; moving out of one is not.

Operation Needs Unpin?
pin.as_mut(), pin.as_ref() no — reborrow as a pin
pin.get_mut() / DerefMut yes
pin.set(value) no — drops in place, then writes
pin.into_inner() yes — releases the value
unsafe { pin.get_unchecked_mut() } no — you take responsibility

One more invariant worth quoting: the Drop guarantee. Once a value is pinned, its memory must not be reused until its destructor has run — not just "not moved." That rules out sloppy patterns like pinning inside a ManuallyDrop (the destructor will never run, so the storage can be reused while pin-dependent code still trusts it), and it is why Vec::set_len-style tricks on pinned contents are unsound.

Self-check: if you can mem::replace the value out through your own API, pinning cannot be structural for it. The std docs call this structural pinning — whether a field is pinned when its parent is — and getting it wrong is the classic unsoundness in hand-written futures and combinators.

Where Pin Shows Up in Everyday Async Code

You will rarely write unsafe { Pin::new_unchecked(..) }. You will meet Pin in four ordinary places — recognize each on sight.

1. Manual polling. Testing a future or driving one inside a sync function:

let mut fut = Box::pin(load_config("app.toml"));
let mut cx  = Context::from_waker(noop_waker_ref());
match fut.as_mut().poll(&mut cx) {
    Poll::Ready(cfg) => println!("{cfg}"),
    Poll::Pending    => println!("needs an executor"),
}

Box::pin is required because poll demands Pin<&mut Self>, and Pin::new(&mut fut) only compiles when the future is Unpin. An async fn future is Unpin only if everything it carries across an .await is Unpin and none of it points back into the future — the instant it holds a self-borrow (like the head reference from the first chapter), it is !Unpin. Assume !Unpin until the compiler says otherwise.

2. Reusing a future in select!. Each loop iteration re-polls the same timer through &mut, and &mut F: Future holds only for F: Unpin:

let sleep = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(sleep);                    // makes `&mut sleep` pollable
loop {
    tokio::select! {
        _ = &mut sleep => break,
        job = queue.recv() => process(job),
    }
}

Tokio's select! documentation says it plainly: passing a future by reference requires it to be Unpin, achieved with Box::pin or stack pinning. Note the macro itself accepts non-Unpin futures when you hand it ownership — the pinning happens inside the macro-generated state; the &mut reuse pattern is what forces the bound.

3. Storing futures. A struct field, a Vec, or a channel of work items: Box<dyn Future<Output = T> + Send> is not enough (poll still needs Pin), so real code writes Pin<Box<dyn Future<Output = T> + Send>>, usually aliased:

type Task = Pin<Box<dyn Future<Output = io::Result<usize>> + Send>>;
let queue: Vec<Task> = jobs.map(|j| Box::pin(run(j)) as Task).collect();

4. The errors. Keep this table; each row is a real compiler answer:

You wrote Compiler says Fix
Pin::new(&mut fut) Unpin bound not satisfied Box::pin / pin!
_ = &mut sleep in select! &mut F only implements Future when F: Unpin tokio::pin!(sleep)
mem::swap(&mut *pinned, ..) cannot move out of a pinned value (DerefMut needs Unpin) don't move pinned data
self.get_mut() in poll for !Unpin type Pin<&mut Self> does not implement DerefMut get_unchecked_mut() + safety comment

Two habits make this concrete rather than theoretical. First, probe a type instead of guessing:

fn assert_unpin<T: Unpin>(_: &T) {}
assert_unpin(&std::future::ready(42));                       // Ready<T>: plain struct -> Unpin
assert_unpin(&tokio::time::sleep(Duration::from_secs(1)));   // compile error: Sleep: !Unpin

The compiler answers the Unpin question instantly — no runtime check exists to ask — and for an async fn future the answer is conditional: all-Unpin state with no self-borrow usually passes, a self-borrow across an await does not. Second, remember who pins on your behalf: tokio::spawn moves the future into the task's heap allocation already pinned, and macros like join!, select!, and timeout pin their operands inside the state they build. The requirement surfaces only when you keep the future: polling by hand, &mut reuse, or storing it — where the conventional type is Pin<Box<dyn Future<Output = T> + Send>> (the BoxFuture alias from the futures crate), created with exactly one call: Box::pin.

The interview answer in one paragraph: futures are self-referential state machines, so after the first poll they must not move; Pin encodes that promise in the type with no runtime cost, Unpin opts ordinary types out of the restriction, and Box::pin/pin! are how you satisfy poll's Pin<&mut Self> receiver. Practical consequence: tokio::spawn pins internally for you, but anything you keep — manual polls, &mut reuse, stored futures — needs explicit pinning.

For the unsafe half of this contract — new_unchecked, get_unchecked_mut, and the safety comments they demand — continue to The unsafe Keyword. For the cancellation story that made dropped futures worth reasoning about, revisit Timeouts, Cancellation, and Async I/O.

Quiz

1. When does an async fn's future become address-sensitive?

Question 1 options

2. You have `let p: Pin<Box<Unmovable>>` where Unmovable: !Unpin. Which statement is correct?

Question 2 options

3. Inside a hand-written `poll(self: Pin<&mut Self>, ..)` you call `self.get_mut()` on a type that is !Unpin. What happens?

Question 3 options

4. Why must `&mut sleep` work across loop iterations of a select! while a raw async block passed by reference does not?

Question 4 options

Flashcards

Question

Why does a polled future need to stay at a fixed address?

Answer

Because it can be self-referential: locals live across an await are stored as fields and references into those fields are stored as values, so after the first poll a move would leave those pointers dangling. std::pin calls such values address-sensitive, and names compiler-generated Future state machines as the key example.

Question

What exactly does Pin guarantee, and what does it pin — the wrapper or the pointee?

Answer

From the moment a Pinning pointer is created, the pointee stays valid at that same address until its drop runs (the Drop guarantee). It pins the pointee: Pin<Box<T>> can be moved freely because moving the box does not move the T; only operations that would move the T are restricted.

Question

What is Unpin, which types have it, and how do you opt out?

Answer

Unpin is an auto-trait meaning the type has no address-sensitive state, so Pin imposes no restriction — primitives, String, Vec, Box<T>, references and most structs have it. Opt out by adding a PhantomPinned field (or !Unpin inner types); then Pin's rules apply and you need unsafe to move the value.

Question

Is an async fn future Unpin, and when does it matter in practice?

Answer

Only if everything it carries across awaits is Unpin and nothing points back into the future — a self-borrow across an await makes it !Unpin. It matters whenever YOU keep the future: manual poll (needs Pin<&mut Self>), reusing it via &mut in select!, or storing it; tokio::spawn and join! take ownership and pin internally.

Question

Compare Box::pin and std::pin::pin! for making a future pollable.

Answer

Box::pin allocates on the heap and returns Pin<Box<F>> — stable for the future's whole life and storable/'static, at the cost of an allocation. pin! (and tokio::pin!) pins a value in place on the stack, returning Pin<&mut F> valid until the scope ends — zero allocation, but the future cannot leave the scope. Both satisfy poll's Pin<&mut Self> receiver.

Revision Notes

Key Takeaways

  • 1. A future is freely movable until its first poll; from then on its state may point into itself, so the address must not change
  • 2. Pin is a zero-cost library contract on the pointee — no compiler magic, no runtime check — and the Drop guarantee covers the memory too
  • 3. Unpin cancels every restriction; most types are Unpin, and PhantomPinned is how a type opts out
  • 4. Box::pin buys a permanent heap address, pin! buys a stack address valid to end of scope; both satisfy poll's Pin<&mut Self>
  • 5. Everyday symptoms are compile errors: Pin::new, &mut reuse in select!, and get_mut in poll all fail their Unpin bound on !Unpin futures

Interview Tips

  • • "Why does Future::poll take Pin<&mut Self>?" — async fn state machines are self-referential; after first poll a move dangles; Pin encodes don't-move-me in the type at zero cost
  • • "What is Unpin and why do most people never see it?" — auto-trait meaning moving is harmless; almost every everyday type has it, so Pin is transparent until you hold a self-borrowing future across an await
  • • "Box::pin or stack pin?" — heap: stable, storable, 'static, one allocation; stack: free but scoped to the block; mention Pin<Box<T>> stays movable while the pointee does not
  • • "Give an example of code that fails without pinning" — &mut fut in select! (impl Future for &mut F needs Unpin) or self.get_mut() in a manual poll (DerefMut needs Unpin), then name the fix: tokio::pin! / Box::pin / get_unchecked_mut with a safety comment

Cheat Sheet

Pin and Unpin — Cheat Sheet

The problem: an async fn's state machine can hold references into its own fields; after the first poll, moving it dangles. poll(self: Pin<&mut Self>) forbids the move at the type level.

Lifecycle: movable until first poll → address fixed while polled → memory reused only after drop runs (Drop guarantee).

Core types:

let b: Pin<Box<F>>  = Box::pin(fut);   // heap pin, stable for the future's life
let s = std::pin::pin!(fut);           // stack pin (1.68+), lives to end of scope
let t = tokio::pin!(fut);              // stack pin used by tokio macros
pin.as_mut().poll(cx);                 // reborrow and poll repeatedly
pin.get_mut()                          // &mut T — requires T: Unpin
pin.get_unchecked_mut()                // unsafe: you promise no move

Unpin: auto-trait meaning "moving me is always fine." Implemented by String, Vec, Box<T>, references. Opting out needs PhantomPinned. Pin<Box<T>> itself is Unpin — the pointer may move, the pointee may not.

Situation Unpin?
plain values, combinators over them yes
async fn future with only Unpin parts, no self-borrow yes
async fn future holding a self-reference across an await no
tokio::time::Sleep, custom Future with PhantomPinned no

Error → fix:

`Unpin` bound not satisfied (Pin::new / &mut fut in select!)  -> Box::pin or pin!
cannot move out of a pinned value (mem::swap through Pin)     -> never move pinned data
Pin<&mut Self> does not implement DerefMut (get_mut in poll)  -> get_unchecked_mut + SAFETY

Continue Learning