Stages 6–7

Interfaces, Function Pointers & Polymorphism

One generic call, many concrete behaviors — built by hand with function pointers.

6.1 Function pointers, slowly

fn-pointer.c
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.
  • context points at the specific object the behavior operates on — it plays the role of self.

Pointer to data answers “which object?”. Pointer to function answers “which behavior?”. An interface bundles both.

6.2 A generic interface: the ops table

device.h
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 / buzzer_device.c
/* 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

Dispatch trace
  1. 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"

uart.h
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

  1. 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.

composition.c — preferred
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.

Design decision tool

🧭 Pick your scenario