插件系统核心是运行时加载与类型擦除:必须用纯虚接口+extern "C"工厂函数(如create_plugin)实现安全类型转换,禁用reinterpret_cast裸指针强转,否则因虚表/ABI不一致必然崩溃。

插件系统必须解决的核心问题:运行时加载与类型擦除
直接用裸指针实现插件系统几乎必然崩溃。C++ 没有原生插件模型,dlopen/dlsym(Linux)或 LoadLibrary/GetProcAddress(Windows)返回的是 void*,你无法安全地把它 reinterpret_cast 成任意类指针——虚表偏移、ABI 差异、构造函数未调用都会导致未定义行为。
真正可行的路径只有一条:把插件接口抽象为纯虚基类,所有插件导出一个工厂函数,返回指向该接口的指针。这个工厂函数本身必须是 C 链接(extern "C"),否则符号名会因 C++ name mangling 无法被主程序找到。
实操建议:
- 定义统一接口,如
IPlugin,只含虚析构和业务方法,不带成员变量或非 trivial 类型 - 每个插件 SO/DLL 必须导出一个 C 链接函数,例如:
extern "C" IPlugin* create_plugin() - 主程序用
dlsym(handle, "create_plugin")获取函数指针,再调用它——此时返回的是已构造好的对象指针,不是 rawvoid*强转结果 - 插件内部必须确保
create_plugin返回的对象生命周期由插件自身管理(或明确约定由主程序 delete)
为什么不能用 reinterpret_cast<IPlugin*>(dlsym(...))
这种写法在绝大多数情况下会立即崩溃或静默错误。典型现象包括:调用虚函数时跳转到随机地址、this 指针错位、RTTI 失效、dynamic_cast 返回 null。
立即学习“C++免费学习笔记(深入)”;
根本原因在于:共享库中 IPlugin 的虚表布局、vptr 偏移、异常处理帧信息,与主程序编译时生成的 IPlugin 定义哪怕只有细微差异(比如不同编译器版本、不同 STL 实现、甚至仅是 debug/release 混用),都会导致二进制不兼容。
正确做法是让插件自己完成类型转换:
- 插件内定义的
IPlugin必须和主程序头文件完全一致(建议共用同一份头) -
create_plugin函数体内用new ConcretePlugin(),并显式返回static_cast<IPlugin*>(ptr) - 主程序拿到的是“经过插件确认过类型安全”的指针,而非自己强行解释的内存块
Linux 下 dlopen 常见陷阱与绕过方式
最常遇到的错误是 dlopen: cannot open shared object file: No such file or directory,但文件明明存在。这通常不是路径问题,而是依赖缺失或 rpath 错误。
关键点:
- 插件 SO 不应链接主程序的私有符号;若需 STL 或 CRT,必须用
-fPIC -shared编译,并显式链接(如-lstdc++),不能依赖主程序提供 - 避免使用
RUNPATH或RPATH指向开发路径(如/home/user/build),发布时用$ORIGIN:编译插件时加-Wl,-rpath,'$ORIGIN' -
dlopen默认使用RTLD_LOCAL,插件符号不会污染全局;若插件间需互相调用,改用RTLD_GLOBAL,但要小心符号冲突 - 调用
dlsym后务必检查返回值是否为nullptr,不要假设函数一定存在
Windows 上 LoadLibrary + GetProcAddress 的 ABI 注意事项
Windows 插件比 Linux 更脆弱,因为 MSVC 的 ABI 在不同版本间不保证兼容(尤其涉及异常、RTTI、模板实例化时)。即使使用相同编译器,/MD 与 /MT 混用也会导致堆分配/释放跨 DLL 失败。
必须遵守的约束:
- 主程序和所有插件必须使用完全相同的运行时库选项(推荐 /MD,即动态链接 CRT)
- 禁止在插件接口中传递 STL 容器(
std::string,std::vector)、智能指针或任何模板类;改用 C 风格字符串(const char*)、裸数组、或自定义 POD 结构 -
GetProcAddress返回的是FARPROC(即void*),必须用函数指针类型强转,例如:using CreateFn = IPlugin*(*)(); auto fn = reinterpret_cast<CreateFn>(GetProcAddress(h, "create_plugin")); - 插件 DLL 的入口点(
DllMain)中禁止调用LoadLibrary或同步等待其他 DLL,容易死锁
复杂点不在指针本身,而在如何让两个独立编译的二进制模块,在没有运行时类型系统兜底的情况下,就虚函数调用、内存归属、异常传播达成一致——这些细节一旦漏掉一个,调试成本远高于设计成本。


















