Hello! Welcome to the sixth lesson in our module on Function Pointers.
In our previous lesson, you built a generic sorting function that used a single function pointer to delegate comparison logic to the caller. This one-to-one callback mechanism is powerful, but many systems require a one-to-many notification model. Imagine an event in an AUTOSAR stack—like a new CAN message arriving. You don't want just one software component to be able to react; you might need the diagnostics module, the application logic, and a data logger all to be notified simultaneously.
This is the focus of today's lesson. Your goal is to design a simple event-driven API with callback registration and triggering. You will learn how to build a "subject" that can notify multiple "observers" when an event occurs. This is a cornerstone of modular and extensible software design, widely known as the Observer design pattern.
From One-to-One to One-to-Many: The Observer Pattern
The core idea is to decouple the source of an event (the Subject) from the entities that need to react to it (the Observers). The Subject doesn't need to know anything about the Observers; it only knows that it must maintain a list of registered listeners and notify them when something happens.
This diagram illustrates the general principle. A "Library" (our Subject) can invoke a "Callback function" (our Observer) that resides in the main application, creating a reverse flow of control that is incredibly flexible.

An event-driven API based on this pattern needs to provide three key features:
- Registration: A function for Observers to register their callback. (
register_listener) - Deregistration: A function for Observers to unregister their callback. (
unregister_listener) - Triggering: An event that causes the Subject to iterate through its list of registered callbacks and invoke them.
A First Implementation: A Simple Button Notifier
Let's design a minimal system: a Button module that can notify multiple listeners when it's "pressed".
We'll start with an array of function pointers to store the registered callbacks.
// button.h
#define MAX_LISTENERS 5
// Define the callback function pointer type
typedef void (*button_callback_t)(void);
// API functions
void button_init(void);
int button_register_callback(button_callback_t cb);
void button_deregister_callback(button_callback_t cb);
void button_press(void); // This is our event trigger
The implementation in button.c would manage this array.
button_register_callbackfinds aNULLslot in the array and stores the new callback.button_deregister_callbackfinds the matching callback and sets its slot toNULL.button_pressiterates through the array and calls every non-NULLfunction pointer.
This approach is simple, but it has limitations. The fixed-size array isn't scalable, and more importantly, the callback void (*cb)(void) is not very useful—the listener has no context. Which button was pressed? What state should the listener update?
The resource below builds a similar, but more realistic, event loop for network programming. Its design is very clear and directly applies here.
Understanding asynchronous I/O; building an event loop from ...
The article 'Understanding asynchronous I/O' demonstrates a clean, event-driven API. While it uses Linux epoll for I/O events, the API design for registering and triggering callbacks is a great model for any event system.
Read through the 'Interface', 'Implementation', and 'Demo' sections. Focus on these three parts: The Server struct, which contains cb_array, an array of function pointers. The on() function, which registers a callback by placing it in the array. The event loop in startServer(), which, upon an event, retrieves the appropriate callback from the array and executes it: server->cb_array[EventTypeMessage](&req); Notice how the callback is passed a Request* struct, providing it with necessary context.
A Professional Implementation for Embedded Systems
The simple array-based approach works, but for production systems like the ones you build, a more robust and scalable solution is needed. Let's study a professional implementation of the Observer pattern designed specifically for embedded C.
This pattern addresses key challenges:
- Scalability: Uses a linked list instead of a fixed-size array to manage listeners.
- Type Safety & Context: Provides a clever and efficient way for a listener to access its own data within the callback using the
container_ofmacro, a common technique in the Linux kernel and other low-level software. - Encapsulation: Bundles callback information into a struct.
The following video provides an excellent walkthrough of this advanced pattern. It's highly relevant to your work in automotive middleware.
Embedded C Programming Design Patterns: Callback
The video 'Embedded C Programming Design Patterns: Callback' provides a professional and highly relevant implementation of this pattern, tailored for contexts like the one you work in. It demonstrates how to build a scalable and type-safe notification system.
Watch the following segments to understand a complete implementation: Concept Overview (00:57 - 02:42): Get a high-level view of the pattern's purpose and components. Basic Implementation (06:40 - 08:55): See how to define the callback struct and the subject's API (add_callback, remove_callback, and the trigger). Observer Implementation & container_of (08:46 - 12:05): This is a key part. Pay close attention to how the observer embeds the callback struct and uses the container_of macro to get a type-safe pointer to itself within the callback. This is a powerful alternative to passing a generic void* user_data. Scaling to Multiple Listeners (11:51 - 14:17): See how the implementation is extended from a single callback to a linked list of callbacks to support multiple observers.
Pitfalls and Advanced Considerations
Designing a robust event-driven system requires careful attention to potential issues. Given your experience with safety-critical systems, these points will be particularly relevant.
1. Dangling Callbacks (Use-After-Free)
This is the most common and dangerous pitfall.
- Problem: An observer is deallocated or goes out of scope, but it has not been deregistered from the subject. When the subject later tries to invoke the callback, it calls a dangling pointer, leading to a crash or undefined behavior.
- Solution: Enforce strict ownership rules. The owner of an observer object is responsible for deregistering it before the object is destroyed. This is often done by pairing registration and deregistration in the object's
create/destroyorinit/deinitfunctions.
Patterns in C - Part 4: OBSERVER
The article 'Patterns in C - Part 4: OBSERVER' provides a formal C implementation of the Observer pattern and discusses its subtleties.
Read the section 'Observer Registration', which starts just after Listing 5. Pay attention to the createDigitalWatch and destroyDigitalWatch functions (Listing 6). See how attach is called on creation and detach is called on destruction, ensuring the callback's lifetime is correctly managed.
2. Modification During Notification
- Problem: What happens if, during a notification loop, one of the callbacks tries to register or deregister another callback? This can corrupt the list of observers you are currently iterating over.
- Solutions:
- Forbid it: The simplest solution. Use a flag to prevent modifications during notification and
assertif a violation occurs. - Make a copy: Before the notification loop, copy the list of observers. Iterate over the copy. New registrations/deregistrations will affect the original list and will be reflected in the next event trigger.
- Use a safe iterator: A more complex solution where the list data structure and iteration logic are designed to handle additions/removals during traversal.
- Forbid it: The simplest solution. Use a flag to prevent modifications during notification and
The "Patterns in C" article discusses the first solution, which is often sufficient and promotes a simpler system design.
Test your understanding!
Let's model a simplified part of an ADAS system. You have a RadarSensor module that detects objects. When a new object is detected, it should trigger an ON_OBJECT_DETECTED event. Two other modules need to listen to this event:
- A
CollisionWarningSystem - An
EventDataRecorder(similar to a black box)
Your Task:
Based on the patterns you've just learned (especially from the video), write the C code to define the necessary structs and function signatures for this system. You don't need to implement the functions fully, just define the architecture.
- Define a callback function pointer type,
object_detected_cb_t. It should pass information about the detected object (e.g.,uint32_t object_id,float distance_m). The callback also needs access to its own module's instance data. - Define a generic
ObserverNodestruct that can be part of a linked list and holds the callback information. - Define the
RadarSensor(Subject) struct. It should contain the head of the observer linked list. - Define the
CollisionWarningSystemandEventDataRecorder(Observer) structs. Each should embed theObserverNodeand any other state they need (e.g.,bool is_active). - Declare the public API for the
RadarSensor:radar_register_listener()andradar_notify_object_detected().
Show answer
#include <stdint.h>
#include <stdbool.h>
// Forward declarations
struct ObserverNode;
struct CollisionWarningSystem;
struct EventDataRecorder;
// 1. Callback function pointer type
// It takes a pointer to the observer node and the event data.
typedef void (*object_detected_cb_t)(struct ObserverNode* node, uint32_t object_id, float distance_m);
// 2. Generic ObserverNode struct for a linked list
typedef struct ObserverNode {
struct ObserverNode* next; // For linked list
object_detected_cb_t on_object_detected;
} ObserverNode_t;
// 3. The Subject: RadarSensor
typedef struct {
uint32_t sensor_id;
ObserverNode_t* listeners_head; // Head of the observer linked list
} RadarSensor_t;
// 4. The Observers
typedef struct CollisionWarningSystem {
ObserverNode_t observer; // Embedded observer node
bool is_active;
int warning_level;
} CollisionWarningSystem_t;
typedef struct EventDataRecorder {
ObserverNode_t observer; // Embedded observer node
uint32_t log_buffer[1024];
size_t log_index;
} EventDataRecorder_t;
// 5. Public API for the RadarSensor
void radar_register_listener(RadarSensor_t* sensor, ObserverNode_t* listener);
void radar_deregister_listener(RadarSensor_t* sensor, ObserverNode_t* listener);
void radar_notify_object_detected(RadarSensor_t* sensor, uint32_t object_id, float distance_m);
/*
// --- Example callback implementations (stubs) ---
// Using the container_of macro (assumed to be defined elsewhere)
#define container_of(ptr, type, member) ((type *)((char *)(ptr) - offsetof(type, member)))
void cws_on_object_detected(ObserverNode_t* node, uint32_t object_id, float distance_m) {
CollisionWarningSystem_t* self = container_of(node, CollisionWarningSystem_t, observer);
if (self->is_active && distance_m < 20.0) {
// printf("CWS: Warning! Object %u at %.1fm!\n", object_id, distance_m);
self->warning_level = 10;
}
}
void edr_on_object_detected(ObserverNode_t* node, uint32_t object_id, float distance_m) {
EventDataRecorder_t* self = container_of(node, EventDataRecorder_t, observer);
if (self->log_index < 1024) {
// self->log_buffer[self->log_index++] = object_id;
// printf("EDR: Logged object %u\n", object_id);
}
}
*/
Conclusion
You have now moved from a simple one-to-one callback to designing a one-to-many event notification system, a fundamental architecture for any complex, modular software.
Key Takeaways:
- The Observer pattern decouples event producers (Subjects) from consumers (Observers), enabling extensible and maintainable code.
- An event-driven API is built around functions for registration, deregistration, and a trigger mechanism.
- The Subject manages a list of callbacks, typically using a dynamic data structure like a linked list for scalability.
- Passing context to callbacks is crucial. This can be done with an explicit
void* user_dataparameter or more elegantly with thecontainer_ofmacro pattern common in embedded and kernel C. - Robustness requires careful lifetime management of observers to prevent dangling callbacks and handling modification during notification.
Preview of the Next Lesson:
In this lesson, you saw two ways to provide context to a callback: the container_of macro and passing a data-filled Request struct. The next lesson, "Pass context-specific data to a callback function using a void pointer," will focus entirely on the latter pattern. We will explore the power and flexibility of using a void* context pointer, analyze its type-safety trade-offs, and see how it enables callbacks to work with arbitrary application-specific data structures.