I'm experimenting: can I use a method of a particular object as a callback? This can be used in typical callback situations, e.g.: in a UART driver. But also in interrupt handlers.
- lightweight: small runtime cost, low firmware hit. Can be used for tiny embedded systems.
- reusable: don't assume parameters and types for the handler, when writing this little framework -> C++ templates
- hip -> object oriented, modern C++ constructs
- type safe -> again C++ templates
- allow a classic C function, a lambda, a static class method or an object method as handler
Callback template class
The template callback class. This is in essence the whole mechanism. A generic class that allows you to register and call event handler functions, lambda's and methods.
#include <functional> template <typename R, typename... Args> // restrict to arithmetic data types for return value, or void requires std::is_void<R>::value || std::is_arithmetic_v<R> class Callback { public: Callback() : _callback(nullptr){} inline void set(std::function<R(Args... args)> callback) { _callback = & callback; } inline void unset() { _callback = nullptr; } /* * R can either be an arithmetic type, or void */ inline R call(Args... args) { if constexpr (std::is_void<R>::value) { if (_callback == nullptr) { return; } (*_callback)(args...); } if constexpr (! std::is_void<R>::value) { if (_callback == nullptr) { return 0; // R can only be a arithmetic type. 0 should work as default. } return (*_callback)(args...); } } private: std::function<R(Args... args)> *_callback; };
This generic class allows for handlers that pass any amount of parameters. No assumption of the types.
When we use the class in our code, we 'll tell it what the parameters are, and their type. For this blog, I am going to pass it 2 integers. The return value is also an integer.
Callback<int, int, int> cb;
In the declaration above, the first int is the return value type, the 2 others are parameter 1 and 2. Once you write this code, the object cb will only accept handler methods that take two integers as parameter, and return an integer.
Example of class that will handle the callback
This could be a gio class (blinky a led when an interrupt occured?), a gui widget, or a business object. Typical guidelines apply: a handler should be swift. In particular if it's an interrupt handler.
class MyClass { public: int handler(int num1, int num2) { return num1 + num2; } };
The example does not do a lot today. It just sums up the two integers passed. But as a POC this will do.
Actual code using the two
#include <cstdio> int main() { MyClass myClass; Callback<int, int, int> cb; // Use a lambda to capture myClass and call the member method cb.set([&myClass](int num1, int num2) -> int { return myClass.handler(num1, num2); }); int a = 4; int b = 5; int o = cb.call(a, b); printf("Value: %i\n", o); // We might be on an embedded system, use printf() and not std::cout }
Why use a lambda to pair the handler and Callback ? In the code, I did not learn the MyClass how to register itself. And the Callback doesn't know anything about who or what it 'll call. These classes (and their objects) are unaware of each other. Loose coupling.
In the application logic, I tie the two together. The lambda function code (the anonymous function, starting with the [, and ending with the } above), is used as the vehicle to hold the code that the Callback object will execute when needed. The lambda function could as well be a straigtforward piece of code, not related to any object:
Or we could pass completely different executable code. As long as the lambda matches the signature given when instantiating the Callback class. |
In an embedded system,
- myClass would be one of your application objects.
- cb could be part of a driver, or the HAL layer
- cb.set() would be called by you, to register the handler
- cb.call() would be done by the driver, or HAL ISR, when it wants to notify you (call your callback handler
The image suggests that the SCPIParser class registers its callback handler. Looking back, I would not do it like that. I'd link the handler and the callback in the application's setup code. So that neither SCPIParser nor USBDriver need to know (of) each other. And that's how I ended up doing it in the example code of this post.
In the case of this example, it will execute myClass.handler(). If you 'd have many MyClass objects, it would call the exact one that's registered in with set().
Anything special?
This approach allows to write a reusable callback system. It is type safe. But it does not assume what the type is of the object it 'll call the handler off. (Or if it's actually an object).
It's a small amount of code, that performs a function that you regularly need in an embedded or event driven design.
What does type-safe mean in this context? If your interrupt handler assumes a function that passes 2 integers, and you pass it a callback function that has different parameters, And you get all of that, without having to specify the number of variables, and their types, in the Callback class code. We get a reusable class. Type safe. Without the need to foresee all possible combinations. |
this library now also works for Arduino (tested with IDE2, and an Arduino MKT GSM 1400)
For Arduino, you can download callbackmanager.h to your computer, then use Sketch -> Add File ... to get the callbackmanager included to your project.
I added a sketch to test the callback manager to this gist: test_arduino.ino
|
shortcuts:
- I assume fixed return value type. A richer implementation can use template for that too, and should allow void. edit: this works
- I pass arguments by value. A better design would also allow pass by reference. And to pass const references. edit: this works
- should I use a shared pointer, so that I can never call the handler of an object that no longer exists? (solved by alternative: provide an unset() method).
- it only allows to call one handler. Good for ISR use. But in an event driven design, you may want to register more handlers.
Inspiration:
- I started from this example by Geoffrey Hunter.
- mbed's Callback class, although I didn't use any of the code
- cplusplus' tutorial for std::function, to validate if it works for static class member, object member, classic C function and lambda function.
Code is available on Gist.
For Pico C/C++ SDK users, I attached an archive with a callmanager submodule. It contains the header file and a CMake file.
Expand it in your project, and register the module in your own CMake file:
add_subdirectory(callbackmanager)
# ...
target_link_libraries(${CMAKE_PROJECT_NAME} pico_stdlib callbackmanager)
I test this code in C++ callbacks and templates: make the Callback class call an object member, a static member, a C function and a pure lambda function