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行,或者嵌套了多层条件编译,建议拆成函数。宏不是万能的,可读性永远是第一位的。

嗯,关于宏与回调机制,就聊这么多。这套思路我在多个产品中验证过,稳定性和可维护性都很好。你可以在自己的项目里试试,从最简单的回调注册宏开始,慢慢搭建属于你自己的事件框架。

宏与回调机制 · 知识体系图 回调函数注册宏 事件分发宏 观察者模式宏实现 函数指针 注册/注销/通知 按键/串口/定时器 事件驱动框架 避坑指南 中断耗时 · 线程安全 · 宏调试 · 过度宏化 宏封装回调机制,让C代码拥有事件驱动的表达能力
总结:宏与回调的结合,是嵌入式C中实现松耦合、高内聚架构的利器。从简单的注册宏,到完整的事件分发、观察者模式,宏能帮你用最少的代码,写出最清晰的结构。但记住——宏是工具,不是目的。保持可读性,比炫技更重要。

公众号:蓝海资料掘金营,微信deep3321