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. asynchronous USB transfer complete callbackDMA transfer complete callback)
  • Callbacks as handlers for interrupt, timer, and other OS services

Many of the C callback 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.

Table of Contents:

  1. Storing Callbacks
  2. Storing Function Pointers
    1. Storing Function Pointers Without Dynamic Memory
  3. Examples of Registering Functions with C++11
    1. Static Functions
    2. Lambda Functions
    3. Instance Member Functions
    4. C Functions
  4. Supporting Unregister Functionality
  5. Putting It All Together
  6. Further Reading

Storing Callbacks

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);
}

If dynamic memory allocation isn’t available for your system, take a look at the Embedded Template Library (ETL). The ETL provides a vector type which uses static memory allocation.

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.

Storing Function Pointers Without Dynamic Memory

One downside to using std::function is that functional objects after a certain size will cause a dynamic memory allocation to occur.

If you cannot use dynamic memory allocation schemes on your system, there are static memory alternatives available:

Both types will allow you to store any type of Callable object without dynamic memory allocation.

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 (or a static memory equivalent), 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 Callableobject). 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::functionobject 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

17 Replies to “Improving Your Callback Game”

  1. Hey Shane,

    Thanks for pointing that out, I did not think to include the size overhead in the original article. Additionally, there is additional function call overhead when using std::function.

    The overhead is the same order of magnitude that is incurred by using other C++ features such as smart pointers. In my opinion, the overhead is still of negligible size on most modern systems, even embedded ones.

    Raw function pointers will work for most cases and are perfectly valid. std::function enables capturing additional callback types, such as lambdas with capture variables and std::bind calls.

  2. Useful if you have a heap, but many embedded systems don’t have dynamic memory and thus using std::vector is impossible.

  3. std::vector is just an example, the technique can be extrapolated to other containers. std::array works just as well without heap allocation. We also utilize the Embedded Template Library for static memory container alternatives: https://embeddedartistry.com/blog/2018/12/13/embedded-template-library.

    For a non-heap alternative to std::function (the real troublesome item in this example), we use SG14’s proposed inplace_function: https://github.com/WG21-SG14/SG14/blob/master/SG14/inplace_function.h

    It’s on my list to update the article with these alternatives for embedded applications without dynamic memory allocation schemes.

  4. Thanks for adding clarity and beauty to an otherwise terse, dense and sometimes convoluted (without value) subject, such as embedded system design.

    (I image this company will be very successful).

  5. The problem with std::function is that using functional objects of larger size (eg lambdas with a lot of captured variables) will lead to dynamic allocation. There is currently no way to control this and it is implementation-defined when a lambda is permitted to allocate or not. For this reason, bare-metal projects with banned dynamic allocation should most likely write their own std::function-like wrapper that is non-owning and can be very efficiently implemented to a degree that even the extra overhead is completely eliminated (check http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0792r0.html)

  6. This is a very nice write up. Might consider adding an example to the ‘Instance member functions’ chapter which shows how to use lambdas instead of the std:bind. I.e. something like

    Client c;
    register_callback([&c](uint32_t p){c->func(p);})

    which I think is nicer to read than the bind variant. Works also from within a class by replacing &c with this, which is the main use case for me.

  7. Hi, thanks for the article Phillip.
    For me it’s a bit unclear what’s the function prototype for “register_callbac(…??…)” when registering a lambda function, I mean this:
    Thank you

    register_callback(
    [](uint32_t v) {
    printf(“lambda callback: %u\n”, v);
    });

  8. Hi Tano,

    There are a number of options. Easiest is to specify the parameter as a std::function or a static-memory equivalent, such as done here.

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

    If the lambda does not capture, it may be passed to an input parameter that expects a function pointer.

    You can also use a template parameter, although you would need to execute that lambda immediately rather than storing it for later.

    template<typename F>
    void f(F &lambda) { /* ... */}
    
  9. What about std::bind? what do I do if project won’t allow it as well? Does the libraries have something to replace it?

  10. Hi Shanti,

    Why does the project not allow std::bind? Usually the problem is that the std::function requires an allocation, and you can use a static memory implementation (like etl::delegate).

    Anyway, aside from using std::bind, you could create a lambda instead, such as what luni64 described in his comment above.

    Client c;  
    register_callback([&c](uint32_t p){c->func(p);})
    
  11. I see that the article and some comments mention etl::delegate as an alternative for callbacks. The downside of etl::delegate is that it is non-owning: if a lambda is used as callback. the closure must still be alive when the callback is invoked. SG14::inplace_function (also mentioned by the writer) does not have that drawback and has the option of controlling how large the inplace_function (thus closure) is allowed to become.

Share Your Thoughts

This site uses Akismet to reduce spam. Learn how your comment data is processed.