Skip to content
← All Rust articles

Rust explainer

Pin and Unpin: keeping a value at a stable address

About 6 min to read. Allow extra time to work through examples.

Learn why some futures need a stable address, what Pin guarantees, and how Unpin changes the operations available through a pinned pointer.

This article uses Rust. You can make it your practice mode to browse related content across the site.

The Future trait receives self: Pin<&mut Self> when it is polled. To understand why, first consider a value whose correctness depends on its address.

Which moves can invalidate a pointer?

Suppose a struct contains an inline array and a raw pointer to one of that array's elements. Moving the struct to another address relocates the array, but does not automatically update the stored pointer. An API that later dereferences that pointer must prevent this situation or repair the pointer.

Moving a value does not necessarily move everything it owns. A String or Vec stores its elements in a separate allocation. Moving its header transfers ownership of that allocation without relocating the elements. A pointer into the buffer does not become invalid solely because the header moved, though reallocation or destruction can invalidate it.

This distinction matters: swapping two structs, each containing a String and a pointer into its own string buffer, does not by itself break those associations. The pointer and owning header move together.

Pinning addresses the case where the pointee's own address must stay fixed. It does not automatically make raw pointers valid or prevent a nested buffer from reallocating.

Why polling a future can require a stable address

An async function may retain a reference to one of its own stored locals across a suspension point. An inline array makes the concern clear:

async fn wait_with_borrow() {
    let bytes = [7_u8, 8, 9];
    let first = &bytes[0];
    std::future::pending::<()>().await;
    std::hint::black_box(first);
}

This future never completes because pending() never becomes ready. The example shows a borrow needed after an await, rather than a useful operation. A suspended state machine may need to retain both the array and a reference into it. Moving that state could invalidate the reference.

Calling an async function constructs its future without running the body. The returned future can be moved before it is pinned and polled. Once it is pinned, code must preserve the pinning contract even when no poll is active. Not every future is self-referential; the common poll interface must support those that are.

Pin protects the pointee

Pin<Ptr> wraps a pointer such as &mut T or Box<T>. For an address-sensitive target, it represents a promise that the target will stay at its address until it is dropped. Moving the pointer wrapper is different from moving its target.

This complete example moves a pinned box while its allocation stays put:

use std::marker::PhantomPinned;

struct Stationary {
    byte: u8,
    _pin: PhantomPinned,
}

fn main() {
    let pinned = Box::pin(Stationary {
        byte: 7,
        _pin: PhantomPinned,
    });
    let before = std::ptr::from_ref(&pinned.as_ref().get_ref().byte);
    let moved_box = pinned;
    let after = std::ptr::from_ref(&moved_box.as_ref().get_ref().byte);
    assert_eq!(before, after);
}

PhantomPinned prevents automatic Unpin implementation. It does not pin a value by itself: an ordinary unpinned Stationary can still be moved.

Unpin allows ordinary mutable access

Unpin means a type does not require Pin's address-stability restrictions. It is an auto trait, so ordinary structs generally receive it when their fields do. Types may also implement it explicitly when their invariants allow that. Compiler-generated async futures generally require pinning to use this interface; do not infer their Unpin status just by inspecting their locals.

For an Unpin target, safe methods can recover an ordinary mutable reference:

use std::pin::Pin;

fn main() {
    let mut number = 7;
    let pinned = Pin::new(&mut number);
    *pinned.get_mut() = 8;
    assert_eq!(number, 8);
}

For a target that is not Unpin, Pin<&mut T> does not generally expose &mut T. Such a reference would permit moving the value with operations like mem::replace. Shared access remains available, as do methods designed to receive Pin<&mut Self> or Pin<&Self>.

Pinning does not mean immutable data. A type can offer mutations that preserve its address-sensitive invariants. Pin::set can even replace a pinned value by first dropping the old value in place, then installing the replacement.

Choose a safe constructor

  • Use std::pin::pin! for local pinned storage. Inside an async function, that storage can be part of the enclosing future rather than a literal stack slot.
  • Use Box::pin when you want owned heap storage that can be returned or kept in another object.
  • Use Pin::new for a pointer whose target implements Unpin.

For example, polling a local async future once can look like this:

use std::future::Future;
use std::pin::pin;
use std::task::{Context, Waker};

fn main() {
    let future = async { 7 };
    let mut future = pin!(future);
    let mut context = Context::from_waker(Waker::noop());
    assert!(future.as_mut().poll(&mut context).is_ready());
}

Moving a Pin<Box<F>> field does not move its F allocation. This often makes a wrapper easier to implement, but whether the entire wrapper is Unpin still depends on its other fields and invariants.

Unsafe code and field access must preserve the contract

Pin is a library abstraction whose safe interface relies on contracts upheld by its constructors, pointer types and unsafe implementations. Restricting DerefMut is an important part of that interface, not the entire safety argument.

Merely defining fn into_inner(self) on a type does not let callers move it out of a pinned reference: they still need an owned self to call that method. The danger is an API or unsafe implementation that actually exposes a forbidden move, such as obtaining &mut Self from a pinned receiver and replacing a field that was promised to remain pinned.

Projecting Pin<&mut Parent> to Pin<&mut Field> makes that promise for the field. The parent's Unpin implementation, destructor and mutation methods must respect it. Packed fields can introduce alignment and destructor moves, so they cannot be treated as ordinary structurally pinned fields.

Pin::new_unchecked is for code that can justify these obligations. It is not a way to silence a pinning error. Its contract continues after the temporary Pin wrapper has gone away.

Keep storage valid through destruction

A pinned value's storage must stay valid until its destructor has run. For example, an intrusive list node may need its destructor to remove pointers that other nodes hold to it before its storage disappears.

Safe mem::forget is still permitted. Forgetting a pinned box leaks its allocation, so it does not move or invalidate the target. Forgetting a borrowed Pin wrapper does not give permission to reuse the borrowed storage in violation of an earlier unsafe pinning promise.

Pin does not guarantee that destructors always execute. It requires that storage not be invalidated before the required in-place destruction. Leaking storage and freeing it without running the necessary destructor are different.

Practice ordinary pinned access in Unpin in practice, then examine field projection in Pin projection by hand.

step 10 of 25

Async From First Principles

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