dtolnay / async-trait

Type erasure for async trait methods
Apache License 2.0
1.81k stars 84 forks source link

Feature Request: Decorator for methods for local macro-generated methods #112

Closed pwoolcoc closed 1 year ago

pwoolcoc commented 4 years ago

I have a trait in an existing crate of mine that I'm trying to convert to an async-trait, but am running into a problem with the order in which proc macros and macro_rules macros are run. My code currently generates some methods for the trait impl using macros, like such:

macro_rules! generate_foo {
    () => {
        fn foo() {}
    }
}

trait Foo {
    fn foo();
}

struct Bar;

impl Foo for Bar {
    generate_foo!();
}

When I change foo to async fn foo in the trait, I get an issue because the async_trait proc macro runs before the generate_foo macro is expanded, so I end up with generated code like this:

#![feature(prelude_import)]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std;
use async_trait::async_trait;

macro_rules! generate_foo { () => { async fn foo() { } } }

trait Foo {
    #[must_use]
    fn foo<'async_trait>()
    ->
        ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> +
                             ::core::marker::Send + 'async_trait>>;
}

struct Bar;

impl Foo for Bar {
    async fn foo() { }
}

I understand why this is happening and don't consider it a bug that async_trait needs to, or even could, fix, but I was wondering if there could be an attribute to put on methods that could perform the code generation? I.e., I could do something like the following:

macro_rules! generate_foo {
    () => {
        #[async_trait::async_method]
        async fn foo() { }
    }
}

struct Bar;

impl Foo for Bar {
    generate_foo!();
}

I would be willing to work on this feature, but I figured I would check and see if this was something useful to others.

taiki-e commented 4 years ago

When I change foo to async fn foo in the trait, I get an issue because the async_trait proc macro runs before the generate_foo macro is expanded, so I end up with generated code like this:

I think the correct way to do this is to generate the body of the method, not the method (async-trait modifies the method signature, but not the body):

macro_rules! generate_foo_body {
    () => {
        // ...
    };
}

#[async_trait]
trait Foo {
    async fn foo();
}

struct Bar;

#[async_trait]
impl Foo for Bar {
    async fn foo() {
        generate_foo_body!();
    }
}

playground (if the method has arguments)

taiki-e commented 4 years ago

async-trait modifies the method signature, but not the body

Ah, this isn't strictly correct because async-trait actually replaces self argument and Self keyword. However, self argument can be passed as an argument to the macro, and use of Self keyword is can be avoided.

pwoolcoc commented 4 years ago

@taiki-e thanks, I'll give that a try

rami3l commented 3 years ago

Another remark, for those facing with a similar problem: You can always try to write a function that returns a BoxFuture in the generate_foo part... BUT this workaround gets extremely complicated very fast.

So I think @pwoolcoc 's proposal holds its value, but when I look into the code, I realized that converting an async function depends on its Context, which is possibly not easy to figure out without triggering the macro on a trait or its implementation.

hjfreyer commented 3 years ago

Using the macro to generate just the body of the method isn't always satisfying, especially if the method signature itself is long and fiddly.

The most flexible fix would be some way to signal, "expand all the macros in here before applying #[async_trait]".

Does anyone know of a workaround?

dtolnay commented 1 year ago

I would prefer not to build anything for this into this crate, but it would be reasonable for somebody else to explore a more fully-featured macro in a different crate which handles this situation.