Sadly, async fn
sugar indeed conservatively assumes that the input will be borrowed from, since the async { … }
generator / state machine "starts immediately" / the whole body of the function is lazily evaluated, meaning the borrows need to be upheld until the future is .await
ed:
async
fn example<'thing> (thing: &'thing Thing)
-> Ret
{
todo!("FUNCTION BODY…");
}
becomes:
fn example<'thing, 'returned_future> (thing: &'thing Thing)
-> impl 'returned_future + Future<Output = Ret>
where
&'thing Thing : 'returned_future,
// +-<----------------<---------------<------+
// | |
{ // vvvvv ^
async move /* thing */ { // |
// make it so each and every parameter is captured |
let _ = &thing; // ->---------->----------------->----+
todo!("FUNCTION BODY…");
}
}
The key thing (heh) here is that the span during which the returned future is usable, that is, 'returned_fut
, must satisfy that each and every capture is, in and of itself, usable during that span / region. Thus, if the returned future / async …
generator captures thing: &'thing Thing
, then we end up with that &'thing Thing : 'returned_future
constraint (and so on for the other function params).
That constraint becomes:
-
Thing : 'returned_fut
, i.e., the referee / borrowee must, itself, be guaranteed not to dangle during 'returned_fut
;
-
and 'thing : 'returned_fut
, i.e., 'thing ≥ 'returned_fut
(or more precisely, 'thing ⊇ 'returned_fut
: the span of usabily of the returned future must be contained within / cannot escape the span of the borrow of *thing
, lest that borrow dangles).
By the way, in order for &'thing Thing
to be well-formed, we also have an automatic Thing : 'thing
constraint. Thus, since 'thing : 'returned_fut
, we realize tht the first Thing : 'returned_fut
constraint is redundant / superfluous)
All that to say that we end up with the returned future not being able to outlive 'thing
, and so on for each lifetime appearing among the function parameters' types.
Again, this makes sense since the body of that async fn
, i.e., the body of the async
generator / state machine is only reached when the future is polled, which can be arbitrarily late among the future's lifetime:
let fut;
{
let thing: Thing = …;
fut = example(&thing);
}
… fut.await; // <- body of `async fn example` is only reached at this point
So, if thing
is (captured since potentially) used in that async fn
body, Rust ought to cause a compilation error to prevent that use-after-free bug from making it to the runtime.
Hence your error.
You have two solutions –which are somewhat the same when we think about it–.
-
Do not use thing
inside the future's block, and make sure Rust knows that you did it on purpose: as I mentioned, Rust assumes that any parameter to an async fn
may be mentioned by the function's body, and thus makes each be captured by the returned future. This is so that if the first version of your not-totally implemented API forgot to use thing
in its function body, and if a downstream user of your library used that function, then a later change in the implementation only does not cause a change in the function's signature, not even in its lifetimes, maintaining a robust abstraction boundary that avoidd breakage on such downstream code.
So in order to change that, you need to manually perform that equivalent unsugaring I showcased above:
fn example<'thing, 'returned_future> (thing: &'thing Thing)
-> impl 'returned_future + Future<Output = Ret>
where
&'thing Thing : 'returned_future,
// +-<----------------<---------------<------+
// | |
{ // vvvvv ^
async move /* thing */ { // |
// make it so each and every parameter is captured |
let _ = &thing; // ->---------->----------------->----+
todo!("FUNCTION BODY…");
}
}
and remove any mentions to thing
inside the async move { … }
block:
- async move /* thing */ {
+ async move /* no-thing */ {
- // make it so each and every parameter is captured
- let _ = &thing;
todo!("FUNCTION BODY…");
}
- (assuming
FUNCTION BODY…
does not mention thing
either).
with that, you can finally remove the bounds regarding the thing: &'thing Thing
capture:
fn example<'thing, 'returned_future> (thing: &'thing Thing)
-> impl 'returned_future + Future<Output = Ret>
where
- &'thing Thing : 'returned_future,
{
async move /* no-thing */ {
todo!("FUNCTION BODY…");
}
}
which can finally be cleaned up to:
// don't care, thus don't even name it
// vv
fn example (thing: &'_ Thing)
-> impl 'static + Future<Output = Ret>
// ^^^^^^^
// the unconstrained / universal / never-ending span
{
async move { … }
}
But not using thing
at all in the body of example
is a bit restrictive; the key thing to do was not to use the borrow of *thing
inside the async move { … }
block.
This means that if we can derive some data off *thing
that, itself, does not borrow *thing
and is thus no longer lifetime-bound, then:
-
We can hoist and perform that operation before and thus outside that async move { … }
block.
For instance, assuming Thing
has a Copy
field, such as age: u8
, we can do:
fn example (thing: &'_ Thing)
-> impl 'static + Future<Output = Ret>
{
let age = thing.age; /* dereference the borrow *before* returning the future */
return async move /* age */ {
todo!("REST OF THE BODY, using age = {}", age);
};
}
This is the key thing to realize: the real body of example
, usually, is that single return
-the-compiler-generated-state-machine statement. But we can do some (non-blocking!) pre-processing before that, and only then move / transfer ownership of the newly computed stuff by that preprocessing step into the to-be-returned future / generator block.
But what if the field is not Copy
? Well, the same works with Clone
types, such as a hypothetical name: String
field:
let name: String = thing.name.clone();
return async move /* name, … */ {
/* stuff with `name`… */
};
And if Clone
is deemed too expensive, that's when a good old layer of Arc
reference-counting comes in handy:
struct Thing {
- long_name: String,
+ long_name: Arc<String>, // or, here, `Arc<str>` to optimize
}
and then:
let name = Arc::clone(&thing.name);
async move { … }
This usually leads to the pattern whereby Thing
is just a wrapper around a Arc<ThingFields>
, and then the function body becomes:
#[derive(/* Arc */ Clone)]
struct Thing(Arc<ThingData>);
struct ThingData { long_name: String, … }
fn example (thing: &'_ Thing)
-> impl 'static + Future…
{
let thing: Thing = thing.clone();
async move { … }
}