动态链接库(DLL)是英文 Dynamic Link Library 的缩写,它是一种不可执行的二进制程序文件,但可以被其他程序调用。
DLL程序的入口函数是DllMain(),下面的代码是入口点函数的示例
// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "stdafx.h" BOOL APIENTRY DllMain( HMODULE hModule, //DLL模块的句柄 DWORD ul_reason_for_call, //调用函数的原因 LPVOID lpReserved //保留参数 ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //DLL被程序加载 case DLL_THREAD_ATTACH: //程序启动一个线程 case DLL_THREAD_DETACH: //程序某个线程停止 case DLL_PROCESS_DETACH: //DLL被卸载 break; } return TRUE; //成功执行本函数 }
现在我们在vs里创建一个工程测试一下,新建-项目-Win32项目 我们将其命名为CreateDllDemo,选择dll,我们在dllmain.cpp中使用MessageBox来测试一下dll加载的情况
// dllmain.cpp : 定义 DLL 应用程序的入口点。 #include "stdafx.h" BOOL APIENTRY DllMain(HMODULE hModule, //模块句柄 DWORD ul_reason_for_call, //调用原因 LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: //被其他程序加载 MessageBox(NULL, L"DLL被加载", L"Message", MB_OK); case DLL_THREAD_ATTACH: //当其他程序启动了一个线程的时候 MessageBox(NULL, L"DLL被线程加载", L"Message", MB_OK); case DLL_THREAD_DETACH: //当其他程序某个线程终止运行的时候 MessageBox(NULL, L"DLL被线程卸载", L"Message", MB_OK); case DLL_PROCESS_DETACH: //被其他程序卸载的时候 MessageBox(NULL, L"DLL被卸载", L"Message", MB_OK); break; } return TRUE; }然后生成解决方案,会产生一个.dll文件,这时候程序是不能直接运行的,需要新建一个测试程序来测试一下,在当前解决方案下新建一个控制台 应用程序,命名为LoadDllDemo
#include "stdafx.h" #include<windows.h> int main() { HMODULE hModule = ::LoadLibrary(L"CreateDllDemo.dll"); if (hModule == NULL) { MessageBox(NULL, L"DLL加载失败", L"TEXT", MB_OK); } return 0; }
当删除Debug目录下的CreateDllDemo.dll时,DLL是加载失败的
DLL有两种两种加载方式,即显示加载和隐式加载
- 隐式加载又叫载入时加载,指在主程序载入内存时搜索DLL,并将DLL载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。
- 显式加载又叫运行时加载,指主程序在运行过程中需要DLL中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。
显示加载
之前我们用到一个函数LoadLibrary(),该函数就是显示加载时所需要用到的函数,该函数的作用是将指定的可执行模块映射到调用进程的地址空间,在显示加载中一般用到LoadLibrary()就会使用FreeLibrary()来释放DLL。下面是来自MSDN的描述
其中GetProcAddress()函数用来获取DLL导出函数的地址,其原型声明如下:FARPROC GetProcAddress(HMODULE hModule, LPCSTR 1pProcName);
函数etProcAddress有两个参数
- hModule:指定动态链接库模块的句柄,即 LoadLibrary() 函数的返回值。
- 1pProcName:字符串指针,表示DLL中函数的名字。
要注意的是LoadLibrary()函数、GetProcAddress()函数、等都是Win32 API 需要定义一个头文件windows.h
既然LoadLibrary是用来加载DLL,那么DLL中是如何进行导出的呢?我们来看一下导出函数部分:新建一个windows应用程序(我们将其命名为DllExportDemo)-DLL 勾选导出符号,在DllExportDemo.h中我们可以看到导出符号和导入符号的使用
// 下列 ifdef 块是创建使从 DLL 导出更简单的 // 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLLEXPORTDEMO_EXPORTS // 符号编译的。在使用此 DLL 的 // 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将 // DLLEXPORTDEMO_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的 // 符号视为是被导出的。 #ifdef DLLEXPORTDEMO_EXPORTS #define DLLEXPORTDEMO_API __declspec(dllexport) //__declspec(dllexport) 导出符号 #else #define DLLEXPORTDEMO_API __declspec(dllimport) //__declspec(dllimport) 导入符号 #endif // 此类是从 DllExportDemo.dll 导出的 class DLLEXPORTDEMO_API CDllExportDemo { public: CDllExportDemo(void); // TODO: 在此添加您的方法。 }; extern DLLEXPORTDEMO_API int nDllExportDemo;
因为我们现在使用的是C++,在进行编译的时候要用C的方式来编译 ,C++的编译方式考虑了函数重载,所以对函数名进行了新的修饰,所以在运行的时候会出现错误
所以我们添加一句 extern "C" DLLEXPORTDEMO_API int fnDllExportDemo(void); //用C的方式进行编译
测试程序
#include "stdafx.h" #include<windows.h> typedef int(*FUNC)(void); //函数类型 int main() { HMODULE hModule = LoadLibrary(L"DllExportDemo.dll"); if (hModule == NULL) { MessageBox(NULL, L"DLL加载失败", L"TEXT", MB_OK); } FUNC dllFunc = (FUNC)::GetProcAddress(hModule, "fnDllExportDemo"); printf("%d", dllFunc()); return 0; }
隐式加载
隐式加载又叫静态链接,隐式连接和显式链接一个很明显的区别就是后者并不需要用到生成的.lib文件
在隐式加载中,我们要用到.lib文件 #pragma comment(lib,"DllExportDemo.lib") 和.h文件#include"DllExportDemo.h" 这个头文件包含之前导入函数的声明
#define DLLEXPORTDEMO_API __declspec(dllimport) 如果将导出函数用在主程序中,可以不用头文件
下面就是隐式加载的测试程序
#include "stdafx.h" #include<windows.h> #include"DllExportDemo.h" #pragma comment(lib,"DllExportDemo.lib") int main() { printf("%d", fnDllExportDemo()); return 0; }
隐式加载和显式加载各有优点,如果采用动态加载方式,那么可以在需要的时候才加载DLL,而隐式加载方式实现起来比较简单,在编写程序的时候就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。
经常会遇到的一下小问题:忘记把.dll文件或者.lib 或者.h 文件复制到相应测试程序的目录下,造成找不到文件的错误