-
Notifications
You must be signed in to change notification settings - Fork 57
Advanced topics callbacks
A crucial factor in the design of the Wiselib is the implementation of the general callback structure. That is, an arbitrary class A can register itself in another class B to get informed on particular events (incoming radio message, timer elapsed, ...). Then, B calls a commonly agreed method of A. Since such functionality is needed in multiple parts of the Wiselib, even in time critical parts, it is important to design a very efficient approach. Efficiency here means both time and memory consumption.
An obvious solution would be to create a base class with an (abstract) member function, and classes derived from this base can be passed to another one that can call the implemented member function. Unfortunately, this approach requires the introduction of virtual member functions, which in turn lets the C++ compiler create a vtable for concerned classes. Such a vtable brings certain disadvantages, at least when dealing with Embedded Systems. First, there is the additional amount of code space that is required for the vtable. Second, the execution time is slowed down, because the vtable contains function pointers that must be looked up on each call of a virtual method. Hence, the general callback structure in the Wiselib should not use the virtual base class approach, but must still be generic to allows arbitrary classes to be registered for callback events.
The solution for generating callbacks in the Wiselib is using so called delegates. They are a solution introduced in C# for holding function pointers in object oriented programming (and thus cover also member function pointers), but can also be implemented with standard C++ syntax. An overview is given on the wiki page giving background information for Delegates. We use the approach of Sergey Ryazanov, presented on a Code Project website.
The basic idea is to create a templated class that holds an object pointer of type void, and a stub pointer that points to a static template function. For example, a (here not templated) delegate class that is used for functions with return type void, and expecting exactly one parameter of type int looks as follows.
class delegate { ... typedef void (*stub_type)(void* object_ptr, int); void* object_ptr; stub_type stub_ptr; ... };
The object pointer is used for storing a pointer to the class that contains the member function that is supposed to be called. The stub pointer is a templated function that calls the chosen member function of the object pointer:
template <class T, void (T::*TMethod)(int)> static void method_stub(void* object_ptr, int a1) { T* p = static_cast<T*>(object_ptr); return (p->*TMethod)(a1); }
The method uses a static cast to cast the object pointer, which is a void pointer, to a pointer of the corresponding class that in turn is given as a template parameter. Then, the defined member function TMethod of that class is called by passing the given parameters (here, its only the integer value a1). So, there is one new method per class and used member function created, which may seem to be a meaningful waste of program memory. However, since this method is written in a header file and inline, it can be completely resolved at compile time, and thus should not produce any overhead. So far, our evaluation with different compilers has shown that there is no overhead produced.
The stub pointer is created via the static member function from_method of the delegate class:
template <class T, void (T::*TMethod)(int)> static delegate from_method(T* object_ptr) { delegate d; d.object_ptr = object_ptr; d.stub_ptr = &method_stub<T, TMethod>; return d; }
In from_method, a new delegate is created with setting the object pointer to the passed parameter (usually a this-pointer when called from another class), and the stub pointer to the address of the above given templated method_stub. The last missing part is the invocation of a delegate. This is done via the operator (), and is implemented as follows.
void operator()(int a1) const { return (*stub_ptr)(object_ptr, a1); }
The stub pointer that in turn points to the static member function method_stub is called with the object pointer as argument, followed by the parameters that should be passed to the registered method.
The above code is completely hidden as a Wiselib utility, and can very easily used as follows. First, a class must provide a method for registration of member functions. For example, the method for registering a callback on received messages in a routing algorithm:
... template<class T, void (T::*TMethod)(node_id_t, size_t, block_data_t*)> inline void reg_recv_callback( T *obj_pnt ) { cbs_.push_back( routing_delegate_t::from_method<T, TMethod>( obj_pnt ) ); } ...
This can be easily used from an application or another routing algorithm:
the_routing_.reg_recv_callback<MyApp, &MyApp::rcv_routing_message>(this);
Another example is the registration of a timer event in the external interface. The registration method may look as follows.
template<typename T, void (T::*TMethod)(void*)> int set_timer( milli_type_t millis, T *obj_pnt, void *userdata ) { ... }
Then, one can add a timer event as follows.
timer_->template set_timer<self_type, &self_type::timer_elapsed>( 15, this, 0 );
The syntax may seem a little bit odd, but is correct. The Timer::template is used to tell the compiler that the set_timer-method is a templated method, which must be done in some circumstances.