Search code examples
rustfuturelifetimerust-futures

How to call an async function with a non-static reference in `poll` in rust?


I've encountered an obstacle when trying to implement Future.

I need to poll another async function in my poll function which only takes in &str. It looks something like this:

async fn make_query(query: &str) -> i32 {
    // Magic happens
    5
}

As this is a function from another library I cannot simply change its signature to take ownership of the values.

Now my struct Executor, for which I want to implement Future, holds a String which should be referenced when calling make_query:

struct Executor {
    query: String,
    future: Mutex<Option<Pin<Box<dyn Future<Output = i32>>>>>
}

impl Future for Executor {
    type Output = i32;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let mut lock = self.future.lock().unwrap();

        if let None = *lock {
            // This is where the error occurs
            *lock = Some(Box::pin(make_query(&self.query)))
        }

        let future = lock.as_mut().unwrap();
        pin!(future).poll(cx)
    }
}

Now, the purpose of this is that I can .await my Executor to automatically make the query:

#[tokio::main]
async fn main() {
    let result = Executor {
        query: "foo".to_string(),
        future: Mutex::new(None)
    }.await;

    println!("Result is {result}");
}

Now, apparently the Mutex requires that every reference passed to it is 'static? I got these error messages:

error: lifetime may not live long enough
  --> src/main.rs:23:26
   |
16 |     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
   |                       - let's call the lifetime of this reference `'1`
...
23 |             *lock = Some(Box::pin(make_query(&self.query)))
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cast requires that `'1` must outlive `'static`

error[E0597]: `self` does not live long enough
  --> src/main.rs:23:47
   |
16 |     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
   |             ---- binding `self` declared here
...
23 |             *lock = Some(Box::pin(make_query(&self.query)))
   |                          ---------------------^^^^--------
   |                          |                    |
   |                          |                    borrowed value does not live long enough
   |                          cast requires that `self` is borrowed for `'static`
...
29 |     }
   |     - `self` dropped here while still borrowed

How do I tell Rust that Executor is only dropped when it has been completely polled? I can't change the function signature of poll as that would violate the trait specifications.

Or, more likely, what is the correct way to do what I want to do?

Here's a link to the playground.

PS: I am aware that calling .await to execute the query might be considered an 'anti pattern'.


Solution

Checkout drewtato's answer for an explanation.

The solution (for me) was to remove the future field from my struct:

struct Executor {
    query: String
}

and then, instead of implementing Future, implement IntoFuture which gets implicitly called when using .await. Here we can use async move {} which moves the value instead of just referencing it.

Note that, at this point, type IntoFuture = impl Future is unstable and I thus had to use Pin<Box<dyn Future>>

impl IntoFuture for Executor {
    type Output = i32;
    type IntoFuture = Pin<Box<dyn Future<Output = Self::Output>>>;

    fn into_future(self) -> Self::IntoFuture {
        Box::pin(async move {
            make_query(&self.query).await
        })
    }
}

Now you can call .await on Executor:

#[tokio::main] 
async fn main() {
    let res = Executor {
        query: "foo".into()
    }.await;

    println!("Result is {res}"); // 5
}

Solution

  • You're trying to make a self-referential struct, which is not safely possible.

    It looks like you're trying to make this async function 'static, which is useful if you are trying to spawn it. You can do this by just creating an async block.

    #[tokio::main]
    async fn main() {
        let s = "foo".to_string();
        
        // This doesn't work since it borrows `s`.
        // let result = tokio::spawn(make_query(&s)).await.unwrap();
    
        // This works since it moves `s` into the future.
        let result = tokio::spawn(async move {
            make_query(&s).await
        }).await.unwrap();
    
        println!("Result is {result}");
    }
    

    This async block is actually self-referential, but async blocks are specifically handled by rust to allow this. If you wanted to create this by hand, you would need unsafe code.

    Note that calling an async function does no work, only awaiting does, so delaying the make_query call has no purpose. If you want to do something before make_query, you can simply put it in the async block.

    I couldn't figure out why you used a mutex. If you needed that for something, add it to the question. It's probably fine to remove it completely.

    Every trait object includes a lifetime, so when you declare a trait object without one like dyn Future<Output = i32>, it usually becomes dyn Future<Output = i32> + 'static. If you wanted to store non-'static data inside a trait object, you'd need to add it to your type.

    struct Executor<'f> {
        query: String,
        future: Mutex<Option<Pin<Box<dyn Future<Output = i32> + 'f>>>>
    }
    

    However, this would only be correct if your struct wasn't self-referential.