Skip to content

silverlyra/bitte

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bitte

Crate Documentation

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 of async fn in public traits is discouraged as auto trait bounds cannot be specified
ℹ️ help: you can alternatively desugar to a normal fn that returns impl 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)].

Installation

Add this to your Cargo.toml:

[dependencies]
bitte = "0.0.1"

For automatic Send + Sync bounds:

[dependencies]
bitte = { version = "0.0.1", features = ["threads"] }

Usage

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;
}

Implementation with #[bitte]

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()
    }
}

Desugaring individual methods

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;
}

Applying thread safety trait bounds

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;
}

Explicit Send and/or 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;
}

Feature Flags

  • threads: Add Send and/or Sync bounds to desugared trait and impl fns

Implementation

There are two ways to implement traits transformed by bitte:

With #[bitte] on impl block (recommended)

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
    }
}

Manual implementation

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
    }
}

Comparison with async-trait

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.

About

Automatically desugar and set Send/Sync bounds on async Rust traits

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages