Rust Pin and Unpin: Memory Stability
• Pin<T> guarantees that once a value is pinned, it won't move in memory—essential for self-referential structs where internal pointers would become invalid after a move
Key Insights
• Pin
The Problem: Why Memory Stability Matters
Rust’s ownership system prevents most memory safety issues, but it has a blind spot: self-referential structures. When a struct contains a pointer to its own data, moving that struct in memory invalidates the pointer, creating a dangling reference.
Consider this broken example:
struct SelfReferential {
data: String,
pointer: *const String, // Points to self.data
}
impl SelfReferential {
fn new(text: &str) -> Self {
let mut sr = SelfReferential {
data: text.to_string(),
pointer: std::ptr::null(),
};
sr.pointer = &sr.data as *const String;
sr
}
fn get_data(&self) -> &str {
unsafe { &*self.pointer }
}
}
fn main() {
let sr = SelfReferential::new("hello");
println!("{}", sr.get_data()); // Works fine
let moved_sr = sr; // Move happens here
println!("{}", moved_sr.get_data()); // Dangling pointer! UB!
}
After the move, moved_sr.pointer still points to the old memory location where sr.data used to live. This is undefined behavior waiting to happen.
This problem becomes critical in async Rust. When you write async fn code with local references, the compiler generates a state machine (a Future) that must store both the data and references to it. These futures are inherently self-referential and cannot be safely moved.
Understanding Pin
Pin<T> is a wrapper type that makes a simple promise: the wrapped value will never move in memory again. It doesn’t prevent all mutations—just movement. This guarantee is exactly what self-referential structures need.
Pin comes in two primary forms: Pin<Box<T>> for heap-allocated data and Pin<&mut T> for borrowed data. Here’s the basic pattern:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct Immovable {
data: String,
pointer: *const String,
_pin: PhantomPinned, // Marks this type as !Unpin
}
impl Immovable {
fn new(data: String) -> Pin<Box<Self>> {
let mut boxed = Box::new(Immovable {
data,
pointer: std::ptr::null(),
_pin: PhantomPinned,
});
let ptr: *const String = &boxed.data;
unsafe {
let mut_ref = Pin::as_mut(&mut Pin::new_unchecked(boxed));
Pin::get_unchecked_mut(mut_ref).pointer = ptr;
}
unsafe { Pin::new_unchecked(boxed) }
}
fn get_data(self: Pin<&Self>) -> &str {
unsafe { &*self.pointer }
}
}
fn main() {
let immovable = Immovable::new("pinned data".to_string());
println!("{}", immovable.as_ref().get_data());
// Cannot move immovable - Pin prevents it
}
Once data is behind a Pin<Box<T>>, you cannot obtain ownership of the inner value again (unless it’s Unpin). The type system enforces memory stability.
The Unpin Marker Trait
Here’s the twist: most Rust types don’t actually need Pin’s protection. Unpin is an auto-trait that says “this type is safe to move even when pinned.” Almost everything implements Unpin by default—integers, strings, vectors, most structs.
Pin only restricts movement for types that are !Unpin (not Unpin). You opt out of Unpin by including PhantomPinned:
use std::marker::PhantomPinned;
// This type is Unpin - can be moved even when pinned
struct MovableStruct {
data: Vec<u8>,
}
// This type is !Unpin - cannot be moved when pinned
struct ImmovableStruct {
data: Vec<u8>,
_pin: PhantomPinned,
}
fn demonstrate_unpin() {
let movable = MovableStruct { data: vec![1, 2, 3] };
let mut pinned = Box::pin(movable);
// This works because MovableStruct is Unpin
let moved_out = Pin::into_inner(pinned);
println!("{:?}", moved_out.data);
let immovable = ImmovableStruct {
data: vec![4, 5, 6],
_pin: PhantomPinned,
};
let pinned_immovable = Box::pin(immovable);
// This won't compile - ImmovableStruct is !Unpin
// let cant_move = Pin::into_inner(pinned_immovable);
}
The Unpin trait is what makes Pin ergonomic. For the 99% of types that don’t have self-references, Pin is essentially transparent. Only the rare !Unpin types get the strong guarantees.
Pinning in Practice: Async Futures
The real-world motivation for Pin is async/await. When you write async code with local borrows, the compiler generates a self-referential Future:
async fn example_async() {
let local_data = String::from("borrowed");
let reference = &local_data;
some_async_operation().await;
println!("{}", reference); // reference used after await
}
The generated Future must store both local_data and reference across the await point. This creates a self-referential structure. Here’s roughly what the compiler generates:
use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
enum ExampleFuture {
Start,
Waiting {
local_data: String,
reference: *const String, // Points to local_data!
inner_future: Pin<Box<dyn Future<Output = ()>>>,
},
Done,
}
impl Future for ExampleFuture {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
// Implementation details...
Poll::Pending
}
}
This is why async functions return impl Future types that must be pinned before polling. You use Box::pin() or the pin! macro:
use std::pin::pin;
async fn run_async() {
let fut = example_async();
let mut pinned = pin!(fut);
// Now we can poll it safely
}
Safe Pinning APIs
When working with pinned data, you need safe ways to access it. Pin provides several methods:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct PinnedStruct {
safe_field: i32,
unsafe_field: String,
ptr_to_unsafe: *const String,
_pin: PhantomPinned,
}
impl PinnedStruct {
fn new(value: i32, text: String) -> Pin<Box<Self>> {
let mut boxed = Box::new(PinnedStruct {
safe_field: value,
unsafe_field: text,
ptr_to_unsafe: std::ptr::null(),
_pin: PhantomPinned,
});
let ptr = &boxed.unsafe_field as *const String;
boxed.ptr_to_unsafe = ptr;
unsafe { Pin::new_unchecked(boxed) }
}
// Safe: i32 is Unpin, can get mutable reference
fn safe_field_mut(self: Pin<&mut Self>) -> &mut i32 {
unsafe { &mut self.get_unchecked_mut().safe_field }
}
// Safe: immutable reference doesn't allow moving
fn get_unsafe_field(self: Pin<&Self>) -> &str {
&self.get_ref().unsafe_field
}
// Safe: using the self-reference
fn get_via_pointer(self: Pin<&Self>) -> &str {
unsafe { &*self.ptr_to_unsafe }
}
}
The key rules: Pin::get_ref() gives you &T, which is always safe. For mutable access, you can only get &mut T if T: Unpin, or you must use get_unchecked_mut() and guarantee you won’t move the data.
Unsafe Pinning and Pin Projection
Pin projection—accessing fields of a pinned struct—requires care. For Unpin fields, it’s safe. For !Unpin fields, you need unsafe code or the pin-project crate:
// Manual unsafe projection
use std::pin::Pin;
struct Container {
pinned_field: ImmovableStruct,
normal_field: i32,
}
impl Container {
fn project_pinned(self: Pin<&mut Self>) -> Pin<&mut ImmovableStruct> {
unsafe {
self.map_unchecked_mut(|s| &mut s.pinned_field)
}
}
}
// Using pin-project crate (much safer)
use pin_project::pin_project;
#[pin_project]
struct SafeContainer {
#[pin]
pinned_field: ImmovableStruct,
normal_field: i32,
}
impl SafeContainer {
fn do_something(self: Pin<&mut Self>) {
let this = self.project();
// this.pinned_field is Pin<&mut ImmovableStruct>
// this.normal_field is &mut i32
}
}
The pin-project crate generates safe projection code and ensures you don’t accidentally violate Pin’s guarantees. Use it unless you have a specific reason to write unsafe code.
Common Patterns and Pitfalls
When implementing pinned types, follow this checklist:
use std::pin::Pin;
use std::marker::PhantomPinned;
struct ProperPinnedType {
// 1. Include PhantomPinned to opt out of Unpin
_pin: PhantomPinned,
// 2. Keep self-referential pointers private
data: String,
ptr: *const String,
}
impl ProperPinnedType {
// 3. Return Pin<Box<Self>> from constructors
fn new(s: String) -> Pin<Box<Self>> {
let mut boxed = Box::new(Self {
_pin: PhantomPinned,
data: s,
ptr: std::ptr::null(),
});
boxed.ptr = &boxed.data;
unsafe { Pin::new_unchecked(boxed) }
}
// 4. Take Pin<&Self> or Pin<&mut Self> in methods
fn get_data(self: Pin<&Self>) -> &str {
&self.get_ref().data
}
// 5. Never expose methods that could move the data
// BAD: fn into_inner(self) -> String
}
// 6. Document Pin requirements clearly
/// This type must remain pinned once constructed.
/// Moving it after initialization causes undefined behavior.
Common mistakes: forgetting PhantomPinned, exposing mutable references to self-referential fields, implementing Unpin accidentally through composition, and using Pin::new_unchecked() without ensuring the data is already heap-allocated or otherwise immovable.
Pin is complex because it solves a genuinely hard problem. But in practice, you’ll mostly interact with it through async/await, where the compiler handles the details. When you do need to implement pinned types, use pin-project and follow the patterns above. The type system will catch most mistakes, but understanding the guarantees helps you write correct unsafe code when necessary.