Most Rust values can move freely. Assign a value to another variable, pass it by value, return it from a function, or replace it inside a container, and the value may end up at a different memory address.
Usually that is exactly what you want. Rust’s ownership model tracks who owns a value, not where that value must remain in memory.
A smaller class of types is different. Some values become address-sensitive: code relies on the value continuing to exist at the same memory location. Compiler-generated futures and carefully designed self-referential structures are common examples.
Pin exists to make those address-sensitive invariants expressible in safe APIs.
The most useful mental model is:
ordinary ownership -> who may use or destroy the value?
pinning -> may the value move to another address?These are separate questions. Pinning does not change ownership, and ordinary ownership does not guarantee a stable address.
Start with what a move means
In Rust, a semantic move transfers ownership:
let first = String::from("report");
let second = first;After the assignment, second owns the string and first cannot be used.
For ordinary String values, it does not matter whether the String struct itself resides at the same address before and after the move. Its public contract does not depend on that address.
The same is true for most Rust types. Their behavior is independent of the location where the value object happens to live.
Pinning becomes relevant only when a type has an invariant that depends on its location.
Pin wraps a pointer, not the value directly
The standard library type is:
Pin<Ptr>Ptr is a pointer-like type such as &mut T or Box<T>.
The important detail is that Pin<Ptr> pins the pointee, not the pointer value itself.
For example:
use std::pin::Pin;
fn inspect(value: Pin<&mut String>) {
println!("{}", value.len());
}The Pin<&mut String> says the referenced String is being accessed through a pinning pointer.
The Pin wrapper itself can still move as an ordinary Rust value. Moving a Pin<Box<T>>, for example, moves the Box handle, not the heap allocation containing T.
That distinction is central to understanding pinning.
Unpin means a type does not care about pinning
Most Rust types implement the auto trait Unpin.
A type that implements Unpin promises that its correctness does not depend on remaining at one address.
For T: Unpin, pinning does not meaningfully restrict normal mutable access:
use std::pin::Pin;
let mut name = String::from("draft");
let mut pinned = Pin::new(&mut name);
pinned.as_mut().get_mut().push_str(".md");
assert_eq!(name, "draft.md");String implements Unpin, so get_mut() can safely return an ordinary &mut String.
Once you have &mut String, operations such as mem::replace are allowed because moving a String value does not violate any pinning-dependent invariant.
This gives an important rule:
Pinmatters only when the pointee does not implementUnpin, or when a generic API must work correctly for types that might not implement it.
PhantomPinned opts a type out of automatic Unpin
Most structs automatically implement Unpin when their fields do.
A type that needs pinning guarantees can opt out by containing PhantomPinned:
use std::marker::PhantomPinned;
struct AddressSensitive {
label: String,
_pin: PhantomPinned,
}PhantomPinned itself does not implement Unpin, so a struct containing it will not automatically implement Unpin.
That changes what safe code may do through Pin.
Suppose a function receives:
use std::pin::Pin;
fn update(value: Pin<&mut AddressSensitive>) {
// ...
}Safe code cannot simply turn that into &mut AddressSensitive and then move the whole value out with mem::replace.
That restriction is what lets unsafe internals rely on the pinned value remaining at the same address.
Pin does not make a value immovable everywhere
A common misunderstanding is that once a type is !Unpin, values of that type can never move.
They can.
A value may move freely before it is pinned:
use std::marker::PhantomPinned;
struct AddressSensitive {
id: u64,
_pin: PhantomPinned,
}
let first = AddressSensitive {
id: 7,
_pin: PhantomPinned,
};
let second = first;That move is valid because no pinning guarantee has been established yet.
The relevant lifecycle is:
construct value
-> moves are still allowed
establish pinning
-> safe code must preserve the pinned location
drop valuePinning is a state established through a pinning pointer. It is not a permanent property that applies from construction onward.
Pin values on the stack with pin!
For temporary local values, the pin! macro is often the simplest safe option:
use std::marker::PhantomPinned;
use std::pin::pin;
struct TaskState {
progress: u8,
_pin: PhantomPinned,
}
let state = TaskState {
progress: 0,
_pin: PhantomPinned,
};
let mut state = pin!(state);The result behaves like a pinned mutable reference whose lifetime is tied to the local scope.
A function can accept it without needing heap allocation:
use std::pin::Pin;
fn report(state: Pin<&TaskState>) {
println!("progress: {}", state.progress);
}
report(state.as_ref());Use stack pinning when the pinned value only needs to live within the current scope.
Use Pin<Box> for owned pinned storage
When a pinned value must be owned and returned or stored beyond a local borrow, heap allocation is common:
use std::marker::PhantomPinned;
use std::pin::Pin;
struct TaskState {
progress: u8,
_pin: PhantomPinned,
}
fn new_state() -> Pin<Box<TaskState>> {
Box::pin(TaskState {
progress: 0,
_pin: PhantomPinned,
})
}Box::pin allocates the value and returns a Pin<Box<T>>.
You may still move the Pin<Box<TaskState>> variable:
let first = new_state();
let second = first;The heap allocation containing TaskState does not move merely because ownership of the box handle moves.
This is why the phrase the pointer can move while the pointee stays pinned is so useful.
Pinned methods make the restriction explicit
A type can require pinning for particular operations by using a pinned receiver:
use std::pin::Pin;
impl TaskState {
fn progress(self: Pin<&Self>) -> u8 {
self.progress
}
}For mutable operations, the receiver can be:
impl TaskState {
fn set_progress(self: Pin<&mut Self>, progress: u8) {
// Access to fields must preserve pinning invariants.
// This example only illustrates the receiver type.
let _ = progress;
}
}The method signature tells callers that the operation expects the value to have entered its pinned state.
It also tells implementors that moving address-sensitive parts out of self may be forbidden.
Field access is where pinning gets subtle
A pinned struct can contain fields with different pinning requirements.
Suppose a type contains:
struct Worker {
name: String,
// imagine another field whose address must stay stable
}If Worker is pinned, it does not automatically follow that every field must be treated as individually pinned.
Deciding whether a field inherits the parent’s pinning guarantee is called pin projection or structural pinning.
This area becomes subtle because returning Pin<&mut Field> is a promise that the field itself will not be moved while pinned. A type must ensure that none of its other safe methods can later violate that promise.
For ordinary application code, avoid hand-writing unsafe pin projections unless you fully understand the invariants involved. Libraries that define address-sensitive types often provide their own safe projection APIs.
Futures explain why Pin appears in async Rust
The Future trait’s poll method receives a pinned mutable reference to the future:
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
fn poll_once<F>(
future: Pin<&mut F>,
cx: &mut Context<'_>,
) -> Poll<F::Output>
where
F: Future,
{
Future::poll(future, cx)
}Why does polling require pinning?
An async block or async fn can compile into a state machine whose later state refers to data stored in an earlier part of that same future. Once such a future begins depending on internal locations, moving it could invalidate those relationships.
The Future trait therefore uses Pin<&mut Self> so the same API works for both:
- futures that are safe to move and implement
Unpin; - futures that require a stable location after polling begins.
You normally do not manage these internals yourself. Executors and async combinators provide the required pinning boundary.
The main practical consequence is that an API requiring F: Future + Unpin is more restrictive than one that accepts a pinned future directly.
Unpin is about the pointee type, not the pointer type
Consider:
Pin<Box<T>>Whether pinning restricts access depends on T.
The relevant question is:
Does T implement Unpin?It is not:
Does Box<T> implement Unpin?This distinction prevents a common mental-model error.
Box<T> is just the owning pointer. Pinning promises concern the value it points to.
Similarly, moving a Pin<Box<T>> variable does not move T.
Pin does not prevent interior mutation
Pinning is about location, not immutability.
A pinned value can still change state if its API allows that change.
For example, an address-sensitive object might update counters, flags, or heap-owned buffers while remaining at the same memory address.
Do not read:
pinnedas:
frozenThe actual rule is closer to:
the pinned value cannot be moved in ways that would violate the pinning contractMutation and movement are different operations.
Pin does not automatically make unsafe code correct
Pin is a library contract that lets safe APIs preserve invariants established by unsafe code.
It does not inspect raw pointers or prove that a custom self-referential structure was initialized correctly.
If unsafe code creates an invalid internal pointer before pinning, Pin does not repair it.
Likewise, unsafe methods such as Pin::new_unchecked place the responsibility on the caller to uphold the pinning contract. Using them merely to silence a type error defeats the safety model.
Prefer safe constructors such as Box::pin and pin! unless you are implementing an abstraction that genuinely requires unsafe pinning internals.
Common mistakes
Adding Pin to ordinary APIs without an address-sensitive invariant
If a type can safely move, an ordinary &mut T is clearer.
Pin<&mut T> adds conceptual and API complexity. Use it when the type or generic contract actually needs location stability.
Assuming !Unpin means a value can never move
A !Unpin value may move before it is pinned. The restriction begins when the pinning guarantee is established.
Thinking Pin<Box> pins the Box variable
The box handle may move. Its pointee is what remains at the promised location.
Treating pinning as immutability
Pinned data can often still mutate. Pinning restricts moves that would invalidate location-sensitive invariants.
Reaching for new_unchecked too quickly
An unchecked pinning constructor is an unsafe promise. If a safe constructor can express the lifecycle, prefer it.
When to use Pin directly
Most Rust application code does not need to define custom pinned types.
You are likely to encounter Pin directly when:
- implementing low-level asynchronous abstractions;
- manually working with
Future::poll; - designing self-referential or intrusive data structures;
- exposing a safe API around address-sensitive unsafe internals;
- integrating with a library whose API already requires pinned values.
For ordinary structs, collections, request objects, configuration, and business-domain models, normal ownership and borrowing are usually enough.
Keep the ownership and location questions separate
Rust ownership answers who controls a value’s lifetime. Pinning answers whether an address-sensitive value may move after a pinning guarantee has been established.
Unpin means a type does not rely on that guarantee. PhantomPinned can opt a custom type out of automatic Unpin. pin! provides local stack pinning, while Box::pin provides owned heap-backed pinning.
The practical lesson is not to pin more values. It is to recognize the narrow situations where memory location becomes part of a type’s correctness contract, then make that contract explicit instead of relying on convention.