In systems programming languages like C and C++, passing a struct by value or copying memory can break internal pointers if an object references its own fields. In C++, this issue is often sidestepped by allocating coroutine frames on the heap via compiler-generated operator new.
In Rust, every type is movable by default. When an object is passed by value, returned from a function, or assigned to a new variable, the compiler executes a bitwise copy (memcpy) of the data and invalidates the old memory location.
This fundamental guarantee broke down with the introduction of async/await. When the Rust compiler transforms an async fn into a cooperative state machine, variables kept alive across .await points often hold references to other local variables within that same stack frame, creating a self-referential struct.
If that state machine moves in memory, those internal pointers point to obsolete stack memory, leading to undefined behavior and memory corruption. std::pin::Pin is the compile-time mechanism that prevents this.
1. Summary & Safety Taxonomy
| Concept | Definition | Safety Role |
|---|---|---|
| Move Semantics | Bitwise memcpy of struct fields to new memory | Default Rust behavior for all unpinned types |
| Self-Referential Struct | A type whose fields store pointers to its own fields | Corrupted if moved after pointer initialization |
std::pin::Pin<P> | Smart pointer wrapper around pointer type P | Guarantees the pointee will never move again before drop |
Unpin Auto Trait | Trait automatically implemented for 99% of types | Tells the compiler: moving this type is completely safe |
!Unpin (PhantomPinned) | Explicit opt-out of the Unpin trait | Enforces the pinning contract via the type system |
2. Why Async State Machines Require Pinning
Consider an asynchronous function that reads into a local buffer:
async fn process_socket() {
let mut buffer = [0u8; 1024];
let reader = &mut buffer;
async_read(reader).await;
}
The compiler transforms this into an enum state machine similar to:
enum ProcessSocketFuture {
State0,
State1 {
buffer: [0u8; 1024],
reader: *mut [0u8; 1024],
},
Done,
}
If the caller executes let mut fut = process_socket(); and later moves it to the heap or passes it across threads, the reader pointer would still target the original stack memory address where buffer initially resided.
graph TD
subgraph Original["Original Memory Location (0x1000)"]
B1["buffer: [0u8; 1024] at 0x1000"]
P1["reader: 0x1000 (valid)"]
P1 -->|Points to| B1
end
subgraph Moved["After memcpy to Heap (0x5000)"]
B2["buffer: [0u8; 1024] at 0x5000"]
P2["reader: 0x1000 (DANGLING)"]
P2 -.->|Points to invalid old memory| B1
end
3. The Pin Type Contract
Pin<P> wraps a pointer (such as &mut T, Box<T>, or Rc<T>).
The crucial invariant is: if T: !Unpin, once a Pin<&mut T> is created, the underlying memory backing T must not be moved until T is dropped.
impl<P: Deref<Target: Unpin>> DerefMut for Pin<P> {
fn deref_mut(&mut self) -> &mut Self::Target { ... }
}
Notice the trait bound: Pin only implements DerefMut if the target implements Unpin. If T is !Unpin, you cannot obtain a bare &mut T via safe code, because having a bare mutable reference allows callers to call std::mem::swap or std::mem::replace, moving the value out of its pinned memory location.
Pinning requires that drop must be called on the memory location before that memory can be reclaimed or overwritten. Writing a custom Drop implementation that moves fields out of a pinned struct violates the safety contract and results in undefined behavior.
4. Dual-Language Implementation
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::ptr::NonNull;
pub struct SelfReferential {
data: String,
pointer: Option<NonNull<String>>,
_marker: PhantomPinned,
}
impl SelfReferential {
pub fn new(text: &str) -> Self {
SelfReferential {
data: text.to_string(),
pointer: None,
_marker: PhantomPinned,
}
}
pub fn init(self: Pin<&mut Self>) {
let this = unsafe { self.get_unchecked_mut() };
this.pointer = Some(NonNull::from(&this.data));
}
pub fn read_pointer(self: Pin<&Self>) -> Option<&str> {
self.pointer.map(|ptr| unsafe { ptr.as_ref().as_str() })
}
}5. Safe Structural Pin Projection
When writing custom data structures that contain pinned fields (such as combinator futures or actors), you need to project Pin<&mut Parent> into Pin<&mut Child>.
Doing this manually requires unsafe code. The de-facto standard solution in the Rust ecosystem is the pin-project procedural macro:
use pin_project::pin_project;
#[pin_project]
pub struct TimedTask<F> {
#[pin]
future: F,
deadline: std::time::Instant,
}
The attribute macro automatically writes the correct Unpin bounds, generates safe project() methods, and guarantees that dropping the parent preserves the pinning invariant for all tagged child fields.