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.