// This is related to https://www.reddit.com/r/rust/comments/goh2be/how_to_store_an_async_future_to_a_function_with_a/ | |
#[derive(Debug)] | |
struct MyStruct { | |
a: usize, | |
b: usize, | |
c: String, | |
// ... | |
} | |
struct StructWithCallback<T> { | |
callback: T, | |
} | |
impl<T> StructWithCallback<T> { | |
fn new(cb: T) -> Self { | |
Self { | |
callback: cb | |
} | |
} | |
} | |
async fn my_function_0<'r>(my_struct: &'r mut MyStruct) { | |
println!("in my_function_0: {:?}", my_struct); | |
// Change stuff. | |
my_struct.a += 1; | |
my_struct.b += 2; | |
my_struct.c = "a different string".to_string(); | |
} | |
async fn my_function_1<'r>(my_struct: &'r mut MyStruct) { | |
println!("in my_function_1: {:?}", my_struct); | |
// Change stuff. | |
my_struct.a += 1; | |
my_struct.b += 2; | |
my_struct.c = "a completely different string".to_string(); | |
} | |
async fn example() { | |
let mut callbacks = vec![ | |
StructWithCallback::new(my_function_0), | |
StructWithCallback::new(my_function_1) | |
]; | |
let mut my_struct = MyStruct { | |
a: 1, | |
b: 2, | |
c: "a string".to_string(), | |
}; | |
// Invoke the stored callbacks. | |
let callback_0 = callbacks[0].callback; | |
callback_0(&mut my_struct).await; | |
let callback_1 = callbacks[0].callback; | |
callback_1(&mut my_struct).await; | |
// Confirm stuff changed. | |
println!("after my_function: {:?}", my_struct); | |
} | |
fn main() { | |
let mut rt = tokio::runtime::Runtime::new().unwrap(); | |
rt.block_on(example()); | |
} |
Yeah - I see. Due to the type interference it assigns it the concrete type.
How does Box::pin avoid that problem - can't a struct do the same thing?
The trick we are using in goose returns a Pin<Box<dyn Future<Output=()>>>
, which is a concrete type (Box<dyn ...>) with a concrete in-memory layout (fat pointer to (Future object, vtable for all the methods in the Future trait)) so it can safely be stored in a vec.
The dyn keyword is the crucial bit here: it implies dynamic dispatch, using a vtable.
Boxing also gets around the fact that the different Future implementations may be differently sized. I generally expect to see Box and dyn together. Box coerces the differently-sized objects into a single size (a pointer to the object on the heap) and the other coerces the different implementations of the Trait into a single thing (pointer to a vtable).
I answered the wrong question there...
The reason that Box<dyn Trait>
works for goose is that it has space in the syntax for the for<'r> lifetime to be applied to the Future (using +). When I tried to change it into a type parameter, I had nowhere to put the lifetime, and so the borrow checker got very sad.
@LionsAd: https://gist.github.com/87ab93979d770314e6698a9867d1e7e5 does what we want. I will have a go at patching Goose to use this pattern.
@LionsAd Unfortunately, making StructWithCallback means that you can't store it in a vec or hashmap. This causes:
Playground is here: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b89d17f00f58f071555339093d76a0a3
Let's keep discussion on here, to avoid spamming the issue too much.