24、宏与回调机制:回调函数注册宏,事件分发宏,观察者模式宏实现。
回调机制,说白了就是“你告诉我该干啥,到时候我喊你”。
在嵌入式世界里,这玩意儿太常见了。按键按下要处理、数据接收完毕要通知、定时器超时要响应……这些场景背后,都离不开回调。而宏,能让回调的注册、分发、管理变得异常优雅。
我个人习惯,在写事件驱动型框架时,一定会用宏来封装回调操作。为什么?因为纯C语言没有C++那种成员函数指针,写起来又臭又长。宏能帮你把脏活累活藏起来,让代码读起来像在描述业务逻辑。
24.1 回调函数注册宏
先看最基础的回调注册。假设我们有一个事件管理器,需要支持多个回调函数的注册与注销。
传统写法是这样的:
typedef void (*event_handler_t)(int event_id, void *arg);
typedef struct {
event_handler_t handler;
void *arg;
} callback_node_t;
#define MAX_CALLBACKS 10
static callback_node_t callbacks[MAX_CALLBACKS];
static int callback_count = 0;
int register_callback(event_handler_t handler, void *arg) {
if (callback_count >= MAX_CALLBACKS) return -1;
callbacks[callback_count].handler = handler;
callbacks[callback_count].arg = arg;
callback_count++;
return 0;
}
嗯,能跑,但每次注册都要写函数名和参数,很烦。我建议用宏包装一下:
#define REGISTER_EVENT_HANDLER(handler_func, user_arg) \
do { \
if (register_callback((handler_func), (user_arg)) != 0) { \
/* 注册失败处理,可以加断言或日志 */ \
} \
} while(0)
这样调用就变成了:
REGISTER_EVENT_HANDLER(my_button_handler, &my_button_data);
看着清爽多了。但这里有个坑——宏参数不要带副作用。我曾经在项目里写过:
REGISTER_EVENT_HANDLER(handler, get_data_ptr());
结果get_data_ptr()被调用了两次,因为宏展开后出现了两次。嗯,从那以后,我所有回调注册宏都强制要求用户先准备好参数,或者在宏内部用临时变量保护一下。
do { ... } while(0) 包裹,并避免参数重复引用。
24.2 事件分发宏
回调注册好了,怎么分发事件?说白了就是遍历回调列表,挨个调用。
传统写法:
void dispatch_event(int event_id, void *arg) {
for (int i = 0; i < callback_count; i++) {
if (callbacks[i].handler != NULL) {
callbacks[i].handler(event_id, arg);
}
}
}
这代码本身没问题,但如果你有多个事件类型、多个优先级、多个分发策略,重复代码就多了。我习惯用宏来生成分发逻辑:
#define DEFINE_EVENT_DISPATCHER(prefix, list, count) \
void prefix##_dispatch(int event_id, void *arg) { \
for (int i = 0; i < (count); i++) { \
if ((list)[i].handler != NULL) { \
(list)[i].handler(event_id, arg); \
} \
} \
}
用起来:
DEFINE_EVENT_DISPATCHER(button, button_callbacks, BUTTON_CALLBACK_MAX)
这就自动生成了一个 button_dispatch 函数。你想想看,如果系统里有按键、串口、定时器、传感器等多个事件源,每个都要写一遍分发函数,那得多累。用宏生成,一行搞定。
## 是预处理器的“粘合剂”,可以把前后两个 token 拼成一个。这是实现“模板化”C代码的核心手段。
24.3 观察者模式宏实现
观察者模式,说白了就是“一对多”的依赖关系。一个主题(Subject)变化,所有观察者(Observer)都收到通知。
在嵌入式C里,我通常用宏来定义整个观察者框架。先看数据结构:
#define DECLARE_OBSERVER(name, max_obs) \
typedef struct { \
observer_callback_t callbacks[(max_obs)]; \
void *args[(max_obs)]; \
int count; \
} name##_subject_t;
然后定义注册和通知宏:
#define OBSERVER_ATTACH(subject, cb, arg) \
do { \
if ((subject).count < (sizeof((subject).callbacks) / sizeof((subject).callbacks[0]))) { \
(subject).callbacks[(subject).count] = (cb); \
(subject).args[(subject).count] = (arg); \
(subject).count++; \
} \
} while(0)
#define OBSERVER_NOTIFY(subject, event_id) \
do { \
for (int _i = 0; _i < (subject).count; _i++) { \
if ((subject).callbacks[_i] != NULL) { \
(subject).callbacks[_i]((event_id), (subject).args[_i]); \
} \
} \
} while(0)
实际使用:
// 声明一个温度传感器主题,最多支持5个观察者
DECLARE_OBSERVER(temp_sensor, 5)
// 定义主题实例
temp_sensor_subject_t temp_subject;
// 注册观察者
OBSERVER_ATTACH(temp_subject, display_temp, &lcd_handle);
OBSERVER_ATTACH(temp_subject, log_temp, &log_buffer);
// 温度变化时通知所有观察者
OBSERVER_NOTIFY(temp_subject, TEMP_CHANGED);
你看,整个观察者模式的核心逻辑,被几个宏封装得干干净净。代码读起来就像在描述“谁关注什么、什么时候通知谁”。
我曾经在一个多传感器采集项目里用了这套宏,当时有6种传感器,每种传感器有3-5个观察者(显示、存储、报警、上传等)。如果用传统写法,光注册和通知代码就要写几百行。用了宏之后,核心逻辑不到50行,而且新增传感器只需要一行声明加几行注册。
24.4 避坑指南与个人经验
最后聊几个我踩过的坑:
- 回调函数执行时间不可控——我曾经在中断里调用了一个回调,结果那个回调里做了延时,直接导致系统卡死。建议:中断级回调只做标记,不要做耗时操作。
- 回调列表的线程安全——如果回调注册和事件分发发生在不同任务/中断中,记得加锁或使用无锁队列。我早期一个项目就因为这个出了随机崩溃,查了三天。
- 宏的可调试性差——宏展开后的代码,调试器里看不到原始行号。我建议在宏内部加一些调试打印的开关宏,方便定位问题。
- 不要过度宏化——如果一个宏超过20行,或者嵌套了多层条件编译,建议拆成函数。宏不是万能的,可读性永远是第一位的。
嗯,关于宏与回调机制,就聊这么多。这套思路我在多个产品中验证过,稳定性和可维护性都很好。你可以在自己的项目里试试,从最简单的回调注册宏开始,慢慢搭建属于你自己的事件框架。
公众号:蓝海资料掘金营,微信deep3321