How to interpret the lifetime trait bound is satisfied when it is imposed on the type of this async block

#[derive(Debug)]
struct Wrapper<'a>(&'a i32);
#[tokio::main]
async fn main(){
     tokio::spawn(async {
       let i = 0;
       let w = Wrapper(&i);
       async {}.await;  // #1
       println!("{w:?}");
    });
}

The signature of tokio::spawn impose that the Future should satisfy F:'static. Block expressions - The Rust Reference says

Executing an async block is similar to executing a closure expression: its immediate effect is to produce and return an anonymous type.

Note: The future type that rustc generates is roughly equivalent to an enum with one variant per await point, where each variant stores the data needed to resume from its corresponding point.

IMO, the difference from the closure type here is, that async block should store the variable i, w in its field because they are needed after #1 is resumed, that is, the anonymous type of the async block should have a field of type Wrapper<'b>. Specifically, it may look something like this:

enum State<'a>{
    OnePoint(i32,Wrapper<'a>)
}

So I wonder why the type of the async block can satisfy 'static?

This async block doesn't borrow any data from its environment; it's self-contained.

Of course, if you were writing the type of the future yourself, you'd have to include the lifetime parameters of the types of the local variables. However, you'd quickly run into problems because you'd discover that the future is self-referential.

The compiler doesn't literally generate a regular enum (that the explanation describes); it generates its own data structure that it knows is self-referential but safely/soundly usable. There are no literal lifetime annotations that you expect, because they would be impossible to express in safe Rust.

1 Like

I haven't deep-dove on async generator construction like I have various other aspects of Rust, so treat this reply accordingly.

Once you start polling the future, it may become self-referential -- the state may involve references into its own captured data. This is only ok if said references can't become invalid by e.g. moving the data, and that's what Pin and Unpin are about. A future that has self-referential state will not implement Unpin.

The type can still be 'static because the compiler-generated type doesn't act like a structure with lifetime parameters. Imagine it's accomplished with raw pointers at the entry and exit points of the generator, say. It's done in such a way that the future can't be used unsoundly with safe code, modulo compiler bugs.

You need unsafe to implement such a future, generally.[1]


  1. There may be some niche exceptions involving leaking or whatnot, I haven't played with it. ↩︎

2 Likes

Do you mean, the compiler also does not generate the type using lifetime parameters if the hand-written type should have?

Hmmm, this sounds reasonable to interpret why the type generated by a compiler that would have lifetime parameters 'a('static: 'a) if it were hand-written by the user can satisfy 'static.

So, could I understand the lifetime constraints of an async block as if it were like a closure? That is, only the captured variable can influence whether the type satisfies some lifetime trait bound.

I assume you meant "can be implemented without unsafe" (love the quadruple negative, btw :grin:)

2 Likes

Not everything about the async output is as simple(!) as a closure. There cases where what state is held across an await point legitimately makes implementing Send or Sync unsound, even though the directly captured data would allow it (e.g. holding a MutexGuard across an await point). And the compiler also still has some rough edges / isn't smart enough, and sometimes the async output doesn't implement Send or Sync or such even though it could.

There may be lifetime related rough edges or complications too, but I'm not sure offhand.

2 Likes

Going for a new record! :sweat_smile: I also rewrote that phrasing about four times before caving and hitting Reply (now five).

2 Likes

Well, I just mean considering whether the type of an async block satisfies a lifetime trait bound is analogous to considering them for closure, right? In other words, the lifetime parameter within the block does not impact whether the type of the async block satisfies a lifetime trait bound, which is similar to closure.

I imagine it is possible to

  • have an async block that captures only 'static stuff
  • gets ahold of something with a non-'static lifetime but not a borrow of something it captured
  • keeps it across an await point
  • is non-'static as a result

But it's too annoying to attempt on my phone, maybe later.

I'm also not experienced enough with async to say for sure if there are no other lifetime bound imposing situations.

1 Like

Consider this one

use std::future::Future;
fn main(){
    fn foo<F:'static>(_:F){}
    let i = 0;
    foo(async {  // #1
        let w:Wrapper<'_>;
        println!("{i}");
    });
}

To measure whether the Future produced by the async block at #1 satisfies the 'static, it behaves as if we would impose 'static on a closure that was at #1.

This is what I was thinking of.

The future remaining 'static seems somewhere between footgunny and unsound to me.

Potentially related issues

1 Like