Expand description
Compatibility adapter between tokio and futures.
There are two kinds of compatibility issues between tokio and futures:
- Tokio’s types cannot be used outside tokio context, so any attempt to use them will panic.
- Solution: If you apply the
Compatadapter to a future, the future will manually enter the context of a global tokio runtime. If a runtime is already available via tokio thread-locals, then it will be used. Otherwise, a new single-threaded runtime will be created on demand. That does not mean the future is polled by the tokio runtime - it only means the future sets a thread-local variable pointing to the global tokio runtime so that tokio’s types can be used inside it.
- Solution: If you apply the
- Tokio and futures have similar but different I/O traits
AsyncRead,AsyncWrite,AsyncBufRead, andAsyncSeek.- Solution: When the
Compatadapter is applied to an I/O type, it will implement traits of the opposite kind. That’s how you can use tokio-based types wherever futures-based types are expected, and the other way around.
- Solution: When the
You can apply the Compat adapter using the Compat::new() constructor or using any
method from the CompatExt trait.
§The multi-thread feature
Because the fallback runtime is single-threaded, a task spawned from inside a Compat
future runs on a current-thread scheduler, where tokio::task::block_in_place panics with
can call blocking only when running on the multi-threaded runtime. That call is how an
async wrapper around a synchronous library runs a blocking call without stalling the tasks
queued behind it, so a dependency written that way cannot be driven through Compat
unless a multi-threaded runtime is already ambient.
Enabling the multi-thread feature backs the fallback runtime with a multi-threaded scheduler
instead, so spawned tasks run on real worker threads and block_in_place behaves as it would
in any other tokio program. The trade-off is that the runtime starts a worker pool (sized to
the available parallelism) rather than a single thread, so it is off by default.
The feature affects the process-wide fallback runtime, and cargo features are additive, so
enabling it anywhere in a dependency graph enables it for every Compat user in that binary.
Nothing that works without the feature stops working with it - a multi-threaded scheduler is
strictly more permissive - but it is worth knowing that the choice is not local to one crate.
§Examples
This program reads lines from stdin and echoes them into stdout, except it’s not going to work:
fn main() -> std::io::Result<()> {
futures::executor::block_on(async {
let stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
// The following line will not work for two reasons:
// 1. Runtime error because stdin and stdout are used outside tokio context.
// 2. Compilation error due to mismatched `AsyncRead` and `AsyncWrite` traits.
futures::io::copy(stdin, &mut stdout).await?;
Ok(())
})
}To get around the compatibility issues, apply the Compat adapter to stdin, stdout, and
futures::io::copy():
use async_compat::CompatExt;
fn main() -> std::io::Result<()> {
futures::executor::block_on(async {
let stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
futures::io::copy(stdin.compat(), &mut stdout.compat_mut()).compat().await?;
Ok(())
})
}It is also possible to apply Compat to the outer future passed to
futures::executor::block_on() rather than futures::io::copy() itself.
When applied to the outer future, individual inner futures don’t need the adapter because
they’re all now inside tokio context:
use async_compat::{Compat, CompatExt};
fn main() -> std::io::Result<()> {
futures::executor::block_on(Compat::new(async {
let stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
futures::io::copy(stdin.compat(), &mut stdout.compat_mut()).await?;
Ok(())
}))
}The compatibility adapter converts between tokio-based and futures-based I/O types in any direction. Here’s how we can write the same program by using futures-based I/O types inside tokio:
use async_compat::CompatExt;
use blocking::Unblock;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut stdin = Unblock::new(std::io::stdin());
let mut stdout = Unblock::new(std::io::stdout());
tokio::io::copy(&mut stdin.compat_mut(), &mut stdout.compat_mut()).await?;
Ok(())
}Finally, we can use any tokio-based crate from any other async runtime. Here are reqwest and warp as an example:
use async_compat::{Compat, CompatExt};
use warp::Filter;
fn main() {
futures::executor::block_on(Compat::new(async {
// Make an HTTP GET request.
let response = reqwest::get("https://www.rust-lang.org").await.unwrap();
println!("{}", response.text().await.unwrap());
// Start an HTTP server.
let routes = warp::any().map(|| "Hello from warp!");
warp::serve(routes).run(([127, 0, 0, 1], 8080)).await;
}))
}Structs§
- Compat
- Compatibility adapter for futures and I/O types.