h1
Runtime
-
2019-04-16
Asynchronous programming in Rust continues to make exciting strides, including the upcoming stabilization of the futures API. But, while these core APIs make it possible to write async Rust code today, it’s not easy: it’s a far cry from the smoothness of synchronous code.
The vision of the Async Ecosystem WG is to refine the async Rust experience until it matches the
quality and ease of working with today’s std
. There are a lot of components in that vision,
including async/await syntax and borrow checker integration. Today, though, we’d like to introduce
another component: Runtime, a crate that makes working with async code feel closer to working with
std
, and a stepping stone toward ecosystem standardization.
Our north star
The goal of the Runtime crate is to smooth out the experience of setting up an asynchronous application. It does this by:
- Removing headaches for setting up a shared async runtime, including both I/O and async executors.
- Carefully hewing to Rust API conventions (following
std
’s lead). - Standardizing a runtime interface to decouple applications from underlying backing implementations – and allowing easy customization through creating your own runtime implementation.
We would love to see a future in which async Rust is part of std
, has ubiquitous platform support,
and the backing implementations can be swapped out the same way allocators can be swapped today. We ultimately
imagine writing a TCP echo server, using potential syntax like await?
and for await
, with code like this:
use std::futures::net::TcpListener;
async fn main() -> std::io::Result<()> {
let mut listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Listening on {}", listener.local_addr()?);
#[spawn(parallel)]
for await? stream in listener.incoming() {
println!("Accepting from: {}", stream.peer_addr()?);
let (reader, writer) = &mut stream.split();
await? reader.copy_into(writer);
}
}
This example is 13 lines, but sets up a cross-platform runtime consisting of an executor, reactor, and threadpool. It then creates a TCP listener on port 8080 that handles incoming connections in parallel on all cores, and asynchronously streams all incoming bytes back out to the sender. That’s a lot!
In typical Rust fashion, we want to make it so choosing between performance and ergonomics doesn’t have to be a choice you ever have to make. And we also want to provide a high degree of flexibility by not coupling these APIs to any particular async runtime, but rather letting you easily swap them out.
Runtime today
While the example above isn’t quite possible today, we’re not far from getting there either. Runtime provides most of what you’re seeing there, modulo some language features:
#![feature(async_await, await_macro, futures_api)]
use futures::prelude::*;
use runtime::net::TcpListener;
#[runtime::main]
async fn main() -> std::io::Result<()> {
let mut listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Listening on {}", listener.local_addr()?);
let mut incoming = listener.incoming();
while let Some(stream) = await!(incoming.next()) {
runtime::spawn(async move {
let stream = stream?;
println!("Accepting from: {}", stream.peer_addr()?);
let (reader, writer) = &mut stream.split();
await!(reader.copy_into(writer))?;
Ok::<(), std::io::Error>(())
});
}
Ok(())
}
Attributes
Runtime introduces 3 attributes to enable the use of await anywhere, and swap between different runtimes. Each Runtime is bound locally to the initializing thread. This enables the testing of different runtimes during testing or benchmarking.
#[runtime::main]
async fn main() {}
#[runtime::test]
async fn my_test() {}
#[runtime::bench]
async fn my_bench() {}
Runtimes
Switching runtimes is a one-line change:
/// Use the default Native Runtime
#[runtime::main]
async fn main() {}
/// Use the Tokio Runtime
#[runtime::main(runtime_tokio::Tokio)]
async fn main() {}
The following backing runtimes are available today:
- Runtime Native (default) provides a thread pool, bindings to the OS, and a concurrent scheduler.
- Runtime Tokio binds to Tokio’s runtime, which provides a thread pool, bindings to the OS, and a work-stealing scheduler.
We hope to eventually be able to provide Runtimes for all platforms (Fuschia, WASM, Android, Redox, et al.) and enable experimentation on building the underlying bindings. Hardware, operating systems, and research continuously improve.
Runtime provides a foundation for Async Rust that anticipates change, and allows us embrace progress and stability without needing to compromise.
Bindings
Currently Runtime comes with async bindings to net::tcp
and net::udp
. We expect to add
UDS and Timer support in the near future.
Async fs bindings are probably a bit further out because currently we don’t know of any runtimes that provide a coherent story, and there are still some core traits missing from Futures to round out the ergonomics.
But all in all we think this provides a great foundation to start working on asynchronous networked applications that don’t want to lock themselves into a single runtime.
Conclusion
We’ve introduced Runtime, a platform-agnostic library that intends to make Async Rust both flexible and easy. It provides network bindings to multiple runtimes today, and provides a stable foundation for writing networked applications that don’t want to be bound to a single vendor.
Runtime is available on GitHub as rustasync/runtime, and on crates.io as runtime. Happy hacking!