Why do Rust lifetimes matter when I move values into a spawned Tokio task?

766 views Asked by At

I'm trying to create a struct that will manage a Tokio task with one tokio::sync::mpsc::Sender that sends input to the task, one tokio::sync::mpsc::Receiver that receives output from the task, and a handle that I can join at the end.

use tokio::sync::mpsc;
use tokio::task::JoinHandle;

// A type that implements BlockFunctionality consumes instances of T and
// produces either Ok(Some(U)) if an output is ready, Ok(None) if an output
// is not ready, or an Err(_) if the operation fails
pub trait BlockFunctionality<T, U> {
    fn apply(&mut self, input: T) -> Result<Option<U>, &'static str>;
}

pub struct Block<T, U> {
    pub tx_input: mpsc::Sender<T>,
    pub rx_output: mpsc::Receiver<U>,
    pub handle: JoinHandle<Result<(), &'static str>>,
}

impl<T: Send, U: Send> Block<T, U> {
    pub fn from<B: BlockFunctionality<T, U> + Send>(b: B) -> Self {
        let (tx_input, mut rx_input) = mpsc::channel(10);
        let (mut tx_output, rx_output) = mpsc::channel(10);

        let handle: JoinHandle<Result<(), &'static str>> = tokio::spawn(async move {
            let mut owned_b = b;

            while let Some(t) = rx_input.recv().await {
                match owned_b.apply(t)? {
                    Some(u) => tx_output
                        .send(u)
                        .await
                        .map_err(|_| "Unable to send output")?,
                    None => (),
                }
            }

            Ok(())
        });

        Block {
            tx_input,
            rx_output,
            handle,
        }
    }
}

When I try to compile this, I get this error for B and a similar one for the other two type parameters:

   |
22 |     pub fn from<B: BlockFunctionality<T, U> + Send>(b:B) -> Self {
   |                 -- help: consider adding an explicit lifetime bound...: `B: 'static +`
...
27 |         let handle:JoinHandle<Result<(), &'static str>> = tokio::spawn(async move {
   |                                                           ^^^^^^^^^^^^ ...so that the type `impl std::future::Future` will meet its required lifetime bounds

I'm having a hard time understanding where the problem is with lifetimes. The way I understand it, lifetime problems usually come from references that don't live long enough, but I'm moving values, not using references. I move b, rx_input, and tx_output into the closure and I keep tx_input, rx_output, and handle in the calling scope. Does anyone know how to satisfy the compiler in this case?

1

There are 1 answers

1
Jeff Garrett On BEST ANSWER

Those values may be references or contain references. Reference types are valid types: B could be &'a str. Or B could be SomeType<'a>, a type with a lifetime parameter, that itself contains a &'a str.

To say that B: 'static means that all lifetime parameters of B outlive 'static (ref). For example, types which own their own data and thus have no lifetime parameters (e.g. String) satisfy this bound. But &'static str also satisfies the bound.

Because tokio::spawn creates something whose lifetime is not statically scoped, it requires a 'static argument.

So to satisfy the compiler, add the 'static bounds:

impl<T: 'static + Send, U: 'static + Send> Block<T, U> {
    pub fn from<B: 'static + BlockFunctionality<T, U> + Send>(b:B) -> Self {