Skip to main content

Crate async_compat

Crate async_compat 

Source
Expand description

Compatibility adapter between tokio and futures.

There are two kinds of compatibility issues between tokio and futures:

  1. Tokio’s types cannot be used outside tokio context, so any attempt to use them will panic.
    • Solution: If you apply the Compat adapter 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.
  2. Tokio and futures have similar but different I/O traits AsyncRead, AsyncWrite, AsyncBufRead, and AsyncSeek.
    • Solution: When the Compat adapter 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.

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.

Traits§

CompatExt
Applies the Compat adapter to futures and I/O types.