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.

Here are the essential rules for void*:
- 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). - 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 thevoid*to a concrete pointer type. - Arithmetic: Pointer arithmetic on
void*is not standard in C. You must cast it to a typed pointer (likechar*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:
- The API Function Signature: The API provider defines a callback function pointer type that includes a
void*parameter, often namedcontext,user_data, orctx.typedef void (*event_callback_t)(int event_id, void* user_data); - 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); - 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); - 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.
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.
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:
- Define a context
structnamedRadarLogContextthat contains auint32_t radar_idand afloat max_detection_range. - Define another context
structnamedCanLogContextthat contains auint32_t can_idanduint16_t message_id. - Implement the callback function
format_radar_log. It should take thevoid* contextandraw_data, cast the context toRadarLogContext*, and format a string like:"RADAR [ID: %u, Range: %.1fm]: Data=0x%02X..." - Show how you would call
log_messagefor a RADAR sensor, passing an instance of yourRadarLogContext.
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 astructthat 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.