Skip to content
← All articles

Pin and Unpin, motivated

Pin is not compiler magic. It is a library type that refuses to hand out &mut T unless T: Unpin — and that single refusal is the entire mechanism protecting self-referential futures.

Pin is where most self-taught async learners stall permanently. Almost always because they met it first, as a wall of types with no motivating question.

You are not in that position. You have written futures. You have written a driver. You have seen poll demand Pin<&mut Self> and wondered why it could not just take &mut self like every other method in Rust. This article answers that question, and the answer turns out to be much smaller than the reputation.

Start with a struct that must not move

Forget futures for a moment.

struct SelfRef {
    data: String,
    slice: *const str,   // points INTO `data`
}

Build one so that slice points at data‘s buffer. Now do this:

let mut a = make_selfref("hello");
let mut b = make_selfref("world");
std::mem::swap(&mut a, &mut b);

swap copies the bytes of a and b past each other. a.slice now lives in a but points into what used to be b‘s data — except b is also a now, and the String buffers moved with their owners’ headers… The result is that both slice fields point somewhere they should not. Reading through either is undefined behaviour.

Nothing here was exotic. No unsafe at the call site. mem::swap takes two &mut T and that is all it needs; so does mem::replace, and Option::take, and assigning through a &mut. Given a &mut T, safe Rust can always move a T.

So a type whose correctness depends on not moving cannot be allowed to hand out &mut to itself. That is the entire problem.

Why futures are exactly that struct

Article 16.9 showed why. An async fn that holds a reference across an .await compiles to a state machine storing both the data and a pointer into it:

async fn f(data: Vec<u8>) {
    let first = &data[0];
    something().await;      // `data` and `first` both live across this
    println!("{first}");
}

data and first are both fields of the future. first points at data. Move the future and first dangles — in safe code, with no unsafe in sight.

But — and this is the part that makes it tractable — the future is only self-referential after it has been polled at least once. A freshly constructed one has run no code, so it holds no internal pointers, and moving it is completely fine.

So the guarantee needed is narrow: once polling begins, this value must never move again.

Pin is a library contract, enforced by API shape

Here is the thing that unlocks it. Pin is not compiler magic. There is no #[pinned] attribute, no borrow-checker extension, no special case in MIR. Pin<Ptr> is a plain struct in core::pin wrapping a pointer, and its power comes entirely from what it refuses to do:

impl<Ptr: Deref> Deref for Pin<Ptr> { .. }                            // always
impl<Ptr: DerefMut> DerefMut for Pin<Ptr> where Ptr::Target: Unpin { .. }   // conditionally

Read the where clause. Pin<&mut T> gives you &mut T only when T: Unpin. For everything else you get &T and nothing more — and with only a shared reference, mem::swap and friends are unavailable, so you cannot move the value.

That is the whole mechanism. One conditional trait impl.

Pin pins the pointee, not the pointer

A phrase worth memorising, because the naming actively misleads.

Pin<&mut T> says: the T will not move. The Pin value itself is an ordinary value that you can move around, return, store in a struct, pass to functions. It is a &mut T wearing a promise about the target.

Similarly Pin<Box<T>> says the T on the heap stays put. The Box — the pointer — moves whenever you move the Pin<Box<T>>, and that is fine, because moving a pointer does not move what it points at. This is exactly why boxing a child makes a combinator Unpin: the parent can move freely because the address that matters belongs to the heap allocation, not to the parent.

Unpin means “I do not care where I live”

Unpin is an auto trait, like Send and Sync. Almost every type has it: u32, String, Vec<T>, &T, your structs, your enums. A struct is Unpin when all of its fields are.

The things that are not Unpin are a short list: compiler-generated futures and coroutines, and anything containing std::marker::PhantomPinned — the marker you add to opt out deliberately.

Two consequences follow, and the first is the resolution of a suspense you have been carrying since item 16.3:

For an Unpin type, Pin does nothing at all. Pin::new(&mut x) is safe and free. Pin<&mut MyStruct> derefs mutably straight to &mut MyStruct. Pin::get_mut hands the reference back with no ceremony. Every future you wrote by hand held nothing but numbers and boxes, so it was Unpin, so self.field += 1 compiled and Pin was invisible. That is not luck and it is not a hole in the system — it is the design working as intended for values that genuinely can move.

For a !Unpin type, Pin is the only thing standing between you and undefined behaviour, and Pin::new is unavailable. To make one you use pin! (stack), Box::pin (heap), or Pin::new_unchecked (unsafe, and you are promising the address is stable).

💡Pin is just a wrapper with a conditional DerefMut. What stops me writing let inner = *pinned_ref; — or destructuring the struct, or calling some method that hands the field out — and moving the value anyway? click to reveal

Nothing, if the type’s own API lets you. And that is the real content of Pin: it is a contract that a library author upholds, not a property the compiler verifies.

Pin blocks the generic escape routes — DerefMut, and therefore mem::swap, mem::replace, Option::take, plain assignment. What it cannot block is a method you wrote that takes self by value or hands out &mut self.field. If your type is address-sensitive and you also ship fn into_inner(self) -> F, you have handed the caller a way to move the pinned data and Pin never saw it.

That is why the std docs list explicit obligations for a type with a structurally pinned field: the Unpin impl must account for it, Drop must not move it (drop receives &mut self, so it is a hole by construction), the type must not be #[repr(packed)], and no safe API may move the field out. Item 16.16 makes you satisfy all four by hand and explains why a blanket impl Unpin there is unsound.

The upside of it being a library contract rather than magic is that it composes: nothing about Pin is special-cased for futures, and you can use it for any self-referential type you build.

The drop guarantee

One more clause, easy to miss and load-bearing.

Once a value is pinned, its memory must remain valid until its Drop runs. You may not deallocate or repurpose the storage without dropping the value first.

This is what makes intrusive data structures sound. A future that has registered itself in a runtime’s linked list — by address — is guaranteed to get a chance to unregister in drop before its memory goes away. Without that clause, dropping the memory without dropping the value would leave the runtime holding a pointer to freed storage.

It also means mem::forget on a pinned value is a genuine soundness question in a way it is not elsewhere, which is why Pin::new_unchecked is unsafe.

Four ways to pin, in order of preference

let fut = pin!(some_future());        // stack, zero cost, scoped to the block
let fut = Box::pin(some_future());    // heap, movable, storable in structs
Pin::new(&mut unpin_thing)            // safe, only for Unpin types
unsafe { Pin::new_unchecked(&mut x) } // you are making the promise

pin! for a local you drive right here. Box::pin when the pinned thing has to be stored or returned — including every combinator in this track. Pin::new when the type is Unpin and you just need the wrapper. new_unchecked only inside a pin projection, with a // SAFETY: comment.

What to carry away

  • Moving a value invalidates pointers into it; safe Rust can move anything it has a &mut to.
  • Polled futures contain pointers into themselves, so they must not move.
  • Pin<Ptr> is an ordinary library type that withholds &mut unless the target is Unpin.
  • Unpin is an auto trait almost everything has, and for those types Pin is a free no-op.
  • Pin pins the pointee; the Pin value itself moves freely.

Item 16.11 turns this into a compile error and a fix. Item 16.16 is where you take on the obligations yourself.