Rust Pin and Unpin
debt(d6/e5/b5/t7)
Closest to 'only careful code review or runtime testing' (d7), but slightly better at d6 because clippy and miri (from detection_hints.tools) can catch some unsafe pin misuse and UB at runtime, though most pin invariant violations are silent until they cause UB.
Closest to 'touches multiple files / significant refactor in one component' (e5). While quick_fix suggests Box::pin or pin! as one-line fixes for the polling case, fixing a broken self-referential type or replacing hand-rolled unsafe projection with pin-project is a meaningful refactor within the future/stream component.
Closest to 'persistent productivity tax' (b5). Per applies_to spanning web/cli/queue/library and its role in async, once you have !Unpin types the pin discipline shapes every poll implementation and field access, but it's localized to async/intrusive code rather than defining the whole system.
Closest to 'serious trap' (t7). The misconception field explicitly states developers expect Pin to lock memory addresses like C++ pinning and think it applies universally, when it's actually a compile-time move-prevention contract that most types opt out of via Unpin — contradicting intuition from other languages.
Also Known As
TL;DR
Explanation
Most Rust values are freely movable: the compiler can memcpy them from one address to another when they are returned, assigned, or pushed into a Vec. That is usually fine because references are tracked by the borrow checker and updated at compile time. But some values contain internal pointers into their own bytes - the classic example is a state machine generated by an `async fn` that borrows across an `await`. If such a value moved, its internal pointer would still point at the old address, becoming a dangling reference and causing undefined behavior. `Pin<P>` exists to statically forbid that move.
`Pin<P>` is a wrapper around a pointer type `P` (typically `Box<T>`, `&mut T`, or a stack pinned reference) that promises the value behind the pointer will never be moved again for the rest of its life. Once you have `Pin<Box<T>>` or `Pin<&mut T>`, you cannot get a `&mut T` back out safely, because a `&mut T` would let a caller call `mem::swap` or `mem::replace` and move the value. Instead you work through methods like `Pin::as_mut`, `Pin::as_ref`, and (with the `pin-project` or `pin-project-lite` crate) structural projection to fields.
The escape hatch is the `Unpin` auto-trait. `Unpin` marks types that are safe to move even when pinned - because they contain no self-references and do not care about their address. Most types are `Unpin` automatically: `i32`, `String`, `Vec<T>`, and any struct whose fields are all `Unpin`. For those, `Pin<&mut T>` is basically the same as `&mut T` and you can call `Pin::get_mut` freely. The types that are not `Unpin` are the interesting ones: futures produced by `async` blocks, generators, and any type containing `PhantomPinned` or a `!Unpin` field. Those are the values Pin exists to protect.
In practice you rarely construct Pin yourself in application code. You pin a future by boxing it with `Box::pin(async { ... })` or by using `tokio::pin!`/`std::pin::pin!` on the stack, then pass it to something that calls `Future::poll`, whose signature `poll(self: Pin<&mut Self>, ...)` is the whole reason Pin exists. Library authors implementing custom futures, streams, or intrusive data structures use `pin-project` to expose safe access to fields while preserving the pinning invariant on the ones that need it.
Common Misconception
Why It Matters
Common Mistakes
- Assuming Pin prevents mutation, when it only prevents moves and still allows mutation through Pin::as_mut and projected fields.
- Trying to get a &mut T out of a Pin<&mut T> for a !Unpin type using unsafe code, breaking the pin invariant and causing undefined behavior.
- Manually implementing Unpin for a self-referential type to silence errors, defeating the safety guarantee Pin exists to provide.
- Writing structural projection by hand with unsafe blocks instead of using pin-project or pin-project-lite, and getting the Unpin propagation wrong.
- Storing a future in a plain Vec or moving it after the first poll, causing panics or UB because Future::poll requires Pin<&mut Self>.
Avoid When
- Application code that only awaits futures through async/await and never manually implements Future or Stream, where the compiler and runtime handle pinning invisibly.
- Ordinary data types with no self-references, where Unpin is automatic and Pin adds no useful guarantee.
- Situations where a simple owned value or Box<T> suffices; reaching for Pin without a !Unpin type is cargo-culting.
- Hand-writing unsafe pin projection when pin-project or pin-project-lite would generate the same code safely.
When To Use
- Implementing a custom Future, Stream, or AsyncRead where poll takes Pin<&mut Self> and inner state may be self-referential.
- Storing a future to poll later, using Box::pin or std::pin::pin! to satisfy the Pin<&mut F> requirement of Future::poll.
- Building intrusive data structures like linked lists whose nodes hold pointers to sibling nodes and therefore must not move.
- Exposing safe access to fields of a !Unpin struct via pin-project, distinguishing structurally pinned fields from freely movable ones.
Code Examples
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// A future that borrows across an await point is !Unpin.
async fn make_greeting(name: String) -> String {
let greeting = format!("hello, {}", name);
// Imagine an await here that borrows into `greeting`.
tokio::task::yield_now().await;
greeting
}
fn drive_once<F: Future>(mut fut: F) {
// BUG: Future::poll needs Pin<&mut Self>, but we only have &mut F.
// Trying to poll a bare future like this does not compile, and
// "fixing" it with unsafe Pin::new_unchecked on a stack value
// that we later move would be undefined behavior.
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
// let _ = fut.poll(&mut cx); // does not compile - no Pin
// let pinned = unsafe { Pin::new_unchecked(&mut fut) }; // unsound if fut moves
// let _ = pinned.poll(&mut cx);
drop((fut, cx));
}
fn main() {
drive_once(make_greeting("world".into()));
}
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
async fn make_greeting(name: String) -> String {
let greeting = format!("hello, {}", name);
tokio::task::yield_now().await;
greeting
}
fn drive_once<F: Future>(fut: F) -> Poll<F::Output> {
// Box::pin heap-allocates the future and hands back Pin<Box<F>>,
// a pointer that promises never to move the future again.
let mut pinned: Pin<Box<F>> = Box::pin(fut);
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
// as_mut reborrows as Pin<&mut F>, which is what Future::poll requires.
pinned.as_mut().poll(&mut cx)
}
// Alternative: pin on the stack with the std::pin::pin! macro. The value
// cannot escape this scope, so it is guaranteed not to move.
fn drive_stack_pinned() {
let fut = make_greeting("world".into());
let mut fut = std::pin::pin!(fut);
let waker = futures::task::noop_waker();
let mut cx = Context::from_waker(&waker);
let _ = fut.as_mut().poll(&mut cx);
}
fn main() {
let _ = drive_once(make_greeting("world".into()));
drive_stack_pinned();
}