Pin<&mut T> & Unpin: Self-Referential Structs and Async Futures
- Last updated
- Prerequisites:
- Rust ownership, references, and move semantics
- Understanding of async/await and future polling
- rust
- pin
- unpin
- self-referential
- async
- futures
Read at your depth
The practical view
Pin<&mut T> guarantees that T will not be moved in memory while pinned. This is needed for self-referential types: a struct that holds a pointer to one of its OWN fields. If such a struct is moved, the pointer dangles. Futures produced by async blocks can be self-referential (a local variable referenced across an await), so the async runtime polls them through Pin. Unpin is an auto-trait: most types are Unpin (safe to move even when pinned); types that are self-referential are !Unpin. You construct !Unpin types via unsafe (Pin::new_unchecked) or through libraries. Pin::new(&mut t) works for Unpin types; pin! macro / Box::pin for !Unpin.
The same idea in other frameworks
N/A — Rust language feature.N/A — Rust feature.N/A — Rust feature.N/A — Rust feature.Legacy vs modern
Moving a self-referential struct vs pinning it
A struct that points to its own field breaks when moved; Pin prevents the move so the internal pointer stays valid.
let mut a = build_self_referential(); // ❌ OLD: pointer inside points to alet pinned = Box::pin(my_future); // ✅ NEW: the future is pinned, cannot moveInterview gotchas
Context
This is the foundational async Rust question — interviewers want the compiler-generated self-reference and the Pin contract that keeps internal pointers valid across polls.
The mechanical answer
An async block compiles into a state machine (a Future). A local variable that is borrowed ACROSS an await point becomes a field of the future struct, and references to it (or to other captured data) may be stored as pointers within the same struct — making the future self-referential. If the future were moved after being polled, those internal pointers would point to the old address — dangling. To prevent this, Future::poll takes self: Pin<&mut Self>: the executor must poll through a Pin, which guarantees the future is never moved while pinned. Box::pin (heap), the pin! macro (stack), or pin-project provide the pinned context. The Pin contract (for !Unpin types) is that the value stays at one address for the lifetime of the Pin — enforced by the type system for construction and by unsafe internals.
Trap
The naive answer is 'Pin makes futures thread-safe' or 'Pin pins to a CPU'. Pin is about memory immovability, not CPU pinning or threads. Another trap: thinking you can move a future out of a Box::pin with into_inner safely — for !Unpin types, into_inner is unsafe because the move would break internal pointers. Candidates who connect self-referentiality to the Pin contract show real async understanding.
Context
This tests the auto-trait semantics — interviewers want to know that Unpin is the default and Pin only restricts the types that genuinely need immovability.
The mechanical answer
Unpin is an auto-trait (like Send/Sync): almost all types are Unpin by default — a value of an Unpin type can be safely moved even while it is inside a Pin. For Unpin types, Pin<&mut T> behaves like &mut T: you can move the value out (Pin::into_inner), and Pin::new is safe. !Unpin types (marked with PhantomPinned, or generated self-referential futures) cannot be moved while pinned without unsafe. The distinction matters because Pin's immovability guarantee is only needed when a type actually holds self-references. This is why most code never thinks about Pin: your structs are Unpin and freely movable; only async futures and purpose-built self-referential types (some intrusive collections, certain FFI wrappers) are !Unpin.
Trap
The naive answer is 'Pin makes everything immovable'. Pin only restricts !Unpin types; Unpin types (the vast majority) are movable even when pinned. Another trap: thinking you need Box::pin for every future — you can use stack pinning (pin! macro) or direct polling for Unpin futures. Candidates who describe the auto-trait default and PhantomPinned marker show real understanding.