Introduction
Function pointers are used to reduce coupling between two pieces of code. A publisher defines a callback function signature and allows anonymous registration of a function pointer. A subscriber creates a function implementation conforming to the publisher’s callback signature and registers a pointer to the function with the publisher at runtime. The publisher code knows nothing about the subscriber code – the registration and callback invocation is anonymous.
Multicast callbacks permit two or more subscribers to register for notification via a callback. When a publisher invokes the callback, all registered subscriber functions get sequentially invoked.
This article provides a simple, type-safe multicast callback module implemented in C.
See GitHub for latest source code:
See related GitHub repositories:
Callbacks Background
Software systems are organized into different software modules. A module’s incoming interface is declared within an interface file. An outgoing interface can be expressed using function pointers registered and invoked at runtime. The publisher module notifies subscribers by invoking anonymous functions via a function pointer. “Anonymous” means the publisher code doesn’t include any subscriber header files. The subscriber knows about the publisher, but not vice versa. In this way, the publisher code module doesn’t change when a new subscriber wants to receive callback notifications.
For instance, say our system has an alarm module. It’s responsible for handling detected alarms. Now, other modules within the system may be interested in receiving alarm notifications. Maybe the GUI needs to display the alarm to the user. A logging module might save the alarm to a persistent storage. And an actuators module may need to stop motor movements. Ideally, the alarm module should not know about the GUI, logger or actuator modules. Instead, subscribers register with the alarm module for notifications so that when an alarm occurs, each subscriber alarm handler function is called.
Using the Code
I’ll first present how to use the code, and then get into the implementation details.
The MULTICASTX_DECLARE
macro exposes a multicast callback interface within a publisher header file. The “X
” is a numeric value indicating the number of function arguments within the callback. For instance, use MULTICAST2_DECLARE
if the callback function has two function arguments. From 0 to 5 arguments are supported by the library.
The first argument is the callback name. The other arguments are the function argument types. The macro below defines a function signature of void MyFunc(int, float).
MULTICAST2_DECLARE(MyCallback, int, float)
The MULTICASTX_DEFINE
macro implements the multicast callback interface within a publisher source file. The macro is placed at file scope. The macro arguments are the callback name, function argument types, and the maximum number of allowed registrars. In the example below, up to 5 function pointers can be registered.
MULTICAST2_DEFINE(MyCallback, int, float, 5)
The two macros fully implement a type-safe multicast callback interface. The macros automatically create three functions based on the macro arguments provided.
void MyCallback_Register(MyCallbackCallbackType callback);
void MyCallback_Unregister(MyCallbackCallbackType callback);
void MyCallback_Invoke(int val1, float val2);
A subscriber registers a callback using the Register()
function:
MyCallback_Register(&NotificationCallback);
Similarly a subscriber unregisters using the Unregister()
function:
MyCallback_Unregister(&NotificationCallback);
The publisher sequentially invokes all registered callbacks using the Invoke()
function.
MyCallback_Invoke(123, 3.21f);
SysData Example
SysData
is a simple module showing how to expose an outgoing multicast callback interface. SysData
stores system data and provided subscriber notification when the mode changes. The interface
is shown below:
#ifndef _SYSDATA_H
#define _SYSDATA_H
#include "multicast.h"
typedef enum
{
MODE_STARTING,
MODE_NORMAL,
MODE_ALARM
} ModeType;
MULTICAST1_DECLARE(SysData_SetModeCallback, ModeType)
void SysData_SetMode(ModeType mode);
#endif // _SYSDATA_H
The SysData
implementation is shown below:
#include "sysdata.h"
MULTICAST1_DEFINE(SysData_SetModeCallback, ModeType, 3)
static ModeType _mode = MODE_STARTING;
void SysData_SetMode(ModeType mode)
{
_mode = mode;
SysData_SetModeCallback_Invoke(_mode);
}
A subscriber connects to SysData
by creating a callback function and registering the function pointer at runtime.
void SysDataCallback1(ModeType mode)
{
printf("ModeCallback1: %d\n", mode);
}
int main(void)
{
SysData_SetModeCallback_Register(&SysDataCallback1);
SysData_SetMode(MODE_STARTING);
SysData_SetMode(MODE_NORMAL);
return 0;
}
Notice that SysDataCallback1()
is called each time SysData_SetMode()
is called. Also note that SysData
doesn’t know about the subscriber as the registration is anonymous.
Implementation
The implementation uses macros and token pasting to provide a type-safe interface for using multicast callbacks. The token pasting operator (##
) is used to merge two tokens when the preprocessor expands the macro. The MULTICAST1_DECLARE
macro is shown below.
#define MULTICAST1_DECLARE(name, arg1) \
typedef void(*name##CallbackType)(arg1 val1); \
void name##_Register(name##CallbackType callback); \
void name##_Unregister(name##CallbackType callback);
In the SysData
example used above, macro expands to:
typedef void(*SysData_SetModeCallbackCallbackType)(ModeType val1);
void SysData_SetModeCallback_Register(SysData_SetModeCallbackCallbackType callback);
void SysData_SetModeCallback_Unregister(SysData_SetModeCallbackCallbackType callback);
Notice every name##
location is replaced by the macro name argument, in this case, being SysData_SetModeCallback
from the declaration below.
MULTICAST1_DECLARE(SysData_SetModeCallback, ModeType)
The implementation macro is shown below:
#define MULTICAST1_DEFINE(name, arg1, max) \
static CB_Data name##Multicast[max]; \
void name##_Register(name##CallbackType callback) { \
CB_MulticastAddCallback(&name##Multicast[0], max, (CB_CallbackType)callback); } \
void name##_Unregister(name##CallbackType callback) { \
CB_MulticastRemoveCallback(&name##Multicast[0], max, (CB_CallbackType)callback); } \
void name##_Invoke(arg1 val1) { \
for (size_t idx=0; idx<max; idx++) { \
name##CallbackType callback = (name##CallbackType)CB_MulticastGetCallback
(&name##Multicast[0], max, idx); \
if (callback != NULL) \
callback(val1); } }
The expanded MULTICAST1_DEFINE
from the SysData
example results in:
static CB_Data SysData_SetModeCallbackMulticast[3];
void SysData_SetModeCallback_Register(SysData_SetModeCallbackCallbackType callback)
{
CB_MulticastAddCallback(&SysData_SetModeCallbackMulticast[0], 3, (CB_CallbackType)callback);
}
void SysData_SetModeCallback_Unregister(SysData_SetModeCallbackCallbackType callback)
{
CB_MulticastRemoveCallback(&SysData_SetModeCallbackMulticast[0], 3, (CB_CallbackType)callback);
}
void SysData_SetModeCallback_Invoke(ModeType val1)
{
for (size_t idx=0; idx<3; idx++)
{
SysData_SetModeCallbackCallbackType callback =
(SysData_SetModeCallbackCallbackType)CB_MulticastGetCallback
(&SysData_SetModeCallbackMulticast[0], 3);
if (callback != NULL)
callback(val1);
}
}
Notice the macro provides a thin, type-safe wrapper around CB_MulticastAddCallback()
and CB_MulticastRemoveCallback()
functions. If the wrong function signature is registered, the compiler generates an error or warning. The macros automate the monotonous, boilerplate code that you’d normally write by hand.
The callback functions are simply stored in an array. The Invoke()
function just iterates over the callback function array and invokes any element that is not NULL
.
The callback add/remove functions just store a generic CB_CallbackType
to be extracted and used by the boilerplate macro code.
#include "multicast.h"
#include <stdbool.h>
#include <assert.h>
void CB_MulticastAddCallback(CB_Data* cbData, size_t cbDataLen, CB_CallbackType callback)
{
bool success = false;
if (cbData == NULL|| callback == NULL || cbDataLen == 0)
{
assert(0);
return;
}
for (size_t idx = 0; idx<cbDataLen; idx++)
{
if (cbData[idx].callback == NULL)
{
cbData[idx].callback = callback;
success = true;
break;
}
}
if (success == false)
{
assert(0);
}
}
void CB_MulticastRemoveCallback(CB_Data* cbData, size_t cbDataLen, CB_CallbackType callback)
{
if (cbData == NULL || callback == NULL || cbDataLen == 0)
{
assert(0);
return;
}
for (size_t idx = 0; idx<cbDataLen; idx++)
{
if (cbData[idx].callback == callback)
{
cbData[idx].callback = NULL;
break;
}
}
}
CB_CallbackType CB_MulticastGetCallback(CB_Data* cbData, size_t cbDataLen, size_t idx)
{
if (cbData == NULL || cbDataLen == 0 || idx >= cbDataLen)
{
assert(0);
return NULL;
}
CB_CallbackType cb = cbData[idx].callback;
return cb;
}
Multithreading
The multicast library can be used on a single or multithreaded systems. If used on a system with operating system threads, it can be made thread-safe. The multicast.c file contains comments on where to place software locks. Once the locks are in place, the multicast API and macros are safe to deploy on a multithreaded system where subscribers that execute on another thread may register and unregister.
On a multithreaded system, if the subscriber code executes on a separate thread, then typically the subscriber's callback function implementation posts a message to a thread queue to be handled asynchronously. In this way, the publisher thread generating the callbacks doesn’t get blocked and the subscriber code will be able to handle the notification asynchronously via an OS message queue.
Conclusion
Multicast callbacks eliminates unnecessary code coupling between modules. Registering multiple callback function pointers offers a convenient notification system. The article has demonstrated one possible design capable of being uniformly deployed on a system. The implementation was kept to a minimum facilitating usage on any system embedded or otherwise. A small amount of macro code automates boilerplate code and gives the C library type-safety.
History
- 9th June, 2017: Initial release