0

I created a small library, which takes a callback function:

MyClass::add_callback(bool (*callback_function)(), byte behaviour) {
  // ...
  bool result = callback_function();
  // ...
}

So far, I'm using it for periodic polling (for which I don't want to use ISR interrupts). However, I like to use it for other periodic calls that require a state. A simple example is a blinking light, which has an on-off state, as well as knowledge when the next state change needs to occur.

I like to change this method or add another method that takes a method, instead of a function. How should I do this? I'm struggling with the C basics, but answers on StackOverflow seem foccused to more modern C variants. For example, this great answer on Stackoverflow recommends to use boost::bind, but I rather not include such a large dependency.

What I'm looking for is something akin to:

MyClass::add_callback(void *obj, bool (*callback_method)(), byte behaviour) {
  // ...
  bool result = Object::callback_method(&obj);
  // ...
}

where Object::callback_method(&obj); should be equivalent to Object obj; bool result = obj.callback_method();.

For bonus points, is it possible to make this independent on the Object class?

I'm happy for pointers to other sites (like StackOverflow), provided it is applicable to a bare Arduino program.

1
  • In case you wonder why I'm not using ISR, I'm a bit of a novice with the Arduino and C. My hardware is a ftDuino, and the Ftduino library uses timers and interrupts on the ATmega32u4, and I want to avoid conflicts for now.
    – MacFreek
    Mar 25, 2021 at 18:36

1 Answer 1

3

The easiest method I know to do this would be to use a "C"-style wrapper for the member function call. This is in fact done to interface C callbacks with C++ objects.

You could do it like this:

struct Blinky {
  //state etc.
  bool isOn() { return /* something useful */ true; }
};

// The "C" wrapper for the callback
bool Blinky_callback(void *vblinky)
{
  return static_cast<Blinky*>(vblinky)->isOn();
}

struct MyClass {
  void add_callback(void *obj, bool (*callback_method)(void *), byte behaviour) {
    // ...
    bool result = callback_method(obj);
    // ...
  }
};

// Globals
MyClass c;
Blinky b;

void setup() {
  c.add_callback(&b, Blinky_callback, 0);
}

A slightly tidier way with a static member (functionally identical):

struct Blinky {
  //state etc.
  bool isOn() { return /* something useful */ true; }

  static bool callback(void *vblinky) {
    return static_cast<Blinky*>(vblinky)->isOn();
  }
};

// ...
c.add_callback(&b, Blinky::callback, 0);

Other variants would be using inheritance and virtual functions.

// Abstract base class
struct Sensor {
  virtual bool do_check() = 0;
};

struct Blinky : public Sensor {
  bool do_check() override { return true; }
}

Then you can have your add_callback look like:

void MyClass::add_sensor(Sensor *sensor) {
  // ...
  bool result = sensor->do_check();
  // ...
}

This is relatively nice to code with, and the compiler has a fighting chance at keeping your code typesafe, if all the "things" you want to control can conform to the same interface. The virtual dispatch won't cost much more (if anything) than the static wrapper in the previous example. You "pay" a few bytes per class and one pointer per instance for the virtual table though.

More modern techniques would involve templates and/or lambdas, or perhaps std::function, but all that seems overkill if your need is as described (and might generate a lot more code than you're expecting, or do dynamic allocations for std::function which is best avoided in context).


Remark: if you want to do it with (non-static) member function pointers, it is possible but the syntax is hairy:

void add_callback(Blinky *obj, bool (Blinky::*callback_method)(), byte behaviour) {
  // ...
  bool result = (obj->*callback_method)();
  // ...
}

And you're tied to that class. Unless you make add_callback a template, but beware extra codegen.

8
  • Thanks for such an extensive answer so quickly. I'll have a look and mark as read once I fully grasp it. At least it give me some good pointers. I seem to be struggling a bit which level of abstraction to use for microcontrollers. Turning back to C after a rather long time of Python and friends, it's interesting to see how poor C's memory management is, and how poor Python's instance protection are. Your answer helps me understand what sort of introspection I can expect from C, especially on an Arduino platform with few high-level libraries.
    – MacFreek
    Mar 25, 2021 at 19:35
  • @MacFreek: none of the code I posted (or that you posted) is C. It is all C++. (And C++ is not modern C, they are two different languages). Look up "RAII" for how you should deal with memory management/object lifetime management. A lot of what you'll find won't be a great fit for small embedded projects, but the core idea is important. (The acronym sucks though.)
    – Mat
    Mar 25, 2021 at 19:43
  • You are right. I used "C" because my Arduino code mostly uses plain functions and globals, rather than classes (which is fine for this purpose).
    – MacFreek
    Mar 25, 2021 at 20:12
  • I'm not sure if this is a C vs. C++ syntax, or historic/modern constructs, but I noted that the Arduino IDE allows archaic constructs. For example, you can leave out the this-> in methods, or leave out the std:: prefix for type definitions. I found the hard way when delay() in my method did not call the Arduino function, but this->delay(). Would there be a way to make the IDE more strict, and only allow recommended constructs, so I only learn things the right way?
    – MacFreek
    Mar 25, 2021 at 20:15
  • Thanks for your code examples. I now understand that I need a wrapper function, preferably in the stateful class with the called method. I was trying to solve it in the calling instance, without wrapper.
    – MacFreek
    Mar 25, 2021 at 20:17

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.