Skip to main content

Welcome to a Grasp lesson

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

Lesson 41

Passing Context Data with Void Pointers

Recommended resources

Hello! Welcome back to our series on function pointers.

In the last lesson, we designed an event-driven API using the Observer pattern. We saw how a "Subject" can notify multiple "Observers" and briefly touched on two ways to provide context to the callback functions: the container_of macro and passing a pointer to a data structure.

Today, we will dive deep into that second method, which is one of the most fundamental and widely used idioms in C programming. Your learning outcome for this lesson is to pass context-specific data to a callback function using a void pointer. This technique is the key to making generic APIs that can still operate on specific, application-defined data, a pattern you'll find everywhere from the C standard library to the OS and middleware layers you work with in automotive systems.

The Power and Peril of void*

The void* or "void pointer" is a generic pointer. It can hold the memory address of a variable of any data type, but it doesn't carry any type information itself. This makes it incredibly flexible but also requires care from the programmer.

Void Pointers in C: Pointing to Different Data Types
This diagram illustrates how a single `void*` variable can hold the address of an `int`, a `float`, or a `char`. It acts as a universal handle to a memory location, but the compiler doesn't know what's at that location.

Here are the essential rules for void*:

  1. Assignment: You can assign a pointer of any type to a void* and vice-versa without an explicit cast (in C, though C++ requires it).
  2. Dereferencing: You cannot dereference a void* directly. The compiler has no idea what type of data it points to, so it doesn't know how many bytes to read or how to interpret them. You must first cast the void* to a concrete pointer type.
  3. Arithmetic: Pointer arithmetic on void* is not standard in C. You must cast it to a typed pointer (like char* for byte-level arithmetic) before performing operations like incrementing or decrementing.

This combination of flexibility and required programmer discipline makes void* the perfect tool for passing application-specific data through a generic API layer.


The Callback Context Pattern

The pattern is straightforward and powerful. It involves three key components:

  1. The API Function Signature: The API provider defines a callback function pointer type that includes a void* parameter, often named context, user_data, or ctx.
    typedef void (*event_callback_t)(int event_id, void* user_data);
    
  2. The Registration: The API user (your application code) registers its callback function and, at the same time, passes a pointer to its own context data. This pointer is stored by the API.
    // User's context data
    MyAppContext my_context = { .value = 42, .name = "MyDevice" };
    
    // Register the callback, passing a pointer to the context
    register_for_event(EVENT_A, my_callback_handler, &my_context);
    
  3. The Invocation: When the event occurs, the API invokes the registered callback, passing back the exact same void* pointer it was given during registration. The API is completely agnostic to what this pointer represents.
    // Inside the API's event loop...
    // event_callback is a stored function pointer, user_data_ptr is the stored void*
    event_callback(event_id, user_data_ptr);
    
  4. Using the Context: Inside the callback implementation, the user code casts the void* back to its original, concrete type to safely access its data.

c
void my_callback_handler(int event_id, void* user_data) {
// Cast the void* back to the known type
MyAppContext* ctx = (MyAppContext*)user_data;

    // Now we can safely use the context-specific data
    printf("Event %d occurred for device %s with value %d\n",
           event_id, ctx->name, ctx->value);
}
```

The following resource provides a complete, self-contained example of this pattern. It implements a simple event registration system that perfectly demonstrates the flow described above.

Self-Contained Callbacks in C

The article 'Self-Contained Callbacks in C' by The Lone Pole clearly demonstrates the basic pattern of passing a void *ctx parameter to provide context.

Please read the first section, 'Basic callbacks'. Pay close attention to the register_callback function, how the struct handler stores both the function pointer and the void* ctx, and how the print_data callback retrieves and uses the context.


The Canonical Example: qsort

You don't have to look far to see this pattern in action. The C standard library's qsort function is a classic example. It can sort an array of any type because it delegates the comparison logic to a callback function that you provide.

The qsort function signature is:

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

The compar function pointer is the callback. It must point to a function that takes two const void* arguments (pointers to the two elements being compared) and returns a negative, zero, or positive integer to indicate their relative order.

How do you compare two void*s? You don't! You cast them to the actual type of the elements in your array and then compare the values.

This video from mycodeschool provides an excellent explanation of qsort, focusing on how the void* parameters in the comparison function are used.

Function pointers and callbacks

This clip from the 'Function pointers and callbacks' video by mycodeschool explains exactly how qsort uses void* and a comparison callback to achieve generic sorting.

Watch the section from 11:14 to 14:58. Focus on how the void * pointers a and b in the comparison function are first cast to int * before they are dereferenced and their values are compared. This is the essence of the 'cast-then-use' pattern.


Passing Complex Context

What if your callback needs access to multiple pieces of data? The void* parameter can only hold a single address. The solution is simple and elegant: make that single address the address of a struct that bundles all the context you need.

This is a very common extension of the basic pattern. You define a custom struct containing all the necessary state, and then pass a pointer to an instance of that struct as the void* context.

A great real-world example comes from the PortAudio library, a cross-platform audio I/O library. Its audio processing callback needs to maintain state between calls (e.g., the current phase of a synthesizer). It solves this by passing a void* userData pointer, which the application uses to point to its own data structure.

Writing a Callback Function

The PortAudio documentation 'Writing a Callback Function' provides a concise, real-world example of using a struct for context.

Read these two brief sections. First, notice the void *userData parameter in the PaStreamCallback typedef. Then, in the example code, see how they define a paTestData struct and how the first line of the patestCallback function is to cast userData back to a paTestData* to access the left_phase and right_phase members.

This technique of creating a wrapper struct for context is universal. It gives you a way to pass an arbitrary amount of structured, type-safe data through an API that was designed to only pass a single, generic pointer.

Test your understanding!

Let's apply this to your domain. Imagine you are building a generic logging component for an ADAS system. It must be able to handle log messages from different sources (e.g., RADAR, Camera, CAN Gateway). You define a generic log_message function that uses a callback to format the raw data into a human-readable string.

The API looks like this:

// The callback must format the raw data using its specific context
// and write the result into `dest_buffer`.
typedef void (*formatter_cb_t)(void* context, const uint8_t* raw_data, char* dest_buffer);

// The generic logging function
void log_message(const char* source, formatter_cb_t formatter, void* context, const uint8_t* raw_data);

Your Task:

  1. Define a context struct named RadarLogContext that contains a uint32_t radar_id and a float max_detection_range.
  2. Define another context struct named CanLogContext that contains a uint32_t can_id and uint16_t message_id.
  3. Implement the callback function format_radar_log. It should take the void* context and raw_data, cast the context to RadarLogContext*, and format a string like: "RADAR [ID: %u, Range: %.1fm]: Data=0x%02X..."
  4. Show how you would call log_message for a RADAR sensor, passing an instance of your RadarLogContext.
Show answer
#include <stdio.h>
#include <stdint.h>

// Pre-defined API from the prompt
typedef void (*formatter_cb_t)(void* context, const uint8_t* raw_data, char* dest_buffer);

void log_message(const char* source, formatter_cb_t formatter, void* context, const uint8_t* raw_data) {
    char log_buffer[256];
    // The formatter callback is responsible for populating the buffer
    formatter(context, raw_data, log_buffer);
    printf("LOG - %s: %s\n", source, log_buffer);
}

// 1. Define RadarLogContext struct
typedef struct {
    uint32_t radar_id;
    float max_detection_range;
} RadarLogContext;

// 2. Define CanLogContext struct
typedef struct {
    uint32_t can_id;
    uint16_t message_id;
} CanLogContext;

// 3. Implement the callback for RADAR logs
void format_radar_log(void* context, const uint8_t* raw_data, char* dest_buffer) {
    // Cast the generic context pointer back to its specific type
    RadarLogContext* radar_ctx = (RadarLogContext*)context;

    // Use the context data to format the log message
    sprintf(dest_buffer, "RADAR [ID: %u, Range: %.1fm]: Data=0x%02X 0x%02X",
            radar_ctx->radar_id,
            radar_ctx->max_detection_range,
            raw_data[0], raw_data[1]);
}

// 4. Show the call to log_message for a RADAR sensor
void test_radar_logging() {
    // Create and initialize an instance of the context
    RadarLogContext front_radar_ctx = {
        .radar_id = 0x01,
        .max_detection_range = 250.0f
    };

    // Some sample raw data from the sensor
    uint8_t radar_payload[] = { 0xA5, 0xFF };

    // Call the generic logging function, passing the specific
    // formatter callback and a pointer to the specific context.
    log_message("FrontRadar", format_radar_log, &front_radar_ctx, radar_payload);
}

/*
// Example main to run the test
int main() {
    test_radar_logging();
    // Expected Output:
    // LOG - FrontRadar: RADAR [ID: 1, Range: 250.0m]: Data=0xA5 0xFF
    return 0;
}
*/

Conclusion

You have now mastered a critical C pattern for creating flexible and reusable APIs. By using a void* parameter in a callback signature, an API can remain generic while empowering the caller to operate on its own specific, complex data structures.

Key Takeaways:

  • A void* is a generic pointer that can hold any address but must be cast back to a concrete type before use.
  • The callback context pattern involves the API receiving and returning a void* "token" that is opaque to the API but meaningful to the caller.
  • This pattern provides runtime flexibility at the cost of compile-time type safety, placing the responsibility of correctness on the programmer.
  • To pass multiple pieces of data, you can use a void* to point to a struct that bundles all the necessary context.
  • This pattern is ubiquitous in C, found in standard libraries (qsort), operating systems, and middleware stacks.

Preview of the Next Lesson:
So far in this module, we've focused on using function pointers for asynchronous notifications via callbacks. In the next lesson, we will explore a different use case: synchronous control flow. You will learn how to create and use arrays of function pointers to implement jump tables, a highly efficient technique for building things like state machines, command dispatchers, or protocol parsers.