We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 16 of 25
Structural pinning and safe pin projection by hand
Every combinator you have written so far cheated: the children lived behind
Box::pin, which made the parent trivially Unpin. Real libraries do not
cheat, because that box is a heap allocation per child on a hot path. This is
how they avoid it.
pub struct Timed<F> { inner: F, polls: u32 } // inner stored INLINE
pub fn count_polls(pends: u32) -> (u32, u32)
Timed counts its polls and resolves to (count, inner_output).
count_polls wraps an async block — a genuinely !Unpin future — so a
Box-based shortcut would not be answering the question.
The starter fails with E0596 on &mut *self, for the reason you now know:
with inner stored inline, Timed<F> is Unpin only when F is, and F is
not known to be.
Structural pinning, decided per field
When a struct is pinned, each of its fields is either structurally pinned
(the field is treated as pinned too, so you may hand out Pin<&mut Field>) or
not (the field is ordinary, so you may hand out &mut Field). You decide, per
field, and the decision comes with obligations.
Here:
-
inner: Fis structurally pinned. It is a future; once polled it may be self-referential; it must not move. -
polls: u32is not. It is a number. Nothing points into it.
To produce the pinned reference to inner you need unsafe, because you are
asserting something the compiler cannot check:
let this = unsafe { self.get_unchecked_mut() };
this.polls += 1;
let inner = unsafe { Pin::new_unchecked(&mut this.inner) };
The std docs list four obligations for a structurally pinned field, and this type satisfies all four:
-
The
Unpinimpl must account for the pinned field. LeaveUnpinauto-derived.Timed<F>is thenUnpinexactly whenFis — which is the truth. -
Dropmust not move it.Timedhas noDropimpl. If it had one it would receive&mut self, from which a field can be moved out, silently breaking the guarantee. Adding aDropimpl to a pin-projecting type is a real hazard; do not do it casually. -
The type must not be
#[repr(packed)], which is allowed to move fields around to satisfy alignment. -
No safe API may move the field out. There is no
fn into_inner(self) -> Fhere, and there must not be.
The trap, and it is the entire point
There is a way to make E0596 disappear in one line:
impl<F> Unpin for Timed<F> {} // DO NOT DO THIS
It compiles. It needs no unsafe. It is unsound, and the compiler will
never tell you.
Why: that impl claims Timed<F> may be moved freely for every F. But
Timed stores F inline, so moving a Timed moves the F inside it — and
if that F is a polled async block holding a reference into its own storage,
the reference is now dangling. Safe code can then trigger it:
Pin::new(&mut timed) becomes available, Pin::get_mut becomes available,
mem::swap on two Timeds becomes available, and you have memory corruption
reachable from entirely safe code.
The correct answer is to write no Unpin impl and let the auto trait do
its job, so that Timed<F>: Unpin if and only if F: Unpin. The test cases
feed you an async block precisely so that the honest version is the one that
has to work.
Compare item 16.11: there the blanket impl was sound, because the child was boxed and genuinely movable. Same line of code, opposite verdict, and the difference is one word in the struct definition. That is what “unsafe means you owe the compiler a proof” looks like in practice.
What pin-project generates
The pin-project crate exists to write this for you: you annotate fields as
#[pin] or not, and the macro emits the projection functions, the correct
conditional Unpin impl, and a compile-time check that you have no manual
Drop. It is not available in this harness, and having written the projection
once you can read what it generates.
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.