If you've written any nontrivial async Rust, you've likely hit a wall with Pin. The compiler tells you something is !Unpin, you add Box::pin, and move on without fully understanding why. This isn't your fault—the API is genuinely tricky, and most explanations skip the concrete mechanics.
Let's fix that. By the end of this, you'll understand exactly what Pin does, why async state machines require it, and how to work with Pin<&mut T> safely in production code.
Async Rust compiles each async fn into a state machine—a struct that holds all local variables across await points. The issue arises when a local variable borrows another local variable.
Consider this async function:
async fn example() {
let data = vec![1, 2, 3];
let slice = &data[..]; // borrows data
tokio::time::sleep(Duration::from_secs(1)).await;
println!("{:?}", slice);
}
The generated state machine looks roughly like:
struct ExampleStateMachine {
state: u8,
data: Vec<i32>,
slice: &'??? [i32], // lifetime problem
}
When the future is moved after being polled, data shifts in memory. Now slice points to the old location—use-after-free waiting to happen. The compiler must prevent this.
Pin is a wrapper around a pointer that guarantees the pointee will not move. It doesn't relocate data; it restricts how you can access it. The guarantee is:
T: !Unpin, you can never get &mut T from a Pin<&mut T> (unless through unsafe).For Unpin types (most types), Pin does nothing—you can call Pin::new() and get a mutable reference freely. The magic is only for types that opt out via !Unpin.
This is compiler-enforced documentation: "This value must not move."
The Rust compiler generates !Unpin for futures that contain self-references. You can verify this:
use std::pin::Pin;
use std::marker::PhantomPinned;
fn assert_unpin<T: Unpin>() {}
async fn self_referential() {
let x = 42;
let rx = &x;
tokio::time::sleep(Duration::from_secs(1)).await;
println!("{}", rx);
}
// This won't compile:
// assert_unpin::<typeof(self_referential())>();
Why PhantomPinned? That's the zero-cost marker that implements !Unpin. When the compiler detects self-references, it injects this marker into the future's state.
The most frequent footgun:
let fut = self_referential();
tokio::pin!(fut); // pins to stack - safe
// This would be UB but compiles without pin:
// std::mem::replace(&mut fut, other_fut); // moves fut
tokio::pin! ensures the future stays at one stack address. For heap allocation:
let fut = Box::pin(self_referential());
// Now fut is a Pin<Box<dyn Future>> - safe to move the Box
The Box itself can move (it's on the stack), but the future inside the heap allocation stays put.
Sometimes you must implement a type that is !Unpin—for example, a lock-free data structure with intrusive linked lists. Here's a minimal self-referential struct:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfReferential {
data: i32,
pointer: *const i32, // raw pointer - no borrow checker
_pin: PhantomPinned,
}
impl SelfReferential {
fn new(data: i32) -> Self {
Self {
data,
pointer: std::ptr::null(),
_pin: PhantomPinned,
}
}
fn init(self: Pin<&mut Self>) {
let this = unsafe { self.get_unchecked_mut() };
this.pointer = &this.data as *const i32;
}
fn get_value(self: Pin<&Self>) -> i32 {
unsafe { *self.pointer }
}
}
Key observations:
- We use raw pointers (*const i32) because Rust's borrow checker cannot track self-references.
- init must take Pin<&mut Self> to guarantee no moves after setup.
- get_value takes Pin<&Self> to ensure the pointer remains valid.
This pattern shows why Pin exists: to give safe APIs around inherently unsafe constructs.
For complex initialization, use a builder that constructs the pinned value safely:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct RingBuffer {
buffer: Vec<u8>,
head: usize,
tail: usize,
_pin: PhantomPinned,
}
struct RingBufferBuilder {
size: usize,
}
impl RingBufferBuilder {
fn new(size: usize) -> Self {
Self { size }
}
fn build(self) -> Pin<Box<RingBuffer>> {
let buf = RingBuffer {
buffer: vec![0; self.size],
head: 0,
tail: 0,
_pin: PhantomPinned,
};
Box::pin(buf)
}
}
The builder ensures the value is pinned before it can be used. This eliminates the possibility of moving after initialization.
pin-project CrateWhen you have a struct containing multiple pinned fields, you need projections—borrowing individual fields while maintaining the pin guarantee. The pin-project crate generates safe code for this:
use pin_project::pin_project;
#[pin_project]
struct TwoFutures {
#[pin]
future_a: Pin<Box<dyn Future<Output = ()>>>,
#[pin]
future_b: Pin<Box<dyn Future<Output = ()>>>,
}
impl TwoFutures {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// Safe projections via generated methods
let this = self.project();
let _ = this.future_a.poll(cx);
let _ = this.future_b.poll(cx);
Poll::Ready(())
}
}
Before pin-project, you'd write unsafe projections:
impl TwoFutures {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
unsafe {
let a = self.map_unchecked_mut(|s| &mut s.future_a);
let b = self.map_unchecked_mut(|s| &mut s.future_b);
let _ = a.as_mut().poll(cx);
let _ = b.as_mut().poll(cx);
}
Poll::Ready(())
}
}
The unsafe version is fragile—if you incorrectly map a field, you break the pin guarantee. pin-project eliminates this risk.
tokio::pin! is convenient but has a subtle limitation:
async fn fetch_data() -> Vec<u8> {
// ... some async work
}
async fn process() {
let fut = fetch_data(); // on stack
tokio::pin!(fut); // pins to stack
// This compiles but if you move 'fut' after pinning, it's UB
// Safe because 'fut' goes out of scope before moving
}
If you need to pass the future to another function, Box::pin is safer:
fn spawn_and_poll(fut: Pin<Box<dyn Future<Output = ()> + Send>>) {
tokio::spawn(fut); // moves the Box, not the future
}
Stack-pinned futures cannot be moved—ever. They die when the scope ends. Box-pinned futures can move their heap allocation.
You encounter a !Unpin error. Steps to resolve:
Identify the self-reference. Look for local variables that borrow other locals across await points. Refactor to avoid the borrow if possible.
Use Box::pin. Almost always the right answer for production code. The heap allocation cost is negligible compared to the async runtime overhead.
Check your trait bounds. Some runtime operations require Unpin:
tokio::spawn(async move {
// If this future is !Unpin, tokio::spawn won't even compile
// because it requires futures to be Unpin
});
tokio::spawn actually accepts Box::pin'd futures because Pin<Box<T>> implements Unpin. The Box is movable; the contents stay fixed.
Rarely, you need to convert Pin<&mut T> to &mut T. This is safe only if you know the value is Unpin:
fn get_mut_unchecked<T>(pinned: Pin<&mut T>) -> &mut T {
// SAFETY: The caller guarantees T is Unpin
unsafe { Pin::get_unchecked_mut(pinned) }
}
Never do this unless you're implementing a low-level runtime primitive. For 99% of users, if you need &mut T from Pin, you're designing something wrong.
Community wisdom says "just Box::pin everything." That's fine for most apps. But if you're building embedded systems or high-throughput services, consider:
tokio::pin!) avoids allocation but limits future lifetime.For async state machines, Rust's compiler often optimizes away the !Unpin marker when it can prove no self-reference exists. This is why simple async functions compile to Unpin futures.
Pin enforces a no-move contract for types that need stable addresses.!Unpin.Box::pin for owned futures, tokio::pin! for temporary stack-pinned values.!Unpin types, always provide safe APIs that accept Pin<&mut Self>.pin-project for safe field projections in complex types.Understanding Pin isn't about memorizing the API—it's about internalizing the memory model. Once you see async state machines as self-referential structs, the rest clicks into place.
Now go build something that doesn't segfault.
Support CodePack with crypto — no account needed, no bank. Pay in USDC or SOL via Solana.
If you found this useful, check out Rust programming books on Amazon or a nice Developer keyboards on Amazon.
Support CodePack with crypto — send BTC, ETH, SOL, LTC, or USDC. No account needed. The monitor checks every 5 minutes.
If you found this useful, check out Rust programming books on Amazon or a nice Developer keyboards on Amazon.