#How to let library user provide callback functions? Similar to weak functions in C

26 messages · Page 1 of 1 (latest)

full cove
#

Hi everyone!

I am writing a library for an embedded device that

  • communicates over the serial port
    (I am creating 2 threads for it 1 for listening, 1 for writing),
  • receives protobuf data,
  • parses it into struct,
  • runs user-defined callback depending on the struct received,
  • writes back the response.

This is how the generated protobuf structs approx looks like so you have an idea:

pub mod message {
    #[derive(Clone, PartialEq, ::prost::Oneof)]
    pub enum Payload {
        #[prost(message, tag="1")]
        EnableCommand(super::EnableCommand),
        #[prost(message, tag="2")]
        DisableCommand(super::DisableCommand),
}
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct EnableCommand {
    #[prost(string, tag="1")]
    pub user_id: ::prost::alloc::string::String,
    #[prost(string, tag="2")]
    pub transaction_id: ::prost::alloc::string::String,
}
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct DisableCommand {
}

I have problem with implementation how to let user define the callback that will be called once the message(struct) is received.
In C I would just use weak functions so the user is able to overwrite the behaviour for each struct.
I don't know if there is something similar in Rust.

What I've tried so far:

  1. I've tried to use the traits with empty default implementation. This look perfect at first glance, but in order for user to overwrite the default trait implementation either trait or struct must be declared on the user side, not library side.
    The trait is, however, defined in the library, and the struct the trait should be implemented for is generated by prost (protobuf serialization lib) also in the library.
  2. I've tried to create a struct with callbacks
pub struct Callbacks {
  pub enable_command: Box<dyn Fn(&EnableCommand) -> ()>,
  pub disable_command: Box<dyn Fn(&DisableCommand) -> ()>,
}

and let user provide this struct on the initialization of the library. This is also not working, because the Box<dyn Fn()> is not Send, and I can't use it in the thread.

What am I missing here, or are there more elegant approaches to it?
I appreciate your help, I am quite new to Rust and may be not aware of common approaches for library development.

hardy flame
#

However, if you decide to go with 2, you need this:

pub struct Callbacks {
  pub enable_command: Box<dyn Send + Fn(&EnableCommand) -> ()>,
  pub disable_command: Box<dyn Send + Fn(&DisableCommand) -> ()>,
}
#

(+ Send can also go after the Fn part, but IIRC you need parens to disambiguate whether you mean the return type is send or the dyn is send)

#

(also, -> () can be omitted, it's the default)

full cove
#

Thank you @hardy flame !

I really like the solution 1 more. But how would I call the callback function from trait, when the implementation was on wrapper of the struct but I get the struct itself not the wrapper.

Example:

trait Callback {
  fn callback(&self) {}
}

// default impl
impl Callback for EnableCommand {}

fn respond_on(msg: &Message) -> Option<Response> {
  match msg.get_payload() {
    None => None,
    Some(payload) => {
        payload.callback();
        generate_response(&payload)
    }
  }
}

The library function "respond_on" doesn't know anything about the wrapper the user would implement the callback for, or is there a way?

hardy flame
# full cove Thank you <@319492293352620044> ! I really like the solution 1 more. But how wo...

Give me a moment, I may have misunderstood your use case. Let's see if I have it right now

  • you have a number of *Command types, that all want to implement Callback
  • however, how they implement Callback should be decided by the user
  • but also, it would be nice if there was a "default", so a user can use EnableCommand as a Callback even if they didn't write their own impl

Is this right?

#

If so, I'd probably not write defaults at all.

  • make the Callback trait
  • make an EnableCommand struct, that implements Callback with the default behaviour
  • if the user wants a different behaviour, they can make their own struct, implement Callback for it, and then pass you an instance of that struct
full cove
#

almost correct. the user doesn't call the callback him/herself, the library does. The only thing that user needs to do is to provide a callback before or during library initialization. So for instance when enable command is received, the user callback would switch the relay to turn on the light. Only user knows how to switch his/her relay thus the user callback.

The library just handles the communication, serialization, deserialization.
So it should call the user callback based on the command received. The commands are predefined.

#

So ideally the user would implement the Callback trait for the pregenerated structs, but that's not possible.
If the user provides me structs with the implemented callback trait, then it would be similar to Solution 2, where I don't receive a struct of function pointers, but a struct of structs with trait impl.

hardy flame
#

Alright. First question: do you want to be able to change callbacks at runtime?

full cove
#

no

hardy flame
# full cove no

Hmm. Can we do a bit of a hybrid?

Firstly, define a trait of all the callback functions. Give them all an empty default implementation

trait Callbacks {
  fn enable_command(user_id: String, transaction_id: String) {}
  fn disable_command() {}
  fn some_other_command(number: u32) {}
}
```Then, you do need to take a type from the user. Notably, since none of the callbacks take `self` in any form, this means you don't need an instance, just a type
```rs
struct YourThing<T: Callbacks> {
  ...
  _boo: PhantomData<T>,
}
```You can now refer to the callbacks by calling, say `T::enable_command(uid, tid)`.

The boilerplate the user would have to put up with is fairly minimal, too:
```rs
struct MyCallbacks;
impl Callbacks for MyCallbacks {} //this gets you all default impls, else override some
full cove
#

Agree that callbacks shouldn't take self.

So basically, it means user will provide the MyCallbacks struct as argument to the library init function, right?

hardy flame
full cove
#

And if I should pass it to another thread, I should also wrap the struct in Arc<Mutex<MyCallbacks>>

hardy flame
#

You'd have nothing to wrap

#

The only reason I have a MyCallbacks type is because a trait needs something to implement it, really this is just a "bundle" of functions

full cove
#

I kinda starting to get what you mean, but not fully 🙂
The question is how to take the MyCallbacks type from user?
Currently I have just an init function that can accept some arguments and spawns some threads.
How can user provide the type as argument to a function?

hardy flame
#

(have you encountered the turbofish operator before, or should I explain it?)

full cove
#

I've definitely used it with collect.
So it doesn't force us to provide an argument with this generic type, but just pass the type?

hardy flame
#

(since the type will probably be a ZST, you could also take it as an argument then throw it away, but that would be a bit unexpected)

full cove
#

This is cool 🙂 It was my missing piece.
Somehow making a box for every callback and passing it in the struct felt so unnatural. Passing just a type with functions makes more sense