arthrop0d

Brief articles on varied topics.

Rust Pin and Unpin for Self-Referential Futures

rustconcurrency

Pin is Rust’s way to guarantee that a value will not be moved after its memory address becomes significant. Unpin is the marker trait for values that remain safe to move even when they are accessed through Pin.

These guarantees matter especially for asynchronous Rust, where a future can contain references to its own stored state.

The problem: a future can be moved between polls

An async block or async fn does not run immediately. The compiler transforms it into a future, a value that stores the function’s state and can be resumed later. An executor repeatedly calls that future’s poll method until it completes.

A simplified asynchronous function might look like this:

async fn process() {
    let mut buffer = [0; 1024];
    read_into(&mut buffer).await;
    use_buffer(&buffer);
}

The compiler turns the function into a state machine. Because buffer must survive the .await, it becomes part of that state machine. At some point, the future may also need a reference to that stored buffer so it can resume the operation later. That creates a self-referential value: one field refers to another field in the same value.

The reference is valid only while the referenced field stays at the same address. If the whole future moves, its fields move too, but the stored reference still contains the old address.

Without a pinning guarantee, an executor could accidentally do this:

  1. Store a future in one location.
  2. Poll it, allowing it to create internal references.
  3. Move the future to a different location.
  4. Poll it again.

The internal reference would now point to the future’s former location. Rust’s ordinary &mut reference does not promise that its target will remain at a fixed address, so it cannot by itself solve this problem.

What Pin guarantees

Pin<P> wraps a pointer-like value P, such as &mut T or Box<T>. It says that the value reached through that pointer must not be moved through the pinned pointer, unless that value implements Unpin.

The important detail is that the pointer wrapper can move. For example, a Pin<Box<T>> handle can be passed around by value. The guarantee concerns the T inside the box: its address remains stable.

Pinning does not make a value immutable. Code can still modify a pinned value through APIs designed for pinned access. It simply cannot replace the value with another value or move it somewhere else in a way that changes its address.

The Future trait expresses this requirement directly. Its polling operation receives a pinned mutable reference:

poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>

An executor must therefore pin a future before polling it. Once polling may have established self-references inside the future, the executor must keep the future in the same storage until it completes or is dropped.

What Unpin means

Unpin is an auto trait: Rust implements it automatically for most types whose contents do not require a stable address. Integers, strings, ordinary structs, and many futures are Unpin.

For an Unpin type, pinning is not a meaningful restriction. It is safe to extract an ordinary mutable reference and move the value, because the type does not rely on its address. This is why generic code often works with either a pinned or unpinned value when its type has an Unpin bound.

Some types are !Unpin, meaning they do not implement Unpin. Compiler-generated futures can be !Unpin when their state may contain address-sensitive references. A type can also opt out of Unpin explicitly, commonly using PhantomPinned, a zero-sized marker that tells Rust the type must not be moved after pinning.

The distinction is not “pinned versus unpinned” as a permanent property of every object. It is a promise about what operations are safe. Pin<&mut T> restricts movement when T is !Unpin; for T: Unpin, ordinary movement remains safe.

Putting a future in pinned storage

A common way to create a pinned future is to allocate it on the heap:

use std::pin::Pin;

let future = async {
    // asynchronous work
};

let pinned: Pin<Box<_>> = Box::pin(future);

Box::pin allocates the future and returns a pinned box. Moving the Pin<Box<_>> value moves only the box handle, not the future inside the allocation. An executor can keep that allocation stable while polling the future.

Heap allocation is not the essence of pinning. A future can also be pinned in stack storage, provided the code ensures that the stack value is not moved for as long as the pin is used. Stack pinning is more restrictive because returning the value, moving it into another variable, or letting its storage end too early would violate the guarantee.

This is also why APIs that manipulate pinned structures need care. Moving an ordinary field out of a pinned struct could move data whose address is being used by another field. Safe APIs expose only operations that preserve the pinning promise; low-level operations that bypass it are unsafe and require the programmer to uphold the address-stability rules.

Why the error messages mention Unpin

A common compiler error says that a future “cannot be unpinned” or that a bound such as F: Unpin is missing. Usually, some API wants to move or poll a future as an ordinary mutable value, but the future is !Unpin. The fix is generally not to force an Unpin implementation. Instead, place the future in stable storage, often with Box::pin, and pass the resulting pinned value to the API.

The central model is simple: an asynchronous future may contain state that refers to itself, so polling must happen through a pinned location. Pin preserves that location, while Unpin identifies types for which moving remains harmless.

← All articles