Async lifetimes and references owned by the current function

I'm having difficulty understanding some errors that seem related to async and await boundaries.
Here's my example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=bfec125338bada5ada716417c7b41cab

Unfortunately I haven't been able to reduce it further.

    error[E0597]: `foo` does not live long enough
      --> src/main.rs:53:25
       |
    46 |     pub async fn run_filters(&'a self, mut foos: Vec<Foo<'a>>) -> Vec<Foo<'a>> {
       |                              -------- lifetime `'1` appears in the type of `self`
    ...
    53 |                 if func(&foo).await {
       |                    -----^^^^-
       |                    |    |
       |                    |    borrowed value does not live long enough
       |                    argument requires that `foo` is borrowed for `'1`
    ...
    56 |             }
       |             - `foo` dropped here while still borrowed

I don't understand how the borrowed value doesn't live long enough when the function is awaited immediately. I also cannot see where the '1 lifetime is from. I'd like to understand this better.

    error[E0515]: cannot return value referencing local variable `self`
       --> src/main.rs:131:13
        |
    126 |             FooDelay::Seconds(x) => self.run(x).await
        |                                     ---- `self` is borrowed here
    ...
    131 |             Some(foos[0])
        |             ^^^^^^^^^^^^^ returns a value referencing data owned by the current function

The only reference being returned (contained within the Foo struct) is one that was passed into FooQuery to begin with. I have assumed the borrow of self would be completed after the await, but perhaps that assumption is false?

It sounds like you should drastically reduce the amount of lifetimes in your program. Check out this version.

Generally as I understand it, the purpose of 'a in your program is to be the lifetime of the FooSession stored outside of your various other structs. So therefore you should not put that lifetime on anything else but references to FooSession, and things containing references to FooSession.

  1. Don't put it on self, e.g. no &'a mut self.
  2. Don't put it on trait objects, rather use the for<'a> syntax to talk about all lifetimes at the same time.
type FooFilter = Box<dyn for<'a> Fn(&'a Foo<'a>) -> Pin<Box<dyn Future<Output = bool> + 'a>>>;
  1. When creating filters, you cannot use variables in the async block that are captured from the environment. Additionally the things from the environment should be owned, thus you get:
pub fn with_id(self, id: &str) -> Self {
    let id = id.to_string();
    self.with_filter(Box::new(move |f| {
        let res = f.id == id;
        Box::pin(async move { res })
    }))
}

It might become even easier to reason about if you get rid of FooSession, or at least don't make it a reference.

Be aware that this has nothing to do with async/await. It would be the same in a non-async program.

2 Likes

I'm not sure I can get rid of the session or the reference to it - I need to think about that some more.

My example is based on my thirtyfour crate for automating web browsers via WebDriver. I'm building a more elaborate query interface for it.

There is one WebDriverSession inside WebDriver and all WebElement structs that get returned have a reference to the WebDriverSession. In this way Rust can guarantee at compile time that no WebElement can outlive the browser session. This seems like a reasonable thing to want to guarantee but I'm still unsure whether I will come across some valid use-case that violates it. The other possibility is that the code using this crate to automate browsers may become more complicated because of this constraint. I need to try it out with the constraint (with the reference) and see how it feels to use. Certainly the crate itself is far more complex this way but I'm hoping the code that uses it need not be. There's a cost/benefit trade-off here for sure.

Your response is extremely helpful. Thank you so much!

Huh, I've tried using for<'a> Fns in type aliases before, and not had much luck, I thought type aliases didn't respect Trait bounds, and not respecting lifetime bounds was a consequence of that, Any idea why it works here?

@alice
Turns out I had a logic bug in the filter code too :roll_eyes:

Thanks to your help I now have a working example: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=9529a715a2ebf2bf624498fde77077cb

Now I'm hoping my actual code doesn't stray too far from the example :wink:

Thanks again!

@ratmice you probably didn't put it in a box.

Yup, it was &'a dyn for <'b> Fn(...), I ended up generating the type via a macro instead of aliases :frowning:, just suprised it makes a difference -- that said I don't really want to derail the thread, just suprised is all.