Skip to content

Introduction

Callbacks are often necessary when programming embedded systems. In C, callbacks are usually implemented as function pointers. A function pointer is typically defined as follows:

typedef void (*fun_ptr)()
One can then define a function with the same signature and pass it as a callback to another function that takes a function pointer as an argument. With this approach, a different signature must be defined for each type of callback. For example, if we need to define a function that returns an integer and takes an integer array as a parameter, the following function pointer should be defined:
typedef int (*fun_ptr)(int*)
To avoid redefining function pointers for each specific case, arguments are often defined as void*. This allows one to define more generic callback functions at the cost of removing type-specific casting. The compiler will not detect any errors made by the programmer regarding type mismatches. This is

Using Function Pointers

The use of function pointers and context passed as void* is implemented with code such as:

// C style — pointer to function taking void* context
typedef void (*callback_t)(void *ctx);

void register_callback(callback_t cb, void *ctx);

// Usage
void my_handler(void *ctx, int event) {
    my_state_t *s = (my_state_t *)ctx;
    // use s
}
register_callback(my_handler, &my_state);

An example of the use of function pointers is given below:

src/callback_fp.c
src/callback_fp.c
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_fp.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with function pointer implementation
 *        Classic C callback pattern: function pointer + void* context.
 * 
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <stdio.h>
#include <stdlib.h>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────
typedef enum {
    EVENT_NONE    = 0,
    EVENT_PRESSED = 1,
    EVENT_RELEASED = 2,
} event_t;

typedef struct {
    int      id;
    unsigned press_count;
    unsigned release_count;
} button_state_t;

// Callback signature: takes opaque context pointer and event code
typedef void (*callback_t)(void *ctx, int event);

// ── Callback registry ─────────────────────────────────────────────────────────
typedef struct {
    callback_t  cb;
    void       *ctx;
} registry_entry_t;

#define MAX_CALLBACKS 8

static registry_entry_t registry[MAX_CALLBACKS];
static int              registry_count = 0;

static void register_callback(callback_t cb, void *ctx)
{
    if (registry_count >= MAX_CALLBACKS) {
        LOG_ERR("ERROR: callback registry full");
        return;
    }
    registry[registry_count].cb  = cb;
    registry[registry_count].ctx = ctx;
    registry_count++;
}

static void dispatch_event(int event)
{
    for (int i = 0; i < registry_count; i++) {
        if (registry[i].cb) {
            registry[i].cb(registry[i].ctx, event);
        }
    }
}

// ── Callbacks ─────────────────────────────────────────────────────────────────

// Handler for button A
static void button_handler(void *ctx, int event)
{
    // ctx must be cast back to the concrete type — not type safe
    button_state_t *state = (button_state_t *)ctx;

    switch ((event_t)event) {
        case EVENT_PRESSED:
            state->press_count++;
            LOG_INF("[button %d] pressed  (total: %u)",
                    state->id, state->press_count);
            break;
        case EVENT_RELEASED:
            state->release_count++;
            LOG_INF("[button %d] released (total: %u)",
                    state->id, state->release_count);
            break;
        default:
            break;
    }
}

// A second handler registered for the same events
static void logger_handler(void *ctx, int event)
{
    const char *label = (const char *)ctx;
    LOG_INF("[logger '%s'] event=%d", label, event);
}

// Function called from main()
void callback_fp(void)
{
    button_state_t btn_a = { .id = 0, .press_count = 0, .release_count = 0 };
    button_state_t btn_b = { .id = 1, .press_count = 0, .release_count = 0 };

    // Register handlers — each carries its own context via void*
    register_callback(button_handler, &btn_a);
    register_callback(button_handler, &btn_b);
    register_callback(logger_handler, (void *)"audit");

    // Simulate events
    dispatch_event(EVENT_PRESSED);
    dispatch_event(EVENT_PRESSED);
    dispatch_event(EVENT_RELEASED);

    LOG_INF("Final counts: btn_a pressed=%u btn_b pressed=%u\n",
            btn_a.press_count, btn_b.press_count);

    // Registering the following callbacks is valid but may cause a crash
    // This shows that type-unsafe cast are possible
    // int* a = NULL;
    // register_callback(button_handler, a);
    // register_callback(logger_handler, &a);
    // dispatch_event(EVENT_RELEASED);
}
src/callback_fp.h
src/callback_fp.h
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_fp.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with function pointer declaration
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

#ifdef __cplusplus
extern "C" {
#endif

void callback_fp();

#ifdef __cplusplus
}
#endif

This example uses the classic C function pointer pattern, in which the context is passed as void*. Using void* implies an unsafe cast inside callback handlers.

Replace void* Context with a Capturing Lambda

In C++, it is possible to type the context and to avoid the void* pointer and related unsafe casts. An elegant solution is to pass a lambda with a capture list that replaces the function pointer and void* pair cleanly:

// C++ style — lambda captures state directly, no void* needed
my_state_t state;

register_callback([&state](int event) {
    // state is captured by reference — no cast needed
    state.handle(event);
});
This solution can be used when the callback is stored for the lifetime of the object and is the right choice for many embedded programming use cases.

A more complete example is given below:

src/callback_lambda.cpp
src/callback_lambda.cpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda.cpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with lambda function implementation
 *        As compared to callback_fp, replace void* context + function pointer with a
 *        capturing lambda. The capture list carries state — no cast needed.
 * 
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>

// std
#include <cstdio>
#include <functional>

LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);

// ── Types ─────────────────────────────────────────────────────────────────────
enum class Event {
    None     = 0,
    Pressed  = 1,
    Released = 2,
};

struct ButtonState {
    int      id            = 0;
    unsigned press_count   = 0;
    unsigned release_count = 0;
};

// ── Callback registry ─────────────────────────────────────────────────────────
// Uses std::function here only to hold heterogeneous lambdas in the registry.
static constexpr int MAX_CALLBACKS = 8;

using Callback = std::function<void(Event)>;

static Callback registry[MAX_CALLBACKS];
static int      registry_count = 0;

static void register_callback(Callback cb)
{
    if (registry_count >= MAX_CALLBACKS) {
        LOG_ERR("ERROR: callback registry full");
        return;
    }
    registry[registry_count++] = std::move(cb);
}

static void dispatch_event(Event event)
{
    for (int i = 0; i < registry_count; i++) {
        if (registry[i]) {
            registry[i](event);
        }
    }
}

int callback_lambda()
{
    ButtonState btn_a{ .id = 0 };
    ButtonState btn_b{ .id = 1 };

    // ── Before: C style ───────────────────────────────────────────────────────
    // register_callback(button_handler, &btn_a);   // void* — unsafe cast inside
    //
    // ── After: capturing lambda ───────────────────────────────────────────────
    // The lambda captures btn_a by reference.
    // No void* parameter, no cast, full type safety.

    register_callback([&btn_a](Event e) {
        switch (e) {
            case Event::Pressed:
                btn_a.press_count++;
                LOG_INF("[button %d] pressed  (total: %u)",
                        btn_a.id, btn_a.press_count);
                break;
            case Event::Released:
                btn_a.release_count++;
                LOG_INF("[button %d] released (total: %u)",
                        btn_a.id, btn_a.release_count);
                break;
            default:
                break;
        }
    });

    register_callback([&btn_b](Event e) {
        switch (e) {
            case Event::Pressed:
                btn_b.press_count++;
                LOG_INF("[button %d] pressed  (total: %u)",
                        btn_b.id, btn_b.press_count);
                break;
            case Event::Released:
                btn_b.release_count++;
                LOG_INF("[button %d] released (total: %u)",
                        btn_b.id, btn_b.release_count);
                break;
            default:
                break;
        }
    });

    // A stateless lambda — equivalent to a plain function pointer, zero overhead
    register_callback([](Event e) {
        LOG_INF("[logger] event=%d", static_cast<int>(e));
    });

    // Simulate events
    dispatch_event(Event::Pressed);
    dispatch_event(Event::Pressed);
    dispatch_event(Event::Released);

    LOG_INF("Final counts: btn_a pressed=%u  btn_b pressed=%u",
            btn_a.press_count, btn_b.press_count);

    // The callback must be of type Callback (std::function<void(Event)>)
    // It is not possible to pass a callback of another type like
    // register_callback([](int e) {
    //
    //});

    return 0;
}
src/callback_lambda.hpp
src/callback_lambda.hpp
// Copyright 2025 Haute école d'ingénierie et d'architecture de Fribourg
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/****************************************************************************
 * @file callback_lambda.hpp
 * @author Serge Ayer <serge.ayer@hefr.ch>
 *
 * @brief callback with lambda function declaration
 *
 * @date 2025-07-01
 * @version 1.0.0
 ***************************************************************************/

#pragma once

void callback_lambda();

It’s important to note that stateless lambdas (those with no capture) are equivalent to plain function pointers with zero overhead but with type safety!