Starting with Rust 1.75, it’s possible to use async fn in a trait:
pub trait UserDatabase {
async fn get_user(&self, id: u64) -> Result<User, …>;
}But if you use an async function in a pub trait, Rust issues a warning:
⚠️ warning: use ofasync fnin public traits is discouraged as auto trait bounds cannot be specified
ℹ️ help: you can alternatively desugar to a normalfnthat returnsimpl Future
This warning means that the async fn won’t be usable in a multithreaded program. For example, if Tokio’s multithreaded runtime is used, you cannot spawn a task to run the Future returned by get_user. Tokio may execute the future on other thread(s), so the future needs to be marked as sendable to other threads.
The compiler warning recommends “desugar”ing the async fn into something like:
pub trait UserDatabase {
fn get_user(&self, id: u64) -> impl Future<Output = Result<User, …>> + Send
where Self: Sync;
}By using the impl Future syntax instead, you’re able to apply the necessary Send and/or Sync bounds which allow your trait to be used in multi-threaded programs.
When you do this, any type that wants to implement your trait is also unable to use async fn, and must apply a similar desugaring:
impl UserDatabase for Pool<Postgres> {
fn get_user(&self, id: u64) -> impl Future<Output = Result<User, …>> + Send {
async move {
self.query(…).await
}
}
}Instead of doing this desugaring by hand, you can use bitte:
use bitte::bitte;
#[bitte]
pub trait UserDatabase {
async fn get_user(&self, id: u64) -> Result<User, …>;
}
#[bitte]
impl UserDatabase for Pool<Postgres> {
async fn get_user(&self, id: u64) -> Result<User, …> {
// ...
}
}By default, Bitte won’t add any Send or Sync bounds; you can switch that default by enabling the threads feature, or individually by writing #[bitte(Send, Sync)].
Add this to your Cargo.toml:
[dependencies]
bitte = "0.0.1"For automatic Send + Sync bounds:
[dependencies]
bitte = { version = "0.0.1", features = ["threads"] }Apply #[bitte] to transform all async methods in a trait:
use bitte::bitte;
#[bitte]
trait AsyncRepository {
async fn find_by_id(&self, id: u64) -> Option<String>;
async fn save(&mut self, data: String) -> Result<u64, String>;
// Non-async methods remain unchanged
fn cache_size(&self) -> usize;
}This transforms to:
trait AsyncRepository {
fn find_by_id(&self, id: u64) -> impl std::future::Future<Output = Option<String>>;
fn save(&mut self, data: String) -> impl std::future::Future<Output = Result<u64, String>>;
fn cache_size(&self) -> usize;
}Apply #[bitte] to impl blocks to write natural async methods:
struct MyRepo {
data: HashMap<u64, String>,
}
#[bitte]
impl AsyncRepository for MyRepo {
async fn find_by_id(&self, id: u64) -> Option<String> {
self.data.get(&id).cloned()
}
async fn save(&mut self, data: String) -> Result<u64, String> {
let id = rand::random();
self.data.insert(id, data);
Ok(id)
}
fn cache_size(&self) -> usize {
self.data.len()
}
}Apply #[bitte] to specific methods:
trait AsyncMixedTrait {
#[bitte]
async fn transformed(&self) -> String;
// This won’t be desugared to impl Trait
async fn still_async(&self) -> String;
}When the threads feature is enabled, Send + Sync bounds are automatically added:
#[bitte] // With threads feature: adds Send + Sync
trait AsyncService {
async fn process(&self, input: Vec<u8>) -> Vec<u8>;
}Transforms to:
trait AsyncService {
fn process(&self, input: Vec<u8>) -> impl std::future::Future<Output = Vec<u8>> + Send
where
Self: Sync;
}Override the default behavior:
#[bitte(Send, Sync)] // Explicitly enable
trait AlwaysThreadSafe {
async fn method(&self) -> u32;
}
#[bitte(?Send, ?Sync)] // Explicitly disable
trait LocalOnly {
async fn method(&self) -> u32;
}
// Mix and match per method
trait MixedBounds {
#[bitte(?Send)] // No Send bound
async fn local_only(&self) -> u32;
#[bitte(Send)] // Force Send bound
async fn thread_safe(&self) -> u32;
#[bitte(?Send, ?Sync)] // No bounds
async fn no_bounds(&self) -> u32;
}threads: AddSendand/orSyncbounds to desugared trait and implfns
There are two ways to implement traits transformed by bitte:
Apply #[bitte] to your impl block to write natural async methods:
#[bitte]
impl AsyncRepository for MyRepo {
async fn find_by_id(&self, id: u64) -> Option<String> {
// Natural async syntax
Some(format!("item-{}", id))
}
async fn save(&mut self, data: String) -> Result<u64, String> {
// Your async implementation
Ok(42)
}
fn cache_size(&self) -> usize {
0
}
}You can also manually implement the desugared methods:
impl AsyncRepository for MyRepo {
fn find_by_id(&self, id: u64) -> impl std::future::Future<Output = Option<String>> {
async move {
Some(format!("item-{}", id))
}
}
fn save(&mut self, data: String) -> impl std::future::Future<Output = Result<u64, String>> {
async move {
Ok(42)
}
}
fn cache_size(&self) -> usize {
0
}
}Prior to Rust 1.75, most code that needed async in traits used the async-trait crate.
use async_trait::async_trait;
#[async_trait]
pub trait UserDatabase {
async fn get_user(&self, id: u64) -> Result<User, …>;
}The async_trait macro also desugars async fns, but turns them into a Box<dyn Future> instead:
pub trait UserDatabase {
fn get_user<'async_trait>(
&'async_trait self,
id: u64,
) -> Pin<Box<dyn Future<Output = Result<User, …>> + Send + 'async_trait>>
where
Self: Sync + 'async_trait;
}You may still want to use async-trait – it’s not version 0.0.1, it’s already used in 8,000+ crates, its desugared traits are dyn-compatible, it lets you support older Rust versions, and it handles references in trait fn parameters.