4、Windows下的动态加载:LoadLibrary/GetProcAddress 的使用、跨平台兼容性考量
说实话,动态加载这事儿,在Windows上跟Linux完全是两套玩法。我最早从Linux转Windows开发时,被LoadLibrary和GetProcAddress这套API折腾得不轻。明明都是加载动态库,接口风格却天差地别。今天咱们就把Windows这套东西掰开揉碎了讲清楚。
4.1 Windows动态加载的核心API
Windows下动态加载DLL,核心就三个函数:LoadLibrary、GetProcAddress和FreeLibrary。说白了,它们分别对应Linux下的dlopen、dlsym和dlclose。
核心函数签名:
// 加载DLL
HMODULE LoadLibraryA(LPCSTR lpLibFileName);
HMODULE LoadLibraryW(LPCWSTR lpLibFileName);
// 通常用宏:LoadLibrary 自动选择A/W版本
// 获取函数地址
FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName);
// 卸载DLL
BOOL FreeLibrary(HMODULE hLibModule);
嗯,这里要注意:GetProcAddress的第二个参数是函数名,必须是ANSI字符串,不能是宽字符。我当年刚接触时,想当然地传了个宽字符串进去,结果死活拿不到地址,查了半天文档才反应过来。
4.2 一个完整的加载示例
咱们直接看代码。假设我们有一个DLL,导出了一个加法函数:
// plugin.h - 插件接口定义
typedef int (*AddFunc)(int a, int b);
// main.c - 动态加载DLL
#include <windows.h>
#include <stdio.h>
int main() {
HMODULE hDll = NULL;
AddFunc add = NULL;
int result = 0;
// 1. 加载DLL
hDll = LoadLibrary("math_plugin.dll");
if (hDll == NULL) {
printf("加载DLL失败,错误码: %lu\n", GetLastError());
return -1;
}
// 2. 获取函数地址
add = (AddFunc)GetProcAddress(hDll, "add");
if (add == NULL) {
printf("获取函数地址失败,错误码: %lu\n", GetLastError());
FreeLibrary(hDll);
return -1;
}
// 3. 调用函数
result = add(3, 5);
printf("3 + 5 = %d\n", result);
// 4. 卸载DLL
FreeLibrary(hDll);
return 0;
}
我个人习惯在加载DLL后立即检查GetLastError(),这能帮你快速定位问题。比如DLL路径不对、依赖缺失、或者函数名拼写错误,都能通过错误码看出来。
小技巧:用GetLastError()获取错误码后,可以用FormatMessage把错误码转成可读的字符串。我在项目中经常封装一个PrintLastError()函数,调试时特别方便。
4.3 函数导出名的那些坑
你想想看,C++编译的DLL,函数名会被编译器修饰(name mangling)。比如一个简单的int add(int a, int b),在DLL里可能变成?add@@YAHHH@Z这种鬼样子。
为什么会这样?因为C++支持函数重载,编译器需要把参数类型编码进函数名里。但这对动态加载来说简直是灾难——你总不能每次去查DLL的导出表吧?
解决办法有两个:
- 用
extern "C"导出:告诉编译器按C的方式导出,不进行名字修饰。 - 用
.def文件:手动指定导出名,甚至可以给函数起别名。
// 推荐做法:用extern "C"防止名字修饰
#ifdef __cplusplus
extern "C" {
#endif
__declspec(dllexport) int add(int a, int b) {
return a + b;
}
#ifdef __cplusplus
}
#endif
注意:即使用了extern "C",在Windows上函数名仍然可能被加下划线前缀(比如_add)。不过GetProcAddress会自动处理这个,你传"add"就行。但如果你用GetProcAddress传"_add",反而会找不到。嗯,这里确实有点反直觉。
4.4 跨平台兼容性考量
说白了,跨平台动态加载的核心就是抽象。你不能在代码里到处写LoadLibrary和dlopen,那维护起来太痛苦了。
我建议的做法是:封装一个统一的动态库加载接口。来看我项目里常用的方案:
// dynlib.h - 跨平台动态库加载接口
#ifdef _WIN32
#include <windows.h>
typedef HMODULE DynLibHandle;
#define DynLibLoad(lib) LoadLibrary(lib)
#define DynLibSym(handle, name) GetProcAddress(handle, name)
#define DynLibUnload(handle) FreeLibrary(handle)
#define DynLibError() "GetLastError: " + GetLastError()
#else
#include <dlfcn.h>
typedef void* DynLibHandle;
#define DynLibLoad(lib) dlopen(lib, RTLD_LAZY)
#define DynLibSym(handle, name) dlsym(handle, name)
#define DynLibUnload(handle) dlclose(handle)
#define DynLibError() dlerror()
#endif
// 使用示例
DynLibHandle hLib = DynLibLoad("plugin.so"); // Linux
// 或 DynLibLoad("plugin.dll"); // Windows
if (hLib == NULL) {
printf("加载失败: %s\n", DynLibError());
return -1;
}
typedef int (*Func)(int);
Func func = (Func)DynLibSym(hLib, "my_func");
if (func == NULL) {
printf("查找符号失败: %s\n", DynLibError());
DynLibUnload(hLib);
return -1;
}
func(42);
DynLibUnload(hLib);
这套宏定义我用了好多年,基本没出过问题。不过要注意:Windows的LoadLibrary返回HMODULE,而Linux的dlopen返回void*,类型不同,所以用typedef统一成DynLibHandle。
4.5 路径与搜索顺序
Windows加载DLL时,搜索顺序是个大坑。默认情况下,LoadLibrary会按以下顺序搜索:
- 应用程序所在目录
- 当前工作目录
- 系统目录(
C:\Windows\System32) - 环境变量
PATH中的目录
我曾经遇到过一个诡异的问题:DLL明明放在程序同目录下,但LoadLibrary就是加载失败。查了半天发现,程序启动时把当前工作目录改到了别处。从那以后,我养成了一个习惯——加载DLL时永远用绝对路径。
// 推荐:用绝对路径加载DLL
char dllPath[MAX_PATH];
GetModuleFileName(NULL, dllPath, MAX_PATH); // 获取exe路径
// 去掉exe文件名,拼接DLL名
char* lastSlash = strrchr(dllPath, '\\');
if (lastSlash) *(lastSlash + 1) = '\0';
strcat(dllPath, "plugins\\math_plugin.dll");
HMODULE hDll = LoadLibrary(dllPath);
避坑指南:我曾经在项目中用相对路径加载插件,结果用户把程序安装在带空格的目录下(比如C:\Program Files\MyApp),LoadLibrary直接跪了。后来统一用GetModuleFileName获取绝对路径,再也没出过问题。
4.6 知识体系总览
下面这张图总结了Windows动态加载的核心流程和跨平台抽象思路:
4.7 总结
Windows下的动态加载,说白了就是三件套:LoadLibrary加载、GetProcAddress取地址、FreeLibrary卸载。但实际用起来,函数名修饰、路径搜索、错误处理这些细节才是真正的坑。
我个人经验是:尽早封装跨平台接口。哪怕你当前只做Windows开发,也建议用宏或函数指针把平台相关代码隔离开。等哪天项目要移植到Linux或macOS时,你会感谢当初的自己。
核心要点回顾:
LoadLibrary返回HMODULE,失败返回NULLGetProcAddress第二个参数必须是ANSI字符串- C++导出函数务必加
extern "C" - 加载DLL尽量用绝对路径,避免搜索顺序问题
- 用宏或函数指针封装跨平台接口,降低维护成本
公众号:蓝海资料掘金营,微信deep3321