Pin<P> actually guarantees, and how it stops a value from moving in memory;Unpin marker means, and why almost every type you will ever write is Unpin;Pin with a PhantomPinned field;Pin exists at all, and the central role it plays in async Rust.Learn Rust Series):Pin is the feature people in the Rust community find most mysterious, and it is worth demystifying because it quietly underpins all of async Rust. The core idea is actually narrow, once you strip away the folklore: some values contain a pointer that points into their own memory, and moving such a value would leave that internal pointer aiming at the address it used to live at, which is now stale. Pin<P> is the type-level promise that a value will not move, and that promise is exactly what makes self-referential data sound. You will very rarely construct a Pin by hand, but understanding it explains a great deal about the signatures you are about to meet ;-)
For the last several episodes we have been working through the awkward-but-important corners of ownership -- move semantics in episode 31, interior mutability in episode 32, the reference-counted trio of Rc, Arc and Weak, and last time Cow. Today's topic is the last of that cluster, and it is the one that ties directly into the async world you will step into soon. But first, as always, let me clear last episode's homework.
Episode 36 was Cow, clone-on-write. There were three exercises, and here is full runnable code for each.
Exercise 1 asked you to write fn trim_trailing(s: &str) -> Cow<str> that borrows the input when it has no trailing whitespace, and returns an owned trimmed copy only when it does, confirming each variant with matches!:
use std::borrow::Cow;
fn trim_trailing(s: &str) -> Cow<str> {
if s.ends_with(char::is_whitespace) {
Cow::Owned(s.trim_end().to_string()) // allocate a fixed copy, only when needed
} else {
Cow::Borrowed(s) // already clean: hand back the caller's own string, zero allocation
}
}
fn main() {
let clean = trim_trailing("clean");
let messy = trim_trailing("messy ");
println!("[{clean}] borrowed? {}", matches!(clean, Cow::Borrowed(_))); // [clean] borrowed? true
println!("[{messy}] owned? {}", matches!(messy, Cow::Owned(_))); // [messy] owned? true
}
The whole point is that "clean" never touches the allocator -- it flows straight back out as a Cow::Borrowed pointing at the caller's string -- while "messy " is the only input that pays for a fresh String. If most of your strings are already trimmed, and in real systems they usually are, this dodges an allocation on nearly every call.
Exercise 2 wanted you to take a Cow<[i32]> starting as Cow::Borrowed(&[10, 20, 30]), read its length twice while confirming it is still borrowed, then call to_mut().push(40) and confirm it has flipped to owned:
use std::borrow::Cow;
fn main() {
let mut data: Cow<[i32]> = Cow::Borrowed(&[10, 20, 30]);
println!("len {}", data.len()); // reads through Deref: no clone
println!("len {}", data.len()); // still borrowing, still no clone
println!("borrowed? {}", matches!(data, Cow::Borrowed(_))); // true
data.to_mut().push(40); // FIRST write: clones the slice into a Vec, THEN pushes
println!("{:?}", data); // [10, 20, 30, 40]
println!("owned? {}", matches!(data, Cow::Owned(_))); // true
}
Reading .len() twice goes straight through Deref (episode 19) and touches the borrowed slice without allocating a single byte. It is to_mut -- and nothing before it -- that upgrades the borrowed &[i32] into an owned Vec<i32>, because you cannot push onto a borrowed slice. You pay for ownership at the exact moment of the first write, and not one instruction sooner.
Exercise 3 asked you to write fn ensure_extension<'a>(name: &'a str, ext: &str) -> Cow<'a, str> that borrows name if it already ends with ext and otherwise appends it, then use into_owned to store the result in a struct with no lifetime parameter:
use std::borrow::Cow;
fn ensure_extension<'a>(name: &'a str, ext: &str) -> Cow<'a, str> {
if name.ends_with(ext) {
Cow::Borrowed(name) // already correct: no allocation
} else {
Cow::Owned(format!("{name}{ext}"))
}
}
struct File { path: String } // note: NO lifetime parameter at all
fn main() {
let a = ensure_extension("notes.txt", ".txt"); // borrowed
let b = ensure_extension("notes", ".txt"); // owned
let f = File { path: b.into_owned() }; // escape the borrow into a plain String
println!("{a} | {}", f.path); // notes.txt | notes.txt
}
The into_owned call is the escape hatch: it consumes the Cow, cloning if it happened to be borrowed and simply handing the value over if it was already owned, so you end up with a plain String that carries no lifetime and drops cleanly into an ordinary struct field. Right, homework cleared -- now, Pin.
Recall from episode 31 that a move in Rust is, at the machine level, just a memcpy of a value's bytes to a new location, after which the old location is considered dead. Ninety-nine percent of the time this is completely harmless, which is exactly why Rust moves values around so freely -- returning them from functions, pushing them into a Vec that reallocates, swapping them between variables. The value's address is not part of its identity, so nobody cares that it changed.
You can watch the address change with your own eyes:
fn main() {
let a = String::from("hello");
let addr_before = &a as *const String as usize;
let b = a; // move: the three-word String header is copied to b's slot
let addr_after = &b as *const String as usize;
println!("before: {addr_before:#x}");
println!("after: {addr_after:#x}"); // a DIFFERENT address -- the value relocated
}
Now imagine a struct that stores some data and a raw pointer aimed at that very data. Before the move, the pointer is correct. After the move, the struct's bytes sit at a brand-new address, but the internal pointer still holds the old address -- and that old slot is now stale, reused, garbage. Any read through that pointer is undefined behaviour. This is precisely why Rust normally refuses to let you build a safe self-referential struct: it cannot keep an internal pointer valid across a move, and it reserves the right to move values whenever it likes. The two facts are in direct conflict, so the borrow checker simply forbids the situation.
That conflict is the entire reason Pin exists. If we could somehow promise the compiler "this particular value will never move again", the internal pointer would stay valid forever, and self-reference would become sound. Pin is that promise, encoded in the type system.
Pin<P> is a wrapper around a pointer type P -- think Pin<Box<T>> or Pin<&mut T> -- and it guarantees that the value the pointer points at will never move again before it is dropped. Note the shape carefully: you do not pin a T, you pin a pointer to a T. The pin lives at the pointer level, and the value it protects sits behind that pointer, typically on the heap where its address is stable.
For the overwhelming majority of types, this guarantee is no restriction at all, thanks to a marker trait called Unpin. Almost every type is Unpin, which means "safe to move even while pinned". i32, String, Vec<T>, your own everyday structs -- all Unpin. For these types a Pin is a curiosity, a wrapper that promises something they never needed. Only types that are deliberately !Unpin, which you opt into with a PhantomPinned field, actually feel the pin bite:
use std::marker::PhantomPinned;
fn assert_unpin<T: Unpin>() -> &'static str { "is Unpin (safe to move)" }
struct MustNotMove { _pin: PhantomPinned }
fn main() {
println!("i32 {}", assert_unpin::<i32>());
println!("String {}", assert_unpin::<String>());
// assert_unpin::(); // would NOT compile: MustNotMove is not Unpin
let _ = MustNotMove { _pin: PhantomPinned };
}
Unpin is an auto trait, just like Send and Sync from episode 25: the compiler implements it automatically for any type whose fields are all Unpin. The one way to escape it is to embed a field that is itself !Unpin, and the standard library hands you exactly such a field for this purpose -- std::marker::PhantomPinned, a zero-sized type whose only job is to make its container non-Unpin. Uncomment the assert_unpin::<MustNotMove>() line above and the compiler rejects it, because MustNotMove inherited !Unpin from its PhantomPinned field.
So Pin<Box<i32>> is, as I said, a curiosity and not a constraint, because i32 is Unpin. You can see the boundary directly with a function that demands Unpin:
use std::marker::PhantomPinned;
fn needs_unpin<T: Unpin>(_: T) {}
struct Movable(i32);
struct Fixed { _p: PhantomPinned }
fn main() {
needs_unpin(Movable(1)); // fine: Movable is Unpin
// needs_unpin(Fixed { _p: PhantomPinned }); // would NOT compile: Fixed is not Unpin
let _ = Fixed { _p: PhantomPinned };
}
The take-away is a comforting one: pinning only ever restricts the rare !Unpin types, and those are precisely the ones that hold self-references. For everything else you write in day-to-day Rust, Pin may as well not exist -- it steps aside entirely.
For an Unpin type, Box::pin gives you a pinned box that you can still freely read and mutate through, because moving the underlying value was safe all along:
use std::pin::Pin;
fn main() {
let mut pinned: Pin<Box<i32>> = Box::pin(42);
println!("{}", *pinned); // 42
*pinned = 43; // i32 is Unpin, so mutating through the pin is fine
println!("{}", *pinned); // 43
}
Because i32 is Unpin, the pin here is not stopping you from doing anything -- you dereference it, you assign through it, business as usual. Contrast that with a !Unpin type, where the pin would refuse to hand you a plain &mut at all, because a &mut would let you std::mem::swap the value out and thereby move it.
And to hammer the point home, because i32 is Unpin, you can even recover a plain &mut i32 from the pinned box through Pin::get_mut, which is a method that only exists for Unpin targets:
use std::pin::Pin;
fn main() {
let mut p: Pin<Box<i32>> = Box::pin(10);
let r: &mut i32 = Pin::get_mut(p.as_mut()); // allowed ONLY because i32 is Unpin
*r += 5;
println!("{}", *p); // 15
}
Pin::get_mut is your evidence that the whole apparatus is inert for ordinary types: it safely tears the pin right off, no unsafe required, precisely because there was never anything to protect. Try the same trick on a !Unpin type and get_mut is simply not available -- you would have to reach for the unsafe get_unchecked_mut and take responsibility yourself. The interesting case, then, is exactly when the type is !Unpin, which is what comes next.
Here is the real thing at last: a struct that stores its data and a raw pointer into that data, made sound by pinning it on the heap so it can never move. It needs a little unsafe, because we are upholding an invariant the compiler cannot verify on its own -- namely, that the data really does stay put:
use std::pin::Pin;
use std::marker::PhantomPinned;
use std::ptr;
struct SelfRef {
data: String,
ptr_to_data: *const String, // points into our own `data` field
_pin: PhantomPinned, // makes SelfRef !Unpin
}
impl SelfRef {
fn new(text: &str) -> Pin<Box<SelfRef>> {
let mut boxed = Box::pin(SelfRef {
data: String::from(text),
ptr_to_data: ptr::null(),
_pin: PhantomPinned,
});
let self_ptr: *const String = &boxed.data;
// SAFETY: we only set the internal pointer; we never move the data out,
// and the struct is pinned on the heap, so `data`'s address is stable.
unsafe {
let pinned: Pin<&mut SelfRef> = Pin::as_mut(&mut boxed);
Pin::get_unchecked_mut(pinned).ptr_to_data = self_ptr;
}
boxed
}
fn via_pointer(&self) -> &str {
// SAFETY: ptr_to_data points into our own `data`, which is pinned in place.
unsafe { &*self.ptr_to_data }
}
}
fn main() {
let s = SelfRef::new("hello");
println!("direct: {}", s.data); // hello
println!("via ptr: {}", s.via_pointer()); // hello -- read through the self-pointer
}
Let me walk through what makes this sound. Box::pin allocates the SelfRef on the heap and immediately pins it, so its address is now fixed for the rest of its life. We then take the address of the data field and store it in ptr_to_data. Because the struct can never move -- the pin plus the PhantomPinned field together guarantee that -- the address we captured stays valid forever, and via_pointer can dereference it safely. Remove the PhantomPinned field and SelfRef would become Unpin, the pin would stop protecting anything, and the whole construction would collapse into undefined behaviour the moment the value moved. The marker is not decoration; it is load-bearing.
Notice, too, that the unsafe blocks carry // SAFETY: comments explaining why each one is justified. That is the idiomatic Rust convention (episode 25 touched on the discipline around unsafe): every unsafe block should document the invariant it relies on, so a future reader -- possibly you, six months later -- can check the reasoning still holds.
You will almost certainly never build a SelfRef by hand in real code, so a fair question is: why does any of this matter? The answer is that the compiler builds self-referential structs for you, automatically, every single time you write an async block.
An async fn compiles down to a state machine. While that state machine is suspended at an .await point, it needs to remember its local variables so it can resume later -- and some of those locals may be references into other locals held by the same state machine. That makes the generated state machine self-referential, in exactly the way our SelfRef was. It therefore must not move once it has started running, or those internal references would dangle. And that is why the Future trait's poll method takes self: Pin<&mut Self> rather than a plain &mut self:
use std::pin::Pin;
use std::task::{Context, Poll};
// A sketch of the real std::future::Future trait:
trait MyFuture {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
// ^^^^^^^^^^^^^^^^^^^ the Pin is here to stop the self-referential
// state machine from moving between polls.
}
The payoff of this whole episode is not that you will write Pin code -- you almost never will. It is that when you reach async Rust and meet Pin<&mut Self> staring back at you from a trait signature, you will understand what it is guarding against: the exact move problem we watched break a self-reference at the top of this post. The runtime pins the future once, typically with Box::pin or a stack-pinning macro, and from then on it polls the future in place, address stable, self-references intact.
Since quite some of you arrived here from the Learn Python Series, a glance sideways is illuminating, because Python simply does not have this problem, and seeing why sharpens the Rust picture. In Python every value lives on the heap and every variable is a reference to it, so an object's address never changes for its whole life -- there is no such thing as a "move" that relocates the bytes. Python's async def coroutines are self-referential under the hood in the same conceptual way Rust's are, but the interpreter can hold internal references safely because the coroutine object, like every object, is pinned in place for free by the runtime:
async def worker():
data = "hello" # a local
view = data # another local referring to the same object
await some_io() # the coroutine suspends here, keeping data and view alive
return len(view) # resumes later; the object never moved, so this is safe
Python pays for that convenience with a garbage collector and a layer of indirection on every single access -- every value is boxed, always. Rust refuses that blanket cost: it moves values by value, keeps them on the stack when it can, and pays nothing for the common case. Pin is the price of getting self-reference back only where you genuinely need it, without imposing Python's universal heap-boxing on everything else. I argue that is the better bargain for systems code, where you want to know exactly when an allocation happens and exactly when a value can move. Same underlying need -- keep a self-referential thing from relocating -- but Rust makes it an explicit, checked, opt-in contract in stead of an invisible always-on runtime service.
Pin<P> wraps a pointer and promises the pointed-at value will never move again before it drops. You pin a pointer to a value, not the value itself.Unpin is an auto trait meaning "safe to move even when pinned". Almost every type is Unpin, so for ordinary code Pin is inert and steps aside.!Unpin by giving it a PhantomPinned field -- a zero-sized marker whose only job is to opt the container out of Unpin.Box::pin pins a value on the heap. For Unpin types you can still freely read, mutate, and even recover a &mut via Pin::get_mut. For !Unpin types you must go through the unsafe get_unchecked_mut and uphold the no-move invariant yourself.!Unpin (the pin actually enforced), with // SAFETY: comments documenting why each unsafe block is justified.async block compiles to a self-referential state machine, which is why Future::poll takes self: Pin<&mut Self>.Three exercises, from gentle to chewier. Type them yourself before the next episode -- that is where the understanding really sticks.
Pin<Box<u64>> with Box::pin, read the value through it, mutate it via Pin::get_mut, and print the result. In a comment, explain why get_mut is allowed here (hint: u64 is Unpin).SelfRef -- call it ptr_to_len -- that stores the address of the data field a second time (or points at a different owned field you add), set it inside new, and add a method that reads through it. Confirm both self-pointers agree with the direct field access.fn needs_unpin<T: Unpin>(_: T) {} and then define a small struct containing a PhantomPinned field. Try to pass a value of that struct to needs_unpin, read the compiler error carefully, and write down in a comment which trait bound failed and why.Thanks for reading, en tot de volgende keer! ;-)