插件DLL必须用extern "C"和__declspec(dllexport)双重导出符号,接口需为纯虚类且无STL类型,生命周期由插件管理,主程序通过RAII封装调用,ABI版本需运行时校验。

插件 DLL 必须导出 C 风格符号,否则 GetProcAddress 找不到
Windows 下用 LoadLibrary + GetProcAddress 加载插件时,C++ 编译器会对函数名做 name mangling,比如 CreatePlugin 可能变成 ?CreatePlugin@@YAPAVIPlugin@@XZ。这时候 GetProcAddress(hMod, "CreatePlugin") 一定返回 nullptr。
解决方法只有加 extern "C" 和 __declspec(dllexport) 双重约束:
extern "C" {
__declspec(dllexport) IPlugin* CreatePlugin();
__declspec(dllexport) void DestroyPlugin(IPlugin* p);
}注意:不能只写 extern "C" 而漏掉 __declspec(dllexport) —— 后者控制符号是否进入 DLL 的导出表,前者只控制符号命名方式。两者缺一不可。
常见错误现象:GetProcAddress 返回空指针,但 GetLastError() 是 0(不是 ERROR_PROC_NOT_FOUND),说明符号名对不上,而非函数不存在。
立即学习“C++免费学习笔记(深入)”;
std::shared_ptr 不能跨 DLL 边界传递裸指针或自定义 deleter
工厂函数返回的插件对象如果用 std::shared_ptr<iplugin></iplugin> 包裹,且其 deleter 是插件 DLL 内定义的 lambda 或函数(比如调用 DestroyPlugin),那么在主程序中析构该 shared_ptr 时,会尝试在主程序堆上调用插件 DLL 的代码——这在不同 CRT 版本或不同编译选项下极易崩溃。
安全做法是让插件自己管理生命周期,工厂只返回裸指针,并由主程序显式调用 DestroyPlugin:
- 插件 DLL 导出
DestroyPlugin(IPlugin*),内部用delete或对应 allocator 释放 - 主程序用 RAII 封装:定义一个
PluginPtr类,构造时调用CreatePlugin,析构时调用DestroyPlugin - 避免任何 STL 容器(如
std::vector<:shared_ptr>></:shared_ptr>)跨 DLL 传递,除非所有模块用完全相同的 CRT 和编译器版本
插件接口必须是纯虚类,且禁止含内联函数、非 POD 成员、模板
插件接口类(如 IPlugin)若含内联实现、std::string 成员或虚继承,会导致主程序和插件对同一虚表布局理解不一致。尤其当主程序用 MSVC 2019 编译,插件用 2022 编译时,vtable 偏移可能错位,调用任意虚函数都可能跳到非法地址。
正确接口定义要点:
- 仅含纯虚函数(
= 0),无函数体,无默认参数 - 无数据成员,或仅含 POD 类型(
int、void*、char[64]等) - 析构函数必须是纯虚且有实现:
virtual ~IPlugin() = default;不行,要写virtual ~IPlugin() {} - 禁止使用
std::类型作为参数或返回值;字符串统一用const char*或const wchar_t*
示例接口:
struct IPlugin {
virtual int Initialize() = 0;
virtual void Process(const void* input, size_t len) = 0;
virtual const char* GetVersion() const = 0;
virtual ~IPlugin() {}
};插件路径、加载失败与 ABI 兼容性检查必须由主程序主动控制
不要依赖系统 PATH 或隐式 DLL 搜索顺序。主程序应明确指定插件目录(如 "./plugins/"),遍历 .dll 文件,逐个 LoadLibraryEx 并设 LOAD_WITH_ALTERED_SEARCH_PATH,避免意外加载同名系统 DLL。
更关键的是 ABI 兼容性无法在链接期发现,只能靠运行时校验:
- 插件 DLL 导出一个
GetPluginABI()函数,返回整数(如0x202301表示 2023.01 版 ABI) - 主程序在
LoadLibrary后立即调用它,比对预设的 ABI 版本号,不匹配则FreeLibrary并报错 - 错误信息如
"Plugin 'xxx.dll' ABI mismatch: expected 0x202301, got 0x202212"比泛泛的"Failed to load plugin"有用得多
这个 ABI 标识必须手动维护,不能靠编译时间戳或 Git commit —— 因为 ABI 变更往往只发生在接口头文件修改时,和构建时间无关。
动态插件最难缠的从来不是加载本身,而是两个模块对同一块内存、同一个虚表、同一个调用约定的理解是否真正一致。每个 extern "C"、每个纯虚函数声明、每个 __declspec(dllexport),都是在给这种一致性上保险栓。漏掉任何一个,问题都可能延迟到某个特定输入、某次内存重用、某个优化级别下才爆发。


















