Surprising behavior with #[async_trait] + impl Trait in argument position

I observed the following interesting behavior when combining the commonly used async-trait crate with anonymous type parameters:

#[async_trait::async_trait]
trait Multicall {
    fn messages(&self) -> &[String];
    // This doesn't work:
    // async fn foo(&self, mut callback: impl FnMut(&str) + Send)
    // But we can do:
    async fn foo<C: FnMut(&str) + Send>(&self, mut callback: C)
    {
        for s in self.messages() {
            callback(s);
        }
    }
}

struct S {
    messages: Vec<String>,
}
impl Multicall for S {
    fn messages(&self) -> &[String] { &self.messages }
}

#[tokio::main]
async fn main() {
    let s = S { messages: vec![
        "Hello!".to_string(),
        "World!".to_string()
    ] };
    s.foo(|x| println!("Say: {}", x)).await;
}

Note there aren't really any lifetime bounds necessary for the reference passed to the closure. But apparently the async_trait macro will do some "magic" if we use the impl keyword, resulting in the following error (if we uncomment the signature with impl):

error[E0495]: cannot infer an appropriate lifetime due to conflicting requirements
  --> src/main.rs:5:19
   |
5  |     async fn foo(&self, mut callback: impl FnMut(&str) + Send)
   |                   ^^^^
   |
note: first, the lifetime cannot outlive the lifetime `'life0` as defined on the method body at 5:14...
  --> src/main.rs:5:14
   |
5  |     async fn foo(&self, mut callback: impl FnMut(&str) + Send)
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that the expression is assignable
  --> src/main.rs:5:19
   |
5  |     async fn foo(&self, mut callback: impl FnMut(&str) + Send)
   |                   ^^^^
   = note: expected `&Self`
              found `&'life0 Self`
note: but, the lifetime must be valid for the lifetime `'life1` as defined on the method body at 5:14...
  --> src/main.rs:5:14
   |
5  |     async fn foo(&self, mut callback: impl FnMut(&str) + Send)
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src/main.rs:10:22
   |
10 |             callback(s);
   |                      ^

I wonder if that is intended, and whether I can/should rely on other the variant using <C: FnMut(&str) + Send> to keep working in future releases of async-trait?

What does the code look like if you do cargo expand?

That’s definitely async-trait misbehaving. The expansion is

trait Multicall {
    fn messages(&self) -> &[String];
    #[must_use]
    #[allow(
        clippy::let_unit_value,
        clippy::type_complexity,
        clippy::type_repetition_in_bounds,
        clippy::used_underscore_binding
    )]
    fn foo<'life0, 'life1, 'async_trait>(
        &'life0 self,
        callback: impl FnMut(&'life1 str) + Send,
    ) -> ::core::pin::Pin<
        Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
    >
    where
        'life0: 'async_trait,
        'life1: 'async_trait,
        Self: ::core::marker::Sync + 'async_trait,
    {
        Box::pin(async move {
            let __self = self;
            let mut callback = callback;
            let _: () = {
                for s in __self.messages() {
                    callback(s);
                }
            };
        })
    }
}

which is not what you want.

Edit: Fixing the fact that async trait introduces a lifetime argument for FnMut(&str) – even though that’s supposed to mean for<'a> FnMut(&'a str) – by explicitly writing the desugared bound

async fn foo(&self, mut callback: impl for<'a> FnMut(&'a str) + Send)

doesn’t help either; this produces

trait Multicall {
    fn messages(&self) -> &[String];
    #[must_use]
    #[allow(
        clippy::let_unit_value,
        clippy::type_complexity,
        clippy::type_repetition_in_bounds,
        clippy::used_underscore_binding
    )]
    fn foo<'life0, 'async_trait>(
        &'life0 self,
        callback: impl for<'a> FnMut(&'a str) + Send,
    ) -> ::core::pin::Pin<
        Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
    >
    where
        'life0: 'async_trait,
        Self: ::core::marker::Sync + 'async_trait,
    {
        Box::pin(async move {
            let __self = self;
            let mut callback = callback;
            let _: () = {
                for s in __self.messages() {
                    callback(s);
                }
            };
        })
    }
}

compared to the version with the explicit type argument that does work, which expands to

trait Multicall {
    fn messages(&self) -> &[String];
    #[must_use]
    #[allow(
        clippy::let_unit_value,
        clippy::type_complexity,
        clippy::type_repetition_in_bounds,
        clippy::used_underscore_binding
    )]
    fn foo<'life0, 'async_trait, C: FnMut(&str) + Send>(
        &'life0 self,
        callback: C,
    ) -> ::core::pin::Pin<
        Box<dyn ::core::future::Future<Output = ()> + ::core::marker::Send + 'async_trait>,
    >
    where
        C: 'async_trait,
        'life0: 'async_trait,
        Self: ::core::marker::Sync + 'async_trait,
    {
        Box::pin(async move {
            let __self = self;
            let mut callback = callback;
            let _: () = {
                for s in __self.messages() {
                    callback(s);
                }
            };
        })
    }
}

The crucial bit missing in the former expansion is the equivalent of the C: 'async_trait bound.


A proper expansion should probably do something like callback: impl 'async_trait + for<'a> FnMut(&'a str) + Send for the code with explicit for<'a> … bound in the input; and it should detect Fn* traits and not put explicit lifetime arguments for elided lifetimes in those bounds, translating the original code to just callback: 'async_trait + FnMut(&str) + Send.

1 Like

Thanks for sharing the expanded code. For some reason, after installing cargo-expand, I don't get well-formatted code (even though rustfmt is installed).

Nice try though :+1:

I opened a bug ticket.

1 Like

Do you have rustfmt installed for nightly, too? (rustup +nightly component add rustfmt.) I’m not 100% certain which one it uses (stable or nightly rustfmt); the expanding itself relies on nightly Rust.

1 Like

I think so?

% rustup +nightly component add rustfmt
info: component 'rustfmt' for target 'x86_64-unknown-freebsd' is up to date

:neutral_face:

Hmm, okay. In the meantime, at least you can use ‘Tools’ → ‘Expand macros’ on play.rust-lang.org.

2 Likes