I'm playing with object oriented firmware for embedded controllers.
In this blog I review object oriented callbacks. How to invoke the correct function of the correct object.
I've been using the C / C++ mix for a decent time now, with good results. During the blog series, I focused on getting things compiling and working. This was partly based on getting C++ work on an environment that's C focused, but a toolchain that supports C++. Then, as proof of concept, port parts of the MBED library. When I tried to get the project to compile, I used a single MBED class in my project, then pulled in any missing dependencies until no compile errors left. For those things I didn't find a fast source for, I created a mock implementation in a file called missing.h Then test and work further until that class was fully functional, and missing.h was empty. I didn't work clean then. I ported without deep analysis.
That analysis is the second step. Run the code, try to understand what parts are called, what is ballast from copying over. For easy classes, like DigitalI/O and SPI, that's straightforward. These are thin layers over hardware. The complex ones are the interrupt and timeout implementations. Because they use callbacks. The MBED callback functionality was complex at the time I ported. They have simplified it since. But I ended up with a 4000 line API header. With a lot of code there because it helped to pass compile, but I did not understand. I've now cleaned that down, resulting in an approx. 100 lines OO callback |
The goal of this callback API is to allow "something" to call a function of an object.
This is powerful. An object can have rich context. Performing an operation on it takes a lot of bookkeeping out of your program.
In my examples, callbacks are used when timers (timeouts) fire and when interrupts happen.
You can hook a particular object, and one of its methods to an interrupt or timeout. And when that triggers, it calls the method on that object.
I kept it simple as that. No more context, no parameters.
MBED in pre-5 versions tried to make it as flexible as possible, but that resulted in several thousand lines of templated code.
Since version 5 they have taken a different approach. I took it more to the extreme by just dealing with this single scenario above. Focus.
I am using object pointers cast to uint32_t values. Then cast those back to object pointers later. Some people will frown.
I've used the same template constructs of pre-version5 MBED:
template <typename F> class Callback; template <typename R> class Callback<R()> { // ... }
Constructors and destructors. It supports direct creation with a known object and method, or the option to attach object and method later.
The last constructor allows to attach an object, instead of an object pointer cast to an uint32_t.
public: /** Create a Callback with a static function * @param func Static function to attach */ Callback(R(*func)() = 0) { if (!func) { memset(this, 0, sizeof(Callback)); } else { generate(func); } } /** Attach a Callback * @param func The Callback to attach */ Callback(const Callback<R()> &func) { if (func._ops) { func._ops->move(this, &func); } _ops = func._ops; } /** Destroy a callback */ ~Callback() { if (_ops) { _ops->dtor(this); } } /** Create a Callback with a member function * @param obj Pointer to object to invoke member function on * @param method Member function to attach */ template<typename T, typename U> Callback(U *obj, R(T::*method)()) { generate(method_context<T, R(T::*)()>(obj, method)); } private: // Dynamically dispatched operations const struct ops { R(*call)(const void *); void (*move)(void *, const void *); void (*dtor)(void *); } *_ops;
And as last, the methods and members that help with remembering what object and method to call, and then invoking that ...
public: // ... /** Call the attached function */ R call() const { return _ops->call(this); } /** Test if function has been attached */ operator bool() const { return _ops; } private: // Generate operations for function object template <typename F> void generate(const F &f) { static const ops ops = { &Callback::function_call<F>, &Callback::function_move<F>, &Callback::function_dtor<F>, }; memset(this, 0, sizeof(Callback)); new (this) F(f); _ops = &ops; } // Wrappers for functions with context template <typename O, typename M> struct method_context { M method; O *obj; method_context(O *obj, M method) : method(method), obj(obj) {} R operator()() const { return (obj->*method)(); } }; // Function attributes template <typename F> static R function_call(const void *p) { return (*(F *)p)(); } template <typename F> static void function_move(void *d, const void *p) { new (d) F(*(F *)p); } template <typename F> static void function_dtor(void *p) { ((F *)p)->~F(); } // ... }
... and a helper template (I vastly reduced the ones available in MBED to 1):
/** Create a callback class with type infered from the arguments * * @param obj Optional pointer to object to bind to function * @param method Member function to attach * @return Callback with infered type */ template<typename T, typename U, typename R> Callback<R()> callback(U *obj, R(T::*method)()) { return Callback<R()>(obj, method); }
I will have to do a follow up post that shows the difference between the constructors that take a class and method, and those that take an object and method.
For now, it suffices too say that both can do the same.
But the first one requires the callback method to be static and know some internals.
The latter works with common member functions and is object-aware by nature.
License |
---|
This is heavily based on MBED library source. /* mbed Microcontroller Library * Copyright (c) 2006-2015 ARM Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ |
Top Comments