Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 38

Implementing Callbacks with Function Pointers

Recommended resources

Hello! Welcome to the sixth module of our course on C pointers.

In our last lesson, we focused on making function pointer syntax manageable by using typedef. You learned to create clean, readable aliases for complex function pointer signatures, a key practice for writing professional C code.

Today, we'll put that skill to use. We're going to explore one of the most powerful applications of function pointers: implementing callback mechanisms. You'll learn how to pass function pointers as arguments to other functions, allowing a function to "call back" to code provided by its caller. This pattern, a form of inversion of control, is fundamental to event-driven programming and is ubiquitous in the operating systems, drivers, and middleware you work with daily.

What is a Callback?

At its core, a callback is a mechanism where a function accepts a pointer to another function and invokes it at a specific time or in response to an event.

Think of it like subscribing to a notification service. You provide your phone number (the callback function pointer) to the service (a lower-level library or driver). You don't know exactly when they'll call, but you've instructed them to notify you when a specific event happens. The service doesn't need to know who you are or what you'll do when you get the call; it just needs the number to dial.

This decouples the high-level policy (what to do) from the low-level mechanism (when to do it). This is a cornerstone of modular design in systems like AUTOSAR, where a generic COM module can notify any number of application-level components about incoming messages without needing to know anything about them.

The diagram below, from the Beningo Embedded Group, illustrates this flow. The application registers a Callback function with the Driver Library. Later, when an event occurs, the Signal Handler in the driver invokes the registered callback, transferring control back to the application layer.

Callback Function Mechanism Diagram
This diagram shows a higher-level 'Application' layer registering a 'Callback' function with a lower-level 'Driver Library Kernel'. An event in the driver triggers the 'Signal Handler' to invoke the application's callback.

The Core Mechanism: Passing a Function as an Argument

Let's start by seeing how this works at a fundamental level. The following video provides a concise introduction to the concept.

Function pointers and callbacks

This clip from mycodeschool demonstrates the basic syntax and execution flow of a callback. It shows one function receiving a pointer to another and then 'calling it back'.

Watch from 00:24 to 02:54. Pay attention to how function B takes a pointer to function A as its argument and then executes A from within its own body.

As you saw, the syntax is straightforward: a function parameter is simply declared as a function pointer. The calling function can then pass the name of any compatible function as the argument.

Here is a practical code example that solidifies this idea.

Function Pointer in C – Complete Guide with Examples

The article 'Function Pointer in C – Complete Guide with Examples' provides a great example of a generic arithmetic function that uses a callback to perform different operations.

Read the section '5) Passing Function Pointers as Arguments in C:'. Focus on the 'Dynamic Arithmetic Operations' example. Notice how the single ArithMaticOperation function can perform addition, subtraction, or multiplication based on the function pointer it receives.

This ArithMaticOperation function demonstrates the key benefit: code reuse and flexibility. Instead of writing separate logic for each operation inside a large switch statement, the core logic is generic, and the specific operation is injected by the caller.


A Classic and Powerful Use Case: Generic Sorting

One of the most classic examples of callbacks is creating a generic sorting algorithm. The sorting logic (like Bubble Sort or Quick Sort) remains the same, but the criteria for ordering elements can be customized by the caller.

For instance, you might want to sort an array of integers in:

  • Increasing order
  • Decreasing order
  • By their absolute value

Instead of writing three separate sort functions, you can write one generic sort function that accepts a callback to a comparison function.

The following video walks through this exact scenario, first building a custom sort function and then showing how the C standard library's qsort function uses the same principle.

Function pointers and callbacks

This video from mycodeschool provides an excellent, in-depth look at using callbacks for sorting. It's a perfect practical application of the concept.

Please watch from 02:54 to 14:58. This long segment is worth the time. It covers: Building a BubbleSort function that accepts a function pointer for comparison. Writing different comparison callbacks to change the sort order (increasing, decreasing, absolute value). Introducing the standard library's qsort, which uses a void* and a comparison callback for maximum generality.

The qsort example is particularly relevant as it introduces the use of void* pointers to make the function truly generic—it can sort arrays of any type. The callback function is responsible for correctly casting the void* pointers back to the appropriate type before performing the comparison. This is a common pattern in generic C APIs.


Callbacks in an Embedded and Automotive Context

While sorting is a great example, callbacks are most often used in embedded systems for handling asynchronous events. Your work with ADAS communication stacks and AUTOSAR middleware relies heavily on this pattern. When a CAN frame arrives or an Ethernet packet is received, the low-level driver or middleware needs to notify the correct application component.

Let's explore a design pattern specifically for implementing robust callbacks in embedded C.

Embedded C Programming Design Patterns: Callback

This video from Martin K. Schröder on 'Embedded C Programming Design Patterns' discusses the callback pattern in detail, framing it as the 'Observer' pattern. It introduces professional techniques relevant to your work.

This video is dense with valuable information. Please watch from 00:57 to 12:05. Focus on the following key ideas: Decoupling: How callbacks separate the event source (e.g., a button) from the observers (00:57-02:30). Benefits & Drawbacks: The trade-offs in complexity and performance (02:16-06:40). Implementation: Using a struct to hold the callback pointer. This is a crucial technique for extensibility (06:40-08:55). Context Passing with container_of: A clever, type-safe way to get a pointer to the parent object from within the callback, avoiding messy void* userdata parameters (08:46-10:48). Pitfalls: The danger of the observer being destroyed while the callback is still registered, leading to a dangling pointer—a critical functional safety concern (10:39-12:05).

The techniques discussed in this video, such as wrapping function pointers in structs and using macros like container_of (prevalent in the Linux kernel), represent a mature, robust approach to callback design that you can apply directly to your work in gateway development and AUTOSAR middleware. The point about unregistering callbacks is especially critical in systems where objects have dynamic lifetimes and functional safety is a requirement.

Test your understanding!

You are designing a simplified router for a vehicle gateway. The router's job is to receive a generic PDU_t (Protocol Data Unit) and dispatch it to the correct handler based on its PduId.

You have these two PDU handlers:

  • void process_radar_pdu(const PDU_t* pdu);
  • void process_camera_pdu(const PDU_t* pdu);

Your Task:

  1. Define a PDU_t struct containing a uint32_t PduId and a uint8_t payload[8].
  2. Create a typedef named PduHandler_t for a function pointer that can point to either of the handler functions.
  3. Create a RouterRule_t struct that contains a PduId and a PduHandler_t function pointer.
  4. Implement a route_pdu function. It should take a const PDU_t* pdu and an array of RouterRule_t rules. It should loop through the rules and, if it finds a matching PduId, it must call the corresponding handler function (the callback).
  5. In main, create an array of RouterRule_t rules to map specific PduIds to the process_radar_pdu and process_camera_pdu functions. Then, create a sample PDU and pass it to route_pdu to test your logic.
Show answer
#include <stdio.h>
#include <stdint.h>

// --- Definitions ---

// 1. PDU struct
typedef struct {
    uint32_t PduId;
    uint8_t payload[8];
} PDU_t;

// PDU IDs for our example
#define RADAR_PDU_ID 0x100
#define CAMERA_PDU_ID 0x200

// 2. Typedef for the handler function pointer
typedef void (*PduHandler_t)(const PDU_t* pdu);

// 3. Struct for a routing rule
typedef struct {
    uint32_t pdu_id_to_match;
    PduHandler_t handler;
} RouterRule_t;

// --- Callback Implementations ---

void process_radar_pdu(const PDU_t* pdu) {
    printf("Processing RADAR PDU (ID: 0x%X). Payload[0]=0x%02X\n", pdu->PduId, pdu->payload[0]);
}

void process_camera_pdu(const PDU_t* pdu) {
    printf("Processing CAMERA PDU (ID: 0x%X). Payload[0]=0x%02X\n", pdu->PduId, pdu->payload[0]);
}

// 4. The dispatcher/router function
void route_pdu(const PDU_t* pdu, const RouterRule_t rules[], size_t num_rules) {
    for (size_t i = 0; i < num_rules; ++i) {
        if (rules[i].pdu_id_to_match == pdu->PduId) {
            // Found a match, invoke the callback!
            if (rules[i].handler != NULL) {
                rules[i].handler(pdu);
            }
            return; // PDU handled
        }
    }
    printf("No handler found for PDU ID 0x%X\n", pdu->PduId);
}

// --- Main Application ---
int main() {
    // 5. Create the routing table
    RouterRule_t routing_table[] = {
        {RADAR_PDU_ID, process_radar_pdu},
        {CAMERA_PDU_ID, process_camera_pdu}
    };
    size_t num_rules = sizeof(routing_table) / sizeof(routing_table[0]);

    // Create a sample PDU from the radar
    PDU_t radar_data = {
        .PduId = RADAR_PDU_ID,
        .payload = {0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0}
    };
    
    // Create a sample PDU from the camera
    PDU_t camera_data = {
        .PduId = CAMERA_PDU_ID,
        .payload = {0xAA, 0xBB, 0, 0, 0, 0, 0, 0}
    };

    // Route the PDUs
    printf("Routing PDU with ID 0x%X...\n", radar_data.PduId);
    route_pdu(&radar_data, routing_table, num_rules);

    printf("\nRouting PDU with ID 0x%X...\n", camera_data.PduId);
    route_pdu(&camera_data, routing_table, num_rules);

    return 0;
}

This exercise directly mirrors the logic used in communication stacks and gateways, where incoming data is dispatched to specialized processing routines based on identifiers.


Conclusion

Today, we explored the powerful and fundamental concept of callbacks. By passing function pointers as arguments, you can create flexible, reusable, and modular code where low-level modules can signal events to high-level modules without being tightly coupled.

Key Takeaways:

  • A callback is a function passed as an argument to another function, intended to be "called back" later.
  • This pattern enables inversion of control, decoupling the "what" from the "when."
  • It is the foundation for event-driven programming, crucial for handling asynchronous hardware and communication events in embedded systems.
  • Advanced implementations often wrap callback pointers in structs and use techniques like container_of to provide context to the callback in a type-safe manner.

Preview of the Next Lesson:
We will now do a deep dive into one of the most common applications of this pattern. In the next lesson, we will implement a generic sorting function that accepts a custom comparison function via a function pointer. While we touched on qsort today, you will now implement a similar mechanism from scratch, giving you a complete understanding of how to build your own generic, callback-driven utility functions.