Explanation of this lifetime issue

struct OurFuture<'a, D: 'a> {
    block: Pin<Box<dyn Future<Output = ()> + Send + 'a>>,
    data: D

fn foo() {
    let mut data = Data(1);
    let data_mut = data.borrow_mut();
    let block = async move { data_mut.0 += 1; }.boxed();
    let ourfuture = OurFuture { block, data }; // data and future live together
error[E0505]: cannot move out of `data` because it is borrowed
   --> src/main.rs
101 |     let data_mut = data.borrow_mut();
    |                    ----------------- borrow of `data` occurs here
102 |     let block = async { data_mut.0 += 1; }.boxed();
103 |     let ourfuture = OurFuture { block, data };
    |                     -------------------^^^^--
    |                     |                  |
    |                     |                  move out of `data` occurs here
    |                     borrow later used here

Ok I get that I can't do this, but why? What are the borrowing consequences exactly? An example of something bad happening/later misuse, if this was allowed.

Why am I doing this?

ourfuture.block.await will use pointer stored in data_mut which points into the invalid stack slot, formerly used by data.

    let mut data = Box::new(Data(1));
    let data_mut: Box<&mut Data> = Box::new(data.borrow_mut());
    let block = async move { data_mut.0 += 1; }.boxed();
    let ourfuture = OurFuture { block, data };

Could I just box them?

Well, then...

mem::replace(&mut ourfuture.data, Box::new(Data(2)));
// now the `Box` formerly stored in `data` is dropped,
// and the corresponding pointer is dangling
ourfuture.block.await; // accesses the dangling pointer

This is what I thought of as well. I just needed to verify that this is the only issue that could come up besides drop order. Now with that said, if I promised, as a user controlled invariant, to never mutate outfuture.data and only mutate ourfuture.block (through an .await for ex), then is there a way I could get this past the compiler?

Well, the main issue is just "this is disallowed by Rust borrow checking rules". Simple as that.

The reasoning for this "disallowed", however, is close to what I've outlined above.

That's exactly what happens with ordinary Futures: they're pinned in place (to the heap, as Pin<Box<_>>, or to the stack, as Pin<&mut _>), and the unsafe code doing that maintains the required invariant - namely, "if something's pinned, it's pinned until drop and dropped in that exact place".

The most straightforward thing to do here is to use an async block that owns the data. In exchange for being pinned (before it is executed), it allows constructing a self-referential future.

use std::borrow::BorrowMut;
struct Data(i32);

fn foo() {
    let mut data = Data(1);
    let future = async move {
        let data_mut = data.borrow_mut();
            data_mut.0 += 1;

Another option for general self-reference is ouroboros which provides a safe interface to construct self-referential structs, at the cost of some extra heap allocation (sort of like “just box them” as mentioned above, but done carefully).

Doing it yourself gets very messy, and should be avoided since it's easy to accidentally write unsound code. The history of self-referential structs in Rust is littered with unsound libraries. So far, ouroboros has had all known bugs fixed.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.