c++如何实现动态加载dll/so插件_c++ dlopen与GetProcAddress使用【实战】

Windows用LoadLibrary/GetProcAddress、Linux/macOS用dlopen/dlsym加载动态库;需注意路径、导出声明、函数签名匹配、错误检查及资源释放。

Windows 下用 LoadLibraryGetProcAddress 加载 DLL

Windows 不支持 dlopen,必须用 Win32 API。核心是先加载模块,再按符号名取函数指针。常见错误是传错路径、没加 .dll 后缀、或函数声明与导出不一致。

  • LoadLibrary 返回 HMODULE,传入路径应为绝对路径或位于 PATH 或当前目录的相对路径;推荐用 GetFullPathName 预处理
  • 导出函数必须用 extern "C" + __declspec(dllexport)(DLL 编译时),否则 C++ 名字修饰会导致 GetProcAddress 找不到
  • 获取函数指针后务必用 typedef 声明匹配签名,强制类型转换容易因参数/调用约定不一致引发栈损坏
typedef int (*add_func)(int, int);
HMODULE hMod = LoadLibrary(L"myplugin.dll");
if (hMod) {
    add_func add = (add_func)GetProcAddress(hMod, "add");
    if (add) {
        int res = add(3, 5); // 正常调用
    }
    FreeLibrary(hMod); // 记得释放
}

Linux/macOS 下用 dlopen / dlsym 加载 SO

POSIX 标准接口,行为统一但细节更敏感:路径、符号可见性、链接方式都影响成败。最常踩的坑是未加 -fPIC 编译 SO,或忘记 -rdynamic 导致主程序符号不可见(当插件需回调主程序时)。

  • dlopen 第二个参数建议固定用 RTLD_LAZY | RTLD_LOCAL:延迟绑定降低启动开销,LOCAL 避免符号污染全局符号表
  • dlsym 返回 void*,必须显式转为目标函数指针类型,不能直接赋值给普通函数变量(C++ 类型检查严格)
  • SO 文件名必须带 .so 后缀,且运行时需确保在 LD_LIBRARY_PATH 中,或用 ./libxxx.so 绝对/相对路径
#include 
typedef int (*add_func)(int, int);
void* handle = dlopen("./myplugin.so", RTLD_LAZY | RTLD_LOCAL);
if (handle) {
    add_func add = reinterpret_cast(dlsym(handle, "add"));
    if (add) {
        int res = add(3, 5);
    }
    dlclose(handle);
}

跨平台封装要注意的三件事

写一套代码同时跑 Windows/Linux,不能只靠宏开关,底层语义差异必须抹平。最容易被忽略的是错误诊断和生命周期管理。

  • 错误信息不可靠:GetLastError()dlerror() 都是“上次调用”结果,必须在每次 LoadLibrary/dlopen 后立刻检查,中间穿插其他系统调用会覆盖它
  • 函数指针类型必须统一定义,例如用 using plugin_add_t = int(*)(int,int);,避免各处 typedef 不一致
  • 资源释放逻辑要对称:FreeLibrary 成功后句柄立即失效;dlclose 只是减引用计数,真正卸载可能延迟——若插件内有全局对象析构依赖主程序状态,可能 crash

GetProcAddress 找不到函数的五个真实原因

不是名字拼错了那么简单。调试时优先查这几点:

  • DLL 编译时没加 __declspec(dllexport),或用了 DEF 文件但导出名写错(比如写了 add@8 却用 "add" 查)
  • C++ 源文件里函数没加 extern "C",导致导出的是 ??add@@YAHHH@Z 这类修饰名
  • 目标函数是类成员函数或模板实例,无法直接导出——必须包装成自由函数
  • 进程位数不匹配:32 位程序加载 64 位 DLL 直接失败(LoadLibrary 返回 NULL),错误码是 ERROR_BAD_EXE_FORMAT
  • 依赖的其它 DLL 缺失,LoadLibrary 失败但你以为是函数没找到;用 Dependencies.exe(Windows)或 ldd(Linux)查依赖树

动态加载本身不难,难的是让符号能被稳定、可预测地找到。别省略导出声明,别绕过类型安全强制转换,更别假设路径一定存在——这些地方出问题,日志里往往只有一行空指针解引用。