Improving Your Callback Game

Callbacks abound in embedded system design. You can find all sorts of use cases:

  • Registering for a notification when a specific event happens (e.g. starting to record video)
  • Registering for a notification when an asynchronous event has completed (e.g. async usb transfer complete callback, DMA transfer complete callback)
  • Callbacks as handlers for interrupt, timer, and other OS services

Many of the C implementations that I have encountered throughout my career are very simplistic: they only allow for one pointer to be registered! For example:

typedef void (*func_ptr_t)(uint32_t);
static func_ptr_t cb_ptr_ = NULL

int register_cb(func_ptr_t my_func)
{
     int r = -1;

     if (cb_ptr_ == NULL)
     {
          cb_ptr = my_func;
          r = 0;
     }
     //else - already in use! Fail!

     return r;
}

If we are limited to C, the easiest way to expand upon this basic callback registration implementation is to use a linked list library and maintain a list of callbacks. This approach requires the usual linked list overhead: translating back and forth between the list pointer and the container struct.

Let's see how C++ can help us fix this with very little typing.

std::vector

What's the simplest way to maintain a list of callbacks? Manage it with std::vector!

With std::vector, you can simply use one function to push the callback onto the list:

std::vector<cb_t> callbacks_;

// Register a callback.
void register_callback(const cb_t &cb)
{
    // add callback to end of callback list
    callbacks_.push_back(cb);
}

Storing Function Pointers

C++11 provides a std::function type. This type can be considered a safer version of a function pointer, and can reference any type of Callable target.

Instances of std::function can store, copy, and invoke any Callable target -- functions, lambda expressions, bind expressions, or other function objects, as well as pointers to member functions and pointers to data members.

Like many of the C++11 features that I enjoy, std::function provides much better type safety than a simple raw function pointer. You can't call the function with the wrong type/number of arguments!

In all of the examples provided here, I use the following typedef for cb_t:

typedef std::function<void(uint32_t)> cb_t;

This means that cb_t will accept all function signatures that take a uint32_t as input and do not return a value.

Examples of Registering Functions with C++11

As the C++ spec detailed above, the beauty of using std::function is that you can handle any type of function object you can think of.

Static Functions

Since the instance pointer is not required, registering a static class method is about as simple as can be. Consider this simple class:

class Client {
public:
    static void func(uint32_t v) const
    {
        printf("static instance member callback: %u\n", v);
    }
};

Registering the callback simply requires referencing the function pointer of the static member function:

register_callback(&Client::func);

Lambda functions

Lambda functions are also called "anonymous functions". They can be defined at any point and are not tied to a function name. It is often useful to create small lambda functions for simple purposes, such as running code on a dispatch queue or registering a callback.

Thanks to std::function, we are able to use lambda functions as a callback:

// register a lambda function as a callback
register_callback(
    [](uint32_t v) {
        printf("lambda callback: %u\n", v);
    });

Instance Member Functions

std::bind is another C++11 addition. std::bind allows you to create a std::function object that acts as a wrapper for the target function (or Callable object). std::bind also allows you to keep specific arguments at fixed values while leaving other arguments variable.

std::bind also allows you to create a function object using both the instance and method pointers! This grants you the ability to specify a specific object's member function as the callback function.

Consider this simple class below:

class Client {
public:
    void func(uint32_t v) const
    {
        printf("instance member callback: %u\n", v);
    }
};

If we wanted to register a callback to a specific client object's func, we should use the following:

Client c;
register_callback(std::bind(&Client::func, &c, std::placeholders::_1));

We are binding Client::func with the instance pointer (&c). The std::placeholders::_1 argument notes that we intend for the uint32_t input to remain a variable value.

C functions

What if you wanted to interface some C functions with your C++ callback?

Simply denote the function as extern "C":

extern "C" void c_client_callback(uint32_t v);

And register the callback normally:

register_callback(&c_client_callback);

If you need to register the callback code from a C directly, you will need to provide a C shim function for your C++ code.

Supporting Unregister Functionality

Supporting an unregister_callback API with a std::vector can be easy or hard depending on how you store the function target.

If you are using raw function pointers (perhaps you shouldn't), then simply iterating through the std::vector and comparing the function pointers will suffice. To remove the element, call .erase() on the matching std::vector iterator.

If you are going to use C++11's std::function as we described above, you will need to use the "handle method":

  • When registering a callback, store a unique number with the callback type in your std::vector
  • Return this handle to the caller
  • When unregistering, the caller must use the handle. Iterate through the vector until your handles match, then call erase() on the std::vector iterator

Here's an example storage type. Your register function would look roughly the same but return a uint32_t that is used to look up the std::function object later.

struct cb_storage_t {
    std::function<void(uint32_t)> cb;
    uint32_t handle;
};

std::vector<cb_storage_t> callbacks_;

Putting It All Together

I've added a callbacks.cpp example to the embedded-resources git repository. You can find it here.

Make sure you compile with -std=c++11 or higher set!

Further Reading