旧插件对象不能立即销毁,须待所有调用线程退出;状态迁移应通过序列化字节流完成,避免跨so stl abi不兼容;函数指针切换需原子操作且调用侧不可缓存;虚表与rtti跨so失效,应禁用dynamic_cast。

旧插件对象不能立刻销毁,必须等所有调用线程退出
热替换时最常踩的坑,就是 dlclose() 调完就以为万事大吉。实际上,只要还有工作线程正在执行旧插件里的函数(哪怕只在栈上留着一个局部变量),dlclose() 后该库的代码段和数据段就可能被内核回收,后续访问直接触发 Segmentation fault。
真正安全的做法是:先停止向旧插件派发新请求,再等待所有已进入旧插件逻辑的线程自然退出。常见手段包括:
- 用原子计数器记录当前正在执行旧插件函数的线程数,每次进入前
++,返回前-- - 切换前调用
pthread_kill()或信号屏障,通知工作线程“即将切换”,让它们主动检查是否需提前退出 - Linux 下可读取
/proc/self/maps,确认旧 so 的内存映射是否还存在;Windows 下用EnumProcessModules验证FreeLibrary是否真生效
状态迁移必须由插件自己控制,主程序不碰内部数据
很多团队试图让主程序把旧插件的某个 std::map 或 std::shared_ptr 直接“移交”给新插件,这几乎必然失败——两个 so 的 STL 实现可能 ABI 不兼容,sizeof(std::string) 都可能不同。
正确路径是只传递原始数据或序列化结构:
- 旧插件提供
serialize_state()函数,返回void*+ size_t(比如打包成 flatbuffer 或 protobuf) - 新插件提供
restore_state(const void*, size_t),自行解析并重建内部状态 - 主程序只负责搬运字节流,不解释内容,也不调用任何跨 so 的构造/析构
函数指针切换必须原子,且调用侧不能缓存本地副本
用 std::atomic<void></void> 或 std::atomic<:function>></:function> 管理函数指针是基础,但容易忽略两点:
- 赋值前必须确保新函数地址已通过
dlsym()成功获取,且非nullptr;否则会把空指针原子写入,调用即崩溃 - 所有业务线程必须每次调用都执行
func_ptr.load(),不能在循环里缓存为局部变量——否则永远看不到新版本 - 切换瞬间要加
std::atomic_thread_fence(std::memory_order_release),防止编译器或 CPU 重排导致新函数体还没初始化完,指针就已对外可见
虚表和 RTTI 跨 so 失效是硬伤,别指望 dynamic_cast 安全转型
如果插件导出的是继承自主程序定义的基类的对象,然后主程序用 dynamic_cast 去转,90% 情况下会返回 nullptr 或直接 crash。根本原因是 GCC/Clang 的 RTTI 信息默认不跨共享库导出,typeid 比较在不同 so 里得到的是不同地址。
绕过方式只有两种:
- 彻底放弃虚继承,改用纯 C 风格 ABI:所有接口函数用
extern "C"导出,参数只传void*和 POD 类型 - 若必须用 C++ 接口,主程序和插件共用同一份头文件,并启用
-fvisibility=hidden+ 显式__attribute__((visibility("default")))控制符号导出,但依然无法解决虚表布局变更风险
真正难的不是换掉代码,而是让正在运行的协程、定时器回调、异步 IO 完成例程这些“活上下文”,全部脱离对旧插件地址空间的依赖——它们可能正停在某条指令上,等着你把新函数塞进寄存器。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











