Implementing From trait for boxed async closure

Hi, i'm trying to send boxed async closure between threads:

type BoxedAsyncFn<T> = Box<dyn ( for<'a> FnOnce(&'a T) -> BoxFuture<'a, ()>) + Send + 'static>;
struct AsyncFn<T: 'static>(BoxedAsyncFn<T>);

And i have problems with specifying proper bounds while implementing From trait.

What works:

//Thread 1
let test_3: BoxedAsyncFn<i32> = Box::new(|arg: &i32| {
    Box::pin(async move {
        println!("test - {}", arg)
    })
});

//Thread 2
let arg = 3;
test_3(&arg).await

But explicitly creating Boxes and pins everywhere is not ergonomic and i would like to boxing and pinning be handled by from trait.

What i want to work:

//Thread 1
let test_3 = |arg: &i32| async move {
    println!("test - {}", arg)
};
let async_fn = AsyncFn::<i32>::from(test_3);

//Thread 2
let arg = 3;
BoxedAsyncFn::<i32>::from(async_fn)(&arg).await;

From trait should look like this:

impl<T, C, Fut> From<C> for AsyncFn<T>
where
    T: 'static + Sync,
    Fut: Future<Output=()> + Send,
    C: 'static + Send + FnOnce(&T) -> Fut
{
    fn from(value: C) -> Self {
        Self {
            0: Box::new(move |arg: &T| {
                Box::pin(value(arg))
            }),
        }
    }
}

But this results in: Playground example

error: lifetime may not live long enough
error[E0310]: the parameter type `Fut` may not live long enough

What i already tried:

  • Adding lifetime annotations to From trait - makes everything worse (after adding this i get error: "the type parameter Fut is not constrained by the impl trait" what doesn't make any sens, because i only added lifetime annotation)
  • Making function that explicitly marks both lifetimes (of argument to closure and future) the same (fn assume_same_lifetime in example) - doesn't help
  • Changing future lifetime to 'static - this enforces me to use 'static on argument to closure what i'm trying to avoid

I would appreciate any form of help!!

async move can't "undo" the fact that arg: &T is a temporary reference that may be destroyed immediately after the closure has been called, so your Future is not 'static.

In your other implementations you're passing through the 'a that keeps the &'a T safe.

Also, definition of generic functions returning futures is kinda broken in Rust, because the where clause that defines the Fn closure type is separate from where clause that defines the Fut: Future, but they have to specify the same lifetime.

There's a hack for it:

Thank you for your suggestion!

Unfortunately this doesn't solve problem with lifetimes, even after explicitly adding them, error still occurs and is somewhat ambiguous.

I had super trait item shadowing problems, because of the rust edition used in async_fn_traits crate.Therefore just copied trait and impl block to my code.

I changed boxed closure type to:

type BoxedAsyncFn<T> = Box<dyn for<'a> AsyncFnOnce1<&'a T, OutputFuture=BoxFuture<'a, ()>, AsyncOutput=(), Output=BoxFuture<'a, ()>>>;

Moved from From trait because of the blanket impl in core to fn new:

impl<T: 'static + Sync> AsyncFn<T> {
    pub fn new<'a, C>(value: C) -> Self
    where
        C: AsyncFnOnce1<&'a T, AsyncOutput=()> + Send + 'static,
        <C as AsyncFnOnce1<&'a T>>::OutputFuture: Send
    {
        Self {
            0: Box::new(move |arg: &'a T| {
                Box::pin(value(arg))
            }),
        }
    }
}

Playground example

Does this really need macro?

Related previous thread. Something that bites a lot around this topic is that the compiler is horrible at inferring higher-ranked, borrowing closures -- closures that take any lifetime as an input and pass that on to their output. Sometimes you can use the "funnel" trick to annotate your way around it...

// Identity function for the type of closures we want
fn funnel<T, C: Closure>(c: C) -> C
where
    C: FnOnce(&T) -> Foo<'_>
{ c }

...but futures are unnameable so this doesn't always help, as explored in that other thread. In particular...

...this requires dealing with the unnameable futures before they are type erased. I don't know of anyway to fix Rust's poor inference so that this will work how you wish:

let async_fn = AsyncFn::<i32>::new(
    |arg: &i32| async move { println!("test - {}", arg) }
);

However, your new method itself can work with the funneling approach, since the desired return type is nameable due to type erasure at that point.

fn new_funnel<T, C>(c: C) -> C
where
    C: 'static + Send + for<'a> FnOnce(&'a T) -> BoxFuture<'a, ()>,
{ c }

// N.b. I also adjusted the bounds from your latest playground as
// you had bounds for a single lifetime, but you need higher-ranked
// bounds for `BoxedAsyncFn<T>`
impl<T: 'static + Sync> AsyncFn<T> {
    pub fn new<C>(value: C) -> Self
    where
        C: 'static + Send + for<'a> AsyncFnOnce1<&'a T, AsyncOutput = (), OutputFuture: Send>,
    {
        Self {
            0: Box::new(new_funnel(move |arg: &T| Box::pin(value(arg)) ))
        }
    }
}

That being said, I don't think it really helps the overall design -- you've just pushed the inference problems onto callers of new. And if their only fix is type erasure too, you'll probably end up double-boxing the returned future type.

So I suspect it's still best for now that the pinning and boxing happens in the original closure.

Thank you for the link! There is already open issue in rust-lang github: #58052 that would probably fix this. Additionally i stumbled upon post that describes PR that solves this in nightly.

This is the first problem with a coroutine-closure, which returns a coroutine that is allowed to borrow from the closure's upvars, since there's no way to link the input lifetime (of the closure borrow) with the output lifetimes (in the coroutine's upvars).

Which explains why it can't work on stable, but with help of feature async_closure everything just works.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.