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.


Photo by Olga Kovalski on Unsplash

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:

  1. it remains at the same memory location and is not moved
  2. its memory location cannot be invalidated4
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 Unpin trait

This is an auto trait and a marker trait.

pub auto trait Unpin { }

Definition of 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 Unpin trait on the Pin struct’s behaviour,5 let’s use the example structs defined previously.

Unpin case

let value: Pin<&mut StructIsUnpin> = pin!(StructIsUnpin::default());
let res: &mut StructIsUnpin = value.get_mut(); // Compiles

!Unpin case

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.

References

https://doc.rust-lang.org/core/pin/index.html

Footnotes

  1. The documentation states that the compiler doesn’t guarantee the address stability of types, and memory moves typically occur when there are semantic moves like in assignments and passing values into functions. ↩

  2. A unique, unnamed type code-generated by the Rust compiler. ↩

  3. This is not the only scenario where address-sensitive types are created, but a pertinent one. ↩

  4. The subtlety here is that an object’s memory may not be moved, but that address may have been invalidated. An example is Option<T>::take (unsafe) to invalidate certain memory addresses or Some(T) on Unpin. ↩

  5. It took me a long while to realize Pin<Ptr> is a trait, Ptr is not! The latter is a struct that is generic over async. ↩