322: The Future Trait and Poll
Difficulty: 4 Level: Expert A Future is just one method: `poll`. The runtime calls it. If the work is done, return `Ready`. If not, return `Pending` and arrange to be woken later.The Problem This Solves
When you write `async fn` and `.await`, the compiler generates state machine code for you. But what is a Future, actually? Understanding the `Future` trait answers this โ and it matters when you need to implement your own async primitive, integrate non-async code into an async runtime, or debug why a future is never waking up. Without understanding `poll`, async code feels like magic. You don't know why `await` suspends, how the runtime knows when to resume, or what the `Waker` is for. This leads to subtle bugs: futures that never wake, busy-polling that wastes CPU, or deadlocks from holding locks across `.await`. Implementing `Future` manually also reveals that the entire async machinery is surprisingly simple โ one method, two return values, one callback mechanism.The Intuition
Imagine a restaurant. You order food (create a future). The waiter doesn't stand next to the kitchen watching โ they go serve other tables. The kitchen calls the waiter when the order is ready (the `Waker`). The waiter comes back and delivers (returns `Poll::Ready`). The `poll` method is: "Is the food ready?" The answer is either `Ready(food)` or `Pending` (with a promise to call you when it is). The runtime keeps a list of pending tasks and polls them when they signal readiness. In Python, `asyncio` hides this behind coroutines. In JavaScript, Promises chain callbacks. Rust exposes the mechanism directly โ which gives you full control and zero runtime overhead.poll() โ Poll::Ready(value) โ work done, here's the result
poll() โ Poll::Pending โ not done yet, we'll wake you when ready
How It Works in Rust
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct DelayedValue { value: i32, remaining: u32 }
impl Future for DelayedValue {
type Output = i32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.remaining == 0 {
Poll::Ready(self.value) // work is done
} else {
self.remaining -= 1;
cx.waker().wake_by_ref(); // tell the runtime: try again soon
Poll::Pending // not ready yet
}
}
}
`Pin<&mut Self>` prevents the future from being moved in memory while it's being polled โ important for self-referential state machines that the compiler generates from `async fn`.
`cx.waker().wake_by_ref()` is how you tell the runtime "I'll be ready soon, poll me again." In real I/O, the OS (via epoll/kqueue) calls the waker when a socket becomes readable.
The `block_on` in this example is a minimal hand-rolled executor โ it just loops calling `poll` until `Ready`. Real runtimes (tokio, async-std) are far more sophisticated but implement the same interface.
What This Unlocks
- Custom async primitives: Integrate timers, file I/O, or OS events directly into the async ecosystem.
- Zero-cost abstractions: No heap allocations, no virtual dispatch โ just a state machine in a struct.
- Debugging async code: When a task hangs, you know to look at whether `wake()` is being called.
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Core async abstraction | `Lwt.t` (promise/thread) | `Future` trait with `poll` |
| State transition | implicit in Lwt machinery | explicit `Poll::Ready` / `Poll::Pending` |
| Wakeup mechanism | callback registered on promise | `Waker::wake()` via `Context` |
| Pinning | not needed | `Pin<&mut Self>` prevents moves |
| Executor | Lwt main loop | any runtime that calls `poll` |
//! # The Future Trait and Poll
//!
//! Understanding the core Future trait: `poll`, `Poll::Ready`, `Poll::Pending`,
//! and how to implement custom futures manually.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
/// A future that returns a value after being polled a certain number of times.
/// Demonstrates the manual implementation of the Future trait.
pub struct DelayedValue {
value: i32,
remaining_polls: u32,
}
impl DelayedValue {
/// Create a new delayed value that will be ready after `polls` poll calls.
pub fn new(value: i32, polls: u32) -> Self {
Self {
value,
remaining_polls: polls,
}
}
}
impl Future for DelayedValue {
type Output = i32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.remaining_polls == 0 {
Poll::Ready(self.value)
} else {
self.remaining_polls -= 1;
// Schedule a wakeup so the runtime knows to poll again
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
/// A future that is immediately ready with a value.
pub struct Ready<T> {
value: Option<T>,
}
impl<T> Ready<T> {
pub fn new(value: T) -> Self {
Self { value: Some(value) }
}
}
impl<T: Unpin> Future for Ready<T> {
type Output = T;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.get_mut().value.take() {
Some(v) => Poll::Ready(v),
None => panic!("Ready polled after completion"),
}
}
}
/// A future that counts how many times it was polled before returning.
pub struct PollCounter {
target: u32,
current: u32,
}
impl PollCounter {
pub fn new(target: u32) -> Self {
Self { target, current: 0 }
}
}
impl Future for PollCounter {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.current += 1;
if self.current >= self.target {
Poll::Ready(self.current)
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
/// A minimal single-threaded executor that blocks until a future completes.
/// This is a simplified version - real executors are much more sophisticated.
pub fn block_on<F: Future>(mut fut: F) -> F::Output {
// Create a no-op waker (simplest possible implementation)
unsafe fn clone(ptr: *const ()) -> RawWaker {
RawWaker::new(ptr, &VTABLE)
}
unsafe fn noop(_: *const ()) {}
static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
let waker = unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) };
let mut cx = Context::from_waker(&waker);
// SAFETY: We never move `fut` after pinning
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
// Keep polling until ready
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => continue,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_delayed_value_immediate() {
let future = DelayedValue::new(42, 0);
assert_eq!(block_on(future), 42);
}
#[test]
fn test_delayed_value_with_polls() {
let future = DelayedValue::new(100, 5);
assert_eq!(block_on(future), 100);
}
#[test]
fn test_ready_immediate() {
let future = Ready::new("hello");
assert_eq!(block_on(future), "hello");
}
#[test]
fn test_poll_counter_counts_correctly() {
let future = PollCounter::new(3);
assert_eq!(block_on(future), 3);
}
#[test]
fn test_poll_counter_single_poll() {
let future = PollCounter::new(1);
assert_eq!(block_on(future), 1);
}
#[test]
fn test_delayed_value_preserves_value() {
let future1 = DelayedValue::new(-42, 2);
let future2 = DelayedValue::new(i32::MAX, 1);
assert_eq!(block_on(future1), -42);
assert_eq!(block_on(future2), i32::MAX);
}
}
(* OCaml: manual future-like with continuations *)
type 'a state = Pending of (unit -> 'a state) | Ready of 'a
let rec run = function
| Ready v -> v
| Pending f -> run (f ())
let delayed_value n steps =
let rec loop i =
if i = 0 then Ready n
else Pending (fun () -> loop (i-1))
in loop steps
let () =
Printf.printf "Got: %d\n" (run (delayed_value 42 3))