Rust Ownership & Mutability
Sugih Jamin in conversation with Gemini
February 28th, 2026 and March 4th, 2026
As with C, Rust differentiates mutability of pointer vs mutability of pointed memory:
| C Syntax | Rust Syntax | Meaning |
| :— | :— | :— |
| const int *p | let mut p: &i32 | Mutable pointer to immutable data. |
| int *const p | let p: &mut i32 | Immutable pointer to mutable data. |
| const int *const p | let p: &i32 | Immutable pointer to immutable data. |
| int *p | let mut p: &mut i32 | Mutable pointer to mutable data. |
A “container” (array, dictionary, string, struct, and closure) owns the data held in its memory and Rust follows the Inherited Mutability principle: if you own a box, and that box is marked mutable, you have permission to reach inside and mutate its contents. Rust intentionally couples these to avoid the confusion where a pointer might be const but the data it points to is accidentally modified through an alias.
If you want a variable that is “immutable” on the outside but has “mutable” memory inside, you use types like Cell or RefCell. This allows you to:
- Keep the variable
let s(immutable binding). - Still modify the data inside (mutable memory).
In Rust, we call this Interior Mutability. Using RefCell, you can mutate data even if the variable holding it is declared with let (immutable). This is used for specific patterns like the Observer Pattern or when you need to satisfy a trait that expects an immutable reference but you truly need to update an internal counter or cache. This moves the borrow checking from compile-time to runtime.
Closure
In Rust, a closure is not just a function pointer; it is effectively an anonymous struct that “contains” its environment.
Closure-as-a-Container:
- The “Fields”: Every variable you mention inside the closure becomes a field in this hidden struct.
- The “Storage”:
- If it captures by reference (
&Tor&mut T), the struct contains a pointer. - If it captures by value (using the
movekeyword), the struct actually owns the data—-the memory is moved into the struct.
- If it captures by reference (
- The Mutability: This is where the “container” part matters most. Because the closure is a struct, the Inherited Mutability rules apply to it:
- If the “container” (the closure variable) is immutable (
let), you cannot modify its internal fields. - If the “container” is mutable (
let mut), you can modify its internal fields.
- If the “container” (the closure variable) is immutable (
The “Hidden Struct” Visualization
For:
let mut s = String::from("hello");
let mut closure = || {
s.push_str(" world"); // Modifies s
};
closure desugars to:
// 1. The compiler generates this struct "container"
struct MyClosure<'a> {
s: &'a mut String, // A field containing the reference
}
// 2. The closure body becomes a method on this struct
impl<'a> FnMut<()> for MyClosure<'a> {
fn call_mut(&mut self) {
self.s.push_str(" world"); // Reaching into the container to mutate
}
}
// 3. Your 'let mut closure' is just an instance of this struct
let mut closure = MyClosure { s: &mut s };
The compiler infers the mutability of s based on how you use the variable within the closure’s body.
-
Usage-Based Inference
The compiler looks at what the closure does with
s. Because you calls.push_str(), which requires a mutable reference (&mut self), the compiler determines that the closure must capturesby mutable reference. -
The Internal “Struct”
When you define the closure, Rust effectively generates a “hidden” struct to hold its environment. For the above code, it looks something like this:
struct Closure<'a> { s: &'a mut String, // Captured as a mutable reference }This is why the original
smust be declared aslet mut s—-the closure needs to be able to take that mutable borrow from the start. -
Why the closure itself must be
mutBecause the closure’s internal state (the reference to
s) must be modified or used to perform a mutation, the closure implements theFnMuttrait.- The
call_mutmethod of this trait requires a mutable reference to the closure itself (&mut self). - Therefore, you must declare the closure as
let mut closureto ever actually call it.
- The
Summary of the Chain:
- Body uses
push_str-> Requires&mut String. - Compiler captures
svia&mut-> Closure now “contains” a mutable reference. - Closure implements
FnMut-> Calling it modifies its internal state (the borrow). - Requirement -> The closure variable itself must be
mutto be invoked.
Adding move
let mut s = String::from("hello");
let mut closure = move || {
s.push_str(" world"); // Modifies s
};
closure desugars to:
// Before (Borrow)
struct ClosureBorrow<'a> {
s: &'a mut String, // Just a pointer
}
// After (Move)
struct ClosureMove {
s: String, // The closure now OWNS the string data
}
Even though the closure now owns the string, you still need let mut closure because calling the closure still involves reaching into that “container” struct and calling push_str on the field it owns.
Selective move
In Rust, the move keyword is an “all or nothing” switch—-it forces the closure to take ownership of every variable it uses from the environment. To achieve a “selective” move where you move one variable but only borrow another, you use variable rebinding just before defining the closure.
If you want to move data_to_move but only borrow data_to_share, you explicitly create a reference for the shared data and then use a move closure.
let data_to_move = String::from("I am being moved");
let mut data_to_share = String::from("I am being borrowed");
// 1. Create an explicit reference for what you want to borrow
let shared_ref = &mut data_to_share;
// 2. Use a 'move' closure
// 'data_to_move' is moved in (ownership transfer)
// 'shared_ref' is also moved in (but it's just a pointer!)
let mut closure = move || {
println!("Moved: {}", data_to_move);
shared_ref.push_str("... and modified!");
};
closure();
// println!("{}", data_to_move); // ERROR: This would fail, it's gone.
// // WORKS: Still exists here, but ONLY IF 'closure' is never used again (NLL).
// If you called 'closure()' again below, this line would trigger a compile error.
println!("Shared: {}", data_to_share);
The Alternative:
If you don’t want to use the move keyword because you have many variables to borrow and only one to move, you can perform a “dummy move” inside a standard closure:
let to_move = String::from("Move me");
let to_borrow = String::from("Borrow me");
let closure = || {
let _internal_move = to_move; // This forces 'to_move' to be captured by value
println!("Borrowed: {}", to_borrow);
};
References
Master Reference
| Rust Pattern | External Owner | Thread-Safe? | Lifetime | Data Mutability | Pointer Mutability | C Equivalent |
| :— | :— | :—: | :— | :— | :— | :— |
| let mut x: T | Unique (Stack) | Send, not Sync | Stack/Scope | Mutable | Mutable | T x; |
| let x = &mut *storage | Borrowed | No | Tied to Owner | Mutable | Immutable | T * const x |
| let mut x = &mut *storage | Borrowed | No | Tied to Owner | Mutable | Mutable (only if Lifetime ≥ ‘Original) | T * x |
| Cell<T> | Unique (Stack) | Send, not Sync | N/A | Mutable (Copy) | Immutable | struct { T val; } |
| RefCell<T> | Unique (Stack) | Send, not Sync | N/A | Mutable (Runtime) | Immutable | struct { T val; lock; } |
| Box<T> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Immutable | Immutable | const T * const x |
| let mut x = Box<T> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable | Mutable | T * x |
| Box<Cell<T>> | Unique (Heap) | Send, not Sync | Heap (Owned) | Mutable (Copy) | Immutable | T * const x (memcpy) |
| Box<RefCell<T>> | Unique (Heap) | Send, not Sync | Heap (Owned) | Mutable (Runtime) | Immutable | T * const x (w/ check) |
| Rc<T> | Shared (Heap) | No | Heap (Ref-Count) | Immutable | Immutable | const T * const x |
| Rc<RefCell<T>> | Shared (Heap) | No | Heap (Ref-Count) | Mutable (Runtime) | Immutable | T * const x (w/ RC) |
| Arc<T> | Shared (Heap) | Yes (if T: Sync) | Heap (Atomic) | Immutable | Immutable | const T * const x |
| Arc<Mutex<T>> | Shared (Heap) | Yes | Heap (Atomic) | Mutable (Locked) | Immutable | T * const x (w/ Mutex) |
| let x: Pin<Box<T>> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable (if Unpin) | Immutable | N/A |
| let mut x: Pin<Box<T>> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable (if Unpin) | Mutable | N/A |
For “Thread-Safe?” assumes T is Send in all cases.
Ownership
Who is responsible for dropping the memory, determines Lifetime.
- External Ownership (Container):
let x = ...: Unique external ownership (Stack/Scope).Box<T>: Unique external ownership (Heap).Rc<T>: Shared external ownership (Heap).Arc<T>(Atomic Reference Count): Thread-safe version ofRc.
- Borrowing: &T (Immutable), &mut T (Mutable).
Mutability
-
Binding Mutability: pointer mutability
let xvslet mut xdetermines if the variable (pointer address) can be reassigned. -
Normal (Inherited) Mutability: Core of Rust’s design philosophy. Found in
Box,let, andmut Box. The outer binding controls the inner data.Mutability comes from the outside (the owner). If the Box is mut, the data is mutable.Box<T>is only mutable if the variable holding it ismut. -
Interior Mutability: Mutability comes from the inside (the wrapper). Even if the Box is not mut, the RefCell allows you to change the data.
Cell/RefCellallow changing data even if the wrapper is behind an immutable reference.
Inherited Mutability:
In contrast to languages where pointer-constness and data-constness are explicitly separated
(int *const x vs const int *x in C), mutability in Rust is a property of the binding and the
path to the data, not the data type itself.
When you own a Box<T>, you own the T. Therefore, the mutability of the T is inherited from the
mutability of the Box.
- If the
Boxis immutable, theTis immutable. - If the
Boxis mutable, theTis mutable. This is the intended design of Owner-Based Mutability. Because you are the sole owner of thatBox, the compiler trusts you to decide if the whole thing (address and content) is mutable or not.
Box is a transparent wrapper. Rust’s goal is to make a Box<i32> behave as much like a plain
i32 as possible, just located on the heap. If you had a local variable let mut x = 5;, you can
change its value. Rust views the heap data inside a Box as just an extension of that variable. If
you “own” the memory, you “own” the right to define its mutability.
How Deref Handles Mutability
“Automatic dereferencing” is split into two distinct traits:
Deref: Provides&T(immutable). Used when the compiler seeslet x = Box.DerefMut: Provides&mut T(mutable). Used when the compiler seeslet mut x = Box. The compiler only invokesDerefMutif the variable was declared withmut. If you declarelet x = Box::new(5), the compiler refuses to useDerefMut, even though theBoxis capable of it.
Fixed Address, Mutable Content
#![allow(unused)]
fn modify_value(ptr: &mut i32) {
*ptr += 100; // Change the data
// ptr = &mut 500; // ERROR: Cannot reassign the local variable 'ptr'
// because the BINDING 'ptr' is not 'mut'.
// AND must always points to storage with ptr's
// original lifetime at owner
}
fn main() {
let mut x = Box::new(5);
let mut y = Box::new(-1);
*x = 10; // Change data
/* #1
let x = x; // RE-BIND as immutable (Shadowing)
x = y; // ERROR: The address is locked
*x = 20; // ERROR: and the data is locked.
*/
let x = &mut *x; // 'x' cannot be reassigned to a new address
/* #2
x = &mut *y; // ERROR: The address is locked
*x = 20; // but the data is changeable.
*/
// #3
//modify_value(x);
println!("{}", x); // Output: 110
}
Interior Mutability:
Capability granted by the container
Interior Mutability Wrappers
Cell and RefCell bypass the let / let mut restriction by managing access themselves.
-
Cell<T>: Mutates by replacing the value. No references allowed. Best for smallCopytypes.Cell<T>works by moving values in and out (copying) rather than issuing references, it is the only way to achieve mutation without any runtime borrow checking, but it is limited to types that are cheap to copy (like i32, bool, or structs that implement Copy). -
RefCell<T>: Mutates by issuing a runtime-checked&mut T. Best for complex types in single threads.
Rc, Cell, and RefCell are restricted to a single thread to prevent data races.
Thread Safety:
Determined by Arc and Mutex.
-
Arc<T>(Atomic Reference Count): Thread-safe version ofRc. Mutex<T>: The thread-safe version ofRefCell. It allows “Interior Mutability” by issuing a lock-protected&mut T, locking the data so only one thread can access it at a time. Essential for multi-threaded shared data.- Just like
Rc<RefCell<T>>, theArchandles the lifetime (keeping the memory alive across threads), while the Mutex handles the mutability (ensuring only one thread can change the data at a time).
- Just like
-
Send/Sync: Rust traits that define if a type can be moved between threads (Send) or shared between threads (Sync).RcandRefCellare neither. Pin<T>is a wrapper used to guarantee that the data it points to on the heap (theT) cannot be moved to a different heap location or swapped out for anotherTunlessTimplementsUnpin. IfTimplementsUnpin(likei32),Pinis transparent and allows normal mutable access. IfTis!Unpin,Pinblocks&mut Taccess to ensure safety. This pattern is very common in Async Rust. When you have aFuturethat is self-referential (it has pointers to its own fields), you mustPinit to the heap so that those internal pointers don’t break.use std::pin::Pin; fn main() { // Note: NO 'mut' on x! let x = Pin::new(Box::new(5)); // We can still get a mutable reference to the data! // This is because Pin::as_mut() only needs &self, not &mut self let mut pinned_ref = x.as_ref().get_ref(); // Immutable view println!("{:?}", pinned_ref); // To actually change it: let mut x_pinned = Box::pin(5); // Pin allows access to &mut T ONLY because i32 is Unpin. // This is NOT Interior Mutability; it is standard DerefMut behavior. *x_pinned.as_mut() = 10; println!("{:?}", x_pinned); pinned_ref = x_pinned.as_ref().get_ref(); println!("{:?}", pinned_ref); }
Generators with UnpinCell
The Interface Conflict: The Iterator::next method takes &mut self, which implies the iterator can be moved between calls. To support self-references (like a generator holding a reference to its own internal buffer across a yield), the object must be pinned.
The Problem (The Pinning “Blockade”)
Currently, if you have a Pin<&mut MyStruct>, you cannot get a &mut Field to any field inside it unless the field is Unpin. This makes it very hard to write generators that need to “swap” data or move internal buffers.
We are constantly fighting between Ownership (moving things) and Pinning (staying put).
- Today: If you want to move a value out of a pinned generator, you usually have to wrap it in
Option<T>and.take()it, or useunsafe. This is “clunky.” - With
UnpinCell: You don’t need themovekeywords to be so aggressive because the compiler can “see” which parts of your generator are allowed to move and which aren’t. It allows theAsyncIteratorto act like a normalIteratorfor its non-self-referential parts.
Currently, an Iterator must be Unpin. If we had UnpinCell, we could make an Iterator that contains self-referential (Pinned) state but exposes its data through an Unpin interface.
struct MyGenerator {
// This field is "pinned" (it has internal pointers)
internal_state: SomeSelfReferentialType,
// This field is wrapped in UnpinCell
// It tells Rust: "Even if MyGenerator is Pinned, you can still
// get a normal &mut i32 to this field."
data: UnpinCell<i32>,
}
impl MyGenerator {
fn work(self: Pin<&mut Self>) {
// 1. We can't move 'internal_state' (Safety!)
// 2. But we CAN get a normal mutable reference to 'data'
// without using unsafe code!
let val: &mut i32 = self.data.get_mut();
*val += 1;
}
}
“Static Frame” versus “Dynamic Data” model:
-
The “Skeleton” vs. The “Payload”
In a complex, self-referential generator (the state machine), the compiler creates a “Skeleton” of pointers that must never move because they point to other fields within that same struct.
- The Skeleton (Pinned): These are the internal state variables, the “yield points,” and the pointers that allow the generator to resume exactly where it left off. If these move, the addresses break.
- The Payload (
UnpinCell): These are the actual values you are calculating—the integers, strings, or buffers. These values usually have no reason to be “bolted to the floor.”
By wrapping the Payload in
UnpinCell, you “deconstruct” the struct’s strict pinning requirements. You tell the compiler: “Keep the skeleton pinned, but let me reach in and move the payload around as if it were a normal, movable variable.” -
Yielding and Sharing with Futures
The exact reason why this is necessary: The Hand-off.
- Yielding: When a generator yields a value, that value often needs to be moved out of the generator and into the caller’s scope. If the value is “trapped” inside a pinned struct, Rust’s current rules make it very difficult to move it out without using
Option::takeorunsafe.UnpinCellmakes this “move-out-of-pin” operation safe. - Sharing with Futures: In an
async genblock, you might want to pass a value to anasyncclosure or a sub-Future. That sub-Future might require exclusive ownership (&mut T) of the data.
Without
UnpinCell, the fact that the generator is pinned effectively “infects” every field inside it, making it impossible to give a simple&mut Tto a sub-Future.UnpinCellacts as a “Pinning Shield” that protects the payload from the skeleton’s constraints. - Yielding: When a generator yields a value, that value often needs to be moved out of the generator and into the caller’s scope. If the value is “trapped” inside a pinned struct, Rust’s current rules make it very difficult to move it out without using
-
The “Lending” Bridge
With
UnpinCell, we can finally build Lending Iterators that are actually ergonomic. a. The Iterator (the Skeleton) stays Pinned in memory. b. The Item it produces (the Payload) is stored in anUnpinCell. c. Thenext()method can return a mutable reference to that item becauseUnpinCellallows us to “extract” a normal&mut Tfrom aPin<&mut Self>.
UnpinCell doesn’t move memory
The value inside the cell stays at a fixed memory offset within the struct, but the compiler allows you to treat it as if it could move.
To understand how a mutable reference tracks the item, we have to look at the difference between Physical Movement and Semantic Permissions.
-
The “Fixed offset” Reality
Inside a
Pin<&mut MyStruct>, theUnpinCell<T>field is physically “bolted to the floor” at a specific byte offset (e.g., 16 bytes from the start of the struct).- The Skeleton (Pinned): Cannot move.
- The Payload (
UnpinCell<T>): Also cannot physically move (because it’s part of the pinned struct).
So why do we say it “can move”? Because
UnpinCellgives you the semantic permission to use functions likestd::mem::replaceorstd::mem::swapon that memory. -
Immovable Mutable Reference
When you call
self.payload.get_mut(), you get a standard&mut T.- This reference points to that fixed physical address (e.g.,
0x1000). - As long as that
&mut Texists, the Borrow Checker ensures nobody else can touch that memory. - The item doesn’t “move” while you hold the reference; it stays at
0x1000.
- This reference points to that fixed physical address (e.g.,
-
The “Swap” Illusion
The “movement” happens when you use that
&mut Tto perform a Swap.let r: &mut T = pin_struct.payload.get_mut(); // Points to 0x1000 let new_value = T::new(); std::mem::swap(r, &mut new_value);What happened here?
- The physical memory at
0x1000(the “Payload” slot) now contains thenew_value. - The old value was moved into the local
new_valuevariable on the stack. - The reference
rwas always pointing at the “slot” (0x1000), not the specific instance of the data.
- The physical memory at
-
Why this is forbidden without
UnpinCellIn a normal pinned struct (the Skeleton), fields might contain pointers to each other.
- Field A points to Field B.
- If you “Swap” Field B for a new value, Field A is now pointing to a valid address, but the data it expected (the internal state of B) has been replaced by something else.
- This is a Logic/Safety Violation.
UnpinCell tells the compiler: “There are no internal pointers pointing to this specific field. It is safe to swap the contents of this slot because no other part of the Skeleton is ‘watching’ this specific memory address.”
Field B is not allowed to be an
UnpinCell. The compiler does not check if the content of anUnpinCellis an address. You are allowed to put a reference (&T) or a pointer inside anUnpinCell. However, the compiler does enforce a much stricter rule: Internal Pointers (Self-References) are not allowed to point into anUnpinCell.
The “Deconstruction” Answer
The UnpinCell doesn’t allow the item to “drift” around memory while you are using it. Instead, it allows you to replace the contents of a pinned slot without violating the rules of the larger pinned structure.
This is exactly how Generators yield values:
- The Generator (Skeleton) stays pinned at
0x1000. - It calculates a value and puts it in its
UnpinCell“Yield Slot” at0x1010. - The
next()method gives the caller a&mut Tto that slot. - The caller can then
swapthe value out of that slot and into their own local variable.
Pinned Places
This makes “Pinned” a first-class citizen of the grammar, like mut.
- You could declare a variable as
pin let x = .... - The compiler then knows that every reference derived from
xis also pinned.
With Pinned Places:
- Direct Access: You could just write
self.field. Ifselfis in a “Pinned Place,” the compiler automatically treats the access as pinned. Noproject()needed. - Better than
UnpinCell: WhileUnpinCelllets you “escape” a pin, Pinned Places makes the pin “transparent.”
-
The Refined Analogy
UnpinCell<T>: You have a building where everything is technically protected by a “Historic Landmark” status (The Pin). You have to apply for a special permit (UnpinCell) just to move a single chair or paint one door. It’s a localized exception to a global restriction.Pinned Places: You define the Structural Load-Bearing Columns (the fields with self-references) aspin. The rest of the building (the other fields) remains “Normal.” The “Building Code” (The Compiler) only enforces immovability on the specific columns you tagged.
-
Why “Pinned Places” is the “Final Boss” of Ergonomics
The “Lament” of
Pintoday is that it is infectious. If you pin a struct, the entire struct is pinned. You lose the ability to easily get an&mut Tto any of its normal fields.With Pinned Places:
- Granularity: You only tag the “Skeleton” fields (the internal pointers) as
pin. - Inherited Freedom: Any field not tagged pin stays “Unpin” by default.
- No Projections: You don’t need the
pin-projectmacro or a “Hand-off” viaUnpinCell. You just access the field. If it’s apinfield, you get aPin<&mut T>. If it’s a normal field, you get a standard&mut T.
// Hypothetical Rust 2027 Syntax struct MyGenerator { // THE SKELETON: Load-bearing, self-referential pin internal_ptr: *const i32, // THE PAYLOAD: Furniture, movable, standard &mut access data: i32, } impl MyGenerator { // 'self' is in a Pinned Place. // The compiler knows 'internal_ptr' is pinned, but 'data' is not. fn poll_next(pin self: &mut Self) -> Poll<Option<i32>> { // Direct access! No .project() macro needed. let val: &mut i32 = &mut self.data; // This remains a Pinned reference automatically let ptr: Pin<&mut *const i32> = &mut self.internal_ptr; *val += 1; Poll::Ready(Some(*val)) } } - Granularity: You only tag the “Skeleton” fields (the internal pointers) as
Pin projection
In Rust, pin projection transforms a pinned reference to a struct into references to its individual fields, maintaining the pinning guarantee for specific “structural” fields while allowing normal access to others.
The following comparisons show how a struct with one pinned field (timer) and one unpinned field (completed) is projected using current library solutions and major language proposals.
pin-projectCrate (Current Standard)
The [pin-project]https://docs.rs/pin-project/latest/pin_project/attr.pin_project.htm) crate uses a procedural macro to generate a “shadow” projection struct.
Before Projection:
You have a single opaque wrapper Pin<&mut MyFuture>.
#[pin_project]
struct MyFuture<Fut> {
#[pin] timer: Fut, // Structurally pinned
completed: bool, // Not pinned
}
After Projection:
Calling .project() returns a new type where the fields have been split.
// Inside a method: let this = self.project();
struct MyFutureProjection<'a, Fut> {
timer: Pin<&'a mut Fut>, // Pinned projection
completed: &'a mut bool, // Normal mutable reference
}
- Pinned Places (Proposed Syntax)
The pinned places proposal (by withoutboats) suggests making pinning a property of the memory location itself rather than just a pointer wrapper.
Before Projection:
// A "pinned place" on the stack
let pinned mut my_fut: MyFuture<Fut> = ...;
After Projection:
The compiler would automatically understand which fields are structural. Projection would ideally use native reference syntax.
// Projection happens via field access on the pinned place
let timer_ref: &pinned mut Fut = &pinned mut my_fut.timer;
let comp_ref: &mut bool = &mut my_fut.completed;
- MinPin (Proposed Syntax)
The MinPin proposal (by Niko Matsakis) introduces pinned as a type modifier to make Pin behave more like a first-class reference.
Before Projection:
struct MyFuture<Fut> {
pinned timer: Fut, // Keyword marks structural pinning
completed: bool,
}
After Projection:
MinPin aims for “automatic reborrowing,” allowing you to use fields directly if you hold a pinned reference to the parent.
impl<Fut> MyFuture<Fut> {
fn poll(self: pinned &mut Self) {
// Pinned field projects to a pinned reference automatically
let _: pinned &mut Fut = &pinned mut self.timer;
// Unpinned field projects to a normal reference
let _: &mut bool = &mut self.completed;
}
}
Summary Comparison Table
| Implementation | Pinned Field Becomes | Unpinned Field Becomes | Projection Method |
|---|---|---|---|
pin-project |
Pin<&mut T> |
&mut T |
Manual .project() call |
| Pinned Places | &pinned mut T |
&mut T |
Native &pinned mut op |
| MinPin | pinned &mut T |
&mut T |
Implicit via field access |
Custom Future Implementation
Implementing a custom Future with the pin-project crate allows you to safely access fields of a pinned struct without using unsafe code. This is essential when your future wraps an inner future (structural pinning) or needs to modify its own internal state (non-structural pinning).
In this example, TimedFuture wraps an inner future and tracks when it started. The inner field must be pinned to be polled, while start_time can be accessed as a normal mutable reference.
use pin_project::pin_project; // 1.1
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;
#[pin_project]
pub struct TimedFuture<F> {
#[pin]
inner: F, // Structural: we need Pin<&mut F> to call .poll()
start_time: Instant, // Non-structural: we only need &mut Instant
}
impl<F: Future> Future for TimedFuture<F> {
type Output = (F::Output, std::time::Duration);
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// 1. Perform the projection
// 'this' is a generated struct where 'inner' is Pin<&mut F>
// and 'start_time' is &mut Instant.
let this = self.project();
// 2. Poll the inner future using its pinned reference
match this.inner.poll(cx) {
Poll::Ready(output) => {
let duration = this.start_time.elapsed();
Poll::Ready((output, duration))
}
Poll::Pending => Poll::Pending,
}
}
}
To use this future, you wrap another asynchronous operation and .await the result.
async fn example_usage() {
let inner_future = async {
// Simulate some async work
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
"Done!"
};
let timed = TimedFuture {
inner: inner_future,
start_time: Instant::now(),
};
let (result, duration) = timed.await;
println!("Result: {} took {:?}", result, duration);
}
Key Components
#[pin_project]Attribute: Applied to the struct to generate theproject()method.#[pin]Attribute: Marks a field as structurally pinned. This tells the macro to project it asPin<&mut Field>..project(): Consumes thePin<&mut Self>and returns a projection object containing the split references.- Field Access: Once projected, you can access unpinned fields (like
start_time) as standard mutable references (e.g.,*this.start_time).
Manual pin projection
If you have a pinned struct and need to project it to its fields without the crate, you would implement it like this:
use std::pin::Pin;
struct MyFuture<F> {
inner: F, // We want this to be structurally pinned
completed: bool, // This will be unpinned
}
impl<F> MyFuture<F> {
// Structural projection: Pin<&mut Self> -> Pin<&mut F>
fn project_inner(self: Pin<&mut Self>) -> Pin<&mut F> {
// SAFETY: We must guarantee that 'inner' is only ever
// accessed via a Pin as long as the parent is pinned.
unsafe { self.map_unchecked_mut(|s| &mut s.inner) }
}
// Non-structural projection: Pin<&mut Self> -> &mut bool
fn project_completed(self: Pin<&mut Self>) -> &mut bool {
// SAFETY: 'completed' is not structurally pinned, so we
// can safely get a raw &mut to it.
unsafe { Pin::get_unchecked_mut(self).completed }
}
}
But still no yield from or yield* for Rust
yield cannot cross closure boundaries:
- Boundary Restriction: A
yieldstatement can only suspend the function it is directly inside. It cannot “reach out” through a closure to suspend the parent generator. - The “Yield Closure” Proposal: There has been research into “Yield Closures” (closures that can
yield back to their caller), but even in those proposals, a closure’s
yieldwould only suspend the closure itself, not thegenblock that calledmap_or_else(). - The Delegation (
yield from) is a “vertical” feature. It requires one state machine to “yield” control to a sub-state machine and then get it back without the caller noticing.
In Rust, every async gen block is its own statically sized struct.
- If
Generator Awants toyield fromGenerator B, the compiler must decide: IsBstored insideA? Or isAjust holding a pointer toB? - If
Bis insideA, the “Move Soup” gets worse because now you have Nested Pinning. - To make
yield fromzero-cost and ergonomic, you need a Lending Signature (polymorphic over lifetimes) becauseAneeds to “lend” its drive-power toB. Without a native language-level “Lending” concept,yield fromwould require a heap allocation (to erase the types) or a massive macro expansion that breaks the borrow checker.
Why AsyncIterator is not stable yet?
The path to stabilization in the Rust standard library is blocked by three massive “Real World” technical hurdles. Rust’s “Stability Guarantee” means that once these are in std, they can never be changed.
-
The “Lending” Iterator Conflict (The Biggest Blocker)
The current
AsyncIteratortrait in std returns an Owned item:fn poll_next(self: Pin<&mut Self>, ...) -> Poll<Option<Self::Item>>However, many high-performance use cases (like zero-copy networking) require a Lending Iterator (also called a “Borrowing Iterator”).
- The Issue: A Lending Iterator returns an item that
borrowsfrom the iterator itself. - The Conflict: The current Iterator and AsyncIterator designs don’t support this. If they stabilize AsyncIterator now as a “non-lending” trait, they might split the ecosystem into two incompatible types of streams.
- The Goal: They are trying to find a way to make
AsyncIterator“Generic over Lending” using GATs (Generic Associated Types), but this is proving to be a compiler-logic nightmare.
- The Issue: A Lending Iterator returns an item that
-
The “Trait Alias” and Ecosystem Fragmentation
There is a massive amount of code already using the
futures::Streamtrait.- The Risk: If
std::async_iter::AsyncIteratoris slightly different fromfutures::Stream, the entire Rust ecosystem will break in half. - The Work: The team is currently working on “Trait Transformer” logic that would allow
Streamto automatically “count” as anAsyncIterator. This requires deep changes to how the compiler handles trait matching (Specialization), which is not yet stable.
- The Risk: If
-
The
DropProblem (Async Drop)This is the “dark side”.
- The Scenario: What happens if you are halfway through an
async genloop and you suddenlybreak? - The Crisis: The generator’s state machine is currently suspended at a
yieldpoint. To clean up, it needs to run its destructors (Drop). But in an async context, those destructors might need to be async (e.g., closing a database connection). - The Gap: Rust does not have Async Drop yet. Stabilizing
AsyncIteratorwithout a solution for how to “Async Drop” a cancelled generator could lead to resource leaks or “blocking-in-async” bugs that are impossible to fix later. Nuance: Technically,AsyncIteratorcould be stabilized without Async Drop. However, thegenblocks (the ergonomic driver) cannot be safe without Async Drop. Since the project wantsgenblocks to be the primary way users write iterators, they are effectively coupled.
- The Scenario: What happens if you are halfway through an
-
Recent Activity: The “Async 2024” Push
While individual issues might look old, the project is currently in its most active phase in years. The Rust Project Goals for 2025/2026 highlight that “Async-Sync Parity” is a Flagship Goal.
While the syntax (
gen) and primitives (Async Closures) are arriving, the Trait Definition (AsyncIterator) is being held back to ensure it can support Lending patterns (GATs) and Safe Cancellation (Async Drop).Recent major milestones include:
- Rust 1.85 (Feb 2025): Stabilization of Async Closures, a feature that had been in development for years to solve the “Lending” problem.
- Rust 2024 Edition (Current March 2026): The introduction of the
genkeyword, reserving the syntax needed for native generators. - Async Drop Progress (2026): Ongoing work under the “Feature Gate”
![feature(async_drop)]to finally solve the cancellation safety issue.
References: