Under The Hood
backend rust Rust 1.x (std::pin)

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

react equivalent
N/A — Rust language 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.

before → after
Moving a self-referential struct (broken)
let mut a = build_self_referential(); // ❌ OLD: pointer inside points to a
Pin prevents the move
let pinned = Box::pin(my_future); // ✅ NEW: the future is pinned, cannot move

Interview 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.

Under The Hood — a multi-depth technical interview hub.

Third-party licenses Press ⌘ K to search.