I'm trying to wrangle some async code in a testing suite, and I've stumbled upon an ownership problem that I can't explain nor fix.
Here's what I think is probably a minimal example of my setup:
use futures::Future;
struct Container {
data: String // specifically, something which is !Copy
}
impl Container {
async fn get_immutable_property(&self) -> bool {
// something async here... but which does NOT require &mut self
true
}
async fn do_mutable_work(&mut self) -> Result<(), ()> {
//...do things here that DOES require &mut self...
Ok(())
}
pub async fn do_async_work<'a, Fut: Future<Output = bool> + 'a>(
&'a mut self,
get_condition: impl Fn(&'a Container) -> Fut,
) -> Result<(), ()> {
while !get_condition(self).await {
self.do_mutable_work().await?;
}
Ok(())
}
}
// elsewhere...
#[tokio::main]
async fn main() -> Result<(), ()> {
let mut container = Container { data: String::new() };
// lifetime problems here
container.do_async_work(|container| async move {
container.get_immutable_property().await
}).await
}
As you can see in the playground, this causes an ownership error when attempting run do_mutable_work()
which takes &mut self
.
This error seems to be caused by the lifetime annotations on do_async_work()
; removing them makes the ownership conflict disappear, but instead causes lifetime issues in the closure passed to do_async_work()
inside main: playground link.
Now, I know I could "trivially" fix this by just strongarming the safety using, probably, some Arc<Mutex<Container>>
construction and, as long as I don't have deadlocks, that will work and avoid all ownership and lifetime issues for my case. However, that will require a decent amount of refactoring, and intuitively, it feels like this code should be correct without requiring reference counting and synchronisation: as a human,
- I know that my
&Container
will outlive my future since I call the future from inside a method on Container, where it isawait
ed, and thusself
is guaranteed to outlive it; - I also know that after calling
get_condition(self)
, I again immediatelyawait
it, drop the return value (I've even tried silly refactorings just to make sure thebool
is dropped before the loop body), and only then callself.do_mutable_work()
, thus it shouldn't violate borrow rules;
However I don't seem to be able to convince the compiler of both of these facts at once.
So my question is two-fold:
- Is my human intuition correct, and is this code safe and sound and being unnecessarily rejected by the compiler? And
- If so, is there some way I can encode this into the type system, without
unsafe
, thus avoiding having to switch everythingArc<Mutex<>>
for this problem?
Of course if my own understanding is flawed and the code is NOT safe as written, then a Mutex would be the correct approach, but I really don't want to add it if it's not actually necessary and is possible to avoid.