Skip to content

← Async From First Principles step 11 of 25

Medium Framework

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

A future's poll method receives self: Pin<&mut Self>, not &mut self. A pinned value is one that promises not to move to a different memory address until it is dropped. A combinator (a future that wraps and drives other futures) usually needs to update its own fields inside poll, so it has to get mutable access through that Pin. This exercise shows the simplest safe way to do that: store the child future in a Pin<Box<_>>.

The starter code does not compile. Your job is to make it compile and work without writing unsafe.

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

The contract

Wrapper counts how many times its own poll has been called. Each call to Wrapper::poll adds 1 to the count and then polls the child once. If the child returns Poll::Ready, the wrapper returns Poll::Ready(count); otherwise it returns Poll::Pending.

count_polls(pends) wraps the provided future wait(pends) and runs it with the provided block_on. The provided Countdown future returns Poll::Pending exactly pends times and then Poll::Ready(()), so the wrapper is polled pends + 1 times and count_polls(pends) returns pends + 1. For example, count_polls(0) returns 1 and count_polls(3) returns 4.

Tests use 0 <= pends <= 1000. The provided block_on stops with a panic after 100,000 polls, so it can only complete inputs up to 99_999; you do not need to change that limit. Keep Countdown, wait, block_on and the signatures of Wrapper::new and count_polls as they are. You may change the type of Wrapper's private inner field.

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>>`

Pin<Ptr> is a standard library type that wraps a pointer such as &mut T or Box<T>. Reading through it is always allowed when Ptr implements Deref, so self.count can be read even in the starter. Writing through it with *self needs DerefMut, and the standard library only provides that when the pointed-to type implements Unpin. In simplified form, the standard library's implementation is:

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

The reason is that a plain &mut T lets safe code move the value, for example with std::mem::swap or std::mem::replace. For a type that relies on staying at one address, that would break the pinning promise. Unpin is the marker for types that do not rely on their address, so for those types handing out &mut T is harmless. The other safe ways to get &mut T out of a pin follow the same rule: Pin::get_mut also requires T: Unpin, and the only ways around it are unsafe functions such as Pin::get_unchecked_mut.

Unpin is an auto trait. The compiler implements it automatically for a struct when every field implements it, unless the struct has an explicit implementation. Most ordinary types, including u32, are Unpin. Futures created by async fn and async blocks are not, even when they hold no local variables.

The starter's Wrapper<F> stores F directly. Inside the generic impl, the compiler only knows F: Future, so it cannot prove Wrapper<F>: Unpin, and &mut *self is rejected. In count_polls, F is the async fn future returned by wait, which really is not Unpin.

Three ways to get &mut Self without unsafe

  1. Make every field Unpin. Then the struct is Unpin automatically and &mut *self compiles.
  2. Call Pin::get_mut(self). It is safe and returns &mut Self, but it has the same Self: Unpin requirement as &mut *self.
  3. Put the child future behind Box::pin. Box<T> is Unpin for every T, so Pin<Box<F>> is Unpin for every F. The future stays at a fixed heap address; moving the Pin<Box<F>> only moves the box pointer.

Adding an F: Unpin bound to the impl is not a fix here. The library code would compile, but count_polls would then fail with E0277 because the future returned by wait is not Unpin.

Option 3 is the fix for this exercise, and it is really option 1 applied to the inner field: once inner has type Pin<Box<F>>, every field of Wrapper<F> is Unpin, so &mut *self compiles. You then poll the child with this.inner.as_mut().poll(cx). Pin::as_mut turns a &mut Pin<Box<F>> into a short-lived Pin<&mut F>, which is what Future::poll needs, without moving the box.

Implementing Unpin by hand

impl<F: Future> Unpin for Wrapper<F> {} is a safe implementation and needs no unsafe keyword. (The F: Future bound has to be repeated because the struct declares it; without it the impl fails with E0277.) Safe code alone cannot misuse it: with a plain F field, it gives you &mut F, but polling that field would still need either F: Unpin or an unsafe call such as Pin::new_unchecked.

The obligation falls on any unsafe code that creates a Pin<&mut Field> from a Pin<&mut Self>, which is called pin projection. A field accessed that way is called structurally pinned, and that code is only sound if the containing type implements Unpin only when that field's type does too. A hand-written Unpin implementation can therefore make existing unsafe projection code unsound. In the boxed version, nothing needs to be projected, so no such conflict exists.

Cost

Box::pin performs one heap allocation for each boxed child future that is not zero-sized, and polling the child goes through one extra pointer. Library combinators, such as those in the futures crate, often store children inline and use pin projection (usually generated by a crate such as pin-project-lite) to avoid that allocation. Whether the allocation matters for your code is something to measure rather than assume.