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
148: Pinning and Async
I'll be honest with you: Pin is one of those concepts in Rust that feels like a brick wall until you see exactly what it's preventing. Most of the time, the compiler handles this for you behind the scenes of an async block, but when you start implementing your own Future or dealing with low-level async primitives, you have to face the music.
The core issue is self-referential structs. In Rust, moving a value usually just means copying bytes to a new memory location. That's fine for an integer, but if your struct contains a pointer to one of its own fields, moving that struct makes the pointer point to the old, now-invalid memory address. This is exactly what happens inside the state machines generated by async blocks.
Creating a self-referential future
Let's build a small Future that manually tracks a piece of data. I want this future to hold a buffer and a pointer to that buffer. In a real-world scenario, this happens when an async function has a local variable and then takes a reference to it across an await point.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::marker::PhantomPinned;
use std::ptr;
struct MySelfReferential {
data: String,
pointer_to_data: *const String,
_pin: PhantomPinned, // This tells Rust the type is !Unpin
}
impl MySelfReferential {
fn new(text: &str) -> Self {
Self {
data: text.to_string(),
pointer_to_data: ptr::null(),
_pin: PhantomPinned,
}
}
fn init(self: Pin<&mut Self>) {
let this = unsafe { self.get_unchecked_mut() };
this.pointer_to_data = &this.data;
}
}
Notice the PhantomPinned. I added that because, by default, almost everything in Rust is Unpin (meaning it's safe to move). Since I'm intentionally creating a self-referential structure, I need to opt-out of that safety.
The mistake: Moving the Future after initialization
Here is where I usually trip up when I'm prototyping this. I'll initialize the pointer, and then I'll move the object—perhaps by pushing it into a Vec or passing it to another function—without realizing I've just invalidated my pointer.
// This is a conceptual example of the bug I often hit
let mut my_fut = MySelfReferential::new("Hello Pinning");
// We pin it to the stack to initialize it
let mut pinned_fut = unsafe { Pin::new_unchecked(&mut my_fut) };
pinned_fut.as_mut().init();
println!("Pointer before move: {:?}, Data: {:?}", pinned_fut.pointer_to_data, &pinned_fut.data);
// MISTAKE: If I were to move 'my_fut' now,
// pointer_to_data would still point to the old address of 'my_fut'.
// The compiler stops us if we try to move a Pin, but if we bypass it
// with unsafe or forget that we're dealing with raw pointers, we crash.
In a real async executor, if the executor moves your Future to a different thread or a different spot on the heap after it has already started polling (and thus initialized its internal references), your program would enter the realm of Undefined Behavior. That's why poll takes Pin&mut Self instead of just &mut Self.
Locking memory with Pin and Box
To actually use this in a way that's safe, we need to ensure the memory address never changes. The easiest way to do this is by putting the value on the heap using Box::pin. This pins the data to a specific heap location; even if the Pin wrapper itself is moved, the data it points to stays put.
impl Future for MySelfReferential {
type Output = String;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
// We can't just use 'self' because we need to access the fields.
// Since we know we are pinned, we can use get_unchecked_mut
let this = unsafe { self.get_unchecked_mut() };
if this.pointer_to_data.is_null() {
// Initializing the self-reference on the first poll
this.pointer_to_data = &this.data;
Poll::Pending
} else {
// Now it's safe to dereference because the memory is pinned
let value = unsafe { &*this.pointer_to_data };
Poll::Ready(value.clone())
}
}
}
#[tokio::main]
async fn main() {
// Box::pin puts the future on the heap and pins it there.
let fut = Box::pin(MySelfReferential::new("Pinned and Safe"));
let result = fut.await;
println!("Result: {}", result);
}
By using Box::pin, we've guaranteed that MySelfReferential will never move again. The pointer we stored during the first poll call remains valid for the entire lifetime of the future. This is the "secret sauce" that allows Rust's async system to be so efficient—it can store references to local variables within the future's own state without risking memory corruption.
📋 Practical Task
Implement a Pinned Self-Referential Buffer
Your task is to create a struct called BufferFuture that simulates a data buffer. The struct should contain a Vec<u8> and a raw pointer ptr: *const u8 that points to the first element of that vector.
- Implement the
Futuretrait forBufferFuture. - In the first call to
poll, the future should initialize theptrto point to the start of the vector and returnPoll::Pending. - In the second call to
poll, the future should dereference theptrto read the first byte and return it asPoll::Ready(u8). - Ensure the struct is
!Unpinby usingPhantomPinned. - In your
mainfunction, useBox::pinto execute the future and print the resulting byte.
There are no comments for now.