blog.petitviolet.net

Rust async/await with Trait

2021-07-24

Rust

Rust supports asynchronous programming with async and await syntax since Rust 1.39.0.

https://blog.rust-lang.org/2019/11/07/Rust-1.39.0.html

Asynchronous Programming in Rust would be the best material to learn asynchronous programming in Rust.

This post describes not asynchronous programming but how to use async with Trait, which was not easy as I expected.

What the problem is

I expected this code works.

trait MyTrait {
    async fn my_function(&self, test: &str) -> Result<String, MyError>;
}

struct MyError {
    message: String,
}

However, the result of cargo build is the following:

$ RUSTFLAGS=-Awarnings cargo build
...(snip)...

error: expected one of `!`, `+`, `::`, `;`, `where`, or `{`, found `}`
 --> src/main.rs:3:1
  |
2 |     async fn my_function(&self, test: &str) -> Result<String, MyError>
  |              ----------- while parsing this `fn`                      - expected one of `!`, `+`, `::`, `;`, `where`, or `{`
3 | }
  | ^ unexpected token

error[E0706]: functions in traits cannot be declared `async`
 --> src/main.rs:2:5
  |
2 |     async fn my_function(&self, test: &str) -> Result<String, MyError>
  |     -----^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |     |
  |     `async` because of this
  |
  = note: `async` trait functions are not currently supported
  = note: consider using the `async-trait` crate: https://crates.io/crates/async-trait

...

Obviously Rust doesn’t allow using async within trait definitions. Instead, as the error message says, we can use async-trait crate to add async fn support in traits. This crate is so easy to use that I couldn’t be aware what’s happening behind the scene.

How to use async fn in traits without async-trait

In short,

trait MyTrait {
    fn my_function(&self, test: &str) -> Pin<Box<dyn Future<Output = Result<String, MyError>> + Send>>;
}

Then,

$ cat ./src/main.rs
use std::{future::Future, pin::Pin};

trait MyTrait {
    fn my_function(&self, test: &str) -> Pin<Box<dyn Future<Output = Result<String, MyError>> + Send>>;
}
struct MyError {
    message: String,
}

$ RUSTFLAGS=-Awarnings cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.13s

Yes!

More Realistic Example

A typical use-case of asynchronous programming is sending HTTP requests. In this example, use seanmonstar/reqwest, which is very handy for dealing with asynchronous HTTP requests on Tokio.

This code snippet has only codes relate to async/await, so if you’re interested in full codes, you can see it at petitviolet/rustranslator.

use std::{future::Future, pin::Pin};


#[derive(Debug)]
struct TranslateError {
    message: String,
}
impl TranslateError {
    pub fn new(msg: String) -> Self {
        Self { message: msg }
    }
}

type TranslateResult = Result<String, TranslateError>;
trait Translator {
    fn translate(
        &self,
        text: &str,
        from: &Lang,
        to: &Lang,
    ) -> Pin<Box<dyn Future<Output = TranslateResult> + Send>>;
}

struct Google { /* */ }
struct GoogleRequestBody { /* */ }
struct GoogleResponseBody { /* */ }

impl Translator for Google {
    // equivalent to `async fn translate(&self, text: &str, from: &Lang, to: &Lang) -> TranslateResult {`
    fn translate(
        &self,
        text: &str,
        from: &Lang,
        to: &Lang,
    ) -> Pin<Box<dyn Future<Output = TranslateResult> + Send>> {
        let body = GoogleRequestBody::new(text, from, to);

        let client = reqwest::Client::new();
        let req = client
            .post(self.translate_text_url())
            .header("Authorization", format!("Bearer {}", &self.access_token))
            .json(&body)
            .send();
        let res = async move {
            req.await
                .unwrap()
                .json()
                .await
                .map(|res: GoogleResponseBody| res.text())
                .map_err(|err| TranslateError::new(format!("error! {:#?}", err)))
        };
        Box::pin(res)
    }
}

As you see, the signature of a function of a trait looks weird. The output type is Pin<Box<dyn Future<Output = T> + Send>>. This method signature is explained in official compiler error index page as E0706.

https://doc.rust-lang.org/error-index.html#E0706