dlopen加载.so需用extern "c"导出函数、绝对路径调用、dlsym后判空;windows用loadlibrary需__declspec(dllexport)和extern "c";跨平台接口须纯虚类+pod类型,禁用std::shared_ptr等abi敏感类型。

怎么用 dlopen 加载 .so 文件(Linux/macOS)
Linux/macOS 下最直接的方式就是 dlopen,但它不是 C++ 标准函数,得连 -ldl,而且默认不支持 C++ 符号重载解析——你 dlsym 拿到的函数地址,如果原函数是 C++ 成员函数或带模板/重载的,大概率找不到。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 插件导出函数必须用
extern "C"封装,禁用 name mangling,比如:extern "C" { PluginInterface* create_plugin(); void destroy_plugin(PluginInterface* p); } - 加载前检查路径是否绝对——
dlopen("plugin.so")会按LD_LIBRARY_PATH搜索,但行为不稳定;推荐用dlopen("/full/path/to/plugin.so", RTLD_LAZY) - 调用
dlsym后务必判空:if (!create_fn) { fprintf(stderr, "symbol not found: %s\n", dlerror()); } - 别在插件里 new 全局对象或调用
atexit——卸载时可能崩溃,尤其多线程环境下
Windows 怎么用 LoadLibrary 替代 dlopen
Windows 没有 dlopen,对应的是 LoadLibrary + GetProcAddress,但要注意:MSVC 编译的 DLL 默认不导出 C++ 符号,且函数名会被修饰(decorated),GetProcAddress 查不到 create_plugin,只能查 ?create_plugin@@YAPAVPluginInterface@@XZ 这种。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用
.def文件显式导出,或者在函数声明加__declspec(dllexport),并仍套extern "C" - 调用
LoadLibrary前确保路径是绝对路径或位于PATH中;相对路径容易因工作目录变化而失败 -
FreeLibrary不等于“立刻卸载”——如果有线程还在执行该 DLL 里的代码,系统会延迟卸载,此时再调LoadLibrary同名 DLL 可能复用旧映像,导致状态混乱 - 避免在 DLL 的
DLL_PROCESS_ATTACH里做耗时初始化,主程序启动卡顿就从这儿来
怎么设计跨平台插件接口(C++ ABI 兼容性陷阱)
C++ 没有稳定 ABI,不同编译器、甚至同一编译器不同版本(如 GCC 11 vs 12)、不同 STL 实现(libstdc++ vs libc++),都可能导致 std::string 或 std::vector 在插件和主程序间传参崩溃。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 接口类必须纯虚(
class PluginInterface { public: virtual ~PluginInterface() = default; virtual int process(...) = 0; };),且所有函数参数/返回值只用 POD 类型(int、double、const char*、void*) - 禁止通过接口传递
std::string、std::shared_ptr、异常、RTTI 类型信息——这些全靠 ABI 对齐,一错就段错误 - 内存所有权必须明确:谁分配谁释放。例如插件返回的字符串,得配套提供
free_string(char*)函数,不能让主程序调delete[] - 如果必须传结构体,用
struct+#pragma pack(1)防对齐差异,并在头文件里写死字段顺序和大小
为什么 std::shared_ptr 不能跨 DLL 边界安全使用
表面上看 std::shared_ptr 是个指针+引用计数,但它的控制块(control block)分配在堆上,而 Windows 下每个 DLL 有自己的 CRT 堆,Linux 下虽共享堆但 shared_ptr 的删除器类型信息仍依赖 ABI。结果就是:主程序 delete 插件创建的 shared_ptr,可能调用错的析构器或释放到错的堆。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 彻底回避跨边界的智能指针——插件只返回裸指针(
PluginInterface*),销毁由插件自己提供的destroy_plugin函数负责 - 如果非要共享资源,用句柄(
int或uintptr_t)代替指针,主程序把句柄传给插件的use_handle函数,内部查表转真实对象 - 不要相信“我用的都是 GCC 12”,CI 环境、用户机器、第三方 SDK 的编译环境完全不可控
最难的不是加载,是让两个独立编译的二进制模块,在不共享内存布局、不约定 ABI、不统一异常模型的前提下,还能交换数据而不崩溃。每一步妥协,都是为兼容性留的后门。










