Trouble understanding `self` escapes the method body here

Hi
I'm very new to Rust. I've been through the various books etc but I'm consistently having issues with code like the following.

This is a simple repro that follows the function prototype of a package I'm using

#[derive(Default)]
struct DoSomething {
    x: i32,
}

fn some_func(loader: impl FnOnce() + 'static) {}

impl DoSomething {
    async fn do_something(&mut self) {
        some_func(|| {
            self.x = 100;
        });
    }
}

I get a compiler error in the call to some_func

error[E0521]: borrowed data escapes outside of method
  --> src\main.rs:44:9
   |
43 |       async fn do_something(&mut self) {
   |                             ---------
   |                             |
   |                             `self` is a reference that is only valid in the method body
   |                             let's call the lifetime of this reference `'1`
44 | /         some_func(|| {
45 | |             self.x = 100;
46 | |         });
   | |          ^
   | |          |
   | |__________`self` escapes the method body here
   |            argument requires that `'1` must outlive `'static`

Now I get that because the closure captures the reference of self and combined with 'static on some_func, the borrow checker can't be sure that self will be valid when the closure is eventually executed. Is that correct?

Now the million dollar question. Whats the right way to do something like this?

For completeness, the package I'm using is called callback-future which allows me to turn a callback into a future. The full code I want to get working is:

    async fn some_async(&mut self) -> Result<String, i32> {
        CallbackFuture::new(|complete| {
            self.cb = Some(complete);
        })
        .await
    }

The idea is to store the complete Fn passed into the closure so I can execute when some later async workflows have completed.

Use other "referencing" mechanisms other than &-borrowing.

In Rust it is customary to:

  • use Arc (or downgraded to a Weak) to get a new : 'static "referencing handle" to the Arc-wrapped value,

    • this, in turn, by design, involves multiple handles being able to access the resource, i.e., involves sharing the resource / concurrent access, i.e., you will only have & access to it. Which is why you'll then need &-compatible mutability wrappers, called shared mutability wrappers (or interior mutability even though I personally don't like that name), to compensate.

      Hence the pervasive Arc<Mutex<T>>, or Arc<RwLock<T>>. In fact, ::callback-future uses that itself!

  • or, often better, to use channels. They're less flexible but thus able to be a more optimized implementation of Arc<Mutex<OptionOrVec<T>>>

In your case, using the latter:

use some::channel; // _e.g._, `std::sync::mpsc` to begin with

type CompletionCb<T> = Box<dyn 'static + Send + FnOnce(T)>;

// in your struct:
    cb: Option<channel::Receiver<CompletionCb<String>>>,

    async fn some_async(&mut self) -> Result<String, i32> {
        let (sender, receiver) = channel::new();
        self.cb = Some(receiver);
        //                  trick: when requiring `'static`, put this
        //                  vvvv
        CallbackFuture::new(move |completion_cb| {
            sender.send(completion_cb).ok();
        })
        .await
    }

and then you can do cb.{try_,}recv() to get access to the completion cb:

if let Ok(resolve) = self.cb.as_ref().unwrap().try_recv() {
    resolve("Hello, World!".into()); // assert_eq!(self.some_async().await, "Hello, World!")
}

XY problem

Note that if you are gonna be using channels, then you may as well skip the CallbackFuture wrapper altogether, which is redundantly reïmplementing yet another async-channel-like API, potentially less efficiently than how the proper such channels do it:

    async fn some_async(&mut self) -> Result<String, …> {
        let (sender, receiver) = async_channel::new();
        self.cb = Some(sender);
        receiver.recv().await
    }

    // 
    self.cb.unwrap().send("Hello, World!".into())
4 Likes

Thats great. Thanks. I had tried the channels method previously but totally overlooked the final await on receiver.

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.