arthrop0d

Brief articles on varied topics.

Rust Pin and Unpin Explained

rustsoftware engineering

Pin is a Rust wrapper that promises a value will not be moved in memory, while Unpin marks values for which that promise is unnecessary. Together, they let Rust safely handle self-references and asynchronous state machines whose correctness depends on a stable address.

The problem: moving can invalidate an address

In Rust, assigning or passing a value can move it: ownership changes and the value's bytes are copied to a new location. A value on the stack may move when a variable is returned, placed in a collection, or replaced. A value behind a Box can also be moved if the box's allocation is changed, although moving the Box itself normally leaves the heap allocation where it is.

Most values do not care about their address. An integer remains valid wherever its bytes are. But a self-referential value stores a pointer or reference to one of its own fields. If the whole value moves, that pointer still contains the old address and no longer points into the value.

struct SelfReferential {
    data: String,
    pointer: *const u8,
}

If pointer is set to the buffer inside data, moving SelfReferential can make it stale. A stale pointer can lead to incorrect reads or memory unsafety. Ordinary Rust references make it difficult to construct this pattern safely, but low-level libraries and compiler-generated code sometimes genuinely need it.

The same issue appears in asynchronous Rust. An async block or async fn is compiled into a future: a value containing the operation's local variables and its progress through suspension points. When the future reaches .await, it stores enough state to resume later. If one part of that state refers to another part by address, the future must not move while it is being polled.

What Pin guarantees

Pin<P> wraps a pointer type P, such as &mut T or Box<T>. The important distinction is that the Pin wrapper can move, but the value it points to cannot be moved through that pinned pointer when the value is address-sensitive.

For example:

use std::pin::Pin;

let future = async {
    // asynchronous work
};

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

Box::pin allocates the future on the heap and returns a pinned box. Moving pinned to another variable moves the box handle, not the allocation containing the future. The future therefore keeps the same address.

Pin does not make the value immutable. Code can still change fields through permitted mutable access. The restriction is specifically about moving the value itself out of its location. Safe APIs do not let code turn a Pin<&mut T> into an ordinary &mut T unless Rust knows that moving T is safe.

This is why the Future trait's polling method receives a pinned mutable reference:

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

fn poll<F: Future>(future: Pin<&mut F>, context: &mut Context<'_>) -> Poll<F::Output> {
    future.poll(context)
}

An executor—the runtime that schedules futures—pins a future before repeatedly calling poll. The future may return Poll::Pending, meaning it needs to be polled again later, but its address remains stable between polls.

What Unpin means

Unpin is a marker trait: it has no methods, but it tells Rust that a type is safe to move even when it is behind Pin. Most ordinary types implement Unpin, including numbers, strings, and most structs.

This means pinning a String does not meaningfully restrict it. Because String: Unpin, safe code can obtain the ordinary mutable access needed to move or replace the String's value. The allocation used by the string's contents is a separate concern; Unpin describes whether the String value itself may move.

A type that must remain at one address can opt out of Unpin. The standard library provides PhantomPinned for this purpose:

use std::marker::PhantomPinned;

struct AddressSensitive {
    data: String,
    _pinned: PhantomPinned,
}

A type containing PhantomPinned is normally not Unpin. Once such a value is pinned, safe code cannot move it out through Pin. In practice, a library usually hides construction and field access behind carefully designed methods, because creating a self-reference and implementing safe pinning operations may require unsafe code.

Pin says: “this location will remain stable.”
Unpin says: “this type does not need that guarantee.”

Pinning is about the pointee, not the pointer

A common source of confusion is assuming that Pin freezes everything. It does not prevent the Pin<Box<T>> handle from moving, and it does not prevent the owner from dropping the value. It prevents the address-sensitive T from being moved while pinned.

Pinning also applies to fields. If a pinned struct contains a field that must stay put, code cannot freely extract that field because extraction would move it out of the struct. A pin projection is an operation that produces a pinned reference to such a field while preserving the guarantee for the parent. Projection must be implemented carefully; libraries often provide projection macros or wrapper types so users do not have to write the unsafe details themselves.

The guarantee is therefore a contract enforced by the type system and by the unsafe code that implements pinning-related abstractions. Rust's compiler prevents ordinary safe code from violating the contract, but an incorrect unsafe implementation can still break it.

Why this appears in error messages

When working with async code, you may see errors saying that a future cannot be moved, that a future is not Unpin, or that a method requires Pin<&mut _>. These messages usually mean the future's generated state machine may contain address-sensitive state. The fix is generally not to force a move, but to pin the future—often by storing it in Box::pin or using an appropriate pinning utility supplied by the runtime.

The result is a precise division of responsibility: ordinary values remain easy to move, while self-referential values and suspended asynchronous operations receive a stable address for as long as their APIs require it.

← All articles