Skip to main content

Welcome to a Grasp lesson

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

Lesson 41

Callback Functions with Pointers

Recommended resources

Welcome back! In our last lesson, we focused on using typedef to create clean, readable aliases for complex function pointer types. This practice is essential for maintainability, especially in the large-scale projects you're familiar with.

Today, we'll put that skill to immediate use. We're moving from syntax to architecture by implementing one of the most powerful design patterns in C: the callback mechanism. You'll learn how to pass functions as arguments to other functions, enabling you to write flexible, generic code that delegates specific tasks to the caller. This is the core principle behind event-driven programming and is fundamental to the layered architectures you see in AUTOSAR and other embedded frameworks.

By the end of this lesson, you will be able to implement a callback mechanism by passing function pointers as arguments to other functions, a crucial skill for any advanced C programmer.

What is a Callback?

At its heart, a callback is a function that you write and then pass to another function to be "called back" later when a specific event occurs or a piece of logic is needed.

Callback Mechanism Diagram
This diagram shows the fundamental callback pattern. An 'Application' registers a function with a 'Driver/Kernel' layer. When the driver detects an event (like a hardware interrupt or, as we'll see, a data request), it invokes the registered function, "calling back" into the application code.

The function that receives the callback pointer is often a generic utility. It knows when to act, but not what to do. The callback provides the "what." This decoupling is key to creating modular and reusable code.


A Practical Example: A Flexible Sorting Function

Let's start with a classic example: a sorting function. Imagine you need to sort an array of integers. Sometimes you need it sorted in ascending order, other times in descending order. Instead of writing two separate sorting functions, we can write one generic sorter and tell it how to compare elements using a callback.

To see this in action, we'll follow a short project guide.

Advanced C Pointers: Unlock 5 Essential Secrets 2025

The article "Advanced C Pointers" provides a clear, step-by-step guide to building a flexible sorter using a callback for the comparison logic.

Please read and follow the steps in the section "Project: Let’s Build a Flexible Sorter with a Callback". Pay close attention to how the sort_array function is completely generic, and its behavior is controlled entirely by the compare_logic function pointer passed to it.

Let's analyze the key components from that example:

  1. The Callback Functions: These are the specific implementations. They adhere to a pre-defined signature: take two integers, return an integer.

    // Decides if x should come after y in ascending order
    int sort_ascending(int x, int y) {
        return x - y; // Returns > 0 if x > y
    }
    
    // Decides if y should come after x in descending order
    int sort_descending(int x, int y) {
        return y - x; // Returns > 0 if y > x
    }
    
  2. The typedef: As we learned last lesson, this makes the code clean. We create an alias for the comparison function's signature.

    typedef int (*CompareCallback)(int, int);
    
  3. The Consumer/Invoker: This is the generic sort_array function. It knows the bubble sort algorithm but delegates the comparison logic. The line compare_logic(arr[j], arr[j + 1]) > 0 is where the "call back" happens.

    void sort_array(int arr[], int size, CompareCallback compare_logic) {
        for (int i = 0; i < size - 1; i++) {
            for (int j = 0; j < size - i - 1; j++) {
                // Here is the callback in action!
                if (compare_logic(arr[j], arr[j + 1]) > 0) {
                    // Swap elements
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
  4. The Registration: In main, we call sort_array and pass the address of the specific comparison function we want to use. This is the "registration" step.

    sort_array(my_nums, n, sort_ascending);  // Registers the ascending sort logic
    sort_array(my_nums, n, sort_descending); // Registers the descending sort logic
    

This is a synchronous callback, because sort_array executes the callback immediately as part of its own task and doesn't return until the entire operation is complete.


A Real-World Analogy: qsort() and bsearch()

This pattern is not just a novelty; it's used by standard C library functions. qsort() and bsearch() are generic functions for sorting and searching. They work on any data type because you provide a callback function that knows how to compare two elements of your specific data type.

To deepen your understanding, let's examine another practical example.

What is a "callback" in C and how are they implemented?

This Stack Overflow answer provides an excellent example using bsearch() to search a custom struct array. It demonstrates how different callback functions can be used with the exact same bsearch() call to search based on different struct members.

Read the section titled "Synchronous callback". Focus on the bsearch() example. Notice how three different comparison functions (cmpMyData_iValue, cmpMyData_kValue, cmpMyData_label) are passed to bsearch() to change its behavior at runtime.

For an interview, being able to discuss qsort or bsearch as prime examples of synchronous callbacks demonstrates a solid understanding of C standard library design.


Connecting to Your Automotive Experience

This pattern is everywhere in embedded systems. Let's look at an example straight from your domain: AUTOSAR and Unified Diagnostic Services (UDS).

Consider a UDS "Read Data By Identifier" (0x22) request. The Diagnostic Communication Manager (DCM) module in the BSW (Basic Software) is responsible for processing this request. However, the DCM is generic; it doesn't know what a specific Data Identifier (DID) like 0xF190 (Vehicle Identification Number) actually means or where that data lives.

This is a perfect scenario for a callback.

UDS ReadDataByIdentifier Request Processing with Callback
This sequence diagram shows a UDS request for DID 0xF190 (VIN). The DCM module receives the request and, instead of containing the VIN data itself, it invokes a callback function, `App_ReadData_VIN()`, which resides in the Application Layer. The application function retrieves the data and returns it to the DCM, which then constructs the positive response. This decouples the generic diagnostic protocol handling from the specific application data.

Here's how the callback pattern maps to this flow:

  • The Consumer/Invoker: The DCM module. It knows how to parse UDS requests and build responses.
  • The Callback Function: App_ReadData_VIN(). This function, written by an application developer, knows exactly how to get the VIN.
  • The Registration: This is typically done during system configuration. The AUTOSAR configuration tools generate code or data structures that associate DID 0xF190 with a pointer to the App_ReadData_VIN function. When DCM receives a request for that DID, it looks up the associated function pointer and calls it.

This architectural pattern is powerful because it allows the BSW to remain generic and reusable, while the application layer provides the specific "policy" or logic.


A More Advanced Pattern: Callbacks with void* Context

Sometimes, your callback function needs more information than just the arguments provided by the invoker. A common technique is to pass a void* pointer, often called context or userdata, during registration. This pointer can point to any data structure the callback might need.

Let's examine a common implementation of this pattern.

What is a "callback" in C and how are they implemented?

The same Stack Overflow thread contains an excellent, concise example of this more advanced pattern. This is a very common idiom in driver and OS-level code.

Read the answer by user Russell Bryant. Pay attention to how event_cb_register takes both a function pointer (cb) and a void *userdata. The userdata is stored alongside the function pointer and passed back to the callback when it's executed.

The key elements are:

  1. The typedef with a void* parameter:

    typedef void (*event_cb_t)(const struct event *evt, void *userdata);
    
  2. The registration function stores the user data:

    int event_cb_register(event_cb_t cb, void *userdata);
    
  3. The invocation passes the user data back:

    // callback is a struct holding the function pointer and userdata
    callback->cb(event, callback->data);
    

This allows a single callback function to operate on different data instances, making it highly reusable. For example, you could register the same ADC_ConversionComplete_Callback function for two different ADC channels, each time passing a pointer to a different data structure that holds channel-specific configuration and result buffers.


Conclusion

Today we've seen how to implement callback mechanisms, moving from a simple sorting example to a real-world automotive diagnostic scenario. This pattern is fundamental to writing modular, extensible, and maintainable C code. By delegating specific behaviors to function pointers, you create flexible APIs that separate generic mechanisms from specific policies.

Key Takeaways for Your Interview:

  • Define a Callback: Be ready to explain that a callback is a function passed as an argument to another function, to be invoked when a specific event or task is needed. Use the analogy of a generic worker delegating a specific task.
  • Provide a Classic Example: The flexible sorter or qsort/bsearch are excellent, universally understood examples. Be prepared to sketch out the code.
  • Connect to Your Experience: Use the AUTOSAR/DCM example. It shows you not only understand the C-level concept but also how it's applied in the complex, layered architectures of your professional domain. This is a very strong signal to an interviewer.
  • Mention the void* userdata Pattern: Discussing how to pass context to a callback shows a deeper level of understanding beyond the basic implementation.

In our next lesson, we will build upon this concept to implement a dispatch table—an array of function pointers. This is a powerful technique used to replace if-else or switch chains, often seen in parsers, protocol stacks, and state machines.