TRT
Sign in
mediumExpertconcept

Explain how `async fn` compiles into a state machine in Rust, why self-referential futures require `Pin<&mut Self>`, how stack pinning (`std::pin::pin!`).

Trace the compiler-generated `enum` across `.await` yield points, `Pin` / `Unpin`, `Waker` readiness notifications, and `tokio::select!` drop cancellation.

TRT practice prompt — not a verified question from a named employer.

The Runtime Theory Team2 min read

A strong answer

When the Rust compiler lowers async fn example() { let mut buf = [0u8; 64]; let slice = &mut buf[..]; read_into(slice).await; }, it generates a state-machine enum where the AwaitingRead state holds both buf: [u8; 64] AND slice: *mut [u8] pointing at self.buf inside the exact same struct (a self-referential struct)! Before poll() runs the first time, slice has not been created yet, so you can move the future freely in memory. The instant poll() runs and pauses at .await, self.slice stores the raw memory address of self.buf (e.g., 0x7ffd1000). If the caller subsequently moved the future to address 0x7ffd2000 (e.g., via std::mem::swap(&mut fut1, &mut fut2)), fut1.slice would still point to the old address 0x7ffd1000 (dangling pointer / memory corruption!). Pin<P> solves this at compile time: once a !Unpin value is wrapped in Pin<&mut T>, safe code can never obtain a plain &mut T to pass to mem::swap or mem::replace—guaranteeing the object's memory address stays fixed until Drop::drop runs!

Once a !Unpin value (such as a compiler-generated self-referential async block) is pinned at memory address A via Pin<&mut T> (or Box::pin), its bytes remain at address A without ever being moved until its destructor Drop::drop completes. Because moving a value in safe Rust requires either ownership T or a mutable reference &mut T (for mem::swap / mem::replace), and Pin<&mut T> withholds DerefMut whenever T: !Unpin, safe Rust code cannot relocate a pinned !Unpin future after polling begins.

Because Rust futures compile into sized state-machine structs/enums, composing a().await inside b().await nests FutureA inline inside FutureB—resulting in 1 single allocation per top-level spawned task (instead of 1 heap allocation per nested async function call as in JS/Python)! In production systems (Tokio work-stealing async executor, futures::pin_mut! / std::pin::pin!, hyper / axum zero-allocation HTTP state machines, and tokio::select!.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.

What the interviewer is listening for

  • Traces how Rust Future::poll state machines, Waker readiness, and Pin<&mut T> operates in memory step by step rather than giving a vague textbook definition.
  • States the core correctness invariant and explains why edge cases do not violate it.
  • Quantifies time, space, and hardware costs (Nested async fn Call (a().await), Stack Pin (std::pin::pin!(fut))) with concrete numbers.
  • Identifies real-world failure modes (Putting a multi-step I/O future read_message(&mut stream) directly as an arm inside a loop { tokio::select! { ... } }) and explains how production systems mitigate them.

Common mistakes

  • Treating Rust Future::poll state machines, Waker readiness, and Pin<&mut T> as a black box without explaining the underlying memory or state transitions.
  • Ignoring worst-case tail latency or memory overhead (Declaring a 64 KB stack array let buf = [0u8; 65536]; before an .await inside an async fn).
  • Forgetting boundary conditions and error recovery (Calling blocking std::thread::sleep or synchronous file/CPU work inside a tokio::spawn async task).

This answer walks

Practice follow-ups

  1. 01How does `let fut = std::pin::pin!(my_async_fn());` pin a future directly on the **stack** with zero heap allocation, whereas `Box::pin(my_async_fn())` allocates on the heap?
  2. 02In a `loop { tokio::select! { msg = rx.recv() => ..., _ = tick.tick() => ... } }`, why is putting an async function `read_frame(&mut sock)` directly inside a `select!` arm a data-loss bug when.

Not started

Sign in to save your learning progress.

Sign in to save