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)()
typedef int (*fun_ptr)(int*)
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
// 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
// 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);
});
A more complete example is given below:
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
// 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!