3

I have two structs:

  • Client, which stores a callback and calls it in response to receiving new data. As an example, you can think of this as a websocket client, and we want to provide a hook for incoming messages.

  • BusinessLogic, which wants to hold a Client initialized with a callback that will update its local value in response to changes that the Client sees.

After following compiler hints, I arrived at the following minimal example:

Rust playground link

use rand::Rng;

struct Client<'cb> {
    callback: Box<dyn FnMut(i64) + 'cb>,
}

impl<'cb> Client<'cb> {
    fn do_thing(&mut self) {
        // does stuff

        let value = self._get_new_value();

        // does more stuff

        (self.callback)(value);

        // does even more stuff
    }

    fn _get_new_value(&self) -> i64 {
        let mut rng = rand::thread_rng();
        rng.gen()
    }
}

struct BusinessLogic<'cb> {
    value: Option<i64>,
    client: Option<Client<'cb>>,
}

impl<'cb> BusinessLogic<'cb> {
    fn new() -> Self {
        Self {
            value: None,
            client: None,
            
        }
    }

    fn subscribe(&'cb mut self) {
        self.client = Some(Client {
            callback: Box::new(|value| {
                self.value = Some(value);
            })
        })
    }
}

fn main() {
    let mut bl = BusinessLogic::new();
    bl.subscribe();

    println!("Hello, world!");
}

Problem is, I am still getting the following compiler error:

  Compiling playground v0.0.1 (/playground)
error[E0597]: `bl` does not live long enough
  --> src/main.rs:51:5
   |
51 |     bl.subscribe();
   |     ^^^^^^^^^^^^^^ borrowed value does not live long enough
...
54 | }
   | -
   | |
   | `bl` dropped here while still borrowed
   | borrow might be used here, when `bl` is dropped and runs the destructor for type `BusinessLogic<'_>`

For more information about this error, try `rustc --explain E0597`.
error: could not compile `playground` due to previous error

I understand why I'm seeing this error: the call to subscribe uses a borrow of bl with a lifetime of 'cb, which is not necessarily contained within the scope of main(). However, I don't see how to resolve this issue. Won't I always need to provide a lifetime for the callback stored in Client, which will end up bleeding through my code in the form of 'cb lifetime annotations?

More generally, I'm interested in understanding what is the canonical way of solving this callback/hook problem in Rust. I'm open to designs different from the one I have proposed, and if there are relevant performance concerns for various options, that would be useful to know also.

Herohtar
  • 5,347
  • 4
  • 31
  • 41
Blah Blah
  • 31
  • 1

1 Answers1

1

What you've created is a self-referential structure, which is problematic and not really expressible with references and lifetime annotations. See: Why can't I store a value and a reference to that value in the same struct? for the potential problems and workarounds. Its an issue here because you want to be able to mutate the BusinessLogic in the callback, but since it holds the Client, you can mutate the callback while its running, which is no good.

I would instead suggest that the callback has full ownership of the BusinessLogic which does not directly reference the Client:

use rand::Rng;

struct Client {
    callback: Box<dyn FnMut(i64)>,
}

impl Client {
    fn do_thing(&mut self) {
        let value = rand::thread_rng().gen();
        (self.callback)(value);
    }
}

struct BusinessLogic {
    value: Option<i64>,
}

fn main() {
    let mut bl = BusinessLogic {
        value: None
    };
    
    let mut client = Client {
        callback: Box::new(move |value| {
            bl.value = Some(value);
        })
    };
    
    client.do_thing();

    println!("Hello, world!");
}
  • if you need the subscriber to have backwards communication to the Client, you can pass an additional parameter that the callback can mutate, or simply do it via return value
  • if you need more complicated communication from the Client to the callback, either send a Message enum as the argument, or make the callback a custom trait instead of just FnMut with additional methods
  • if you need a single BusinessLogic to operate from multiple Clients use Arc+Mutex to allow shared ownership
kmdreko
  • 42,554
  • 6
  • 57
  • 106
  • Thanks! Is there a canonical way to implement something like a websocket client in rust? Or in general, some struct that takes arbitrary callbacks and executes them in response to some events – Blah Blah Jan 29 '22 at 02:11