Rust Pin and Unpin Explained: Why They Exist
Understand Pin, Unpin, and self-referential types in Rust. Learn why Pin exists, when you need it, and how to work with pinned data safely.
What you'll learn
- ✓Why moving self-referential types is dangerous
- ✓What Pin guarantees and what it does not
- ✓How Unpin opts types out of pinning restrictions
- ✓How Pin shows up in async Rust
Prerequisites
- •Basic Rust knowledge
- •Familiarity with async/await
Pin is one of those Rust concepts that seems unnecessarily complicated until you understand the problem it solves. The problem is self-referential types: structs that contain a pointer to their own data. Moving such a struct invalidates the pointer, leading to undefined behavior. Pin prevents that move.
The Problem: Self-Referential Structs
Consider a struct that holds a string and a pointer into that string:
struct SelfRef {
data: String,
// Imagine this points to a slice of `data`
slice_ptr: *const str,
}
impl SelfRef {
fn new(text: &str) -> Self {
let mut s = SelfRef {
data: String::from(text),
slice_ptr: std::ptr::null(),
};
s.slice_ptr = s.data.as_str() as *const str;
s
}
fn get_slice(&self) -> &str {
unsafe { &*self.slice_ptr }
}
}
This works as long as SelfRef stays put. But if you move it (by assigning to a new variable, pushing into a Vec, or returning from a function), the data field moves to a new memory address while slice_ptr still points to the old location. The pointer is now dangling.
fn main() {
let a = SelfRef::new("hello");
println!("{}", a.get_slice()); // Works fine
let b = a; // Moves a to b. data is at a new address.
// b.slice_ptr still points to a's old memory!
// println!("{}", b.get_slice()); // Undefined behavior!
}
This is exactly what happens with async futures. The compiler-generated state machines for async fn can contain self-references when a variable is used across an .await point.
What Pin Does
Pin<P> wraps a pointer type P (like &mut T or Box<T>) and prevents you from getting a &mut T from it. Without &mut T, you cannot call std::mem::swap or std::mem::replace, which are the operations that move a value.
use std::pin::Pin;
fn main() {
let mut data = String::from("hello");
// Pin a mutable reference
let pinned: Pin<&mut String> = Pin::new(&mut data);
// You can read through Pin
println!("Value: {}", *pinned);
// But you cannot get &mut String out of Pin<&mut String>
// (unless the type implements Unpin, which String does)
}
The critical guarantee: once a value is pinned, it will not move in memory for the rest of its lifetime.
Unpin: Opting Out
Most Rust types do not contain self-references, so pinning them is unnecessary. These types implement the Unpin marker trait, which says “it is safe to move me even after pinning.”
Almost everything implements Unpin automatically: integers, strings, vectors, and most structs. Only types that are explicitly self-referential (like compiler-generated futures) do not.
use std::pin::Pin;
fn main() {
let mut value = 42i32;
// i32 implements Unpin, so Pin lets you get &mut back
let mut pinned = Pin::new(&mut value);
// This works because i32: Unpin
*pinned = 100;
println!("{}", pinned); // 100
}
When a type implements Unpin, Pin has no effect. You can freely get &mut T from Pin<&mut T>, so the pinning guarantee is lifted. This is why Pin does not bother you for most everyday Rust code.
Pin in Async Rust
The primary consumer of Pin is the async runtime. When you write:
async fn example() {
let buffer = vec![0u8; 1024];
some_io_operation(&buffer).await;
println!("Buffer length: {}", buffer.len());
}
The compiler generates a state machine that holds buffer across the .await point. If the generated future holds a reference to buffer internally, that future is self-referential and must not be moved once polling starts.
This is why Future::poll takes Pin<&mut Self>:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
The executor pins the future before polling it, guaranteeing it stays in place.
Creating Pinned Values
There are several ways to pin a value:
use std::pin::Pin;
// 1. Pin::new() works for Unpin types
let mut x = 42;
let pinned = Pin::new(&mut x);
// 2. Box::pin() pins on the heap (works for any type)
let boxed: Pin<Box<String>> = Box::pin(String::from("pinned"));
// 3. pin! macro (stabilized in Rust 1.68) pins on the stack
use std::pin::pin;
let pinned = pin!(String::from("stack pinned"));
// pinned is Pin<&mut String>
Box::pin() is the most common approach for futures because the heap allocation means the data stays at a fixed address regardless of what happens to the Box itself.
Working with Pin in Practice
When you write async code, you rarely interact with Pin directly. The .await syntax handles it. But when you implement Future manually or write combinators, you need to work with pinned references.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct Timeout<F> {
future: F,
deadline: std::time::Instant,
}
impl<F: Future> Future for Timeout<F> {
type Output = Option<F::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if std::time::Instant::now() >= self.deadline {
return Poll::Ready(None);
}
// SAFETY: we need to project Pin through to the inner future.
// This is safe because Timeout does not move the future after pinning.
let future = unsafe { self.map_unchecked_mut(|s| &mut s.future) };
match future.poll(cx) {
Poll::Ready(val) => Poll::Ready(Some(val)),
Poll::Pending => Poll::Pending,
}
}
}
The pin-project crate makes this much easier by generating safe pin projections:
use pin_project::pin_project;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
#[pin_project]
struct Timeout<F> {
#[pin]
future: F,
deadline: std::time::Instant,
}
impl<F: Future> Future for Timeout<F> {
type Output = Option<F::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
if std::time::Instant::now() >= *this.deadline {
return Poll::Ready(None);
}
// No unsafe needed! pin_project handles it
match this.future.poll(cx) {
Poll::Ready(val) => Poll::Ready(Some(val)),
Poll::Pending => Poll::Pending,
}
}
}
The Mental Model
Think of Pin as a contract:
- For
!Unpintypes: “I promise this value will never move again. You can safely store self-references.” - For
Unpintypes: “Pinning has no effect. Move freely.” - For async code: “The executor pins futures before polling. The compiler relies on this to safely generate self-referential state machines.”
Common Mistakes
Trying to move a pinned value. Once pinned, do not try to std::mem::swap or move the underlying data. The compiler will stop you for safe code, but unsafe code must respect the invariant.
Forgetting pin projections. When you have a Pin<&mut MyStruct> and need Pin<&mut MyStruct.field>, you need a pin projection. Use pin-project instead of writing unsafe projections manually.
Confusing Pin with immutability. Pin does not make data immutable. You can still mutate through Pin<&mut T> using methods that take self: Pin<&mut Self>. It only prevents moving.
Wrapping Up
Pin exists because Rust’s async state machines can be self-referential, and moving self-referential data is unsound. Pin prevents moves, Unpin opts out of the restriction for types that are safe to move, and pin-project makes working with pinned structs ergonomic. In day-to-day async Rust you rarely touch Pin directly, but understanding it helps you read error messages, implement custom futures, and reason about what the runtime does on your behalf.
Related articles
- Rust Rust Async Programming with Tokio: Getting Started
Learn async Rust with Tokio from scratch. Covers async/await syntax, spawning tasks, channels, TCP servers, and common pitfalls with working examples.
- Rust Tokio vs async-std: Choosing a Rust Async Runtime
A practical comparison of Tokio and async-std covering performance, ecosystem, API design, and migration considerations for async Rust projects.
- Rust Rust Async Runtime Internals: How It Works Under the Hood
Explore how Rust async runtimes work internally, including futures, wakers, executors, and the reactor pattern that powers async I/O.
- Rust Rust Unsafe Code Guidelines: When and How to Use unsafe
Learn when unsafe Rust is justified, what the five unsafe superpowers are, and how to write unsafe code that maintains Rust's safety guarantees.