Windows 编程/动态链接库
动态链接库 (DLL) 是随着微软 Windows 操作系统的第一个版本引入的,如今是操作系统的一个基本结构组件。它们不仅存在于操作系统核心,而且是微软创建的许多框架的一部分,如 MFC、ATL、.NET 等,甚至 C 和 C++ 运行时库也作为 DLL 分发。
DLL 允许将某些代码片段编译到一个库中,并供多个程序链接。这意味着只需要存在一个库副本,多个程序可以共享库之间的函数和数据。Windows 系统通过提供程序员可以使用的 DLL 来使自身在用户空间的几个功能中变得可用。
DLL 与静态库的区别在于,当您编译程序时,DLL 不会编译到您的可执行文件中,而是保持为一个单独的模块。此功能有助于保持可执行文件大小较小,并且还允许仅在需要时将 DLL 加载到内存中。此外,DLL 代码在多个不同的进程中共享。几乎每个 Windows 可执行文件都共享 kernel32.dll,许多文件都共享 msvcrt.dll,即 Visual C 运行时。作为独立的实体,DLL 还允许对系统和应用程序进行更新。只需用包含修复或改进的较新版本替换 DLL,即可轻松将更改立即扩展到多个依赖程序。
构建 DLL 文件的具体方法取决于您使用的编译器。但是,DLL 的编程方式是通用的。本章将讨论如何编写 DLL 文件。
通常被称为 "DLL 地狱" 的常见问题一直是 Windows 程序员的困扰,而且似乎在短期内没有解决方案。这个问题是在 90 年代提出的,当时这个词被创造出来。问题在于操作系统允许加载错误版本的 DLL 来响应应用程序的请求,这会导致崩溃。如今,应用程序将 simply refuse to run.
虽然稳定的 DLL 不会打开地狱,但更改或升级 DLL 可能会使依赖于旧 DLL 的旧行为或未记录行为的应用程序中的错误可见。通常,DLL 加载通过文件名解析。对于 Windows XP 及更高版本,所谓的应用程序清单(带有 ID 24 的 XML 资源)参与更精细地解析 DLL 加载。此外,对于 64 位 Windows,文件系统虚拟化存在以解决位数等效性问题。出于未知原因,Microsoft 决定在 32 位和 64 位之间保持 DLL 文件名相同。根本没有 kernel64.dll,kernel32.dll 存在两次,一个用于 32 位,一个用于 64 位。
__declspec 关键字是一个奇怪的新关键字,它不是 ANSI C 标准的一部分,但大多数编译器仍然会理解它。__declspec 允许指定各种非标准选项,这些选项将影响程序的运行方式。具体来说,我们想要讨论两个 __declspec 标识符。
- __declspec(dllexport)
- __declspec(dllimport)
在编写 DLL 时,我们需要使用 dllexport 关键字来表示将对其他程序可用的函数。没有此关键字的函数只能从库本身内部使用。以下是一个例子。
__declspec(dllexport) int MyFunc1(int foo)
在构建 DLL 时,函数的 __declspec 标识符需要在函数原型和函数声明中都指定。
要将 DLL 函数“导入”到常规程序中,程序必须链接到 DLL,并且程序必须使用 dllimport 关键字原型化要导入的函数,如下所示。
__declspec(dllimport) int MyFunc1(int foo);
现在,程序可以使用该函数,即使该函数存在于外部库中。编译器与 Windows 协同工作以处理所有细节。
许多人发现为他们的 DLL 定义一个头文件非常有用,而不是为构建 DLL 保持一个头文件,为导入 DLL 保持一个头文件。以下是在 DLL 创建中常用的宏。
#ifdef BUILDING_DLL #define DLL_FUNCTION __declspec(dllexport) #else #define DLL_FUNCTION __declspec(dllimport) #endif
现在,要构建 DLL,我们需要定义 BUILDING_DLL 宏,而当我们导入 DLL 时,不需要使用该宏。然后可以按如下方式原型化函数。
DLL_FUNCTION int MyFunc1(void); DLL_FUNCTION int MyFunc2(void); .......
(只是一个说明:微软并不打算使用这种 __declspec 语法。相反,他们的意图是在一个“导出”文件中声明 DLL 的公共 API。然而,上面的语法尽管需要使用宏来回切换,但它更方便,而且几乎被当今所有软件使用)。
当 Windows 将 DLL 链接到程序时,Windows 会调用库的 DllMain 函数。这意味着每个 DLL 都需要有一个 DllMain 函数。DllMain 函数需要按如下方式定义。
BOOL APIENTRY DllMain (HINSTANCE hInstance, DWORD reason, LPVOID reserved)
关键字“BOOL”、“APIENTRY”、“HINSTANCE”等都定义在 <windows.h> 中,因此即使您在库中不使用任何 Win32 API 函数,也必须包含该文件。
APIENTRY 或 WINAPI 是表示 Windows API 调用约定的关键字。它们都被定义为 __stdcall。请记住,调用约定是函数签名的一部分。变量“hInstance”是库的 HINSTANCE 句柄,您可以保留它并使用它,也可以丢弃它。reason 将是四个不同值之一。
- DLL_PROCESS_ATTACH
- 一个新程序刚刚第一次链接到库。
- DLL_PROCESS_DETACH
- 一个程序已解除链接库。
- DLL_THREAD_ATTACH
- 一个程序线程已链接到库。
- DLL_THREAD_DETACH
- 一个程序线程刚刚解除链接库。
DllMain 函数不需要针对这些情况进行任何特殊操作,尽管某些库会发现为每个与库一起使用的线程或进程分配存储很有用。
如果库加载成功,DllMain 函数必须返回 TRUE,否则如果库发生错误且无法加载,则返回 FALSE。无法成功打开库的应用程序应正常失败。
以下是一个 DllMain 函数的一般模板。
BOOL APIENTRY DllMain (HINSTANCE hInst, DWORD reason, LPVOID lpReserved) { switch (reason) { case DLL_PROCESS_ATTACH: break; case DLL_PROCESS_DETACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; } return TRUE; }
但是,如果您对任何原因都不感兴趣,可以从程序中删除整个 switch 语句并返回 TRUE。
DLL 库可以以两种方式链接到可执行文件:静态方式和动态方式。
在静态链接到 DLL 时,链接器将完成所有工作,对于程序员来说,函数位于外部库中是透明的。也就是说,如果库编写者在库的头文件中正确使用了 _DECLSPEC 修饰符,那么它是透明的。
在编译 DLL 时,编译器将生成两个文件:DLL 库文件和一个静态链接存根 .LIB 文件。.LIB 文件就像一个小型静态库,它告诉链接器静态链接关联的 DLL 文件。在项目中使用 DLL 时,您可以向链接器提供 .LIB 存根文件,或者某些链接器允许您直接指定 DLL(然后链接器将尝试查找 .LIB 文件,甚至可能尝试自动创建 .LIB 文件)。
DLL 文件的真正强大之处在于它们可以在执行时动态加载到你的程序中。这意味着你的程序在运行时可以搜索并加载新的组件,而无需重新编译。这对于允许在执行时加载插件和扩展的程序来说是一种必不可少的机制。要动态加载 DLL 文件,你可以调用 **LoadLibrary** 函数获取该库的句柄,然后将该句柄传递给其他几个函数之一以从 DLL 中检索数据。LoadLibrary 的原型是
HMODULE WINAPI LoadLibrary(LPCTSTR lpFileName);
HMODULE 是程序模块的 HANDLE。lpFileName 是要加载的 DLL 的文件名。请记住,在加载模块时,系统首先会在你的 PATH 中检查。如果你希望系统首先在其他指定的目录中检查,请先使用 **SetDllDirectory** 函数。
DLL 加载后,你拥有了该模块的句柄,就可以进行各种操作
- 使用 **GetProcAddress** 返回指向该库中函数的函数指针。
- 使用 **LoadResource** 从 DLL 中检索资源。
完成后,如果想要从内存中删除 DLL 文件,可以使用 DLL 的模块句柄调用 **FreeLibrary** 函数。