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.
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
***************************************************************************/
#include "callback_fp.h"
// 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("callback_fp: Final counts: btn_a pressed=%u btn_b pressed=%u",
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 capturing lambda 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
***************************************************************************/
#include "callback_lambda.hpp"
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
// std
#include <cstdio>
#include <functional>
#include <utility>
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);
}
}
}
void 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("callback_lambda: 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) {
//
//});
}
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 capturing lambda 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!
Zero-Cost Capturing Lambda with Template Parameters
When zero-cost alternatives are necessary and memory allocation is not permitted, a simple solution is to specify the callback type as a template parameter, as shown here:
enum class Event { Pressed = 0, Released = 1 };
template<typename CallbackType>
class ClassWithCallback {
public:
explicit ClassWithCallback(CallbackType callback) : _cb(std::move(callback)) {}
void simulateCB() {
_cb(Event::Pressed);
}
private:
CallbackType _cb;
};
// declare a ClassWithCallback instance
ClassWithCallback class_with_cb([](Event e) {
// use e
});
class_w.simulateCB();
src/callback_lambda_template.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_template.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief callback with capturing lambda function implementation
* As compared to callback_lambda, the capturing lambda
* is passed to a template parameter. No std::function used anywhere.
* The lambda type is fully known at compile time and the compiler
* can inline the call. This is a zero-cost alternative.
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "callback_lambda_template.hpp"
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
// std
#include <cstdint>
#include <cstdio>
#include <utility>
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;
};
// ── Hardware abstraction — template callback, no std::function ────────────────
/**
* ButtonDriver stores the callback as a template parameter.
* The lambda type is fully resolved at compile time.
* No type erasure. No heap allocation. No virtual dispatch.
*
* In C this would be:
* void register_callback(callback_t cb, void *ctx);
* register_callback(my_handler, &my_state);
*
* Here the "context" is the lambda capture list — type safe, no cast needed.
*/
template <typename OnEvent>
class ButtonDriver {
public:
explicit ButtonDriver(int hw_id, OnEvent on_event)
: _hw_id(hw_id), _on_event(std::move(on_event)) {}
// Simulate hardware interrupt — calls callback directly, compiler inlines it
void simulate_press() { _on_event(Event::Pressed); }
void simulate_release() { _on_event(Event::Released); }
int id() const { return _hw_id; }
private:
int _hw_id;
OnEvent _on_event; // concrete lambda type — NOT std::function
};
void callback_lambda_template() {
ButtonState btn_a{.id = 0};
ButtonState btn_b{.id = 1};
// ── C style (before) ──────────────────────────────────────────────────────
// void *ctx = &btn_a;
// register_callback(button_handler, ctx);
// Inside button_handler: ButtonState *s = (ButtonState *)ctx; ← unsafe cast
//
// ── C++ style (after) — lambda captures state, no void*, no cast ─────────
ButtonDriver driver_a(0, [&btn_a](Event e) {
// btn_a is captured by reference — type safe, no cast
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;
}
});
ButtonDriver driver_b(1, [&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;
}
});
// Simulate events
driver_a.simulate_press();
driver_b.simulate_press();
driver_a.simulate_press();
driver_a.simulate_release();
LOG_INF("callback_lambda_template: Final counts: btn_a pressed=%u btn_b pressed=%u",
btn_a.press_count,
btn_b.press_count);
// In this example, ButtonDriver<lambda_type_A> and ButtonDriver<lambda_type_B>
// have different types.
// This approach is therefore not adequate when callbacks need to be stored
// in the same array or vector (heterogeneous storage). In this
// case std::function is needed, as in the callback_lambda implementation.
}
src/callback_lambda_template.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 capturing lambda function declaration
* As compared to callback_lambda, the capturing lambda
* is passed to a template parameter. No std::function used anywhere.
* The lambda type is fully known at compile time and the compiler
* can inline the call. This is a zero-cost alternative.
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
void callback_lambda_template();
Capturing Lambdas and Lifetime
When using capturing lambdas, developers must be very careful about the lifetime of objects captured. The code below demonstrates the following important concepts:
- Avoid dangling reference from capturing a local by reference
- Capture by value as the safe alternative
- Use
shared_ptrcapture for shared ownership across callbacks - Use
weak_ptrcapture to avoid extending lifetime unintentionally
src/callback_lifetime.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_lifetime.cpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Demonstrates lifetime and capture discipline.
*
* - Dangling reference from capturing a local by reference
* - Capture by value as the safe alternative
* - shared_ptr capture for shared ownership across callbacks
* - Weak_ptr capture to avoid extending lifetime unintentionally
*
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#include "callback_lifetime.hpp"
// zephyr
#include <zephyr/kernel.h>
#include <zephyr/logging/log.h>
// std
#include <cstdio>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
LOG_MODULE_DECLARE(programming, CONFIG_APP_LOG_LEVEL);
// ── Types ─────────────────────────────────────────────────────────────────────
enum class Event { Tick = 0, Shutdown = 1 };
using Callback = std::function<void(Event)>;
// ── Simple event dispatcher ──────────────────────────────────────────────────────────
class EventDispatcher {
public:
void add(Callback cb) { _callbacks.push_back(std::move(cb)); }
void dispatch(Event e) {
for (auto& cb : _callbacks) {
cb(e);
}
}
private:
std::vector<Callback> _callbacks;
};
// ── Shared resource ───────────────────────────────────────────────────────────
struct SharedLog {
std::vector<std::string> entries;
std::string name;
explicit SharedLog(std::string n) : name(std::move(n)) {}
void append(const std::string& msg) {
entries.push_back(msg);
LOG_INF("[%s] logged: '%s'", name.c_str(), msg.c_str());
}
~SharedLog() {
LOG_INF("SharedLog [%s] destroyed - %zu entries", name.c_str(), entries.size());
}
};
// ── 1. Dangling reference — what NOT to do ────────────────────────────────────
[[maybe_unused]] static Callback make_dangling_callback() {
int local_counter = 100;
// Captures local_counter by reference.
// local_counter is destroyed when this function returns.
// Calling the returned lambda is undefined behaviour.
return [&local_counter](Event) {
// local_counter is a dangling reference here
LOG_INF("[value reference] counter = %d",
local_counter); // Undefined behavior — do not do this
};
}
// ── 2. Capture by value — safe for locals ────────────────────────────────────
static Callback make_safe_callback_by_value() {
int local_counter = 100;
// Captures a copy of local_counter.
// The lambda owns its copy — no dangling reference possible.
return [local_counter](Event e) mutable {
if (e == Event::Tick) {
local_counter++;
LOG_INF("[value capture] counter = %d", local_counter);
}
};
}
// ── 3. shared_ptr capture — shared ownership ─────────────────────────────────
static Callback make_shared_ownership_callback(std::shared_ptr<SharedLog> log) {
// Lambda captures shared_ptr by value — extends lifetime of the log
// for as long as the lambda exists.
return [log](Event e) {
if (e == Event::Tick) {
log->append("tick received");
} else if (e == Event::Shutdown) {
log->append("shutdown received");
}
};
}
// ── 4. weak_ptr capture — observe without extending lifetime ──────────────────
static Callback make_weak_callback(std::weak_ptr<SharedLog> weak_log) {
// Captures weak_ptr — does NOT extend the lifetime of SharedLog.
// The callback checks whether the object still exists before using it.
// This is the correct pattern when the callback should not prevent
// the owner from destroying the resource.
return [weak_log](Event e) {
auto log = weak_log.lock(); // try to acquire strong reference
if (!log) {
LOG_INF("[weak capture] SharedLog already destroyed - skipping");
return;
}
if (e == Event::Tick) {
log->append("tick (weak)");
}
};
}
void callback_lifetime() {
EventDispatcher dispatcher;
// ── Dangling reference (commented out — undefined behavior if called)
// ─────────────────────
//
LOG_INF("── Capture by reference ──");
Callback dangling_cb = make_dangling_callback();
dispatcher.add(dangling_cb); // DO NOT do this — calling it is undefined behavior
dispatcher.dispatch(Event::Tick);
// ── Capture by value — safe ───────────────────────────────────────────────
LOG_INF("── Capture by value ──");
dispatcher.add(make_safe_callback_by_value());
dispatcher.dispatch(Event::Tick);
// ── shared_ptr capture ────────────────────────────────────────────────────
LOG_INF("── shared_ptr capture ──");
{
auto shared_log = std::make_shared<SharedLog>("shared capture");
// Two callbacks share ownership of the same log
dispatcher.add(make_shared_ownership_callback(shared_log));
dispatcher.add(make_shared_ownership_callback(shared_log));
LOG_INF("shared_log use_count before publish: %ld",
shared_log.use_count()); // local + 2 lambdas
dispatcher.dispatch(Event::Tick);
LOG_INF("shared_log use_count after publish: %ld",
shared_log.use_count()); // identical: local + 2 lambdas
// shared_log goes out of scope here but the lambdas in dispatcher still
// hold shared_ptr copies — SharedLog is NOT destroyed yet
}
LOG_INF("shared_log local reference gone - lambdas still hold it alive");
// ── weak_ptr capture ──────────────────────────────────────────────────────
LOG_INF("── weak_ptr capture ──");
{
auto observable_log = std::make_shared<SharedLog>("observable");
dispatcher.add(make_weak_callback(observable_log));
LOG_INF("observable_log alive - dispatching tick:");
dispatcher.dispatch(Event::Tick);
// Destroy observable_log — weak_ptr callback should detect this
LOG_INF("Destroying observable_log...");
observable_log.reset(); // reference count → 0, object destroyed
LOG_INF("observable_log destroyed - dispatching tick:");
dispatcher.dispatch(Event::Tick); // weak_ptr callback gracefully skips
}
// ── Shutdown event ────────────────────────────────────────────────────────
LOG_INF("── Dispatching shutdown ──");
dispatcher.dispatch(Event::Shutdown);
}
src/callback_lifetime.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_lifetime.hpp
* @author Serge Ayer <serge.ayer@hefr.ch>
*
* @brief Demonstrates lifetime and capture discipline.
*
* - Dangling reference from capturing a local by reference
* - Capture by value as the safe alternative
* - shared_ptr capture for shared ownership across callbacks
* - Weak_ptr capture to avoid extending lifetime unintentionally
*
*
*
* @date 2025-07-01
* @version 1.0.0
***************************************************************************/
#pragma once
void callback_lifetime();