Course contentsShow
← Rust
Lesson 1265 of 1,91011. Asynchronous ProgrammingFree lesson

Runtime Independence Strategies

Design libraries that remain runtime-agnostic to support different executor implementations.

In this lesson, you will learn how to design libraries that operate seamlessly across different asynchronous executors like tokio, async-std, or smol.

The Problem of Runtime Coupling

In Rust, a Future is just a state machine that needs to be polled to completion. However, the mechanism to drive that polling—the executor—often requires specific primitives. If your library code directly calls tokio::spawn or uses tokio::time::sleep, you force your users to depend on that specific runtime, even if they prefer another one. This is known as "runtime coupling."

Strategies for Agnostic Design

To remain runtime-agnostic, you must shift the responsibility of "execution" and "timekeeping" to the user or to generic traits.

1. Dependency Injection for Tasks Instead of spawning tasks inside your library, allow the user to define how tasks are executed. You can accept an executor as a parameter or, more commonly, simply design your library to return a Future that the user spawns themselves.

2. Leveraging Traits for IO and Time If your library needs to perform IO or handle timeouts, do not depend on tokio::io or tokio::time. Instead, rely on standard library traits like futures_io::AsyncRead or define your own traits that wrap these operations. For time-sensitive tasks, use the futures-timer crate, which provides a cross-runtime implementation of sleep.

3. Conditional Feature Flags If you absolutely must provide convenience functions for specific runtimes, use Cargo.toml features to keep the heavy lifting optional:

#[cfg(feature = "tokio-runtime")]
pub async fn run_on_tokio() {
    tokio::time::sleep(Duration::from_secs(1)).await;
}

// Keep the core logic runtime-agnostic
pub async fn core_logic<T: AsyncRead>(stream: T) {
    // Logic that uses generic futures and traits
}

By keeping your core business logic as a pure Future and moving runtime-specific IO or spawning into the integration layer, you ensure your crate remains a versatile building block for the entire ecosystem.

Key Takeaway: Design your library to return lazy Futures and rely on standard AsyncRead/AsyncWrite traits rather than hardcoding runtime-specific spawning or timer primitives.