CallbackHelper - fun with modern callbacks

Status
Not open for further replies.

luni

Well-known member
Inspired by this Thread https://forum.pjrc.com/threads/70986-Lightweight-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:
@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.
 
@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?
 
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 :)
 
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-Lightweight-C-callbacks?p=312022&viewfull=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.
 
@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.
 
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?
 
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-abou...unctions. ... 6 Constexpr Lambda Expression.
 
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-Lightweight-C-callbacks?p=312022&viewfull=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...
 
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.
 
Status
Not open for further replies.
Back
Top