The async crash that Pin prevents
You write an async function that parses a stream of data. It reads a header, saves a pointer to a field inside that header, and then pauses to wait for the body. The executor puts your function aside and runs something else. When the body arrives, the executor wakes your function up. It expects the header to be exactly where it was. If the executor moved your function's state to a different memory address while it was sleeping, that pointer now points to garbage. The function reads invalid data or segfaults.
Pin stops that movement. It is a wrapper type that guarantees a value cannot be moved in memory. Async functions in Rust produce Future values. Those futures often hold pointers to their own internal buffers. Pin ensures those pointers stay valid across suspend and resume cycles. Without Pin, the compiler cannot prove that a future's internal state remains consistent after suspension.
Self-reference and the move problem
Rust moves values constantly. When you pass a value to a function, return it, or assign it to a variable, the compiler may move the bytes to a new location. This is efficient and safe for most types. It breaks down when a value contains a pointer to itself.
Imagine a struct that holds a string and a pointer to a substring within that string.
struct SelfRef {
data: String,
// Points to a slice inside `data`.
ptr: *const str,
}
If you create this struct, data lives at some address, say 0x100. The ptr might point to 0x105 inside data. If you move SelfRef to a new address, say 0x200, the data moves to 0x200. The ptr still holds 0x105. It now points to the old memory location. The struct is broken.
This pattern appears everywhere in async code. The compiler turns async blocks into state machines. Those state machines often store buffers and pointers into those buffers. If the state machine moves, the pointers become dangling. Pin solves this by making the move operation unavailable. Once a value is pinned, the API prevents you from moving it.
Pin is a zero-cost wrapper. It does not lock memory at runtime. It does not add overhead. It restricts the types and methods available to you at compile time. If you have a Pin<P>, you cannot get a &mut P out of it unless P implements the Unpin trait. Most types implement Unpin automatically. Future does not. This distinction is the core of pinning.
Minimal example: Pinning a future
The most common way to use Pin is through Box::pin. This allocates the value on the heap and pins it immediately.
use std::pin::Pin;
use std::future::Future;
fn main() {
// Box::pin allocates the future on the heap and returns a Pin<Box<dyn Future>>.
// The future is now pinned. It cannot be moved.
let pinned_future: Pin<Box<dyn Future<Output = ()>>> = Box::pin(async {
println!("Running async code");
});
// You can read through the pin safely.
// Pin implements Deref, so you can access the inner value.
let _ = pinned_future;
}
Pin wraps a pointer. It does not own the data. It guards the pointer. Box::pin creates a Pin<Box<T>>. The Box owns the data. The Pin prevents the Box from being moved in a way that would move the data. Since Box is a heap pointer, moving the Box moves the pointer, not the data. The data stays on the heap. Pin ensures you cannot replace the pointer with a different one or move the data off the heap.
Convention aside: The community prefers Box::pin for async blocks. It is the standard way to create a pinned future. You will rarely see Pin::new used directly with futures. Box::pin handles the allocation and pinning in one step.
How pinning works under the hood
Pin is defined as Pin<P>, where P is a pointer type like Box<T>, &mut T, or &T. Pin implements Deref and DerefMut. This means you can read and write through the pin. The restriction is subtle.
If T implements Unpin, Pin<P> behaves exactly like P. You can get a &mut T out of it. You can move the value. Unpin is a marker trait. It signals that the type is safe to move. Most types are Unpin. String is Unpin. Vec is Unpin. i32 is Unpin.
If T does not implement Unpin, Pin<P> restricts access. You cannot get a &mut T out of a Pin<P>. You can only get a Pin<&mut T>. This keeps the value pinned. Any function that takes a Pin<&mut T> promises not to move the value.
The Future trait requires Pin in its poll method.
use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
/// A simple future that counts up to a limit.
/// This demonstrates the Pin requirement in the poll method.
struct CounterFuture {
current: u32,
limit: u32,
}
impl Future for CounterFuture {
type Output = u32;
/// The poll method receives a pinned reference to self.
/// This signature guarantees the future isn't moved during polling.
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
// Access fields through the pin.
// Since we only read/write fields and don't move self, this is safe.
if self.current < self.limit {
self.current += 1;
Poll::Pending
} else {
Poll::Ready(self.current)
}
}
}
The signature fn poll(mut self: Pin<&mut Self>, ...) is the key. self is a Pin<&mut Self>. The implementation can access fields, but it cannot move self. If you try to move self inside poll, the compiler rejects you. This guarantee allows the future to hold self-references safely. The executor calls poll with a pinned reference. The executor promises not to move the future between polls. The future trusts the executor. Pin enforces this trust at compile time.
Realistic example: Stack pinning and unsafe
Box::pin allocates on the heap. Sometimes you want to pin a value on the stack to avoid allocation. This requires unsafe code. You must prove the value will not move.
use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
fn main() {
let mut future = CounterFuture { current: 0, limit: 5 };
// SAFETY:
// 1. `future` is a local variable on the stack.
// 2. We hold a mutable reference to it for the entire scope.
// 3. No code in this scope moves `future` out of its location.
// 4. The reference remains valid for the lifetime of the Pin.
let mut pinned = unsafe { Pin::new_unchecked(&mut future) };
// Create a dummy waker for demonstration.
let waker = std::task::no_op_waker();
let mut cx = Context::from_waker(&waker);
// Poll the future.
// pinned.as_mut() returns Pin<&mut CounterFuture>.
let result = pinned.as_mut().poll(&mut cx);
println!("Result: {:?}", result);
}
Pin::new_unchecked is unsafe because the compiler cannot verify the invariants. You must ensure the value stays at the same address. If you pass a reference to a value that might move, you violate the pinning guarantee. The compiler cannot help you here. You must reason about lifetimes and ownership.
Convention aside: The community uses the pin_mut! macro from the futures crate or similar utilities to avoid writing unsafe manually. These macros generate the unsafe block for you, but you still need to understand the invariants. In production code, prefer Box::pin unless profiling shows stack pinning is necessary. The allocation cost is usually negligible compared to async overhead.
Pitfalls and compiler errors
Pinning introduces new error patterns. The most common issue is trying to move a pinned value.
If you have a Pin<Box<dyn Future>> and try to extract the Box, the compiler rejects you with E0277 (trait bound not satisfied). Future does not implement Unpin. Pin::into_inner requires Unpin. You cannot unpin a future.
use std::pin::Pin;
use std::future::Future;
fn main() {
let pinned: Pin<Box<dyn Future<Output = ()>>> = Box::pin(async {});
// This fails. E0277: the trait bound `dyn Future<...>: Unpin` is not satisfied.
// let _unpinned = pinned.into_inner();
}
Another pitfall is forgetting that Pin is a wrapper. You cannot pass a Pin<P> where a P is expected. You must use methods that accept Pin. If you try to dereference a Pin to get a mutable reference, you get E0599 (no function or associated item named as_mut found) or similar errors if you misuse the API.
Pin interacts with RefCell in subtle ways. RefCell is Unpin. Pin<RefCell<T>> allows you to get a RefMut<T>. If T is not Unpin, you cannot get a &mut T out of the RefMut. This preserves pinning through interior mutability. If you need interior mutability in a pinned struct, use RefCell or Cell. Do not try to use &mut fields.
Convention aside: Real-world async code often uses the pin_project crate. This crate provides macros that safely project fields out of a pinned struct. It handles the unsafe boilerplate and generates correct Deref implementations. If you are writing custom futures or state machines, reach for pin_project. It is the standard tool for projecting pinned fields.
Decision: when to use Pin
Use Box::pin when you create an async block or future and need to store it in a variable or pass it to an executor. Use Pin::new when you have a local variable that implements Unpin and you need a Pin<&mut T> to call a method that requires pinning. Use unsafe { Pin::new_unchecked } when you are implementing a custom smart pointer or wrapper and can prove the underlying value remains at a fixed address for its lifetime. Reach for Unpin when your struct holds no self-references and you want the compiler to allow moving the value freely. Use pin_project when you need to access fields of a pinned struct in a custom future implementation.
Pin protects self-referential data. Trust the borrow checker and the pinning guarantees. If you can't prove the value won't move, don't pin it manually.