Storing async functions that has a reference argument and captures

I want to store an async fn in a struct, like this:

type Return = Pin<Box<dyn Future<Output = Value>>>;
struct Action {
    func: fn(&Value) -> Return,
}

struct StoreValues {
    value: Value,
    result: Value,
}

async fn do_action(action: Action, store: StoreValues) {
    ...
    // let's say `other_stuff()` needs a mutable reference to `store`, but is guaranteed to not touch `store.value`.
    tokio::join!(action.func(&store.value), other_stuff());
    ...
}

Everything seems fine, until I try to construct an Action:

async fn nothing(arg: &Value) -> Value {
    arg.clone()
}

fn nothing_wrapper(arg: &Value) -> Return {
    Box::pin(nothing(arg))
}

The compiler would say returning this value would require that '1 outlives 'static, where '1 is the lifetime of the refenrence arg: &Value in the argument. It also hints me to add lifetime annotations: type Return<'a> = Pin<Box<dyn Future<Output = Value> + 'a>>. If I do this, the compiler would now say that a mutable reference in other_stuff() and an immutable reference in action.func(&store.value) exists at the same time and that is not allowed.

How can I fix the issue here?

That's correct. If you don’t do that, you can only pass functions whose returned futures don't capture anything.

You cannot do this, whether async is involved or not. Creating a mutable reference to store intrinsically “touches” all of store. You must express other_stuff in a way which doesn't involve &mut store, but only specific fields of store that are not store.value.

This might involve redesigning your data structures (e.g. if StoreValues has more than one field, moving the fields that aren't value into a sub-struct which is easier to pass to other_stuff), but since you haven't shown the actual borrows you need to use, I can't advise further than that generality.

2 Likes

Hello, thank you for your response!

I want to achieve the following design: the action actually will take an async Sender, and through that sender I can send instructions to the outer function do_action to modify the store.result accordingly. So I seperate the message handling logic out to StoreValues::handle_message(&mut self, mes: Message), and that needs a mutable borrow of self.

Are you suggesting that I can define a sub-struct like struct Handled { result: Value }, changeStoreValues to value: Value, result: Handled and implement handle_message on Handled instead?

(Sorry, I'm currently not available to test this out.)

Yes, that is an example of the pattern I mean. But now that you have clarified — note that not every function has to be a method. If the only thing handle_message does is modify the result Value, then it can, and I would say should, be:

fn handle_message(value: &mut Value, mes: Message)

with no new struct involved.

I'm able to test it out, and it works fine. Thank you for your respond!

If you don’t do that, you can only pass functions whose returned futures don't capture anything.

May I ask for some more explanation that why adding lifetime specifiers can do this?

Box<T> implicitly means Box<T + 'static> where 'static is a special annotation that forbids use of any temporary references. Lifetime bounds in generic types don't affect self-contained/owning types at all, but for types containing any loans it creates a requirement that the loan has to last forever.

Box<T + 'a> changes this default from 'static to some lifetime 'a that, based on the context, can be something shorter-lived.

2 Likes

If there was no constraint on borrowing, then you could obtain the future of type Return, stash it in some data structure somewhere, then call it later after the things it borrows have gone away, resulting in a use-after-free bug.

Return needs a lifetime for the same reasons references need lifetimes, because it may contain references.

1 Like

Sorry but I have some further issues. This structure works perfectly fine, until Action::func needs to capture some variable.

This is a simplified version of the problem (this code doesn't capture anything but imagine it captures something):

use std::pin::Pin;
use std::future::Future;

struct Action {
    action: Box<dyn FnMut(usize) -> Pin<Box<dyn Future<Output = usize>>>>,
}

fn main() {
    let outer = |x| Box::pin((|x| async { x })(x));
    let action = Action{ action: Box::new(outer) };
}

Here, the |x| async { x } would not return a future, but rather an {async block} according to the compiler:

error[E0271]: expected `{closure@main.rs:9:17}` to be a closure that returns `Pin<Box<dyn Future<Output = usize>>>`, but it returns `Pin<Box<{async block@src/main.rs:9:35: 9:40}>>`
  --> src/main.rs:10:34
   |
9  |     let outer = |x| Box::pin((|x| async { x })(x));
   |                                   ----- the found `async` block
10 |     let action = Action{ action: Box::new(outer) };
   |                                  ^^^^^^^^^^^^^^^ expected `Pin<Box<dyn Future<Output = ...>>>`, found `Pin<Box<...>>`
   |
   = note: expected struct `Pin<Box<(dyn Future<Output = usize> + 'static)>>`
              found struct `Pin<Box<{async block@src/main.rs:9:35: 9:40}>>`
   = note: required for the cast from `Box<{closure@src/main.rs:9:17: 9:20}>` to `Box<(dyn FnMut(usize) -> Pin<Box<(dyn Future<Output = usize> + 'static)>> + 'static)>`

Is this possible to do?

Every {async block} type implements the Future trait. The problem you are having is that you are failing to coerce it to dyn Future. The subtlety here is that you have to provide an opportunity to do so at the right moment. Here's a program which fails for the same reason that involves no async blocks or even Pin:

let x = || Box::new("hello");
let y: Box<dyn Fn() -> Box<dyn std::fmt::Display>> = Box::new(x);
error[E0271]: expected `{closure@main.rs:2:13}` to be a closure that returns `Box<dyn Display>`, but it returns `Box<&str>`
 --> src/main.rs:3:58
  |
3 |     let y: Box<dyn Fn() -> Box<dyn std::fmt::Display>> = Box::new(x);
  |                                                          ^^^^^^^^^^^ expected `Box<dyn Display>`, found `Box<&str>`
  |
  = note: expected struct `Box<dyn std::fmt::Display>`
             found struct `Box<&str>`

The trick is, a type can be coerced to a dyn type only if it is wrapped in exactly one pointer type (such as Box<T>, &T, or Pin<Box<T>>)[1] at the moment when the compiler notices that coercion must occur to avoid a type error. The coercion will not work if that type to be coerced is wrapped in anything else — including a closure. The problem in my small example is that the return type of the closure has already been settled as Box<&'static str> by the time we try to coerce the closure itself. This could be fixed by adding a return type to the closure, to ensure the code inside the closure includes the necessary coercion:

let x = || -> Box<dyn std::fmt::Display> { Box::new("hello") };

But, a usually-more-elegant fix is to ensure the entire closure is assigned (or returned) to a location with an explicit type:

let y: Box<dyn Fn() -> Box<dyn std::fmt::Display>> =
    Box::new(|| Box::new("hello"));

This works because the Rust compiler has a special rule to help with the thorny problem of defining the signature of a closure: if the closure expression is written inside an expression that gives it a signature (like let x: SomeType = Box::new(|| ...) or some_function_with_a_fn_bound(|| ...)) then that signature will be used instead of inferring from the type of the values inside the closure’s code. So, as a general habit, you should avoid writing let x = || ... whenever possible because it is likely to fail. Whenever possible, keep the || nested inside the part of the code that gives it a type. In your actual case, that's:

let action = Action {
    action: Box::new(|x| Box::pin(async { x })),
};

Note that passing a closure to Box::new doesn't count as giving it a type, because Box::new is generic over arbitrary T, not T: Fn(...), so it doesn't constrain the closure signature; instead, Action is doing that. For boxed closures, the whole Box::new(|| ...) or Box::pin(|| ...) is the expression that you need to give a type to.

I've also removed the IIFE you had in there; it would interfere with getting the right outcomes too, because IIFEs don't have anything to set their closure signature so they can be troublesome too.

Finally, I think I should prove that this still works with a borrow in the future, since that was your original problem:

use std::future::Future;
use std::pin::Pin;

#[derive(Clone)]
struct Value;

type Return<'a> = Pin<Box<dyn Future<Output = Value> + 'a>>;

struct Action {
    action: Box<dyn for<'a> FnMut(&'a Value) -> Return<'a>>,
}

fn main() {
    let action = Action {
        action: Box::new(|val| Box::pin(async { val.clone() })),
    };
}

(You will probably want to make both the closure and the async block move, but that's orthogonal to everything else we discussed. It matters if they are going to capture anything from outside.)


  1. More precisely, it has to be wrapped in something that implements CoerceUnsized, but that’s pointers and a few exceptions that aren’t relevant to user-defined data structures. ↩︎

2 Likes