Future

164. Pin projection — How to actually use the fields behind Pin<&mut Self>

The moment you hand-roll Future::poll, you have a Pin<&mut Self> and a question Rust won’t answer for you: how do I touch my fields? self.inner doesn’t compile, &mut self.inner is what Pin exists to prevent, and the answer — pin projection — is one of those idioms everyone reinvents until they reach for pin-project-lite.

bite-162 covered what Pin<P> is and why async futures need it. This one is about the very next thing you trip over: actually polling the inner future from your own poll method.

The problem

A wrapper that polls an inner future and counts how many times it was polled:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Logged<F> {
    inner: F,
    polls: u32,
}

impl<F: Future> Future for Logged<F> {
    type Output = F::Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        self.inner.poll(cx) // ERROR: can't borrow through Pin<&mut Self>
    }
}

Pin<&mut Self> deliberately won’t deref-mut into &mut Self — that would hand back the exact &mut you need to mem::swap the whole struct out from under whatever pinned it. So self.inner is a non-starter. You have to project: turn a Pin<&mut Self> into a Pin<&mut F> pointing at the inner field.

Manual projection with unsafe

The raw tools are Pin::get_unchecked_mut and Pin::new_unchecked. You take &mut Self out of the pin (unsafe — you’re promising not to move the whole value), borrow disjoint fields, then re-pin the ones that need it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct Logged<F> {
    inner: F,
    polls: u32,
}

impl<F: Future> Future for Logged<F> {
    type Output = F::Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // SAFETY: we promise not to move `self`. `inner` is treated as
        // structurally pinned; `polls` is treated as freely movable.
        let this = unsafe { self.get_unchecked_mut() };
        this.polls += 1;
        let inner = unsafe { Pin::new_unchecked(&mut this.inner) };
        inner.poll(cx)
    }
}

Two unsafe blocks and an invariant you have to remember everywhere else in the file: if some other method ever does mem::replace(&mut this.inner, _), you’ve broken the pin contract and quietly created UB. The compiler will not catch it.

The clean answer: pin-project-lite

pin-project-lite mechanically derives the safe projection. Mark each structurally-pinned field with #[pin]:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use pin_project_lite::pin_project;

pin_project! {
    struct Logged<F> {
        #[pin]
        inner: F,
        polls: u32,
    }
}

impl<F: Future> Future for Logged<F> {
    type Output = F::Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.project();
        *this.polls += 1;
        this.inner.poll(cx)
    }
}

self.project() returns a generated struct where every #[pin] field is a Pin<&mut Field> and every other field is a plain &mut Field. No unsafe, no projection mistakes, no chance of accidentally mem::replace-ing a pinned field — the macro generates the accessors so the wrong move never compiles. This is the pattern tokio, hyper, futures, and effectively every library implementing custom futures lives on.

Structural vs non-structural — the choice you’re making

Marking a field #[pin] locks in three guarantees:

  • You will never move out of it once Self is pinned (no mem::replace, no mem::swap).
  • Its Drop impl runs while the field is still pinned.
  • Accessors hand you Pin<&mut Field>, not &mut Field.

Unmarked fields go the other way: you treat them as freely movable. Pick wrong — pin one structurally and then mem::swap it elsewhere — and you’ve quietly invalidated whatever pointers something else handed out into that field.

Rule of thumb: if a field is itself a future, or any !Unpin type that needs to be polled in place, mark it #[pin]. Counters, flags, owned Strings — leave them unmarked.

#162 May 2026

162. Pin<P> — The Pointer Type That Says 'This Won't Move'

Self-referential structs are the obvious “this should work but doesn’t” pattern in Rust: a struct that holds a buffer plus a reference into that buffer falls apart the moment it moves and the reference dangles. Pin<P> is the type that says “the pointee is fixed in memory” — and is the reason every async fn future you’ve ever .awaited can keep a pointer to its own local variables.

Why Pin exists at all

Take a String and a slice into it:

1
2
3
4
struct Mess<'a> {
    buf: String,
    view: &'a str,   // borrows from buf
}

You can’t actually build this — Rust won’t let you write a struct that borrows from one of its own fields, because the move that follows construction would invalidate the borrow. Any helper that produces a Mess and returns it by value moves buf into the caller’s stack frame; view would still point at the old location. Disaster.

async fn bodies generate exactly this kind of struct under the hood: an enum of “states,” each carrying the locals live across an .await. If one of those locals is a reference to another local, the future is self-referential. Move it after polling and you’ve hit UB.

What Pin actually does

Pin<P> wraps a pointerBox<T>, &mut T, Rc<T>, etc. — and downgrades its API. Specifically: you can no longer reach the inner &mut T unless T: Unpin.

Without &mut T, you can’t std::mem::swap or mem::replace to move the value out. That’s the whole guarantee: the value behind a Pin<P> will never move again, except by running its destructor.

1
2
3
4
5
6
use std::pin::Pin;

let mut boxed: Pin<Box<i32>> = Box::pin(42);
// i32: Unpin, so we can still write through the pin.
*boxed = 99;
assert_eq!(*boxed, 99);

For i32 the pinning is theatre — primitives implement Unpin, the marker that says “moving me is fine.” Pin only bites when the inner type is !Unpin.

Where it bites: polling a future

Future::poll takes self: Pin<&mut Self>. The compiler-generated futures from async {} are !Unpin, so you can’t poll them through a plain &mut. You have to pin first.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};

let fut = async { 7 * 6 };
let mut boxed: Pin<Box<_>> = Box::pin(fut);

let mut cx = Context::from_waker(Waker::noop());

match boxed.as_mut().poll(&mut cx) {
    Poll::Ready(v) => assert_eq!(v, 42),
    Poll::Pending => unreachable!(),
}

Box::pin is the easy answer when you need an owned, heap-allocated, pinned future. The allocation is what makes the address stable — Box already promised that.

Stack pinning with pin!

Heap allocation just to poll a future feels heavy, and it is. The pin! macro pins a value on the stack instead: it shadows the binding so you can never get a non-pinned reference to it again.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::future::Future;
use std::pin::pin;
use std::task::{Context, Poll, Waker};

let mut fut = pin!(async { String::from("ready") });

let mut cx = Context::from_waker(Waker::noop());

if let Poll::Ready(s) = fut.as_mut().poll(&mut cx) {
    assert_eq!(s, "ready");
}

The trick is that pin! re-binds fut to a Pin<&mut _> that borrows from a hidden, scope-local slot. The original value lives until the end of the block; nothing can sneak in and move it. Stable since Rust 1.68.

Unpin: the escape hatch for ordinary types

Unpin is an auto trait: almost everything implements it automatically. String, Vec<T>, your own struct of plain fields — all Unpin. For those, Pin<&mut T> and &mut T are interchangeable via Pin::new and Pin::into_inner:

1
2
3
4
5
6
7
8
9
use std::pin::Pin;

let mut s = String::from("hi");
let pinned: Pin<&mut String> = Pin::new(&mut s);
// String: Unpin, so we get back a normal &mut String.
// `into_inner` is an associated function, not a method — avoids
// shadowing whatever `into_inner` the underlying type might have.
Pin::into_inner(pinned).push_str(", world");
assert_eq!(s, "hi, world");

This is why most of the time you can pretend Pin doesn’t exist. It only shows teeth when something is deliberately !Unpin — async-generated futures, intrusive linked-list nodes, and any self-referential struct you opt into with PhantomPinned.

When you’ll see it yourself

In day-to-day code you almost never write Pin<P> directly — the pin! macro and Box::pin cover polling, and tokio::pin! or tokio::spawn cover the async runtime case. You’ll meet Pin<&mut Self> in earnest when you:

  • Hand-roll a Future. poll takes Pin<&mut Self>, so any state machine you implement by hand has to thread it through.
  • Write a self-referential struct. Stick a PhantomPinned field in, and from then on the only way to use the type is through a Pin.
  • Build a custom executor. Pinning the futures the executor stores is exactly the invariant the Future trait is asking you for.

The mental model that sticks: Pin<P> doesn’t pin the pointer — it pins what the pointer points at. The pointer can move, be copied, be passed around; the value under it has promised to stay where it was first pinned, all the way until Drop. That’s the contract the async machinery is built on, and the reason it’s safe to keep an .await point inside a function call hundreds of frames deep.