插件接口必须用纯虚类并避免STL类型,因std::string等内存布局跨编译器/标准库不一致,易致崩溃;应仅导出virtual函数,数据交换限于POD或自定义结构体,工厂函数需显式导出,卸载前须清理跨模块对象。

插件接口必须用纯虚类 + 稳定 ABI
直接继承 std::shared_ptr 或返回 std::string 会导致 ABI 不兼容——不同编译器或 C++ 标准下,std::string 的内存布局、引用计数实现可能不同。插件主程序和动态库一旦用不同 STL 版本链接,load_plugin() 可能崩溃在构造 std::string 的第一行。
正确做法是:插件导出的接口类只含 virtual 函数,所有数据交换走 POD 类型(int、const char*、固定长度数组)或自定义结构体(不含虚函数、不含 STL 成员)。版本号通过整数常量暴露,不依赖字符串解析:
struct PluginInterface {
virtual ~PluginInterface() = default;
virtual int get_version() const = 0; // 返回 1, 2, ...
virtual const char* get_name() const = 0; // C 风格字符串
virtual int process(int input) = 0;
};dlopen/dlsym 在 Linux 上加载时要处理符号可见性
默认 GCC 编译的 .so 会隐藏所有符号(-fvisibility=hidden),导致 dlsym(handle, "create_plugin") 返回 nullptr,但错误信息只是“symbol not found”,容易误判为路径错。
必须显式导出工厂函数:
立即学习“C++免费学习笔记(深入)”;
- 插件源码中给工厂函数加
__attribute__((visibility("default"))) - 编译时加
-fvisibility=hidden -fvisibility-inlines-hidden(保持内联函数隐藏,只开接口函数) - 主程序调用
dlsym前,先检查dlerror()是否为nullptr,否则后续判断无意义
Windows 同理:DLL 必须用 __declspec(dllexport) 标记 create_plugin,且主程序用 GetProcAddress 前需确认 GetLastError() 为 0。
版本校验不能只比对 major 版本
很多实现只检查 plugin->get_version() == EXPECTED_VERSION,但实际中 minor 升级(如 v1 → v1.1)可能只是新增可选函数,老主程序仍可安全调用核心接口。
建议分层校验:
- major 不同(如 1 vs 2):直接拒绝,ABI 已断裂
- minor 相同或更高(如主程序 v1.0,插件 v1.2):允许加载,但调用前用
dynamic_cast或函数指针查询检测新增接口是否存在 - 插件内部用
static_assert锁死结构体大小,例如static_assert(sizeof(PluginConfig) == 64, "Config layout changed");
卸载插件前必须确保无跨模块对象残留
常见陷阱:插件返回一个 std::shared_ptr<SomeClass> 给主程序,主程序 later 调用 dlclose();此时若 SomeClass 的析构函数在插件 .so 内定义,而 shared_ptr 在主程序堆上销毁,就会 crash——因为虚表地址已失效。
解决方式只有两个:
- 所有资源生命周期严格限制在插件模块内(例如插件提供
destroy_result()函数,主程序必须显式调用) - 或彻底避免跨模块传递 C++ 对象,只传 raw pointer + size + destroy callback 函数指针(该函数也由插件提供并导出)
哪怕只用 std::vector<char> 返回二进制数据,也要小心——它的析构可能触发插件内的内存释放逻辑。最稳的是用 malloc/free 配对,或约定由主程序分配、插件填充、主程序释放。


















