The stabilization of async/await in Rust 1.39, alongside the Pin type introduced in version 1.33, enabled safe self-referential structs crucial for asynchronous state machines. These structures often contain internal pointers referencing data owned by the struct itself, such as buffers and active views into those buffers. When implementing manual futures or complex intrusive data structures, developers must access individual fields through Pin<&mut Self>, creating the need for safe projection mechanisms that preserve memory location guarantees.
When a struct is pinned via Pin, the compiler guarantees its memory address remains constant for the lifetime of the pin, provided the type does not implement Unpin. If the struct holds self-referential pointers, such as a raw pointer into an internal vector, moving the struct would invalidate these pointers, creating dangling references. A naive projection approach that simply dereferences Pin<&mut Self> to &mut Self exposes fields to safe Rust code, which could legally invoke mem::swap or mem::replace on those fields, thereby moving them out of their pinned memory locations and violating the fundamental pinning contract.
Safe projection requires an unsafe conversion that preserves the pinning invariant: if the parent struct is !Unpin, the field projection must return Pin<&mut Field> rather than &mut Field to prevent moving. The implementation must guarantee that the field is structurally pinned, meaning its pinning status is tied to the parent struct's pinning status, typically achieved through pointer arithmetic or Pin::map_unchecked_mut. For fields that implement Unpin, projection may safely return &mut Field because these types are permitted to move even when nested within pinned data, though care must be taken that such moves do not invalidate other self-referential fields.
use std::pin::Pin; use std::marker::PhantomPinned; struct Buffer { data: [u8; 1024], cursor: *const u8, _pin: PhantomPinned, } impl Buffer { // Safe projection to the data field (Unpin) fn data_mut(self: Pin<&mut Self>) -> &mut [u8; 1024] { unsafe { &mut self.get_unchecked_mut().data } } // Projection to the cursor field fn cursor(self: Pin<&mut Self>) -> *const u8 { unsafe { self.get_unchecked_mut().cursor } } }
Context
We were building a high-performance, zero-copy parser for a financial protocol where messages could reference sub-ranges of a reusable internal buffer. The parser state needed to be maintained across asynchronous I/O operations, meaning the struct had to be Pinned to allow self-referential pointers into the buffer.
Problem description
The Parser struct held a Vec<u8> buffer and a &[u8] slice pointing into that buffer representing the current message. When implementing Stream for this parser, the poll_next method receives Pin<&mut Self>. We needed to mutate the buffer (to read more data) while maintaining the validity of the slice reference, requiring careful field projection.
Solutions considered
Solution A: Index-based addressing
Instead of storing a slice &[u8], we stored (usize, usize) indices into the vector. Pros: Completely safe, no Pin complexity, easy to implement. Cons: Runtime bounds checking overhead, less ergonomic API requiring manual slicing on every access, potential for index desynchronization bugs.
Solution B: Unsafe Pin projection with raw pointers
We stored the message as a raw pointer *const u8 and length, implementing manual projection methods using Pin::map_unchecked_mut to access the buffer while keeping the pointer field pinned. Pros: Zero-cost abstraction, maintains self-referentiality, allows direct pointer arithmetic. Cons: Requires unsafe code blocks, risk of undefined behavior if Pin invariants are violated (e.g., implementing Unpin incorrectly).
Solution C: Using the pin-project crate
Leveraging the procedural macros to generate safe projection code automatically. Pros: Ergonomic, well-tested safety invariants, reduces boilerplate. Cons: Additional dependency, macro-generated code can be harder to debug, slight compile-time cost.
Chosen solution and result
We selected Solution B to avoid external dependencies in our embedded systems context and to maintain explicit control over memory layout. We carefully ensured the struct did not implement Unpin by adding PhantomPinned and wrote exhaustive Miri tests to validate the pinning invariants. The result was a parser achieving zero-copy semantics with no allocation per message, sustaining 10Gbps throughput without CPU saturation.
Why is it unsound to implement Unpin for a struct containing self-referential pointers?
Unpin specifically signals that a type is safe to move even when wrapped in Pin, allowing safe code to obtain &mut T from Pin<&mut T> via methods like Pin::into_inner. For a self-referential struct, moving the structure changes the memory address of its contents, invalidating any internal pointers referencing those contents. Implementing Unpin would permit safe code to move the struct while pinned, violating the safety guarantee that Pin provides to async runtimes and leading to use-after-free vulnerabilities. Therefore, such structs must use PhantomPinned to explicitly opt-out of Unpin and prevent accidental auto-implementation.
How does projection differ for enum variants compared to struct fields?
Many candidates assume projection mechanics are identical for enums and structs, but enums present unique challenges because the discriminant determines which variant is active. Projecting Pin<&mut Enum> to a specific variant requires ensuring the variant remains pinned while also preventing the discriminant from changing, as switching variants would move the underlying data. Rust lacks stable built-in support for variant projection because the discriminant and variant data share memory layout considerations; safe projection requires unsafe code that asserts the active variant and guarantees no variant swap occurs while the enum remains pinned.
What is the role of PhantomPinned in preventing automatic trait implementations?
Beginners often overlook that Rust automatically implements Unpin for most types unless they explicitly contain !Unpin fields, which would make the containing type !Unpin by default. PhantomPinned is a zero-sized marker type explicitly defined as !Unpin, serving as a negative implementation bound when included in a struct. Without this marker, even if developers write unsafe projection code assuming the struct is immobile, the compiler could auto-implement Unpin, allowing safe code to extract and move the struct via Pin::into_inner_unchecked, thereby breaking unsafe invariants and invoking undefined behavior.