Skip to content

← Async From First Principles step 4 of 25

Easy Primitives

block_on: poll a future to completion

Calling an async function creates a future. Something must poll that future to obtain its result. Rust provides the async and .await language syntax; the standard library provides types such as Future, Context and Waker. In this exercise, you will use those types to build a small polling loop.

Your task

Repair block_on in the starter and keep run_to_completion as its caller:

pub fn block_on<F: Future>(fut: F) -> F::Output
pub fn run_to_completion(steps: u32) -> u32

The supplied Countdown returns Pending on its first steps polls, then returns Ready with the total poll count. For example:

steps Poll results Return value
0 Ready(1) 1
1 Pending, Ready(2) 2
2 Pending, Pending, Ready(3) 3

Inputs are small enough to finish within the time limit and satisfy steps < u32::MAX, so steps + 1 fits in the return type. Do not change Countdown or replace the generic driver with an arithmetic shortcut.

Set up the poll

The starter already performs two setup steps:

  • pin!(fut) pins the future locally and returns Pin<&mut F>. This lets block_on accept futures without requiring F: Unpin. The macro does not allocate a new heap object; the future itself may still own allocations.
  • Context::from_waker(Waker::noop()) creates the polling context. The no-op waker ignores wake requests. Waker::noop() already returns a reference, so no extra & is needed.

Local pinning is useful for futures whose state must stay at the same address. Types that implement Unpin do not need that restriction. See the pin! documentation for the distinction between local pinning and heap allocation.

Reborrow on each iteration

The starter calls fut.poll(&mut cx) inside the loop. This produces E0382: the first call moves the Pin<&mut F> wrapper, leaving no wrapper for the next iteration. Moving this wrapper is different from moving the future stored behind it.

Use this expression for each poll:

fut.as_mut().poll(&mut cx)

as_mut() creates a shorter-lived pinned mutable reference. The original wrapper remains available for the next iteration. Pin<&mut F> is not Copy, because its mutable reference is not Copy; that does not mean every kind of Pin is non-copyable.

On Ready(value), return value immediately. On Pending, try another poll. The driver should never poll a completed future again. Its output remains generic: block_on(std::future::ready(String::from("done"))) returns the string, even though this exercise's wrapper returns a number.

Why this is only a teaching driver

This loop busy-polls: it keeps calling poll without waiting for a wake request. That works for Countdown, which makes progress on every call. A future waiting for network input could instead consume CPU while making no progress. Runtime-specific futures may also require services this driver does not provide.

A wake-aware executor schedules another poll when work can progress. It may run other tasks or wait when none are ready. Countdown calls wake_by_ref() before each Pending, requesting another poll immediately; it does not need to save the waker for later.

Busy-polling can hide a missing wake request: another poll happens anyway. Under a wake-aware executor, a future that never arranges its next wake can remain unfinished. Building a working waker and avoiding missed wakeups are the next steps toward a useful executor.

The Future contract explains polling and wakeups. This exercise deliberately uses a simpler loop so you can focus on pinning, reborrowing and returning the result.