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 withunsafe
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.
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
.
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
- its memory location cannot be invalidated4
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.
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.
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
traitThis is an auto trait and a marker trait.
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 alsoUnpin
. If so, how then would we mark a struct to be!Unpin
(notUnpin
) for address-sensitive types? This is done explicitly using thePhantomPinned
marker struct:
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
!Unpin
case
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>>
.
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
-
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. ↩
-
A unique, unnamed type code-generated by the Rust compiler. ↩
-
This is not the only scenario where address-sensitive types are created, but a pertinent one. ↩
-
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 orSome(T)
onUnpin
. ↩ -
It took me a long while to realize
Pin<Ptr>
is a trait,Ptr
is not! The latter is a struct that is generic overasync
. ↩