4、Windows下的动态加载:LoadLibrary/GetProcAddress 的使用、跨平台兼容性考量

说实话,动态加载这事儿,在Windows上跟Linux完全是两套玩法。我最早从Linux转Windows开发时,被LoadLibraryGetProcAddress这套API折腾得不轻。明明都是加载动态库,接口风格却天差地别。今天咱们就把Windows这套东西掰开揉碎了讲清楚。

4.1 Windows动态加载的核心API

Windows下动态加载DLL,核心就三个函数:LoadLibraryGetProcAddressFreeLibrary。说白了,它们分别对应Linux下的dlopendlsymdlclose

核心函数签名:

// 加载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的导出表吧?

解决办法有两个:

  1. extern "C"导出:告诉编译器按C的方式导出,不进行名字修饰。
  2. .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 跨平台兼容性考量

说白了,跨平台动态加载的核心就是抽象。你不能在代码里到处写LoadLibrarydlopen,那维护起来太痛苦了。

我建议的做法是:封装一个统一的动态库加载接口。来看我项目里常用的方案:

// 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会按以下顺序搜索:

  1. 应用程序所在目录
  2. 当前工作目录
  3. 系统目录(C:\Windows\System32
  4. 环境变量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动态加载的核心流程和跨平台抽象思路:

Windows动态加载核心流程 LoadLibrary 加载DLL到进程空间 GetProcAddress 获取导出函数地址 FreeLibrary 卸载DLL释放资源 ⚠️ 关键注意事项 • 函数名必须用ANSI字符串,不要传宽字符 • C++导出需用 extern "C" 防止名字修饰 • 加载失败时用 GetLastError() 获取详细错误码 跨平台抽象方案 封装统一接口:DynLibLoad / DynLibSym / DynLibUnload Windows后端 → LoadLibrary / GetProcAddress / FreeLibrary Linux后端 → dlopen / dlsym / dlclose

4.7 总结

Windows下的动态加载,说白了就是三件套:LoadLibrary加载、GetProcAddress取地址、FreeLibrary卸载。但实际用起来,函数名修饰、路径搜索、错误处理这些细节才是真正的坑。

我个人经验是:尽早封装跨平台接口。哪怕你当前只做Windows开发,也建议用宏或函数指针把平台相关代码隔离开。等哪天项目要移植到Linux或macOS时,你会感谢当初的自己。

核心要点回顾:

  • LoadLibrary返回HMODULE,失败返回NULL
  • GetProcAddress第二个参数必须是ANSI字符串
  • C++导出函数务必加extern "C"
  • 加载DLL尽量用绝对路径,避免搜索顺序问题
  • 用宏或函数指针封装跨平台接口,降低维护成本

公众号:蓝海资料掘金营,微信deep3321