4

It is possible to modify a function argument in-place with the following code snippet:

let mut foo = 1;
let mut fun: Box<dyn FnMut(&mut i32) -> _> = Box::new(|f| {
    *f += 1;
});
fun(&mut foo);
assert_eq!(foo, 2);

However, I have the case where the function fun needs to return a future which modifies the argument once the future gets awaited. Basically I have the following scenario:

let mut foo = 1;
assert_eq!(foo, 1);
let mut fun: Box<dyn FnMut(&mut i32) -> _> = Box::new(|f| {
    async move {
        *f += 1;
    }
});
fun(&mut foo).await;
assert_eq!(foo, 2);

rust playground

But this gives the compile error:

error: lifetime may not live long enough
 --> src/main.rs:7:9
  |
6 |       let mut fun: Box<dyn FnMut(&mut i32) -> _> = Box::new(|f| {
  |                                                              -- return type of closure `impl Future<Output = ()>` contains a lifetime `'2`
  |                                                              |
  |                                                              has type `&'1 mut i32`
7 | /         async move {
8 | |             *f += 1;
9 | |         }
  | |_________^ returning this value requires that `'1` must outlive `'2`

error: could not compile `playground` due to previous error

I am not sure how to annotate lifetimes in my code snippet above. I have tried Box<dyn FnMut(&'static mut i32) -> _>, but this gives that foo does not live long enough.

Is there a way to get this working?

1
  • 1
    This is a complicated issue. I don't have time to deep dive now, but boxing the future works: play.rust-lang.org/…. Commented Nov 14, 2022 at 1:52

2 Answers 2

0

You cannot simply change the value from (potentially) another thread without packing it with some synchronization primitive.

Instead, you can pack it with Arc and Mutex and deal with it like a multithreading program:

use std::sync::{Arc, Mutex};

#[tokio::main]
async fn main() {
    let foo = Arc::new(Mutex::new(1));
    assert_eq!(*foo.lock().unwrap(), 1);
    let mut fun: Box<dyn FnMut(Arc<Mutex<i32>>) -> _> = Box::new(|f| {
        async move {
            let mut counter = f.lock().unwrap();
            *counter += 1;
        }
    });
    fun(Arc::clone(&foo)).await;
    assert_eq!(*foo.lock().unwrap(), 2);
}
Sign up to request clarification or add additional context in comments.

10 Comments

Thanks, I was trying to avoid using synchronization primitive since I will not be using multi-threaded future execution.
This is plain wrong. Multithreading is not related, this is an issue stemming from a lacking corner in Rust's type system.
@ChayimFriedman So this solution is good, but the explanation is not correct?
@complikator Yes. What solves here is the Arc, the Mutex is just an artifact (and BTW, prefer atomics where possible).
@ChayimFriedman but to do this I need to call Arc::get_mut(&mut f) which will fail, because there are other arc to given value
|
0

I tried moving the function fun to a generic function definition instead of boxing it, which seems to work:

fn fun<'a>(f: &'a mut i32) -> impl futures::Future<Output = ()> + 'a {
    async move {
        *f += 1;
    }
}

#[tokio::main]
async fn main() {
    let mut foo = 1;
    assert_eq!(foo, 1);
    fun(&mut foo).await;
    assert_eq!(foo, 2);
}

This seems to work since I can annotate the lifetimes in this setup. This solution requires that I await it immediately, since the &mut i32 can only exist in one place at once. I.e. the following wouldn't work:

#[tokio::main]
async fn main() {
    let mut foo = 1;
    assert_eq!(foo, 1);
    let fut = fun(&mut foo);
    assert_eq!(foo, 1);
    fut.await;
    assert_eq!(foo, 2);
}

2 Comments

I think that passing future explicitly without Boxing can be inefficient because the StateMachine that is under the Future implementation needs to be copied between stack
@complikator (a) The state machine for this future is only 16 bytes. (b) Boxing should be less efficient than memcpy. (c) This is a general advice about futures, not related to this question.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.