What are reasonable ways to store a callback in a struct?


#1

I have a struct whose purpose is to run a simulation loop and emit state information after each loop.

I am porting from a C++ implementation in which the class invokes a provided callback, so that’s what I’m trying to do in Rust.

Currently, I’m trying to store that callback in the struct as &Fn, but the borrow checker is picking a fight with me and I’m starting to wonder if I need to wrap it in a standard library container to avoid onerous constraints on use.

Here’s a simplified version of where I am with storing &Fn

trait Barable {
    fn new() -> Self;
    // etc.
}

struct Bar;

impl Barable for Bar {
    fn new() -> Self {
        Bar
    }
}

struct Foo<'call, B : Barable + 'call> {
    bar: B,
    callback: &'call Fn(&B)->()
}

impl<'call, B : Barable + 'call> Foo<'call, B> {
    fn new(callback: &'call Fn(&B)->()) -> Self {
        Foo{
            bar: B::new(),
            callback: callback
        }
    }
}

fn example_callback(bar: &Bar) {
}

fn main() {
    let keep_alive = &example_callback;
    let foo = Foo::new(&keep_alive);
    // Does not live long enough. Why?
    //let foo = Foo::new(&example_callback);
}

I haven’t yet tried implementing the function which sets foo.callback, nor the function which will invoke the callback. I’m stopping here to ask because the need for let keep_alive is weird and gives me the feeling I’m going to have more trouble with lifetimes if I continue as I am.

It seems to me that storing callbacks is a common enough practice that there’s at least one established Way To Do It. What do y’all suggest?


#2

This seems like an unfortunate corner case of type inference. The compiler thinks the borrow is only valid for this single statement:

let foo = Foo::new(&example_callback);
// like the following, which genuinely is forbidden because the closure's scope is
// until the end of the statement
let foo = Foo::new(&|_: &Bar| {});

Binding to a variable makes the scope longer:

let keep_alive = example_callback;
// or
let keep_alive = |_: &Bar| {};

let foo = Foo::new(&keep_alive);

Talking about storing a callback suggests to me that you might want Foo to own it instead of borrowing. That would mean either making the struct generic over the callback’s type

struct Foo<'call, B: Barable + 'call, F: Fn(&B) + 'call> {
    bar: B,
    callback: F,
}

or using a trait object

struct Foo<'call, B: 'call> {
    bar: B,
    callback: Box<Fn(&B) + 'call>,
}

#3

This is true :slight_smile:

The reason that it doesn’t live long enough is that with Foo::new(&keep_alive), you are making a temporary reference to keep_alive that will go away at the end of the line. This would make foo hold a dangling reference. With the let version, it’s no longer a temporary: it will be around until the end of main, and so it’s no longer dangling. Does that make sense?

One way to make this a bit easier is to box up the callback, rather than just holding a reference: https://is.gd/zltsK7

But in this case, you can do better. Fn is a closure type, but you’re using a regular old function. This means that instead, we can do https://is.gd/UWzI03 , which can no longer take closures, but doesn’t have the allocation nor the lifetime issues.


#4

If I’m not mistaken, since fns are becoming zero-sized (i.e. they’re zero-sized in nightly but not in beta yet), boxing them should not allocate.


#5

I mentioned that I will eventually want to set the callback on an existing value, and I expect that if I make the struct generic on the callback’s type this will mean that if I construct on a fn I won’t be allowed to set to a closure - and if I construct on a closure I believe I’ll not be able to set a different closure!

Also, the lifetime 'call is present only because and where storing a &Fn required it. Seeing it still present in examples that eliminate the reference is strange.


#6

This lifetime bound allows the closure to capture variables by reference while Box<Fn() + 'static> (that’s what you’ll get without the bound I believe) will not allow the closure to borrow anything (hello move and Rc). It may be acceptable in your case but could be too limiting.


#7

I ran into that issue right away. I decided calling the lifetime 'closure was more appropriate, but I admit 'call is a plausible name. Thanks!