Why does tokio::spawn complain about lifetimes even with .clone()?
Asked Answered
G

1

6

I am trying to compile the following seemingly straightforward code, but I'm getting an error:

use std::io::Error;

#[derive(Debug)]
struct NetworkConfig {
    bind: String,
    node_key_file: String,
}

async fn network_handler(network_config: &NetworkConfig) -> Result<(), Error> {
    Ok(())
}

async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
    let network_config_copy = network_config.clone();
    tokio::spawn(async move {
        network_handler(&network_config_copy).await
    }).await?
}
error: cannot infer an appropriate lifetime
  --> src/network.rs:43:18
   |
43 | async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
   |              ^^^^^^^^^^^^^^ ...but this borrow...
44 |     let network_config_copy = network_config.clone();
45 |     tokio::spawn(async move {
   |     ------------ this return type evaluates to the `'static` lifetime...
   |
note: ...can't outlive the lifetime `'_` as defined on the function body at 43:34
  --> src/network.rs:43:34
   |
43 | async fn run(network_config: &NetworkConfig) -> Result<(), Error> {
   |                              ^
help: you can add a constraint to the return type to make it last less than `'static` and match the lifetime `'_` as defined on the function body at 43:34
   |
45 |     tokio::spawn + '_(async move {
   |     ^^^^^^^^^^^^^^^^^

From the previous discussions and examples I have found on the subject, I understand that passing a reference to network_config to the spawned closure would cause lifetime problems since the separate thread may outlive network_config. This is why I am moving a clone of network_config to the spawned thread, but there still seems to be a lifetime ambiguity.

Is there any extra hint I could give the compiler so that it correctly gets the lifetimes? Or am I doing the whole thing wrong?

Grosso answered 15/3, 2020 at 1:6 Comment(1)
Also note that implementing the help hint (replacing tokio::spawn by tokio::spawn + '_) given by the compiler leads to a syntax error.Grosso
I
6

If you want clone the NetworkConfig you need to implement the Clone trait:

#[derive(Debug, Clone)]
struct NetworkConfig {
    bind: String,
    node_key_file: String,
}

Otherwise, for the rules of receiver method lookup you will end up with invoking a Clone on a reference through the following Clone implementer:

impl<'_, T> Clone for &'_ T

And the cloned reference will have a lifetime bound to scope of clone() invocation.

With derive(Clone) the run function compiles, but it works only when network_config argument has 'static lifetime, because of tokio::spawn lifetime requirement.

Probably this is not what you want. If this is the case pass NetworkConfig by value and eventually clone it in the caller context.

use std::io::Error;

#[derive(Debug, Clone)]
struct NetworkConfig {
    bind: String,
    node_key_file: String,
}

async fn network_handler(network_config: &NetworkConfig) -> Result<(), Error> {
    println!("using {:?}", network_config);
    Ok(())
}

async fn run(network_config: NetworkConfig) -> Result<(), Error> {
    tokio::spawn(async move { network_handler(&network_config).await }).await?
}

#[tokio::main]
async fn main() {
    let config = config::NetworkConfig {
        bind: "my_bind".to_owned(),
        node_key_file: "abc".to_owned(),
    };

    tokio::spawn(run(config.clone()));
}

You may ask why this works, indeed a reference is still passed to network_handler().

This is because network_config is moved inside the spawn async block and this makes gaining static lifetime for the inferred type of the async block.

Illlooking answered 15/3, 2020 at 13:49 Comment(1)
This answer deserves visibility: it not only solves the problem, but also give more insight into async/lifetime than most tutorials on the subject. Kudos to you @Illlooking !Grosso

© 2022 - 2024 — McMap. All rights reserved.