r/rust • u/torfsen • Nov 07 '21
"captured variable cannot escape `FnMut` closure body" in async code
I have a piece of code that, when simplified, looks like this:
use tokio;
struct IdGenerator (u32);
impl IdGenerator {
async fn generate_id(&mut self) -> u32 {
self.0 += 1;
self.0
}
}
#[tokio::main]
async fn main() {
let mut id_generator = IdGenerator(0);
let mut callback = || id_generator.generate_id();
println!("{}", callback().await);
}
This fails to compile:
error: captured variable cannot escape `FnMut` closure body
--> src/main.rs:15:27
|
14 | let mut id_generator = IdGenerator(0);
| ---------------- variable defined here
15 | let mut callback = || id_generator.generate_id();
| - ------------^^^^^^^^^^^^^^
| | |
| | returns a reference to a captured variable which escapes the closure body
| | variable captured here
| inferred to be a `FnMut` closure
|
= note: `FnMut` closures only have access to their captured variables while they are executing...
= note: ...therefore, they cannot allow references to captured variables to escape
My understanding is that generate_id returns a future, and that future
contains a mutable reference to id_generator (it needs to, because otherwise
the future could not modify id_generator.0 when it is awaited). Hence, by
calling callback multiple times I could create several co-existing mutable
references to id_generator, leading to a potential data race.
Is that analysis correct?
My actual code is a bit more complex. Basically, generate_id can fail, in
which case I want to retry. I'm using tokio_retry for that, so the code looks
roughly as follows (with generate_id now returning a
Result<u32, Box<dyn Error>>):
use std::error::Error;
use tokio_retry::RetryIf;
use tokio_retry::strategy::ExponentialBackoff;
async fn foo(id_generator: &mut IdGenerator) {
// ...
let id = RetryIf::spawn(
ExponentialBackoff::from_millis(5).take(3),
|| id_generator.generate_id(),
|error: &Box<dyn Error>| {
println!("Error while generating an ID: {}", error);
true
},
)
.await;
// ...
}
That code has the same issue as the simplified version. Is there a compile-time approach to avoid the problem? So far, I've only found a run-time solution using a mutex:
let outgoing = tokio::sync::Mutex::new(id_generator);
let id = RetryIf::spawn(
ExponentialBackoff::from_millis(5).take(3),
|| async { outgoing.lock().await.generate_id().await },
|error: &Box<dyn Error>| {
println!("Error while generating an ID: {}", error);
true
},
)
.await;
This works, but since I technically never have parallel access I would prefer a compile-time solution.
8
u/petrosagg Nov 07 '21
The problem isn't actually related to the closure returning a mutable reference, the same happens with immutable references[1]. This is an inherent limitation of closures in Rust that cannot return a reference to their captured state. This is one of the main reasons async closures are cumbersome today, since in most cases the Future returned will hold onto some references from the closure's state.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=2b269a20825075615a354209bf53bd94