1
votes

I'm learning Rust's async/await feature, and stuck with the following task. I would like to:

  1. Create an async closure (or better to say async block) at runtime;
  2. Pass created closure to constructor of some struct and store it;
  3. Execute created closure later.

Looking through similar questions I wrote the following code:

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

struct Services {
    s1: Box<dyn FnOnce(&mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()>>>>,
}

impl Services {
    fn new(f: Box<dyn FnOnce(&mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()>>>>) -> Self {
        Services { s1: f }
    }
}

enum NumberOperation {
    AddOne,
    MinusOne
}

#[tokio::main]
async fn main() {
    let mut input = vec![1,2,3];
    let op = NumberOperation::AddOne;
    
    let s = Services::new(Box::new(|numbers: &mut Vec<usize>| Box::pin(async move {
        for n in numbers {
            match op {
                NumberOperation::AddOne => *n = *n + 1,
                NumberOperation::MinusOne => *n = *n - 1,
            };
        }
    })));

    (s.s1)(&mut input).await;
    assert_eq!(input, vec![2,3,4]);
}

But above code won't compile, because of invalid lifetimes.

  1. How to specify lifetimes to make above example compile (so Rust will know that async closure should live as long as input). As I understand in provided example Rust requires closure to have static lifetime?

  2. Also it's not clear why do we have to use Pin<Box> as return type?

  3. Is it possible somehow to refactor code and eliminate: Box::new(|arg: T| Box::pin(async move {}))? Maybe there is some crate?

Thanks

Update

There is similar question How can I store an async function in a struct and call it from a struct instance? . Although that's a similar question and actually my example is based on one of the answers from that question. Second answer contains information about closures created at runtime, but seems it works only when I pass an owned variable, but in my example I would like to pass to closure created at runtime mutable reference, not owned variable.

2
@IbraheemAhmed not really, although that's a similar question and actually my example is based on one of the answers from that question. Second answer contains information about closures created at runtime, but seems it works only when I pass an owned variable, but in my example I would like to pass to closure created at runtime mutable reference, not owned variable.Inf

2 Answers

2
votes
  1. How to specify lifetimes to make above example compile (so Rust will know that async closure should live as long as input). As I understand in provided example Rust requires closure to have static lifetime?

    Let's take a closer look at what happens when you invoke the closure:

        (s.s1)(&mut input).await;
    //  ^^^^^^^^^^^^^^^^^^
    //  closure invocation
    

    The closure immediately returns a future. You could assign that future to a variable and hold on to it until later:

        let future = (s.s1)(&mut input);
    
        // do some other stuff
    
        future.await;
    

    The problem is, because the future is boxed, it could be held around for the rest of the program's life without ever being driven to completion; that is, it could have 'static lifetime. And input must obviously remain borrowed until the future resolves: else imagine, for example, what would happen if "some other stuff" above involved modifying, moving or even dropping input—consider what would then happen when the future is run?

    One solution would be to pass ownership of the Vec into the closure and then return it again from the future:

        let s = Services::new(Box::new(move |mut numbers| Box::pin(async move {
            for n in &mut numbers {
                match op {
                    NumberOperation::AddOne => *n = *n + 1,
                    NumberOperation::MinusOne => *n = *n - 1,
                };
            }
            numbers
        })));
    
        let output = (s.s1)(input).await;
        assert_eq!(output, vec![2,3,4]);
    

    See it on the playground.

    @kmdreko's answer shows how you can instead actually tie the lifetime of the borrow to that of the returned future.

  2. Also it's not clear why do we have to use Pin as return type?

    Let's look at a stupidly simple async block:

    async {
        let mut x = 123;
        let r = &mut x;
        some_async_fn().await;
        *r += 1;
        x
    }
    

    Notice that execution may pause at the await. When that happens, the incumbent values of x and r must be stored temporarily (in the Future object: it's just a struct, in this case with fields for x and r). But r is a reference to another field in the same struct! If the future were then moved from its current location to somewhere else in memory, r would still refer to the old location of x and not the new one. Undefined Behaviour. Bad bad bad.

    You may have observed that the future can also hold references to things that are stored elsewhere, such as the &mut input in @kmdreko's answer; because they are borrowed, those also cannot be moved for the duration of the borrow. So why can't the immovability of the future similarly be enforced by r's borrowing of x, without pinning? Well, the future's lifetime would then depend on its content—and such circularities are impossible in Rust.

    This, generally, is the problem with self-referential data structures. Rust's solution is to prevent them from being moved: that is, to "pin" them.

  3. Is it possible somehow to refactor code and eliminate: Box::new(|arg: T| Box::pin(async move {}))? Maybe there is some crate?

    In your specific example, the closure and future can reside on the stack and you can simply get rid of all the boxing and pinning (the borrow-checker can ensure stack items don’t move without explicit pinning). However, if you want to return the Services from a function, you'll run into difficulties stating its type parameters: impl Trait would normally be your go-to solution for this type of problem, but it's limited and does not (currently) extend to associated types, such as that of the returned future.

    There are work-arounds, but using boxed trait objects is often the most practical solution—albeit it introduces heap allocations and an additional layer of indirection with commensurate runtime cost. Such trait objects are however unavoidable where a single instance of your Services structure may hold different closures in s1 over the course of its life, where you're returning them from trait methods (which currently can’t use impl Trait), or where you're interfacing with a library that does not provide any alternative.

3
votes

If you want your example to work as is, the missing component is communicating to the compiler what lifetime associations are allowed. Trait objects like dyn Future<...> are constrained to be 'static by default, which means it cannot have references to non-static objects. This is a problem because your closure returns a Future that needs to keep a reference to numbers in order to work.

The direct fix is to annotate that the dyn FnOnce can return a Future that can be bound to the life of the first parameter. This requires a higher-ranked trait bound and the syntax looks like for<'a>:

struct Services {
    s1: Box<dyn for<'a> FnOnce(&'a mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()> + 'a>>>,
}

impl Services {
    fn new(f: Box<dyn for<'a> FnOnce(&'a mut Vec<usize>) -> Pin<Box<dyn Future<Output = ()> + 'a>>>) -> Self {
        Services { s1: f }
    }
}

The rest of your code now compiles without modification, check it out on the playground.