A callback is a reference to a function (or function-like object) that is passed as an argument to another module. The expectation is that the supplied reference will be executed whenever a specific condition occurs (typically, a completed event).
Callbacks can be categorized as synchronous (aka “blocking”) or asynchronous (aka “deferred”), which refers to how the callback function is invoked.
- Synchronous callbacks are invoked before a function returns and in the same thread context as the calling function.
- Asynchronous callbacks are often added to a queue of some time, and (depending on the system’s properties) are likely to execute after the invoking function returns, possibly in another thread of execution.
Callbacks involve the following primary components:
- Storage for the callback
- This can be a variable that holds a single value, a static array of values with a maximum size, or a dynamic list of values.
- In some cases, you will encounter callback functions that are defined via
weaklinkage, or that are declared but not defined. In these cases, you are expected to provide a function with the same name.
- A function that invokes the registered callback(s) at the proper point
- A mechanism for registering the callback
- This is typically a dedicated function, a constructor/initialization function parameter, or a field in a configuration
struct.
- This is typically a dedicated function, a constructor/initialization function parameter, or a field in a configuration
- A concrete implementation of the callback function
- This code is application-specific and supplies the “missing” behavior.
- Registration of the concrete callback implementation with the proper module
Callback Use Cases
Callbacks provide a mechanism for connecting modules together while reducing coupling between them. Callbacks are commonly used to separate application specific logic (related to the completion of an event or operation) from code that could otherwise be kept generic, such as a device driver, library, or communication stack. The calling code only knows about the callback abstraction and invokes stored values at the appropriate location(s). In this sense, callbacks are commonly conceptualized as providing decoupling between layers, since “lower” layers in a software stack can invoke actions in a “higher” layer without the need to know anything about the higher-level module.
Specific applications for callbacks in embedded software include:
- Registering for a notification when a specific event happens (e.g. starting to record video, power state change, button pushed), enabling the application to take a specific action in response to the event.
- Registering for a notification when an asynchronous event has completed (e.g. asynchronous USB transfer complete callback, DMA transfer complete callback), enabling the application to execute the next step without needing to monitor for the completion of the asynchronous event.
- Registering callbacks as handlers for interrupt, timer, and other OS services.
Handling “Default” Cases
For code that uses callbacks, the implementer needs to consider the case where the users has not supplied a callback. Depending on the purpose of the module, any one of these options are suitable:
- Only invoke callback(s) if they are registered, otherwise do nothing.
- Execute a default operation if a user has not registered a callback.
- Force an assertion if a user-supplied callback is required for proper operation.
Diving Deeper into Callbacks
- Improving Your Callback Game explores how we can improve typical callback handling with C++ facilities.
- Callback Types explores different callback styles that you are likely to encounter. We also discuss C++ approaches for handling different scenarios
- Using a C++ Object’s Member Function with C-Style Callbacks provides a solution for passing a C++ object’s member function a C-style callback.
Frameworks and Libraries
- The Embedded Template Library provides reusable infrastructure for supporting callbacks in C++ applications. Basic support is provided by the Callback Service (associated tutorial). The ETL also provides a Delegate class that works as a static memory alternative to
std::function(allowing you to register lambda functions, class member functions, static functions, and other functors as callbacks), as well as a Delegate Service that operates similarly to the Callback Service that uses Delegates.
Related Concepts
- Callbacks are typically used to implement the Observer Pattern
- Callbacks operate similarly to the Template Method Pattern. For both patterns, tight coupling between modules can be handled externally by supplying an implementation for an optional customizable step. Callback operations are conceptually focused on customizing what happens when an operation is completed or event occurs, whereas the Template Method pattern is conceptually focused on customizing what happens during an operation. Given this, callbacks can be viewed as an application of Template Method.
References
- Callback (computer programming) – Wikipedia
In computer programming, a callback or callback function is any reference to executable code that is passed as an argument to another piece of code; that code is expected to call back (execute) the callback function as part of its job. This execution may be immediate as in a synchronous callback, or it might happen at a later point in time as in an asynchronous callback. Programming languages support callbacks in different ways, often implementing them with subroutines, lambda expressions, blocks, or function pointers.
There are two types of callbacks, differing in how they control data flow at runtime: blocking callbacks (also known as synchronous callbacks or just callbacks) and deferred callbacks (also known as asynchronous callbacks). While blocking callbacks are invoked before a function returns (as in the C example below), deferred callbacks may be invoked after a function returns. Deferred callbacks are often used in the context of I/O operations or event handling, and are called by interrupts or by a different thread in case of multiple threads. Due to their nature, blocking callbacks can work without interrupts or multiple threads, meaning that blocking callbacks are not commonly used for synchronization or for delegating work to another thread.
- 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 callback, DMA transfer complete callback).
- Registering callbacks as handlers for interrupt, timer, and other OS services.
- Embedded Basics – Callback Functions | Beningo Embedded Group
A callback function is a reference to executable code that is passed as an argument to other code that allows a lower-level software layer to call a function defined in a higher-level layer(10). A callback allows a driver or library developer to specify a behavior at a lower layer but leave the implementation definition to the application layer.
A callback function is a reference to executable code that is passed as an argument to other code that allows a lower-level software layer to call a function defined in a higher-level layer(10). A callback allows a driver or library developer to specify a behavior at a lower layer but leave the implementation definition to the application layer.
A callback function at its simplest is just a function pointer that is passed to another function as a parameter. In most instances, a callback will contain three pieces:
- The callback function
- A callback registration
- Callback execution
The code that will invoke the callback function within the module is often called the signal handler.
- Increasing code flexibility using callbacks – Embedded.com by Jacob Beningo
A callback is a function that is executed through a pointer where the function being pointed to contains application specific code. Callbacks are normally passed into a function and provides the developer with the flexibility to change the callback code without having to modify the calling function. For example, callbacks are often used in low-level drivers to access application specific behaviors in the application layer. The driver may not know what the timer interrupt should do so it makes a call through a callback to application level code that defines what the interrupt should do. This allows the driver code to stay the same and for the application level code to define its behavior.
- Aaron’s Pithy Thoughts on Architecture
Use Callbacks (or Generate Events)
Another area where it is common to violate acyclicity is where the lower-level module needs to provide some signal back to the higher-level module. If there is only one higher-level dependent, it may be tempting to have the lower-level module call the higher-level one directly. This creates cyclical awareness. Don’t do this.
Design the lower-level modules such that callbacks can be registered for events. Or, alternatively, design them to generate events to which higher-level modules can subscribe.
Consider the
nrfx_uartdriver in the Nordic SDK. A high level module can initiate an RX or TX operation using functions likenrfx_uart_tx(). But … it gets notified of completion via RX_DONE and TX_DONE events. The nrf driver knows nothing about the clients using it.One good question to ask yourself when designing the lower-level layers is, “How easily could I pull this out and use it in another project?” If the answer is “not easily,” that is a sign of tight coupling and poor design.
« Back to Glossary Index
