插件DLL的工厂函数必须用extern "C"导出以禁用C++名称修饰,否则GetProcAddress将找不到符号;同时需检查LoadLibrary返回值及GetLastError错误码,并确保ABI稳定性与跨模块内存管理安全。

插件 DLL 中的工厂函数必须用 extern "C" 导出
Windows 下动态加载插件(.dll)时,如果工厂函数用 C++ 普通方式导出(比如只加 __declspec(dllexport)),链接器会做 C++ name mangling,导致主程序调用 GetProcAddress 时找不到符号。例如 create_processor() 可能被编译成 ?create_processor@@YAPAVProcessor@@XZ,完全不可预测。
正确做法是强制以 C 风格导出,禁用 mangling:
extern "C" {
__declspec(dllexport) Processor* create_processor();
__declspec(dllexport) void destroy_processor(Processor* p);
}
- 必须包裹在
extern "C"块中,不能只对单个函数加 - Linux 下对应的是
__attribute__((visibility("default"))),但 Windows 仍需extern "C" - 若忘记加,
GetProcAddress(hModule, "create_processor")返回nullptr,且无编译或加载错误,极易误判为路径/模块加载失败
dlopen / LoadLibrary 加载后必须检查句柄有效性
插件路径错误、依赖缺失(如 MSVCRT 版本不匹配)、架构不一致(x64 插件被 x86 主程序加载)都会导致 LoadLibrary 返回 NULL(Windows)或 dlopen 返回 nullptr(Linux)。但很多人只检查是否为空,忽略错误码细节。
Windows 下应立即调用 GetLastError() 辅助定位:
HMODULE h = LoadLibrary(L"processor_plugin.dll");
if (!h) {
DWORD err = GetLastError();
// err == 126 → 找不到指定模块(路径错/缺依赖)
// err == 193 → %1 不是有效的 Win32 应用程序(位数不匹配)
}
-
LoadLibrary成功不代表插件逻辑就绪——它只保证 DLL 被映射,不执行任何初始化代码 - 若插件依赖另一个未安装的 DLL(如
vcruntime140.dll),LoadLibrary直接失败,不会抛异常 - 建议在加载后立刻尝试获取一个导出函数指针,双重验证:既加载成功,又符号可见
插件接口基类必须满足 ABI 稳定性约束
主程序和插件分别编译,若共用头文件中的抽象基类(如 Processor),其虚函数表布局、vtable offset、成员对齐等必须严格一致。否则调用虚函数可能崩溃或读错内存。
关键约束包括:
- 基类不能含非 POD 成员(如
std::string、std::vector),它们的实现随 STL 版本变化 - 禁止内联虚函数;所有虚函数必须在 .cpp 中定义,确保 vtable 条目地址由插件提供
- 构造/析构函数不能是 inline,且插件中必须提供
destroy_processor进行显式销毁(避免跨模块调用 operator delete) - 推荐用纯虚接口 + PIMPL 隐藏实现细节,把复杂对象封装在插件内部,只暴露简单 C 风格函数
Linux 下 dlsym 找不到符号?检查 -fvisibility=hidden 和 default 标签
Clang/GCC 默认开启 -fvisibility=hidden,即使写了 __attribute__((visibility("default"))),若没加在函数定义上(而只加在声明上),符号依然不可见。
正确写法(定义处标注):
// plugin.cpp
extern "C" {
__attribute__((visibility("default"))) Processor* create_processor() {
return new ConcreteProcessor();
}
__attribute__((visibility("default"))) void destroy_processor(Processor* p) {
delete p;
}
}
- 头文件中声明无需 visibility 属性,只在定义处加
- 编译插件时务必加
-fPIC -shared,缺一不可 -
nm -D libplugin.so可验证符号是否出现在动态符号表中;若无输出,说明 visibility 或导出配置失效
插件工厂模式真正难的不是“怎么写”,而是“怎么让两个独立编译单元在运行时安全交换对象”。虚函数表、内存分配器、异常传播、STL 类型边界——这些地方只要有一处越界,崩溃就发生在最意想不到的调用点。导出符号只是第一道门,跨模块对象生命周期管理才是埋雷最多的地方。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











