Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 10 of 10

Thread: CallbackHelper - fun with modern callbacks

  1. #1
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,892

    CallbackHelper - fun with modern callbacks

    Inspired by this Thread https://forum.pjrc.com/threads/70986...ht-C-callbacks, I tried how difficult it would be to implement a more modern callback API for hardware interrupts and user classes.

    The simplest and usually recommended solution is to use std::function. While std::function is very efficient, it has a large memory footprint (10s of kB) and it uses dynamic memory allocation. Both can be considered problematic, especially for smaller boards (e.g. T3.2, TLC).

    It turned out that a much simpler implementation can be done with surprisingly little effort. It probably doesn't cover all edge cases but it definitely works fine for the standard use cases. I.e., callbacks of type
    1. Free function
    2. static member function
    3. non static member function
    4. functors
    5. capturing and non capturing lambda expressions
    6. callbacks which pass variables to the caller (i.e. callbacks of types like void(*cb)(int x, double f) etc. of course this also works for lambdas and functors)
    7. No dynamic memory allocation and a really lightweight footprint (on a LC the IntervalTimerEx example adds some 2kB flash and some 340 bytes RAM, after replacing the printf's with println's)

    (Of course Nr. 6 is not so important for hardware interrupts but can be useful for user classes. E.g., the EncoderTool provides a callback of type void(*onEncoderChanged)(int value, int delta) which the library calls whenever the encoder value changes. For convenience this callback gets the current position and the delta to the last position passed in the variables value and delta).

    I packed all the template stuff in the helper class CallbackHelper (https://github.com/luni64/CallbackHelper) which can be used in user libraries. Usage is quite simple, no template wizardry required. To transform any of types from the list above to a callback, basically all one needs to do is the following:
    Code:
    //...
    callbackHelper_t cbh;  // construct a callbackHelper 
    //...
    callback_t* callback = cbh.makeCallback(...);  // ... is anything callable
    //...
    callback->invoke();  // invoke the callback later, e.g. from a hardware ISR.
    Here a working example showing some possibilities:
    Code:
    #include "CallbackHelper.h"
    
    // setup the callback helper to handle up to 5 void(*)(void) callbacks
    using callbackHelper_t = CallbackHelper<void(void), 5>;
    using callback_t       = callbackHelper_t::callback_t;
    
    callbackHelper_t cbh; // construct the callback helper 
    callback_t* callback; // storage for a pointer to the generated callback
    
    
    void freeFunction()
    {
        Serial.println("Free function callback");
    }
    
    void setup()
    {
        // generate a callback from freeFunction, store it in slot 0 (out of 5) and
        // store a pointer to it in callback:
         callback = cbh.makeCallback(freeFunction, 0);
       
       // use a lambda instead:
       //callback = cbh.makeCallback([] { Serial.println("lambda"); }, 0);
    }
    
    void loop()
    {
        callback->invoke();
        delay(100); 
    }
    The repository (https://github.com/luni64/CallbackHelper) contains more usage examples and a proof of concept implementation of a PIT timer using the CallbackHelper. Additionally, I switched my IntervalTimerEx from an std::function API to the CallbackHelper. (https://github.com/luni64/IntervalTimerEx).
    Both, IntervalTimerEx and CallbackHelper are listed in the Arduino and PIO library managers

    I'll switch my other libraries (TeensyTimerTool, EncoderTool) to the CallbackHelper later.

    Hope this is of any use and hasn't too much bugs :-)
    Last edited by luni; 09-06-2022 at 09:19 PM.

  2. #2
    Senior Member+ mjs513's Avatar
    Join Date
    Jul 2014
    Location
    New York
    Posts
    8,406
    @luni

    Thank you for this - was wondering how to make it free standing - when I tried before I failed - probably because I didn;t know what I was doing.

  3. #3
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,892
    Quote Originally Posted by mjs513 View Post
    @luni

    Thank you for this - was wondering how to make it free standing - when I tried before I failed - probably because I didn;t know what I was doing.
    I also had a lot of failures and super complicated code until I slowly understood how that stuff works. At the end it shrank down to just a few lines to get this amazing flexibility. But that's the fun of playing around with that stuff, right?

  4. #4
    Senior Member+ mjs513's Avatar
    Join Date
    Jul 2014
    Location
    New York
    Posts
    8,406
    Quote Originally Posted by luni View Post
    I also had a lot of failures and super complicated code until I slowly understood how that stuff works. At the end it shrank down to just a few lines to get this amazing flexibility. But that's the fun of playing around with that stuff, right?
    Yep otherwise don't think we would do all this. Now I have something else to add to my list of things to try on top of 1.58

  5. #5
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,892
    I meanwhile switched attachInterruptEx from std::function to the CallbackHelper as well. As expected, it reduces the memory footprint massively. @Paul, unfortunately my scope is way to slow to do that speed comparison between the standard function and my pimped version. Would be very much interested if the CallbackHelper version is as fast as the old std::function version (see here: https://forum.pjrc.com/threads/70986...l=1#post312022).

    Status:


    There probably are hidden bugs. So, if someone wants to try that stuff I'd be very interested in test results and general feedback on the usability.

  6. #6
    Senior Member+ mjs513's Avatar
    Join Date
    Jul 2014
    Location
    New York
    Posts
    8,406
    @luni

    Had a chance to play with this a bit and light bulb beginning to come on but still dim . What I did was take Paul's original example using delegates and convert it to your callback - more just so I have something to play with - helps in understanding. So once I figured out your slots thing I gave it a try. Here is my test sketch:
    Code:
    #include "Arduino.h"
    #include "CallbackHelper.h"
    
    void hello_function() {
      Serial.println(" Hello World static function");
    }
    
    class my_class {
    public:
      my_class(int _i) {
        i = _i;
      }
    
      void set(int _i)
      {
          i = _i;
      }
    
      void hello(int f) { Serial.printf(" Hello World member, private i=%d\n", i*f); }
    
    private:
      int i;
    
    };
    my_class my_inst(42);
    
    //------------------------------------------------------
    
    // some aliases to save typing
    using callbackHelper_t = CallbackHelper<void(void), 1>; // handles 5 slots for void(void) callbacks
    using callback_t       = callbackHelper_t::callback_t;  // type of the callbacks
    
    callbackHelper_t cbh; // helps to generate callbacks from various parameters (function pointers, lambdas, functors...)
    callback_t* cb;    // array to store pointers to the generated callbacks
    callback_t* cb1;
    
    void setup() {
      Serial.begin(9600);
      while(!Serial);
      if (CrashReport) Serial.println(CrashReport, 0);
    
      delay(5000);
    
      // normal functions
      Serial.println();
      cb = cbh.makeCallback(hello_function, 0);
      cb->invoke();
    
      // member functions
      Serial.println();
      cb1 = cbh.makeCallback([] { my_inst.hello(100); }, 0); 
      cb1->invoke();
    
      //I like this
      Serial.println();
      for(uint8_t i = 0; i < 10; i++) {
        my_inst.set(i);
        cb->invoke();
      }
    
      // lambda functions
      Serial.println();
      cb = cbh.makeCallback([]{ Serial.println(" Hello World lambda"); }, 0 );
      cb->invoke();
    
      Serial.println();
      cb = cbh.makeCallback([] { Serial.printf("non capturing lambda\n"); }, 0); // simple, non capturing lambda expression -> callback_t
      cb->invoke();
    
      Serial.println("\nend of setup");
    }
    
    void loop() {
    }
    and here is the output:
    Code:
     Hello World static function
    
     Hello World member, private i=4200
    
     Hello World member, private i=0
     Hello World member, private i=100
     Hello World member, private i=200
     Hello World member, private i=300
     Hello World member, private i=400
     Hello World member, private i=500
     Hello World member, private i=600
     Hello World member, private i=700
     Hello World member, private i=800
     Hello World member, private i=900
    
     Hello World lambda
    
    non capturing lambda
    
    end of setup
    Rather simplistic but it does work. Wondering if you are planning on doing something like cb->delete???? Not really important but just wondering.

  7. #7
    Senior Member
    Join Date
    Apr 2014
    Location
    Germany
    Posts
    1,892
    Great, to use it more efficiently it might be good to know that you can reserve more than one slot
    Code:
    using callbackHelper_t = CallbackHelper<void(void), 10>;
    would give you 10 slots for callbacks. You'd store them in a array of pointers to callback_t to use them later

    Code:
    callback_t* myCallbacks[10];
    //...
    myCallbacks[0]cbh.makeCallback(..., 0);
    myCallbacks[1]cbh.makeCallback(..., 1);
    ...
    then you can do later (e.g. in loop)
    Code:
    myCallbacks[1]->invoke();
    Code:
    Wondering if you are planning on doing something like cb->delete???? Not really important but just wondering.
    One could, on the other hand, since this is all on the stack, one can simply overwrite a slot. No need to delete it first. Or do you have some use case in mind I overlooked?

  8. #8
    Senior Member+ mjs513's Avatar
    Join Date
    Jul 2014
    Location
    New York
    Posts
    8,406
    One could, on the other hand, since this is all on the stack, one can simply overwrite a slot. No need to delete it first. Or do you have some use case in mind I overlooked?
    cool - good to know - makes it redundant to use delete. No didn't have anything in mind just wondering since you had an invoke

    Great, to use it more efficiently it might be good to know that you can reserve more than one slot
    Noticed that in your example. Did use that approach on purpose. Wanted to try and dup the example and just help me to get a grasp on what was going on.

    Haven't really generated any callbacks in my code - just use what Kurt and Paul use in MTP and USBHost. So now just getting my head around callbacks and lamdas. Found a nice site on lamdas that was starting to read but got sidetracked: https://dzone.com/articles/all-about...Expression.%20

  9. #9
    Senior Member
    Join Date
    Dec 2016
    Posts
    164
    Quote Originally Posted by luni View Post
    I meanwhile switched attachInterruptEx from std::function to the CallbackHelper as well. As expected, it reduces the memory footprint massively. @Paul, unfortunately my scope is way to slow to do that speed comparison between the standard function and my pimped version. Would be very much interested if the CallbackHelper version is as fast as the old std::function version (see here: https://forum.pjrc.com/threads/70986...l=1#post312022).

    Status:


    There probably are hidden bugs. So, if someone wants to try that stuff I'd be very interested in test results and general feedback on the usability.
    Very nice work luni.

    @Paul : If you see a change to run the speedtest...

  10. #10
    Senior Member PaulStoffregen's Avatar
    Join Date
    Nov 2012
    Posts
    26,799
    No, haven't looked at this yet. Still going though gcc 11.3.1 stuff. Will probably package up 1.58-beta2 before I do anything with this or other new features.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •