Capturing Arc into async block inside closure

Hi, I'm pretty new to Rust.

Trying to build an event/callback mechanism on a closure with async block that captures an Arc. Usually such an async block in tokio::spawn allows for a cloned Arc quite easily. But once I put it into a closure that captures and clones the Arc, I am totally stuck. Can't yet reason about the issue.

Here is a simplified version of the code. The real code will have an event handler tokio task that listens to messages and calls a registered closure (passed across threads/tasks):

use std::sync::Arc;

use futures::Future;
use log::info;

async fn run_event_handler<Fut, Afn>(id: u32, handler: Afn)
where
    Fut: Future<Output=()>,
    Afn: Fn(String) -> Fut + Send + 'static
{
    let handler_id = format!("Handler-{id}");
    handler(handler_id).await;    
}

#[tokio::main(flavor = "multi_thread")]
async fn main() {
    env_logger::init();
    let context = Arc::new("Some Context".to_string());
    for i in 0..=10 {
        run_event_handler(i, {
            let context = context.clone();
            |handler_id| async move {
                info!("Context: {}, id: {}", &context, handler_id);
            }            
        });
    }
}

Compiler errors are as follows:

error[E0507]: cannot move out of `context`, a captured variable in an `Fn` closure
  --> src/bin/test_async_closure.rs:22:37
   |
21 |             let context = context.clone();
   |                 ------- captured outer variable
22 |             |handler_id| async move { info!("Context: {}, id: {}", &context, handler_id) }
   |             ------------            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-------^^^^^^^^^^^^^^^
   |             |                       |                               |
   |             |                       |                               variable moved due to use in generator
   |             |                       |                               move occurs because `context` has type `Arc<String>`, which does not implement the `Copy` trait
   |             |                       move out of `context` occurs here
   |             captured by this `Fn` closure

I tried many combinations including multiple clones, doing clones in different places. None bear fruits. A similar Arc can move into a tokio::spawn without issues. Thanks for any pointers!

Use double move.

let context = context.clone();
move |handler_id| async move {
    info!("Context: {}, id: {}", &context, handler_id);
} 

I was just writing this when it popped up :slight_smile:. Otherwise the async block is trying to move context while it is only borrowed by the closure.

If you only call the handler once, you should use FnOnce to consume the captured context:

async fn run_event_handler<Fut, Afn>(id: u32, handler: Afn)
where
    Fut: Future<Output=()>,
-   Afn: Fn(String) -> Fut + Send + 'static
+   Afn: FnOnce(String) -> Fut + Send + 'static

If you insist on calling handler multiple times, a trick can be this

async fn helper(context: Arc<String>, handler_id: String) {
    info!("Context: {}, id: {}", context, handler_id);
}

run_event_handler(i, {
    let context = context.clone();
    move |handler_id| helper(context.clone(), handler_id)
});
1 Like

Unfortunately, this does not solve the issue.

        run_event_handler(i, {
            let context = context.clone();
            move |handler_id| async move {
                info!("Context: {}, id: {}", &context, handler_id);
            }
        });

or

        run_event_handler(i, {
            let context = context.clone();
            move |handler_id| async move {
                info!("Context: {}, id: {}", &context.clone(), handler_id);
            }
        });

Will run into similar compiler messages.

This solution works. I was hoping for a more ergonomic solution with closures and environment capture where we do not necessarily have to hand over to another helper function, especially with async and Arc.

This is the code that finally works:

        run_event_handler(i, {
            let context = context.clone();
            move |handler_id| helper(context.clone(), handler_id)
        }).await;

Thank you very much!

Yeah, sorry. This handler is called for each arriving event.