These are my learnings from digesting the Rust’s official documentation on the pin module, after taking a dive into Rust Futures. I have not yet fully understood some advanced concepts like Pinning’s interaction with unsafe APIs so they are not mentioned in this article.
Certain types in Rust are address-sensitive. An example is a self-referential type whose fields contain addresses to itself.
struct SelfReferential { data: String // Address of `self.data` reference: *const String}impl SelfReferential { fn new(data: String) -> Self { let reference = &data as *const String; Self { data, reference } } fn assert_consistency(&self) { let addr = &self.data as *const String; assert_eq!(addr, self.reference); }}
If the underlying data of this struct is moved,1 then reference is no longer valid as it points to outdated memory location of data.
fn main() { let my_struct = SelfReferential::new("My string".to_string()); my_struct.assert_consistency(); let moved_struct = my_struct; // Memory may be moved to a new location // moved_struct.assert_consistency(); // May panic}
Self-referential types aren’t that esoteric in Rust—they can be created in asynchronous programming when the async keyword is desugared into anonymous2 state machine implementations.3
To guarantee the soundness of programs that involve address-sensitive types, Rust introduces the concept of pinning đź“Ś.
While a value is being pinned in Rust:
it remains at the same memory location and is not moved
Examples of behaviours prevented by pinning: (A) Data is moved out of its original location. (B) Data remains at the same address but some addresses are invalidated.
Typically a value is pinned until the Pin is dropped, panics or is explicitly unwrapped via Pin::into_inner.
The Pin<Ptr> struct
Pin<Ptr> is a struct that enforces the invariants of pinning.
pub struct Pin<Ptr> { /* private fields */ }
where Ptr is a generic type that implements the Deref or DerefMut trait.
Pin always wraps a pointer type, ensuring that the value the pointer points to, is valid and will remain at that memory address.
Pinning mechanism illustrated: Using Pin<Box<T>> as an example, the Pin struct pins T, not Box, to its location in memory.
Unpin nullifies Pin
However, just because something is wrapped in Pin doesn’t mean that it cannot be moved. That depends on whether the underlying type implements Unpin.
The fact that this is an auto trait means that Rust types are Unpin by default (bool, u32, &str, etc.). This is rightly so, as the semantic for almost all types are based on the content, as opposed to location, of its data.
A struct whose fields are all Unpin, is also Unpin. If so, how then would we mark a struct to be !Unpin (not Unpin) for address-sensitive types? This is done explicitly using the PhantomPinned marker struct:
use std::marker::PhantomPinned;use std::pin::{pin, Pin};/// `u32` is `Unpin`, so `Moveable` is also `Unpin`#[derive(Default)]struct StructIsUnpin { data: Vec<u32>}/// One of the fields is `!Unpin` so the struct is `!Unpin`#[derive(Default)]struct StructIsNotUnpin { data: Vec<u32>, _pin: PhantomPinned, // `!Unpin`}
To illustrate the effect of the Unpintrait on the Pinstruct’s behaviour,5 let’s use the example structs defined previously.
let value: Pin<&mut StructIsNotUnpin> = pin!(StructIsNotUnpin::default());// Line below won't compile: `PhantomPinned` cannot be unpinned// let res: &mut StructIsNotUnpin = value.get_mut();
The !Unpin case doesn’t allow us to obtain a mutable reference to the underlying StructIsNotUnpin struct. Doing so would allow us to move the underlying data out of its current memory address with something like std::mem::swap. This would make for unsound application logic!
The original example, fixed
Below is how the original self-referential struct can be fixed with pinning.
Notably, data is moved to a stable location (typically on the heap with a Box smart pointer), before initializing the memory address.
Also interestingly, SelfReferential::new doesn’t return Self, but Pin<Box<Self>>.
use std::marker::PhantomPinned;use std::pin::Pin;struct SelfReferential { data: String, // Address of `self.data` reference: *const String, // Ensures that the struct's memory cannot be moved once pinned _pin: PhantomPinned,}impl SelfReferential { fn new(data: String) -> Pin<Box<Self>> { // Do not initialize `reference` until `data` // is moved into a stable place (on the heap). // Otherwise, it will be invalidated upon a move. let res = Self { data, reference: std::ptr::null(), _pin: PhantomPinned, }; // This moves the struct's fields from the stack to the heap let mut boxed = Box::new(res); // Now that the data is in a stable place, initialize the reference boxed.reference = &boxed.data as *const String; // Now that the a sound struct is constructed, pin it in memory let pinned = Box::into_pin(boxed); // Return the object pinned } fn assert_consistency(&self) { let addr = &self.data as *const String; assert_eq!(addr, self.reference); }}fn main() { let my_struct: Pin<Box<SelfReferential>> = SelfReferential::new("My string".to_string()); my_struct.assert_consistency(); // The memory address of `Box` may be moved, // but the underlying data it points to // (i.e. the fields of `SelfReferential`) // remain at a stable location on the heap let moved_struct = my_struct; moved_struct.assert_consistency(); // Always passes}
Conclusion
Pinning is a unique concept that is enforced with the interplay of the Pin<Ptr> struct along with the !Unpin trait.
This allows address-sensitive types (!Unpin) to be soundly implemented in your application, forming a foundational building block for asynchronous programming in Rust.