Understanding Pin-safe Types in Rust
26 May 2025Introduction
Rust is famous for giving you memory safety without a garbage collector. But when you start doing lower-level work —
self-referential structs, async state machines, or FFI — you run into a powerful but mysterious feature: Pin
.
In this article, we’ll answer the following:
- What does it mean for a type to be pin-safe?
- Why would a type need to be pinned in the first place?
- How do you build one safely — without fighting the borrow checker?
We’ll walk through simple examples first, then build up to a self-referential type.
What is a Pin-safe Type?
A pin-safe type is a type that can be safely used with Rust’s Pin
API.
You create a Pin-safe
type when:
- You need to guarantee that a value won’t move in memory after being created.
- You want to allow self-referencing inside a struct (e.g., a field pointing to another field).
- You’re building async state machines, generators, or intrusive data structures.
Self-Referential Structs: The Core Problem
Let’s look at a classic case. Say you have a struct like this:
struct Example {
a: String,
b: *const String, // b points to a
}
This is a self-referential struct: b
stores a pointer to another field inside the same struct.
Seems harmless?
Here’s the catch: Rust moves values around freely — into function calls, collections, etc. If you set up a pointer inside a struct and then move the struct, your pointer is now invalid. This opens the door to use-after-free bugs.
Rust’s borrow checker normally prevents you from doing this. But sometimes you do need this — and that’s where Pin
comes in.
Pin
to the Rescue
This is perfect for self-referential types — it guarantees their memory address won’t change.
But you have to build your type carefully to uphold this contract.
A Pin-safe Self-Referential Type
Now let’s build a Pin-safe
type step-by-step.
Step 1: Define the structure
use std::pin::Pin;
use std::marker::PhantomPinned;
struct SelfRef {
data: String,
data_ref: *const String, // raw pointer, not a safe Rust reference
_pin: PhantomPinned, // opt-out of Unpin
}
data
: holds some content.data_ref
: stores a pointer to that data.PhantomPinned
: tells Rust this type is not safe to move after being pinned.
Step 2: Pin and initialize
impl SelfRef {
fn new(data: String) -> Pin<Box<SelfRef>> {
let mut s = Box::pin(SelfRef {
data,
data_ref: std::ptr::null(),
_pin: PhantomPinned,
});
let data_ref = &s.data as *const String;
unsafe {
let mut_ref = Pin::as_mut(&mut s);
Pin::get_unchecked_mut(mut_ref).data_ref = data_ref;
}
s
}
fn get(&self) -> &String {
unsafe { &*self.data_ref }
}
}
Step 3: Use it
fn main() {
let s = SelfRef::new("Hello, world!".into());
println!("Data ref points to: {}", s.get());
}
Key Points
- You must pin the struct before setting the self-reference.
Box::pin
allocates it on the heap and returns a pinned pointer.PhantomPinned
disables auto-Unpin
so it can’t be accidentally moved.unsafe
is required to set the internal pointer — you must guarantee it’s only done after pinning.
Summary Table
Concept | Example | Needs Pin ? |
Why? |
---|---|---|---|
Normal struct | Logger { name: String } |
❌ | No self-references |
Self-referential | SelfRef { data, data_ref: &data } |
✅ | Unsafe if moved |
Async generators | async fn or Future |
✅ | Compiler may generate self-refs |
FFI callbacks | extern "C" with inner pointers |
✅ | Must stay in place for C code |
Conclusion
Most types in Rust are move-safe and don’t need Pin
. But when you’re working with:
- self-referential structs,
- low-level async primitives,
- foreign function interfaces (FFI),
…you may need to reach for Pin
.
A Pin-safe type is your promise to the compiler that “this won’t move again — and I’ve made sure everything inside is OK with that.”