#164 May 26, 2026

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.

← Previous 163. Cow::to_mut — Lazy In-Place Mutation Through Cow Next → 162. Pin<P> — The Pointer Type That Says 'This Won't Move'