Callback with Userdata

Hi,

I'm trying to implement the following callback system:

  1. A frame source (CAN bus) is created
  2. One or more devices are created and their frame handlers + userdata are registered as callbacks in the frame source instance
  3. Whenever a frame arrives, all/specific handlers are called with their userdata as argument

Currently my implementation is like this:

use std::any::Any;
use std::sync::{Arc, Mutex};
use std::{thread, time::Duration};

type Callback = fn(&[u8], Arc<Mutex<dyn Any>>) -> Result<(), Box<dyn std::error::Error + '_>>;

#[derive(Default)]
pub struct FrameSource {
    parser: Vec<(Callback, Arc<Mutex<dyn Any>>)>,
}

#[derive(Default)]
struct Device {
    data1: u8, 
    data2: u8, 
}

impl FrameSource {
    pub fn register_parser(&mut self, parser: Callback, callback: Arc<Mutex<dyn Any>>) {
        println!("registering callback");
        self.parser.push((parser, callback));
    }   

    pub fn process_callbacks(&mut self) -> Result<(), Box<dyn std::error::Error + '_>> {
        let tmp: [u8; 10] = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];

        println!("processing callbacks");

        for (parser, callback) in self.parser.iter() {
            match parser(&tmp, callback.clone()) {
                Ok(_) => (), 
                _ => {
                    println!("failed to process callback");
                }
            }
        };

        Ok(())
    }   
}

fn frame_processor(frame: &[u8], instance: Arc<Mutex<dyn Any>>) -> Result<(), Box<dyn std::error::Error + '_>> {
    let instance = instance.clone();
    let mut instance = instance.lock().unwrap();
    let instance = match instance.downcast_mut::<Device>() {
        Some(instance) => instance,
        None => return Err("failed to cast into Device instance".into())
    };  

    instance.data1 += frame[0];
    instance.data2 += frame[1];

    println!("frame_processor called, data1: {}, data2: {}", instance.data1, instance.data2);

    Ok(())
}

fn parser(frame: &[u8], data: Arc<Mutex<dyn Any>>) -> Result<(), Box<dyn std::error::Error + '_>> {
    frame_processor(frame, data)
}

fn main() {
    let mut frame_source = FrameSource::default();
    let device = Arc::new(Mutex::new(Device::default()));

    frame_source.register_parser(parser, device);

    loop {
        frame_source.process_callbacks().unwrap();
        thread::sleep(Duration::from_secs(1));
    }   
}

This is not very efficient as every frame_processor needs to unpack the userdata.

When I try to unpack it in the parser Rust will complain and I'm not sure how to fix this:

fn frame_processor(frame: &[u8], instance: &Device) -> Result<(), Box<dyn std::error::Error + '_>> {
    instance.data1 += frame[0];
    instance.data2 += frame[1];

    println!("frame_processor called, data1: {}, data2: {}", instance.data1, instance.data2);

    Ok(())
}

fn parser(frame: &[u8], data: Arc<Mutex<dyn Any>>) -> Result<(), Box<dyn std::error::Error + '_>> {
    let instance = data.clone();
    let mut instance = instance.lock().unwrap();
    let instance = match instance.downcast_mut::<Device>() {
        Some(instance) => instance,
        None => return Err("failed to cast into Device instance".into())
    };  

    frame_processor(frame, instance)
}

Error message:

error[E0106]: missing lifetime specifier
  --> src/main.rs:42:95
   |
42 | fn frame_processor(frame: &[u8], instance: &mut Device) -> Result<(), Box<dyn std::error::Error + '_>> {
   |                           -----            -------                                            ^^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `frame` or `instance`
help: consider introducing a named lifetime parameter
   |
42 | fn frame_processor<'a>(frame: &'a [u8], instance: &'a Device) -> Result<(), Box<dyn std::error::Error + 'a>> {
   |                   ++++         ++                  ++                                                   ~~

error[E0228]: the lifetime bound for this object type cannot be deduced from context; please supply an explicit bound
  --> src/main.rs:42:71
   |
42 | fn frame_processor(frame: &[u8], instance: &Device) -> Result<(), Box<dyn std::error::Error + '_>> {
   |                                                                       ^^^^^^^^^^^^^^^^^^^^^^^^^^

After adding the suggestion it still won't compile:

fn frame_processor<'a>(frame: &'a [u8], instance: &'a mut Device) -> Result<(), Box<dyn std::error::Error + 'a>> {

Error:

error[E0515]: cannot return value referencing local variable `instance`
  --> src/main.rs:59:5
   |
54 |     let instance = match instance.downcast_mut::<Device>() {
   |                          --------------------------------- `instance` is borrowed here
...
59 |     frame_processor(frame, instance)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

error[E0515]: cannot return value referencing local variable `instance`
  --> src/main.rs:59:5
   |
53 |     let mut instance = instance.lock().unwrap();
   |                        --------------- `instance` is borrowed here
...
59 |     frame_processor(frame, instance)
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ returns a value referencing data owned by the current function

Is there a way to unpack the userdata only once and then allow to write slim parsers?

Or maybe is there a completely other Rust-way to do it? The struct Device will change per registered processor, so it can be DeviceLed, DeviceButton etc.

Thanks in advance!
Sven

I don't think you want the error type to be tied to the lifetime of the Device? You get an error when downcasting to a device fails, not when it succeeds. Accordingly, just remove the + '_ lifetime annotation, and it compiles.

1 Like

Thank you @H2CO3 thats the solution. Now this was only an excerpt and I tried to find out why I introduced '_ in the first place. Turns out, I'm using let frame ? FrameStruct::parse(frame)?.1 which says that the ? requires:

returning this value requires that `'a` must outlive `'static`

I replaced it with unwrap but I guess I'll understand it in the future when having a deeper look into the ? logic.

Thanks a lot for your fast help!
Sven

I've looked up the difference and found the answer from daboross here: https://www.reddit.com/r/rust/comments/6astgn/what_are_the_rules_for_where_you_can_use_versus/

So basically ? just returns the error from Nom::parse and the error message seems to contain a reference to the parameters of the function. When I match myself and return Err("error".into()) I don't need the lifetime. After some trial and error this works for me when returning the Err returned by Nom:

fn frame_processor<'a, 'b>(frame: &'a [u8], instance: &'b mut Device) -> Result<(), Box<dyn std::error::Error + 'a>>

Yeah, because an error created from a mere string doesn't point anywhere (it contains the string), so it's 'static.

Note that you can infer the correct solution from the fact that it's a Nom error. Because Nom parses the frame, it wants to return an error that points into that frame (likely in order to report a precise error location). Thus, the error inherits the lifetime of frame, so the returned trait object does, too.

1 Like

Don't do this, unless you're writing FFI code that requires it. The "callback+userdata" pattern is only necessary in languages that don't have closures. Just store a boxed FnMut [1] callback and let the caller figure out how to provide the necessary state.

playground

In this example, you don't need Arc, Mutex or downcasting. If you need to support Fn and not only FnMut (but I don't see why you would, unless the signature of process_callbacks is wrong), that's where Arc<Mutex<>> might come in. (I've also "fixed" the dyn Error + '_ in a way that makes sense to me)


  1. since process_callbacks takes &mut self â†Šī¸Ž

3 Likes

@trentj Thanks for your suggestion. When I try to access the device data in the main loop it will fail because it was moved into the closure. I assume this doesn't work for me, because the data needs to be accessed by the callback and another thread. The callback should just translate and store the received data as fast as possible and then return to allow the next callback to run. The data processing is then done in another thread.

You can share the thing with a closure without having to infect the whole API. One example

2 Likes

Wow, thank you, it works! Getting rid of Any is very nice :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.