Pin and Self-Referential Futures
Objective
Future::poll takes self: Pin<&mut Self> because the state machine of an
async fn may point into itself: a local borrowed across an .await lives in
the future, and so does the reference to it. A move would leave that
reference at the old address. Pin is a library promise that the pointee
stays put until dropped; Unpin marks types for which the promise means
nothing. This lesson shows the self-reference in memory, the compiler errors
when you skip pinning, pin! versus Box::pin, structural pinning with
pin-project-lite, and what breaks, under Miri, when unsafe code lies.
Measured with Rust 1.100.0 (rustc 0ef1bc386 2026-11-09), edition 2024, Apple Silicon (aarch64-apple-darwin, macOS), pin-project-lite 0.2.17 (Cargo.lock). Miri is nightly-only, so it ran from nightly-2025-02-20 (miri f280acf4c7).
Use Cases
- Writing a
FutureorStreamwrapper (timeout, metrics, retry) around an inner future: that is pin projection. - Storing futures of different types together:
Pin<Box<dyn Future>>. - Polling the same future across loop iterations (
select!in a loop). - Reading "cannot be unpinned" errors and picking
pin!,Box::pinorUnpin. - Reviewing
unsafecode that callsPin::new_unchecked.
Deep Dive
The self-reference, in memory
rustlet fut = async {
let buf = [9u8; 64];
let r = &buf; // a reference to a local that lives in the future
YieldNow(false).await;
println!("after await: r={:p} r[0]={}", r, r[0]);
};
let mut boxed = Box::pin(fut);
let start = &*boxed as *const _ as *const u8 as usize;
let size = std::mem::size_of_val(&*boxed);
println!("future on the heap: {:#x}..{:#x} ({size} bytes)", start, start + size);
let _ = boxed.as_mut().poll(&mut cx); // Pending
let _ = boxed.as_mut().poll(&mut cx); // prints, Readytextfuture on the heap: 0x101515a40..0x101515a90 (80 bytes) after await: r=0x101515a40 r[0]=9
r points at the first byte of the future: the future holds a pointer to
itself. A hand-made struct SelfRef { data: [u8; 16], ptr: *const [u8; 16] }
shows what a move does: with ptr = &data set inside make(), ptr stayed
0x16ee68ee0 while &data became 0x16ee69040 after returning and
0x101515a20 after vec![s] (debug run; release did the same). A move
copies the value byte for byte, "without running any code to notify that
value that its address has changed" (std docs), so nothing fixes the pointer.
What Pin promises
The std::pin docs: "From the moment a value is pinned by constructing a
Pinning pointer to it, that value must remain, valid, at that same address in
memory, until its drop handler is called." Pin<Ptr> is
#[repr(transparent)] (same layout and ABI as Ptr) and enforces this by
omission: for a !Unpin pointee there is no DerefMut, so safe code never
gets the &mut T that mem::swap or mem::replace needs.
std::mem::swap(&mut *a, &mut *b) on two pin!ned futures fails with
"E0596: cannot borrow data in dereference of Pin<&mut impl Future<Output = u32>> as mutable" and "trait DerefMut is required to modify through a
dereference".
Unpin is the auto trait that cancels all this: Pin::new, get_mut and
DerefMut exist only for Unpin pointees. Almost every type is Unpin;
generated futures are not, and a PhantomPinned field opts a struct out.
With fn assert_unpin<T: Unpin>(_: &T), std::future::ready(1) passed while
an async fn future and a PhantomPinned struct failed with "{async fn body of work()} cannot be unpinned" and "PhantomPinned cannot be unpinned".
Polling an unpinned future: the real errors
rustlet mut fut = work();
let _ = fut.poll(&mut cx); // build 1
let _ = Pin::new(&mut fut).poll(&mut cx); // build 2 (separate file)texterror[E0599]: no method named `poll` found for opaque type `impl Future<Output = u32>` in the current scope = help: method `poll` found on `Pin<&mut impl Future<Output = u32>>`, see documentation for `std::pin::Pin` = help: self type must be pinned to call `Future::poll`, see https://rust-lang.github.io/async-book/part-reference/pinning.html help: consider pinning the expression 10 ~ let mut pinned = std::pin::pin!(fut); 11 ~ let _ = pinned.as_mut().poll(&mut cx); error[E0277]: `{async fn body of work()}` cannot be unpinned = note: consider using the `pin!` macro consider using `Box::pin` if you need to access the pinned value outside of the current scope note: required by a bound in `Pin::<Ptr>::new`
Both errors name the two fixes; the first links the async book's pinning chapter.
pin! versus Box::pin
rustlet mut tasks: Vec<Pin<Box<dyn Future<Output = u32>>>> =
vec![Box::pin(small()), Box::pin(big())]; // big() holds 4 KiB across .await
let local = pin!(big());
let boxed = Box::pin(big());
// prints: poll of each task, then size_of_val of handle and pointeetextReady(1) Ready(2) pin!(big()) handle 8 bytes, future 4098 bytes (in this frame) Box::pin(big()) handle 8 bytes, future 4098 bytes (on the heap) Pin<Box<dyn ..>> handle 16 bytes
pin! (stable since 1.68) pins in place without allocating; its docs prefer
"local pinning", because inside an async fn the pinned local becomes part of
the enclosing future's state. It moves the value in (touching the original
afterwards is E0382, "use of moved value") and cannot leave the scope:
returning pin!(work()) from a function is E0515, "cannot return value
referencing temporary value". Box::pin costs one allocation and gives an
owned handle to return, store, or erase to dyn Future (16-byte fat pointer).
Structural pinning and projection
A wrapper that counts polls of an inner future, written naively:
ruststruct Counted<F> { inner: F, polls: u32 }
impl<F: Future> Future for Counted<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
let this = self.get_mut();
this.polls += 1;
Pin::new(&mut this.inner).poll(cx)
}
}texterror[E0277]: `F` cannot be unpinned 10 | let this = self.get_mut(); | ^^^^^^^ within `Counted<F>`, the trait `Unpin` is not implemented for `F` help: consider further restricting type parameter `F` with trait `Unpin`
The suggested F: Unpin compiles but rejects every async fn future. The
real fix is projection: inner is structurally pinned (Pin<&mut Self>
yields Pin<&mut F>), polls is not (it yields &mut u32). By hand that is
get_unchecked_mut plus Pin::new_unchecked, sound only under the rules the
std::pin docs list: the struct is Unpin only if its structural fields are,
Drop must treat self as pinned, no #[repr(packed)], and nothing moves
data out of a structural field. pin-project-lite generates it with no unsafe:
rustpin_project_lite::pin_project! {
struct Counted<F> { #[pin] inner: F, polls: u32 }
}
impl<F: Future> Future for Counted<F> {
type Output = (F::Output, u32);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project(); // inner: Pin<&mut F>, polls: &mut u32
*this.polls += 1;
this.inner.poll(cx).map(|v| (v, *this.polls))
}
}textmanual: ("ok", 3) lite : ("ok", 3)
(work() awaits two yields, so three polls.) The macro also enforces the
rules: Counted<F> is Unpin exactly when F is, and a plain impl Drop
fails with "E0119: conflicting implementations of trait MustNotImplDrop".
Cleanup goes in impl<F> PinnedDrop for Counted<F> { fn drop(this: Pin<&mut Self>) { .. } } inside the macro, which ran and printed polled 3 times.
Why Pin exists: breaking the promise
Pin::new_unchecked is unsafe because the caller promises the value never
moves again. This program polls a future (creating the self-reference), moves
it out of the stack frame, and polls again:
rustfn start(cx: &mut Context<'_>) -> impl Future<Output = u8> + use<> {
let mut fut = make(); // async { let buf = [9u8; 64]; let r = &buf; yield.await; r[0] }
let _ = unsafe { Pin::new_unchecked(&mut fut) }.poll(cx);
fut // moved while holding a pointer into this frame
}
let mut fut = start(&mut cx);
clobber(); // writes 0xAA over the dead frame
let out = unsafe { Pin::new_unchecked(&mut fut) }.poll(&mut cx);textnative (debug and release): second poll: Ready(170) (expected Ready(9)) miri: error: Undefined Behavior: constructing invalid value at .<coroutine-state(3)>.<captured-var(1)>: encountered a dangling reference (use-after-free)
170 is 0xAA: the future read a dead stack frame and nothing crashed. Miri
stops earlier, at let mut fut = start(..): the returned value already holds
a dangling reference. RFC 2349 chose a library type over a built-in ?Move
trait because that bound "ended up 'infecting' many different APIs where it
wasn't relevant"; with Pin, only APIs that need immovability opt in, and
safe code can never produce the move above.
Trade-offs
pin!orBox::pin:pin!is free but scoped to the frame;Box::pincosts an allocation and can be returned, stored or madedyn. Default topin!, box at API boundaries.- An
Unpinbound is the easy way out:F: Future + UnpinunlocksPin::newandget_mut, but pushesBox::pinonto every caller holding anasync fnfuture. Libraries usually project instead. - pin-project-lite versus pin-project: no proc-macro dependencies is, in
its docs' words, "the only reason to use this crate" (no gain if you already
pull in syn). It lacks tuple structs, custom
Unpinand useful error messages; pin-project has all three. !Unpinis contagious: a struct storing anasync fnfuture by value is!Unpin, so its users must pin it too.- Pin is a contract, not a check: the broken program above "worked"
natively with a wrong value. Run code that uses
new_uncheckedormap_unchecked_mutunder Miri.
Documentation Links
- Module std::pin, Rust standard library
- std::pin::Pin, Rust standard library
- std::pin::pin! macro, Rust standard library
- std::marker::Unpin, Rust standard library
- RFC 2349: Pin, The Rust RFC Book
- Pinning, Asynchronous Programming in Rust
- pin-project-lite 0.2.17, docs.rs
- Miri, rust-lang/miri
- Rust for Rustaceans (Jon Gjengset, No Starch Press 2021), chapter 8: Asynchronous Programming