Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
136: Pin and Unpin Explained
I've always found that the most confusing part of Rust isn't the borrow checker—it's the moment you hit Pin. For a long time, I just ignored it, treating it as "magic for async stuff," but that's a dangerous way to learn. Let's actually break something to see why it exists.
The danger of moving things
In Rust, moving a value is a bit-for-bit copy. It's fast and efficient. But what happens if a piece of data points to itself? Let's try to build a self-referential struct. I want a struct that holds a string and a pointer to that string's data.
struct SelfReferential {
data: String,
ptr: *const String,
}
impl SelfReferential {
fn new(text: &str) -> Self {
let mut s = SelfReferential {
data: text.to_string(),
ptr: std::ptr::null(),
};
s.ptr = &s.data;
s
}
fn print_ptr(&self) {
println!("Data: {}, Pointer points to: {}", self.data, unsafe { &*self.ptr });
}
}
If I run this in a simple main function, it looks fine. I create it, I print the pointer, and it works. But here is the catch: I'm about to move this struct. In Rust, moving is the default behavior when you pass a value to a function or return it.
fn main() {
let my_struct = SelfReferential::new("Hello Pin!");
my_struct.print_ptr(); // Works fine.
let moved_struct = my_struct; // The move happens here.
moved_struct.print_ptr(); // Now we have a problem.
}
If I actually ran this with a debugger, I'd see that moved_struct.ptr is still pointing to the memory address where my_struct used to live. But my_struct is gone; its memory was shifted to a new location for moved_struct. The pointer is now dangling. It's pointing to a ghost. This is a classic memory safety violation, and it's exactly what Pin is designed to prevent.
Trying to lock it down
You might wonder why the compiler doesn't just stop me. The problem is that SelfReferential is just a struct. To the compiler, it's just bytes. It doesn't know that the ptr field has a semantic relationship with the data field.
This is where Pin comes in. Pin<P> is a wrapper around a pointer (like &mut T or Box<T>) that guarantees the data it points to will never be moved again.
But wait—if I just wrap my struct in Pin, can I still move it? Let's see. Most types in Rust are Unpin. This is a marker trait that basically says, "I don't have any self-referential pointers, so feel free to move me even if I'm pinned." Since my SelfReferential struct doesn't explicitly opt-out of Unpin, pinning it does absolutely nothing. It's like putting a "Do Not Move" sign on a piece of furniture that the movers are allowed to move anyway.
Forcing the Pin
To make Pin actually do its job, I need to tell Rust that my type is !Unpin (not Unpin). The easiest way to do this is by adding a PhantomPinned field. This is a zero-sized marker that tells the compiler: "Warning! Moving this type is dangerous."
use std::marker::PhantomPinned;
use std::pin::Pin;
use std::rc::Rc;
struct SelfReferential {
data: String,
ptr: *const String,
_pin: PhantomPinned, // This makes the type !Unpin
}
Now, if I try to get a mut reference to the data inside a Pin, the compiler will stop me unless I use unsafe. It forces me to acknowledge that I'm dealing with something that cannot be moved. In a real-world scenario, like an async block, the compiler generates a state machine (a struct) that contains references to its own local variables. If that state machine moved in memory while it was awaiting a timer, those references would break. That's why Futures are pinned.
Wait, how do I actually use it?
In practice, you'll rarely implement Pin manually for your own structs unless you're writing low-level data structures. Most of the time, you'll use Box::pin(value). This puts the data on the heap and returns a Pin<Box<T>>. Because the data is on the heap, the Box itself can move around the stack, but the actual T stays put at one fixed heap address forever.
My rule of thumb: if you see Pin, someone is dealing with a pointer that depends on the object's address remaining constant. If you aren't doing that, you can safely ignore it. If you are, you're playing in the "unsafe" zone of Rust, and you'd better make sure your PhantomPinned is in place.
📋 Practical Task
Exercise: Fixing the Self-Referential Crash
Below is a buggy implementation of a PinnedBuffer. It attempts to store a value and a pointer to that value, but it ignores the Unpin trait and doesn't use Pin correctly, leading to a potential use-after-move crash.
Your Task:
- Add
std::marker::PhantomPinnedto thePinnedBufferstruct to make it!Unpin. - Modify the
mainfunction to useBox::pin()to ensure the buffer is heap-allocated and pinned. - Ensure the
print_valuemethod correctly handles the pinned reference to print the value via the pointer.
use std::marker::PhantomPinned;
use std::pin::Pin;
struct PinnedBuffer {
value: String,
ptr: *const String,
// TODO: Add marker here
}
impl PinnedBuffer {
fn new(text: &str) -> Pin<Box<Self>> {
let res = PinnedBuffer {
value: text.to_string(),
ptr: std::ptr::null(),
// TODO: Initialize marker
};
// We need to pin it to the heap so we can safely set the pointer
let mut boxed = Box::pin(res);
// SAFETY: We are setting the pointer to the data within the pinned box
let self_ptr: *const String = &boxed.value;
unsafe {
let mut_ref = boxed.as_mut().get_unchecked_mut();
mut_ref.ptr = self_ptr;
}
boxed
}
fn print_value(self: Pin<&Self>) {
println!("Value: {}, Pointer points to: {}", self.value, unsafe { &*self.ptr });
}
}
fn main() {
// TODO: Create a pinned buffer and call print_value
// Ensure you aren't moving the buffer after it's pinned!
}There are no comments for now.