c++oding="utf-8" ?>
不能直接用裸指针传事件,因主程序与插件内存隔离、结构体二进制布局可能因对齐/ABI/模板实例化而不同,导致乱码、崩溃或double-free;必须通过序列化、纯C ABI接口或共享内存池实现安全投递。

为什么不能直接用裸指针传事件到插件
主程序加载插件(如 .so 或 .dll)后,双方内存空间隔离,类型定义不一致:主程序的 struct EventA 和插件里同名结构体哪怕字段相同,也可能因编译器对齐、ABI 版本或模板实例化差异导致二进制不兼容。直接传递指针会触发未定义行为,常见现象是插件读取到乱码、崩溃在 std::string 构造、或 std::shared_ptr 释放时 double-free。
安全分发的前提是「零共享内存布局」——事件必须序列化/反序列化,或通过纯 C ABI 的稳定接口中转。
用纯 C 函数表 + 内存池实现跨 ABI 事件投递
主程序导出一个全局函数指针表(EventBusAPI),只含 void*、int、const char* 等 C 兼容类型,插件通过 dlsym / GetProcAddress 获取。关键点在于事件数据不由插件 malloc/free,而由主程序提供固定大小内存池:
-
event_bus_publish(const char* topic, const void* data, size_t len):主程序拷贝data到内部预分配缓冲区(如 4KB slab),再通知所有监听者 -
event_bus_subscribe(const char* topic, void (*cb)(const void*, size_t), void* user_data):插件注册回调,主程序只保存cb和user_data,不解析data - 插件回调中禁止调用
delete或free传入的data指针——它指向主程序的只读内存池
示例:插件订阅 "log/error" 后,主程序发事件时只保证 data 是 UTF-8 字节数组,长度 ≤1024,插件用 std::string_view(reinterpret_cast 安全读取。
如何避免插件阻塞主线程或互相干扰
事件总线不是 RPC 调用通道。主程序必须明确区分同步/异步语义:
- 默认采用「发布即忘」模式:
event_bus_publish不等待插件处理完成,主程序不感知插件是否卡死 - 若需反馈,插件应自行触发新事件(如
"plugin/ready"),而非在回调里阻塞等待主程序响应 - 主程序可为每个插件分配独立线程+队列,但需插件显式调用
event_bus_set_thread_local_queue()注册——否则所有事件都在主程序线程执行 - 禁止插件在回调中调用
event_bus_publish同一 topic,否则可能引发重入死锁(尤其当主程序用递归锁时)
典型错误:插件在 "config/load" 回调里同步加载大文件并 publish "config/loaded",导致主程序 UI 线程卡顿 2 秒。
C++20 模块化插件中的事件类型安全方案
如果主程序和插件都用同一份 C++20 模块(module EventBus;)编译,且链接时启用 -fmodules-ts 和统一模块映射,可暴露 concept Event 和 template 接口。但这要求:
- 主程序与所有插件使用完全相同的编译器版本、标准库(如全部用 libc++ 17)、以及模块构建参数
- 事件类型必须满足
trivially_copyable,且不含虚函数、非 POD 成员、或依赖 TLS 的静态变量 - 插件不能定义新的
Event子类——只能使用主程序模块导出的struct LogEvent、struct KeyEvent等
这种方案性能最好(零拷贝),但实际工程中 ABI 稳定性极难保障,仅推荐封闭生态(如游戏引擎内嵌插件)使用。
最常被忽略的是事件生命周期管理:无论用哪种方案,只要事件携带了资源句柄(如文件描述符、GPU texture ID),就必须约定清楚谁负责关闭——通常由发布方持有所有权,插件只做瞬时读取,不可缓存或转发。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











