1. 事件循环基础框架

事件循环基础框架 学习卡片

任务说明

本案例要求实现一个最小可运行的事件循环框架,支持事件注册、事件分发和回调执行三个核心能力。事件循环是事件驱动架构的基石:程序不主动轮询状态,而是注册感兴趣的事件及对应回调,随后进入循环等待事件发生,事件到达时由循环负责调用已注册的回调函数。完成本案例后,应能直观理解“注册—等待—分发—回调”这一基本运转机制。

核心设计

事件循环需要解决三个问题:事件如何表示、回调如何注册、循环如何等待并分发。本案例采用以下最小设计:

  • 事件:用整数类型 event_id 标识事件类型,事件载荷用 void* 传递,保持通用性。
  • 注册表:使用固定大小的数组存储事件ID到回调函数的映射,每个事件ID最多注册一个回调(简化处理,避免链表管理)。
  • 事件队列:使用环形缓冲区暂存待处理事件,事件循环从队列头部取出事件,查找注册表并调用对应回调。
  • 触发方式:提供 event_loop_post 接口向队列投递事件,模拟异步事件到达;循环运行后按顺序处理队列中所有事件。

该设计覆盖了事件驱动的基本要素,且代码量小、无外部依赖,便于独立验证。

代码实现

// event_loop.h
#ifndef EVENT_LOOP_H
#define EVENT_LOOP_H

#include <stdint.h>

#define MAX_EVENT_TYPES 16      // 支持的最大事件类型数
#define QUEUE_CAPACITY  8       // 事件队列容量

typedef uint16_t event_id_t;
typedef void (*event_handler_t)(event_id_t id, void* data);

typedef struct {
    event_id_t id;
    void* data;
} event_t;

typedef struct {
    event_handler_t handlers[MAX_EVENT_TYPES];  // 事件ID到回调的映射表
    event_t queue[QUEUE_CAPACITY];              // 环形事件队列
    int head;                                   // 队列头索引
    int tail;                                   // 队列尾索引
    int count;                                  // 队列中事件数量
} event_loop_t;

void event_loop_init(event_loop_t* loop);
int  event_loop_register(event_loop_t* loop, event_id_t id, event_handler_t handler);
int  event_loop_post(event_loop_t* loop, event_id_t id, void* data);
void event_loop_run(event_loop_t* loop);        // 处理队列中所有事件,队列空时返回

#endif // EVENT_LOOP_H
// event_loop.c
#include "event_loop.h"
#include <string.h>

void event_loop_init(event_loop_t* loop) {
    memset(loop, 0, sizeof(event_loop_t));
}

int event_loop_register(event_loop_t* loop, event_id_t id, event_handler_t handler) {
    if (id >= MAX_EVENT_TYPES || handler == NULL) {
        return -1;  // 参数非法
    }
    loop->handlers[id] = handler;
    return 0;
}

int event_loop_post(event_loop_t* loop, event_id_t id, void* data) {
    if (loop->count >= QUEUE_CAPACITY) {
        return -1;  // 队列已满
    }
    loop->queue[loop->tail].id = id;
    loop->queue[loop->tail].data = data;
    loop->tail = (loop->tail + 1) % QUEUE_CAPACITY;
    loop->count++;
    return 0;
}

void event_loop_run(event_loop_t* loop) {
    while (loop->count > 0) {
        event_t ev = loop->queue[loop->head];
        loop->head = (loop->head + 1) % QUEUE_CAPACITY;
        loop->count--;

        if (ev.id < MAX_EVENT_TYPES && loop->handlers[ev.id] != NULL) {
            loop->handlers[ev.id](ev.id, ev.data);
        }
        // 未注册回调的事件被静默丢弃
    }
}
// main.c
#include <stdio.h>
#include "event_loop.h"

// 两个示例事件ID
#define EV_BUTTON_PRESS  1
#define EV_TIMER_TICK    2

// 示例回调函数
void on_button_press(event_id_t id, void* data) {
    int btn = *(int*)data;
    printf("[回调] 按钮事件 id=%d, 按键编号=%d\n", id, btn);
}

void on_timer_tick(event_id_t id, void* data) {
    int count = *(int*)data;
    printf("[回调] 定时器事件 id=%d, 当前计数=%d\n", id, count);
}

int main(void) {
    event_loop_t loop;
    event_loop_init(&loop);

    // 注册事件回调
    event_loop_register(&loop, EV_BUTTON_PRESS, on_button_press);
    event_loop_register(&loop, EV_TIMER_TICK, on_timer_tick);

    // 模拟产生事件(实际系统中由中断/其他线程调用 post)
    int btn_num = 3;
    int tick_count = 100;
    event_loop_post(&loop, EV_BUTTON_PRESS, &btn_num);
    event_loop_post(&loop, EV_TIMER_TICK, &tick_count);
    event_loop_post(&loop, EV_BUTTON_PRESS, &btn_num);  // 同一事件再次触发

    // 运行事件循环,处理队列中所有事件
    event_loop_run(&loop);

    // 队列已空,循环返回
    printf("事件循环处理完毕,队列剩余事件数=%d\n", loop.count);
    return 0;
}

编译与运行

将上述三个文件置于同一目录,执行以下命令编译:

gcc -Wall -o event_demo main.c event_loop.c

运行程序:

./event_demo

验证方法

程序运行后,观察标准输出。根据代码逻辑,事件队列中依次投递了3个事件:按钮事件(数据3)、定时器事件(数据100)、按钮事件(数据3)。事件循环按先进先出顺序取出并分发,因此预期输出为:

[回调] 按钮事件 id=1, 按键编号=3
[回调] 定时器事件 id=2, 当前计数=100
[回调] 按钮事件 id=1, 按键编号=3
事件循环处理完毕,队列剩余事件数=0

判定标准:

  • 三个回调均被正确调用,且打印顺序与投递顺序一致,说明事件队列的FIFO行为正确;
  • 同一事件ID(按钮事件)被多次投递时,每次都能触发回调,说明注册表可重复使用;
  • 最后一行输出队列剩余事件数为0,说明循环正确消费了所有事件并正常退出。

如需进一步验证未注册事件的处理,可在 main 中投递一个未注册的事件ID(如 99),程序应静默跳过且不崩溃,队列计数正常递减。

事件循环工作机制

事件循环核心流程 事件源 中断/任务/定时器 调用 post() 投递 事件队列 环形缓冲区 FIFO 顺序 事件分发器 查表 handlers[id] run() 循环取出 回调 执行 用户 代码 事件注册表(handlers 数组) register(id, handler) 建立映射 分发时通过 id 索引到对应回调 注册表与队列分离:注册表决定“事件如何响应”,队列决定“事件何时处理”

关键机制说明

本实现中,event_loop_run 采用“处理完当前队列中所有事件后返回”的批处理模式,而非无限阻塞等待。这种设计便于在裸机环境或单线程测试中验证框架正确性。在实际嵌入式系统中,可将 run 放入主循环,或由调度器在空闲时调用;事件投递则来自中断服务程序或其他任务。post 函数在单线程模型下无需加锁,若需支持多线程/中断并发投递,需在 post 与 run 的队列操作处增加临界区保护,这属于后续案例的扩展范围。