Skip to content

← Async From First Principles step 11 of 25

Medium Framework

`Unpin` in practice: making `Pin<&mut Self>` usable without unsafe

Every combinator in this track needs to reach its own fields from inside poll, and poll hands you a Pin<&mut Self> rather than a &mut self. This problem is about the cheapest way to make that work — cheap enough that the rest of the track needs no unsafe at all.

The starter does not compile. That is the problem.

pub struct Wrapper<F: Future> { inner: F, count: u32 }
pub fn count_polls(pends: u32) -> u32

Wrapper counts how many times it was polled and resolves to that count when its child does. count_polls(pends) wraps a given async fn that pends pends times, drives it, and returns the count — always pends + 1.

The error

error[E0596]: cannot borrow data in dereference of `Pin<&mut Wrapper<F>>` as mutable
   = help: trait `DerefMut` is required to modify through a dereference,
           but it is not implemented for `Pin<&mut Wrapper<F>>`

Here is the whole mechanism, and it is smaller than it looks. Pin<Ptr> is an ordinary library type. It implements Deref always, and DerefMut only when the pointee is Unpin:

impl<Ptr: DerefMut> DerefMut for Pin<Ptr> where Ptr::Target: Unpin { .. }

That single where clause is the entire enforcement mechanism. Handing out &mut T would let anyone call mem::replace or mem::swap and move the value out of its pinned address, so the API simply refuses to produce one unless T has said, via Unpin, that it does not care where it lives.

Unpin is an auto trait: nearly every type gets it automatically, and a struct gets it exactly when all of its fields have it. Wrapper<F> stores an F inline and F is only known to be Future — it could be an async fn future, which is !Unpin. So Wrapper<F> is not Unpin either, and the deref is refused.

Three ways out

  1. Let auto-Unpin do it. If every field is already Unpin, the struct is too, and &mut *self compiles with no ceremony.
  2. Pin::get_mut(self). A safe function, available when Self: Unpin. Same requirement, different spelling.
  3. Box the address-sensitive child. Pin<Box<F>> is Unpin for any F, because the Box is what holds the address steady — moving the Pin<Box<F>> moves a pointer, not the future. Store the child that way and the parent becomes genuinely movable.

Option 3 is the fix here, and it is the rule the whole combinator tier of this track runs on: if the children live behind Box::pin, the parent is Unpin and needs no pin projection at all. You reach the child with self.inner.as_mut().poll(cx)Pin::as_mut reborrows, exactly as it did in your block_on.

Unpin is a safe trait — and that is not a licence

impl Unpin for MyType {} needs no unsafe. Say that out loud, because everything else near Pin looks like it should. You are allowed to write it.

But “safe to write” is not “always sound”. A hand-written Unpin impl is only correct when no field is structurally pinned — when nothing inside the struct is relying on its address. Boxing the child is what makes that true here. If you later add a bare F field to the same struct and leave the impl in place, you have created real unsoundness with no unsafe keyword anywhere in sight. Item 16.16 shows the version where the field genuinely is pinned in place, and what you owe the compiler then.

Cost

One heap allocation per child. That is why futures and tokio do the harder thing and store children inline with pin projection. It is also why almost nobody needs to: Box::pin is fine unless you are writing a library that allocates on a hot path.