Interfaces, Function Pointers & Polymorphism
One generic call, many concrete behaviors — built by hand with function pointers.
6.1 Function pointers, slowly
typedef void (*SetStateFn)(void *context, bool on);
/* │ │ └─ parameters it accepts
│ └─ "pointer to function"
└─ return type */
SetStateFn fn = led_set; /* store the ADDRESS */
fn(led_ctx, true); /* call through it */- A function pointer stores the address of a function— code, not data.
- Assigning a different function selects behavior at runtime.
contextpoints at the specific object the behavior operates on — it plays the role ofself.
Pointer to data answers “which object?”. Pointer to function answers “which behavior?”. An interface bundles both.
6.2 A generic interface: the ops table
typedef struct {
void (*init)(void *context);
void (*set_enabled)(void *context, bool enabled);
void (*deinit)(void *context);
} DeviceOps; /* a hand-built vtable */
typedef struct {
void *context; /* WHICH object */
const DeviceOps *ops; /* WHICH behavior */
} Device;/* led_device.c */
static void led_set(void *ctx, bool on) {
((LedState *)ctx)->on = on; /* LED on/off */
const DeviceOps LED_OPS = { led_init, led_set, led_deinit };
/* buzzer_device.c */
static void buzzer_set(void *ctx, bool on) {
((BuzzerState *)ctx)->tone = on ? 2000u : 0u;
const DeviceOps BUZZER_OPS = { buzzer_init, buzzer_set, buzzer_deinit };🎭 Dispatch animation — same call, two devices
Both buttons run the same generic line: Device_SetEnabled(device, …). Watch which concrete function the ops table routes to.
🔧 Dispatch demo
- Press a button to start…
Where this pattern lives
HALs, middleware, RTOS driver layers, communication stacks, and plugin systems — anywhere modules must be swappable.
What it costs
More complexity, indirect calls (slightly slower), tougher debugging, possible NULL-callback crashes, and less compile-time type safety.
6.3 Callbacks — "call me when it happens"
typedef void (*RxCallback)(const uint8_t *data,
uint8_t length,
void *user_context);
void Uart_RegisterRxCallback(RxCallback cb,
void *user_context);The driver doesn’t know (or care) what the application does with the data — it just calls whatever was registered. Used everywhere in embedded: UART receive handlers, LIN frame notifications, sensor driver events, timers, state-machine events.
⚡ Callback sequence
📶 Callback demo
- Press button to simulate a UART receive interrupt…
7.1 Composition vs inheritance
C has no native inheritance. The recommended pattern is composition: embed the shared part as a plain member.
typedef struct {
uint8_t id;
bool enabled;
} DeviceBase;
typedef struct {
DeviceBase base; /* embedded, explicit */
uint8_t brightness;
} LedDevice;
led.base.id = 1; /* fully type-checked */
led.brightness = 80;Do: Prefer composition over forced inheritance emulation. Use shared interfaces (ops tables) when behavior differs. Keep base structures small and simple.
Avoid: Unsafe casts without a guaranteed layout. Deep "class hierarchies" rebuilt in C. Hiding base access behind macro magic.