← Home ← Codex ← DEBT ← Engine
Browse by Category
+ added · updated 7d
← Back to glossary

Rust Pin and Unpin

Rust Advanced
debt(d6/e5/b5/t7)
d6 Detectability Operational debt — how invisible misuse is to your safety net

Closest to 'only careful code review or runtime testing' (d7), but slightly better at d6 because clippy and miri (from detection_hints.tools) can catch some unsafe pin misuse and UB at runtime, though most pin invariant violations are silent until they cause UB.

e5 Effort Remediation debt — work required to fix once spotted

Closest to 'touches multiple files / significant refactor in one component' (e5). While quick_fix suggests Box::pin or pin! as one-line fixes for the polling case, fixing a broken self-referential type or replacing hand-rolled unsafe projection with pin-project is a meaningful refactor within the future/stream component.

b5 Burden Structural debt — long-term weight of choosing wrong

Closest to 'persistent productivity tax' (b5). Per applies_to spanning web/cli/queue/library and its role in async, once you have !Unpin types the pin discipline shapes every poll implementation and field access, but it's localized to async/intrusive code rather than defining the whole system.

t7 Trap Cognitive debt — how counter-intuitive correct behaviour is

Closest to 'serious trap' (t7). The misconception field explicitly states developers expect Pin to lock memory addresses like C++ pinning and think it applies universally, when it's actually a compile-time move-prevention contract that most types opt out of via Unpin — contradicting intuition from other languages.

About DEBT scoring →

Also Known As

pin api unpin trait pinning in rust self-referential structs

TL;DR

Pin<P> is a pointer wrapper that guarantees the pointed-to value will not move in memory, enabling safe self-referential types and async futures.

Explanation

Most Rust values are freely movable: the compiler can memcpy them from one address to another when they are returned, assigned, or pushed into a Vec. That is usually fine because references are tracked by the borrow checker and updated at compile time. But some values contain internal pointers into their own bytes - the classic example is a state machine generated by an `async fn` that borrows across an `await`. If such a value moved, its internal pointer would still point at the old address, becoming a dangling reference and causing undefined behavior. `Pin<P>` exists to statically forbid that move.

`Pin<P>` is a wrapper around a pointer type `P` (typically `Box<T>`, `&mut T`, or a stack pinned reference) that promises the value behind the pointer will never be moved again for the rest of its life. Once you have `Pin<Box<T>>` or `Pin<&mut T>`, you cannot get a `&mut T` back out safely, because a `&mut T` would let a caller call `mem::swap` or `mem::replace` and move the value. Instead you work through methods like `Pin::as_mut`, `Pin::as_ref`, and (with the `pin-project` or `pin-project-lite` crate) structural projection to fields.

The escape hatch is the `Unpin` auto-trait. `Unpin` marks types that are safe to move even when pinned - because they contain no self-references and do not care about their address. Most types are `Unpin` automatically: `i32`, `String`, `Vec<T>`, and any struct whose fields are all `Unpin`. For those, `Pin<&mut T>` is basically the same as `&mut T` and you can call `Pin::get_mut` freely. The types that are not `Unpin` are the interesting ones: futures produced by `async` blocks, generators, and any type containing `PhantomPinned` or a `!Unpin` field. Those are the values Pin exists to protect.

In practice you rarely construct Pin yourself in application code. You pin a future by boxing it with `Box::pin(async { ... })` or by using `tokio::pin!`/`std::pin::pin!` on the stack, then pass it to something that calls `Future::poll`, whose signature `poll(self: Pin<&mut Self>, ...)` is the whole reason Pin exists. Library authors implementing custom futures, streams, or intrusive data structures use `pin-project` to expose safe access to fields while preserving the pinning invariant on the ones that need it.

Common Misconception

Pin locks a value to a specific memory address the way pinning does in C or C++, and every Rust type needs to worry about it. In reality Pin is a compile-time contract about not calling move-based APIs, and almost all ordinary types implement Unpin so Pin has no practical effect on them.

Why It Matters

Pin is the mechanism that makes async/await sound: without it, moving a polled future would dangle its internal borrows and cause undefined behavior, so anyone writing futures, streams, or intrusive data structures must understand pinning to build safe abstractions.

Common Mistakes

  • Assuming Pin prevents mutation, when it only prevents moves and still allows mutation through Pin::as_mut and projected fields.
  • Trying to get a &mut T out of a Pin<&mut T> for a !Unpin type using unsafe code, breaking the pin invariant and causing undefined behavior.
  • Manually implementing Unpin for a self-referential type to silence errors, defeating the safety guarantee Pin exists to provide.
  • Writing structural projection by hand with unsafe blocks instead of using pin-project or pin-project-lite, and getting the Unpin propagation wrong.
  • Storing a future in a plain Vec or moving it after the first poll, causing panics or UB because Future::poll requires Pin<&mut Self>.

Avoid When

  • Application code that only awaits futures through async/await and never manually implements Future or Stream, where the compiler and runtime handle pinning invisibly.
  • Ordinary data types with no self-references, where Unpin is automatic and Pin adds no useful guarantee.
  • Situations where a simple owned value or Box<T> suffices; reaching for Pin without a !Unpin type is cargo-culting.
  • Hand-writing unsafe pin projection when pin-project or pin-project-lite would generate the same code safely.

When To Use

  • Implementing a custom Future, Stream, or AsyncRead where poll takes Pin<&mut Self> and inner state may be self-referential.
  • Storing a future to poll later, using Box::pin or std::pin::pin! to satisfy the Pin<&mut F> requirement of Future::poll.
  • Building intrusive data structures like linked lists whose nodes hold pointers to sibling nodes and therefore must not move.
  • Exposing safe access to fields of a !Unpin struct via pin-project, distinguishing structurally pinned fields from freely movable ones.

Code Examples

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

// A future that borrows across an await point is !Unpin.
async fn make_greeting(name: String) -> String {
    let greeting = format!("hello, {}", name);
    // Imagine an await here that borrows into `greeting`.
    tokio::task::yield_now().await;
    greeting
}

fn drive_once<F: Future>(mut fut: F) {
    // BUG: Future::poll needs Pin<&mut Self>, but we only have &mut F.
    // Trying to poll a bare future like this does not compile, and
    // "fixing" it with unsafe Pin::new_unchecked on a stack value
    // that we later move would be undefined behavior.
    let waker = futures::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    // let _ = fut.poll(&mut cx); // does not compile - no Pin
    // let pinned = unsafe { Pin::new_unchecked(&mut fut) }; // unsound if fut moves
    // let _ = pinned.poll(&mut cx);
    drop((fut, cx));
}

fn main() {
    drive_once(make_greeting("world".into()));
}
✓ Fixed
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

async fn make_greeting(name: String) -> String {
    let greeting = format!("hello, {}", name);
    tokio::task::yield_now().await;
    greeting
}

fn drive_once<F: Future>(fut: F) -> Poll<F::Output> {
    // Box::pin heap-allocates the future and hands back Pin<Box<F>>,
    // a pointer that promises never to move the future again.
    let mut pinned: Pin<Box<F>> = Box::pin(fut);
    let waker = futures::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    // as_mut reborrows as Pin<&mut F>, which is what Future::poll requires.
    pinned.as_mut().poll(&mut cx)
}

// Alternative: pin on the stack with the std::pin::pin! macro. The value
// cannot escape this scope, so it is guaranteed not to move.
fn drive_stack_pinned() {
    let fut = make_greeting("world".into());
    let mut fut = std::pin::pin!(fut);
    let waker = futures::task::noop_waker();
    let mut cx = Context::from_waker(&waker);
    let _ = fut.as_mut().poll(&mut cx);
}

fn main() {
    let _ = drive_once(make_greeting("world".into()));
    drive_stack_pinned();
}

Added 16 Jul 2026
Views 34
Rate this term
No ratings yet
🤖 AI Guestbook educational data only
| |
Last 30 days
0 pings T 0 pings F 0 pings S 0 pings S 0 pings M 0 pings T 0 pings W 0 pings T 1 ping F 2 pings S 0 pings S 3 pings M 0 pings T 1 ping W 0 pings T 0 pings F 2 pings S 0 pings S 0 pings M 1 ping T 1 ping W 2 pings T 0 pings F 1 ping S 2 pings S 0 pings M 1 ping T 0 pings W 1 ping T 0 pings F
No pings yet today
SEMrush 1
Google 5 PetalBot 3 SEMrush 3 Applebot 2 Amazonbot 1 ChatGPT 1 Unknown AI 1 Ahrefs 1 Meta AI 1
crawler 18
DEV INTEL Tools & Severity
🟠 High ⚙ Fix effort: High
⚡ Quick Fix
Pin futures before polling with Box::pin or std::pin::pin!, and use pin-project instead of hand-rolled unsafe to access fields of !Unpin types.
📦 Applies To
web cli queue-worker library tokio async-std futures pin-project
🔗 Prerequisites
🔍 Detection Hints
Pin::new_unchecked|PhantomPinned|impl\s+Unpin\s+for|Box::pin\(|std::pin::pin!
Auto-detectable: ✗ No clippy miri
🤖 AI Agent
Confidence: Low False Positives: High ✗ Manual fix Fix: High Context: File Tests: Update


✓ schema.org compliant