We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Async From First Principles step 16 of 25
Structural pinning and safe pin projection by hand
Implement a future wrapper that counts how often it is polled. Store its child future inline and project a pinned reference to that field when polling it.
pub struct Timed<F> {
inner: F,
polls: u32,
}
pub fn count_polls(pends: u32) -> (u32, u32)
Keep Timed::new, Countdown, block_on and count_polls as supplied.
Implement Future for Timed<F> for any F: Future, without adding an
F: Unpin bound or boxing the child.
Required polling behavior
On each call to Timed::poll:
- Increment the poll count, including the call that completes the child.
- Poll
innerexactly once with the supplied context. - Return
Pendingif the child is pending, orReady((polls, value))with its output if the child is ready.
count_polls wraps an async block that waits for Countdown and then returns
7. The countdown returns Pending exactly pends times before returning
Ready, so the required result is (pends + 1, 7).
count_polls(0) -> (1, 7)
count_polls(4) -> (5, 7)
Exercise inputs satisfy pends < 100_000, the limit of the supplied driver.
The driver repeatedly polls with a no-op waker; it is a small test helper,
not an executor that waits for notifications. Do not poll a completed child
again. Poll counts in this exercise fit in u32.
Why ordinary mutable access fails
The starter tries &mut *self where self is Pin<&mut Timed<F>>.
Its inline F is not known to implement Unpin, so the pinned wrapper does
not expose an unrestricted mutable reference. This causes E0596.
A boxed child is a valid design in other code: moving a box does not move its allocation. Here the goal is to support an inline child, avoiding that separate allocation and learning the obligations of field projection.
Choose which fields are structurally pinned
Treat inner as structurally pinned: pinning Timed also promises that this
field will remain pinned, so it can be polled as Pin<&mut F>.
Treat polls as an ordinary mutable field. Updating a counter does not move
the child. Structural pinning is a choice made by the type's API; pinning a
parent does not automatically impose the same API on every field.
To implement the projection, first obtain access to the fields with
get_unchecked_mut, then construct Pin<&mut F> with Pin::new_unchecked.
Both operations are unsafe because their contracts must be justified by the
wrapper's implementation. Use the mutable parent reference only for operations
that preserve the pinned child.
Justify the projection
The supplied representation supports the required promises:
- Leave
Unpinautomatic. With these fields,Timed<F>implementsUnpinwhenFdoes. The counter adds no restriction. - The wrapper has no custom
Dropimplementation that moves its child. Normal field destruction drops the child in place. A future custom destructor would have to preserve that property even though it receives&mut self. - The wrapper is not packed. A packed representation can cause unaligned fields and moves made for destruction, which conflict with this projection.
- No operation available through a pinned wrapper may move out the pinned child or expose unrestricted mutable access that permits such a move.
The last point does not ban an ordinary into_inner(self) -> F method on an
owned, unpinned wrapper. Safe code cannot call it through Pin<&mut Timed<F>>
by moving out a non-Copy wrapper. What matters is whether the API actually
makes a forbidden move possible after pinning.
Add a // SAFETY: comment to each unsafe block explaining why the particular
operation preserves these promises. The projection must remain valid through
the child's destruction, not just until the current call to poll returns.
Do not override Unpin unconditionally
Adding impl<F> Unpin for Timed<F> {} would let callers recover ordinary
mutable access even when the child relies on a stable address. Combined with
this wrapper's pinned-child projection, that permits safe callers to move a
child after it has been polled in place. The generic wrapper would be unsound.
The unsafe projection and the Unpin policy must agree. A blanket Unpin
implementation is not intrinsically wrong for every wrapper; it is wrong for
this inline, structurally pinned child when F is unrestricted.
Understand the limits of the harness
The async child ensures that your implementation works with a future that
does not implement Unpin. It does not prove that a passing solution preserves
all pinning invariants. In particular, an unsound unconditional Unpin
implementation can still produce the expected counts.
The harness checks only the supplied countdown behavior. The generic Future
implementation must still forward the context, preserve the child's output,
and uphold its projection contract for other valid futures. Output tests are
not a substitute for that reasoning.
In application libraries, pin-projection macros can generate the field access API and associated checks. This exercise asks for the projection by hand so that the safety argument is visible.
A passing submission must compile, pass the tests, and pass
clippy -D warnings, while respecting the inline-child and safety requirements.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.