Why pinning exists

Most Rust values can be moved freely. That is usually fine because references are tied to lifetimes, not addresses. However, some designs depend on a value staying in one place after initialization.

Common examples include:

  • a struct that stores a pointer to one of its own fields
  • generators and async futures that suspend and resume later
  • intrusive linked lists or trees where nodes hold links to themselves or neighbors
  • FFI objects that must not be relocated after registration with a C API

If such a value moves, internal pointers can become invalid. Rust prevents many of these patterns at compile time, but sometimes you still need them. Pin provides a controlled escape hatch.


The core idea of Pin

A pinned value is one that cannot be safely moved out of its location through the pinned API.

The important distinction is this:

  • Pin does not mean the value is physically impossible to move.
  • Pin means safe code cannot move it in ways that would violate its invariants.

In practice, Pin<P> wraps a pointer-like type P, such as:

  • Pin<&mut T>
  • Pin<Box<T>>
  • Pin<Rc<T>>
  • Pin<Arc<T>>

The most common form is Pin<Box<T>>, which pins a heap allocation. Once boxed and pinned, the allocation’s address stays stable unless you explicitly use unsafe code to break the guarantee.


When pinning is necessary

Pinning is only useful when a type’s correctness depends on address stability. If your type does not store self-references or otherwise depend on a fixed location, you probably do not need it.

Typical use cases

ScenarioWhy pinning helps
Self-referential structsInternal pointers remain valid only if the struct does not move
Async futuresState machines may contain references across suspension points
Intrusive collectionsNodes store links that assume stable addresses
FFI handlesExternal code may keep raw pointers to Rust-managed data

A good rule: if you can redesign the type to avoid self-references, do that first. Pinning is a specialized tool, not a default design choice.


A practical example: a self-referential type

Self-referential structs are difficult in Rust because moving the struct would invalidate internal pointers. Let’s look at a simplified example.

use std::pin::Pin;
use std::ptr::NonNull;

struct SelfRef {
    data: String,
    data_ptr: Option<NonNull<String>>,
}

impl SelfRef {
    fn new(text: &str) -> Pin<Box<SelfRef>> {
        let mut boxed = Box::pin(SelfRef {
            data: text.to_owned(),
            data_ptr: None,
        });

        let ptr = NonNull::from(&boxed.data);
        // Safe because the boxed value is pinned and won't move.
        unsafe {
            let mut_ref = Pin::as_mut(&mut boxed);
            Pin::get_unchecked_mut(mut_ref).data_ptr = Some(ptr);
        }

        boxed
    }

    fn data_ref(self: Pin<&Self>) -> &String {
        self.data_ptr
            .map(|ptr| unsafe { ptr.as_ref() })
            .expect("pointer initialized")
    }
}

fn main() {
    let pinned = SelfRef::new("hello");
    let text = pinned.as_ref().data_ref();
    println!("{text}");
}

What this example shows

  • The struct stores a raw pointer to one of its own fields.
  • The pointer is initialized only after the value is pinned.
  • The pointer is later used through a pinned reference.

This is a common pattern, but it requires care. The unsafe block is necessary because Rust cannot verify the internal pointer relationship automatically.


Understanding the Pin API

The Pin type is designed around a few key operations.

Pin::new

Used when you already have a pointer-like value and want to pin it, often with types that are known to be safe to pin.

Pin::as_ref and Pin::as_mut

These convert pinned pointers into pinned references:

  • Pin<&T>
  • Pin<&mut T>

They are useful for accessing methods that require pinned access.

Pin::get_ref

Returns a shared reference to the inner value when moving is not a concern.

Pin::get_mut

Returns &mut T only if the type is Unpin. This is important: if a type is Unpin, then moving it is considered safe, so pinning does not impose extra restrictions.

Pin::get_unchecked_mut

Unsafe escape hatch that gives mutable access regardless of Unpin. You must ensure you do not move the value in a way that breaks pinning guarantees.


Unpin: the default case

Most Rust types are Unpin. That means they can be safely moved even if wrapped in Pin.

This surprises many developers at first, but it is intentional. Pinning only matters for types that explicitly opt out of Unpin behavior.

Practical consequence

If T: Unpin, then Pin<&mut T> behaves much like &mut T for many operations. You can often extract a mutable reference safely.

If T: !Unpin, then the pinned location matters, and the API becomes stricter.

How types become !Unpin

A type can be made non-Unpin by including a PhantomPinned field:

use std::marker::PhantomPinned;

struct NotMovable {
    data: String,
    _pin: PhantomPinned,
}

PhantomPinned tells the compiler that this type should not automatically implement Unpin.


Building a pinned type correctly

When designing a pinned type, follow a few rules:

  1. Initialize all fields before exposing the pinned value.
  2. Avoid moving the value after it becomes pinned.
  3. Only use unsafe when you can prove the invariants manually.
  4. Provide APIs that operate on Pin<&mut Self> or Pin<&Self> when the type requires pinning.

A safer construction pattern

A common approach is a two-phase initialization:

  • allocate the object
  • pin it
  • finish initialization using pinned access

This is especially useful when you need to store pointers or register the object with external systems.


Pinning in async Rust

Async functions compile into state machines. These state machines may contain references to their own fields across await points, which is why futures are often treated as pinned values.

You usually do not interact with Pin directly when writing async code, but it is there under the hood. Executors poll futures through Pin<&mut F> because the future must not move while it is being resumed.

Why this matters

If a future were moved after it had stored internal references, those references could become invalid. Pinning ensures the executor respects the future’s address stability.

Best practice for async APIs

When implementing custom futures:

  • prefer composing existing async primitives
  • avoid self-referential future internals unless necessary
  • accept Pin<&mut Self> in poll
  • keep unsafe code isolated and well-documented

Writing a custom pinned future

A custom future must implement poll(self: Pin<&mut Self>, cx: &mut Context<'_>). That signature is not accidental: it guarantees the future is pinned during polling.

Here is a minimal example of a future that completes after being polled twice:

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

struct TwoPollFuture {
    polled_once: bool,
}

impl Future for TwoPollFuture {
    type Output = &'static str;

    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        let this = self.as_mut().get_mut();

        if this.polled_once {
            Poll::Ready("done")
        } else {
            this.polled_once = true;
            Poll::Pending
        }
    }
}

Because TwoPollFuture is Unpin, calling get_mut() is safe. If the future contained self-referential state, you would need a different design and likely more careful pin handling.


Best practices for API design

Pinning affects how users interact with your types, so the API should make the intended constraints obvious.

Prefer pinned methods for pinned invariants

If a method depends on address stability, take Pin<&mut Self> or Pin<&Self> rather than plain references.

Keep pinned and unpinned operations separate

A useful design pattern is to provide:

  • ordinary constructors that create the value
  • a pinning constructor such as Box::pin
  • methods that clearly require pinned access

Avoid exposing raw pointers unnecessarily

Raw pointers are often required internally, but they should not leak into the public API unless the abstraction truly demands it.

Document invariants explicitly

If your type requires pinning, say so in the docs:

  • whether the type is !Unpin
  • which methods require pinned access
  • whether users may move the value before initialization completes

Clear documentation is essential because pinning is a semantic guarantee, not just a type signature.


Common mistakes

Assuming Pin prevents all moves

Pin only constrains safe movement through the API. Unsafe code can still violate the guarantee, so the burden is on the implementer.

Using pinning when borrowing would suffice

Many self-referential designs can be replaced with indices, arenas, or separate ownership. Those alternatives are often simpler and safer.

Calling get_unchecked_mut casually

This is one of the most dangerous APIs in the standard library. Use it only when you can prove that no move will occur and that all pin invariants remain intact.

Forgetting about drop behavior

Pinned types may need custom Drop logic. If a type stores internal references or external registrations, cleanup must respect the same invariants as normal operation.


Pinning versus alternative designs

Before choosing Pin, compare it with other approaches.

ApproachStrengthsTrade-offs
PinPreserves stable addresses in safe APIsRequires careful design and sometimes unsafe code
Indices into a collectionSimple and ergonomicRequires a container that manages identity
Arena allocationStable addresses and efficient allocationExtra allocation strategy and lifetime management
Rc/Arc with indirectionShared ownership and stable heap locationReference counting overhead
Redesign to avoid self-referenceOften simplest and safestMay require more refactoring

In many codebases, an arena or index-based design is easier to maintain than a pinned self-referential struct.


Practical guidance for production code

If you need pinning in a real project, follow these guidelines:

  • Start with a design that avoids self-references.
  • Use Pin<Box<T>> for heap-allocated pinned values.
  • Make the type !Unpin only when necessary.
  • Keep unsafe initialization small and isolated.
  • Expose pinned methods instead of encouraging direct field access.
  • Test drop behavior and edge cases carefully.

A well-designed pinned abstraction should feel ordinary to its users. The complexity should remain inside the implementation.


Summary

Pinning is Rust’s mechanism for expressing address-sensitive values. It is most useful when a type stores internal pointers, participates in async state machines, or interacts with systems that assume stable memory locations.

The key ideas are:

  • Pin protects invariants about movement, not memory in the abstract.
  • Unpin is the default; only some types need pinning.
  • Safe APIs should make pinning requirements explicit.
  • Unsafe code is sometimes necessary, but it should be minimal and well-justified.

Used carefully, Pin lets you build advanced abstractions without sacrificing Rust’s safety guarantees.

Learn more with useful resources