如何通过LoadLibrary动态加载C++类DLL并查找成员函数,同时支持DLL热更新?
如何通过LoadLibrary动态加载C++类DLL并查找成员函数,同时支持DLL热更新?
嘿,我正好折腾过类似的场景,给你梳理下具体的实现思路和实操步骤,应该能帮你搞定问题~
一、先补补普通函数的细节(你可能已经懂,但做个铺垫)
普通非成员函数的动态加载其实很直接,但要注意避免C++名字修饰——你得用extern "C"来导出函数,这样GetProcAddress才能精准找到函数名。比如DLL端这么写:
// DLL代码 extern "C" __declspec(dllexport) void MyPlainFunc() { // 你的函数逻辑 }
主程序里就用LoadLibrary拿到句柄,再用GetProcAddress把函数地址转成对应指针就行,没啥坑。
二、类成员函数的核心问题与解决办法
C++的非静态成员函数没法直接用GetProcAddress捞到,原因有俩:一是编译器会给成员函数做“名字修饰”(不同编译器规则还不一样),二是非静态成员函数必须绑定类实例(隐含this指针)。业界最常用的解决方案是抽象接口+工厂函数,相当于用C风格的函数做中间层,绕开成员函数的导出问题:
步骤1:定义公共抽象接口(主程序和DLL共享这个头文件)
先写一个纯虚基类,把你要调用的成员函数都声明成纯虚函数,一定要加虚析构,不然会内存泄漏:
// IMyClass.h(主程序和DLL都要包含) class IMyClass { public: virtual ~IMyClass() = default; virtual void DoSomething(int param) = 0; // 你要调用的成员函数 virtual int GetResult() = 0; };
步骤2:DLL端实现接口并导出工厂函数
在DLL里写一个继承自抽象接口的具体类,然后导出两个C风格的函数:一个用来创建类实例,一个用来销毁实例(同样用extern "C"避免名字修饰):
// DLL的实现文件 #include "IMyClass.h" // 具体的类实现 class MyClassImpl : public IMyClass { public: void DoSomething(int param) override { // 这里写你的业务逻辑 } int GetResult() override { return 666; } }; // 导出创建实例的工厂函数 extern "C" __declspec(dllexport) IMyClass* CreateMyClass() { return new MyClassImpl(); } // 导出销毁实例的函数(也可以靠虚析构,但显式销毁更安全) extern "C" __declspec(dllexport) void DestroyMyClass(IMyClass* obj) { delete obj; }
步骤3:主程序动态加载并调用成员函数
主程序里通过加载DLL拿到工厂函数的地址,创建实例后就可以像调用普通类对象一样调用成员函数了:
// 主程序代码 #include "IMyClass.h" #include <windows.h> #include <iostream> int main() { HMODULE hDll = LoadLibrary(L"MyClassDll.dll"); if (!hDll) { std::cerr << "DLL加载失败!" << std::endl; return 1; } // 定义函数指针类型 using CreateFunc = IMyClass*(*)(); using DestroyFunc = void(*)(IMyClass*); // 获取工厂函数地址 CreateFunc create = reinterpret_cast<CreateFunc>(GetProcAddress(hDll, "CreateMyClass")); DestroyFunc destroy = reinterpret_cast<DestroyFunc>(GetProcAddress(hDll, "DestroyMyClass")); if (!create || !destroy) { std::cerr << "获取函数地址失败!" << std::endl; FreeLibrary(hDll); return 1; } // 创建实例并调用成员函数 IMyClass* obj = create(); obj->DoSomething(123); std::cout << "结果:" << obj->GetResult() << std::endl; // 销毁实例+释放DLL destroy(obj); FreeLibrary(hDll); return 0; }
三、实现DLL热更新的关键要点
热更新的核心是不重启主程序,无缝切换旧DLL到新DLL,这里要注意几个关键细节:
1. 确保旧DLL能被彻底卸载
- 必须先销毁所有从旧DLL创建的实例(调用
DestroyMyClass),不能留任何引用 - 调用
FreeLibrary释放旧DLL句柄,如果之前多次LoadLibrary,要对应多次FreeLibrary,确保引用计数归0
2. 热更新的完整流程示例
可以在主程序里维护全局的DLL句柄、函数指针和实例,写一个专门的热更新函数:
// 主程序全局变量(也可以封装成类) HMODULE g_hCurrentDll = nullptr; CreateFunc g_CreateFunc = nullptr; DestroyFunc g_DestroyFunc = nullptr; IMyClass* g_CurrentObj = nullptr; // 热更新函数 void HotUpdate(const wchar_t* newDllPath) { // 1. 销毁旧实例 if (g_CurrentObj && g_DestroyFunc) { g_DestroyFunc(g_CurrentObj); g_CurrentObj = nullptr; } // 2. 卸载旧DLL if (g_hCurrentDll) { FreeLibrary(g_hCurrentDll); g_hCurrentDll = nullptr; } // 3. 加载新DLL g_hCurrentDll = LoadLibrary(newDllPath); if (!g_hCurrentDll) { std::cerr << "加载新DLL失败!" << std::endl; return; } // 4. 获取新的工厂函数地址 g_CreateFunc = reinterpret_cast<CreateFunc>(GetProcAddress(g_hCurrentDll, "CreateMyClass")); g_DestroyFunc = reinterpret_cast<DestroyFunc>(GetProcAddress(g_hCurrentDll, "DestroyMyClass")); if (!g_CreateFunc || !g_DestroyFunc) { std::cerr << "获取新DLL函数地址失败!" << std::endl; FreeLibrary(g_hCurrentDll); g_hCurrentDll = nullptr; return; } // 5. 创建新实例 g_CurrentObj = g_CreateFunc(); std::cout << "DLL热更新成功!" << std::endl; }
3. 额外注意事项
- 线程安全:如果主程序是多线程的,热更新时一定要加锁,避免其他线程在更新过程中访问旧实例或DLL资源
- 接口兼容性:新DLL的抽象接口必须和旧DLL完全一致,不能改函数签名,否则主程序调用会崩溃
- 触发时机:可以把DLL的版本号写在配置文件里,主程序定时检查配置,发现版本变化就自动触发热更新
备注:内容来源于stack exchange,提问作者chacebai
相关产品推荐
相关产品推荐

