Custom Future
Implement a Custom Future (Pin, Waker, Poll)
When you need low-level control: a custom hardware driver, a specialized timer,
or a zero-allocation protocol parser—you can implement Future by hand
instead of relying on async/await.
Every custom future must handle three concepts:
| Concept | Why it matters |
|---|---|
Pin<&mut Self> | Guarantees the future won’t move in memory after the first poll. This is critical for self-referential futures (e.g., those holding borrows across .await points). If your struct contains only ordinary fields (no self-references), the compiler auto-implements Unpin and pinning is effectively a no-op. |
Poll::Pending / Poll::Ready | Pending tells the executor “not done yet,” while Ready(value) completes the future. |
cx.waker() | A handle the executor gives you. You must call wake() at some point after returning Pending, or the executor will never poll the future again and it will hang. |
The example below builds a simple Delay future that resolves after a
deadline. It has no self-referential fields, so it is Unpin automatically—
pinning costs nothing. A production timer would register with a reactor;
here we call wake_by_ref() immediately so the executor re-polls us.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
/// A hand-rolled future that resolves after a deadline.
///
/// This is intentionally simple—no timer wheel, no reactor—just a
/// busy-polling future that shows the three things every custom
/// `Future` must handle:
///
/// 1. **`Pin<&mut Self>`** — guarantees our struct won't move in
/// memory. Because `Delay` contains only `Instant` and
/// `Duration` (both `Unpin`), the compiler auto-implements
/// `Unpin` for us and pinning is a no-op here. If the struct
/// held a self-referential borrow (like a hand-written async
/// state machine), pinning would *prevent* the struct from
/// being moved after the first poll, which would invalidate
/// the borrow.
///
/// 2. **`Poll::Pending` vs `Poll::Ready`** — returning `Pending`
/// tells the executor "I'm not done yet; wake me later."
///
/// 3. **`cx.waker()`** — the mechanism to *schedule* a re-poll.
/// Without calling `wake()`, the executor would never poll us
/// again and the future would hang.
struct Delay {
/// When we should resolve.
deadline: Instant,
}
impl Delay {
fn new(dur: Duration) -> Self {
Self {
deadline: Instant::now() + dur,
}
}
}
impl Future for Delay {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if Instant::now() >= self.deadline {
// Deadline reached — resolve the future.
Poll::Ready(())
} else {
// Not ready yet.
//
// We MUST arrange for `wake()` to be called later or the
// executor will never poll us again. A production timer
// would register with a reactor; here we just ask to be
// re-polled immediately. The executor may yield to other
// tasks before coming back to us.
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
#[tokio::main]
async fn main() {
let start = Instant::now();
println!("waiting 10 ms …");
// Use the custom future just like any other async expression.
Delay::new(Duration::from_millis(10)).await;
println!("done in {:?}", start.elapsed());
assert!(start.elapsed() >= Duration::from_millis(10));
}