dtolnay / async-trait

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

Don't include ignored variables in async block. #263

Closed bbaldino closed 6 months ago

bbaldino commented 6 months ago

I was recently playing with a dummy implementation of an async trait that, by design, doesn't do anything in its methods and ran into an issue with a lifetime complaint. You can see a playground example here.

For the Dummy impl case, async_trait ends up generating:

fn insert<'life0, 'async_trait>(&'life0 mut self, _key: K, _value: V)
        ->
            ::core::pin::Pin<Box<dyn ::core::future::Future<Output = ()> +
            ::core::marker::Send + 'async_trait>> where 'life0: 'async_trait,
        Self: 'async_trait {
        Box::pin(

            async move
                {
                let mut __self = self;
                let _key = _key;
                let _value = _value;
                let () = {};
            })
    } 

which then puts requirements on the lifetimes of K and V which causes the error. It'd be nice if variables that are ignored (have a leading underscore) weren't declared in the async block. Or maybe some other async_trait helper that would allow a user to suppress the generated block when it wasn't needed?

dtolnay commented 6 months ago

This is working correctly. This is how async fn always works. When called, async fn does nothing but construct a Future to represent the state of the evaluation of the async code. Nothing in the body of the function begins to run until something begins polling the future.

You are suggesting to eagerly run Drop impls for unused arguments before the future is polled, which would be incorrect behavior.

For example consider what would happen if some function argument holds a mutex guard. According to the semantics of async fn that mutex is supposed to be held for the entire function body or until dropped during some poll call.

bbaldino commented 6 months ago

Hi @dtolnay , thanks for the response. Given that the following seems to compile fine:

trait Bar<K, V> {
    async fn insert(&mut self, key: K, value: V);
}

struct Dummy;
impl<K, V> Bar<K, V> for Dummy {
    async fn insert(&mut self, key: K, value: V) {

    }
}

(i.e. using built-in async trait support instead of async_trait), is there a way to achieve this behavior when using async_trait?

dtolnay commented 6 months ago

The difference between that and the earlier playground link is not about when Drop impls get run, it is about what bounds the trait requires all implementations to guarantee on the async function's desugared return type. The native async fn in trait returns Self::InsertFuture<'life0, K, V> which implements Future<Output = ()> and includes all the lifetime bounds from K and V. The dynamically dispatched one returns Pin<Box<dyn Future<Output = ()> + Send + 'async_trait>> which does not mention K and V; you can add the lifetime bounds by writing them in the signature of insert:

#[async_trait]
trait Bar<K, V> {
    async fn insert(&mut self, key: K, value: V)
    where
        K: 'async_trait,
        V: 'async_trait;
}

#[async_trait]
impl<K: Send, V: Send> Bar<K, V> for Dummy {
    async fn insert(&mut self, key: K, value: V)
    where
        K: 'async_trait,
        V: 'async_trait,
    {
    }
}
bbaldino commented 6 months ago

Got it. Thanks for the info.