23. Rust Pin, Unpin and Async Boundaries¶
Rust Pin restricts access that could move a pinned pointee; it does not freeze an owning pointer or make a future thread-safe. This lesson separates those ideas with a complete dependency-free program and tests, including a wrapper that polls an async future without unsafe field projection.
Learning goals and prerequisites¶
After async, await and Future, you can identify who owns a future, who only borrows it, and where poll requires Pin. Here you will distinguish movement of a handle from movement of its pointee, choose local or heap pinning, and explain why Unpin, Send and lifetimes are independent constraints.
Use stable Rust, Cargo and edition 2024. The examples work without Raspberry Pi hardware access, external crates or an async runtime. Values are fixtures, not sensor measurements.
Rust Pin and Unpin: which value is restricted?¶
An ordinary Box owns heap storage, but its API still permits moving its value out. Pin
Unpin is an auto trait: most ordinary types implement it automatically. Its meaning is that pinning imposes no extra movement restriction on this type. It is not an operation that relocates memory, and it does not imply Copy, Send, Sync or 'static. For T: Unpin, safe Pin::new, get_mut and into_inner expose ordinary access. See the Unpin contract.
Our StableLabel contains PhantomPinned, so it does not automatically implement Unpin. This deliberately demonstrates the API restriction; it contains no self-reference and does not intrinsically need a stable address. PhantomPinned alone does not pin a value: an ordinary, not-yet-pinned StableLabel can still move.
Create and run the complete program¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | |
Expected output:
The address comparison is only an observation in this fixture, not a proof of an unsafe implementation. No raw pointer is dereferenced. Ordinary Box handles also keep their allocations in place when moved; Pin's additional value is the restricted API, not a new allocator.
Local pinning, owning pinning and reborrowing¶
Box::pin owns heap storage. The pin! macro instead creates a local pinned value and returns borrowed access to that storage, without requiring heap allocation. Inside an async body the storage can become part of its generated future, so “local” is more precise than always calling it a stack allocation. A locally pinned value cannot escape the lifetime of its backing storage.
as_mut creates another temporary pinned mutable borrow. It lets a poll call consume that borrow while retaining the owning handle for later calls. as_ref exposes shared pinned access. get_ref returns a shared reference; reading through it does not let you move the pointee out.
The drop test captures a guard before the async body starts. Dropping borrowed Pin<&mut _> ends only that borrow; dropping the owning Box destroys the still-unpolled future and its captured guard. This complements the previous lesson's cancellation-after-poll test.
Safe wrapping is not unsafe structural projection¶
Observed is Unpin because its fields are a movable pinned Box handle and a counter. get_mut therefore gives mutable access to the wrapper. Moving that handle leaves its separately allocated future pinned; as_mut supplies the pinned reference needed by Future::poll. No F: Unpin bound is necessary.
An embedded inner: F is a different design. Projecting Pin<&mut Wrapper
Pin is not general immutability: interior mutation can remain valid, and Pin::set can replace a pointee by dropping its previous value in place first. The obligation concerns address-sensitive state and valid destruction, not banning every write.
Erased futures, thread bounds and lifetimes¶
Different async expressions produce different concrete types, even when they return the same i32. Our trait-object collection chooses dynamic dispatch and heap-owned pinning explicitly. ready_value polls each known-ready fixture once; it panics on Pending and is not a scheduler. The two-poll test repolls only its known YieldOnce fixture after observing notification. Do not use either helper to busy-poll arbitrary I/O or poll a completed future again.
The collection's omitted object lifetime defaults to 'static here. The borrowed example instead specifies + '_ and completes while owner is alive. Pin adds neither a longer lifetime nor Send. A future holding Rc is still unsuitable for a Send-requiring spawn API, even if boxed and pinned. Conversely our !Unpin StableLabel is Send: thread transfer of the owning handle need not move its protected allocation.
A first distinction about variance¶
shorten expresses 'long: 'short: a reference valid for the longer lifetime may be used for the shorter one. This is lifetime subtyping, not an extension of the owner's lifetime. The Reference's variance rules explain how enclosing types affect such conversions.
Shared &T is covariant in T; &mut T is invariant in T, although it remains covariant in its own borrow lifetime. This distinction prevents changing the referent type in ways that could write a too-short reference into longer-lived storage. RefCell
Deliberately failing: Pin::new requires Unpin¶
Use a separate scratch project for this complete failure:
cargo check reports E0277 because safe Pin::new requires an Unpin pointee. Repair it with Box::pin(label) or a local pin!(label), not an unsafe constructor added merely to silence the compiler. Neither repair requires implementing Unpin for this demonstration type.
Exercises and troubleshooting¶
- Repair the failure with Box::pin, then move its handle into a vector. Explain why PhantomPinned by itself did not establish the pinning contract.
- Call get_mut on a pinned mutable reference to StableLabel. Expect E0277. Use shared get_ref access for inspection instead; do not promise that an arbitrary address-sensitive type is Unpin.
- Attempt to move StableLabel out of Pin
> with dereference. Expect E0507. Explain why a movable handle does not authorize moving its pointee. - Try a vector of two unboxed async expressions instead of the erased collection. Expect E0308. Choose one concrete future type or an explicitly erased interface depending on the API you want.
- Change the borrowed example's object lifetime to 'static while retaining its local String borrow. Expect E0597. Compare the repairs: finish within the borrow or deliberately capture owned data with async move.
- Replace Observed's heap-pinned field with F directly. Explain the extra Unpin restriction a safe get_mut-based implementation would need, and why that is not equivalent for generated async futures.
- Use local pin! in a nested scope and attempt to return its borrowed pinned reference. Expect a temporary/lifetime error, not heap ownership. Identify the backing storage before choosing a repair.
Verification and next step¶
On October 10, 2026, this lesson was verified on the authorised Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. Cargo check, nine debug/release tests, formatting, debug/release output comparisons and five further runs passed. Heap and local pinning repairs passed. Seven expected compile failures covered safe Pin::new, get_mut, extracting an async future, moving a pointee out, distinct async types, a 'static object borrow and escaping local pin storage. These checks demonstrate API boundaries, not hardware performance or the soundness of a custom unsafe pinning implementation.
Continue with declarative and procedural macros: follow syntax expansion and test its typing and name-resolution boundaries.