乐者为王

Do one thing, and do it well.

VC++动态链接库(DLL)编程入门

DLL是一个包含可由多个程序同时使用的代码和数据的集合。例如,在Windows操作系统中,Comdlg32动态链接库 执行与对话框有关的常见函数。因此,每个程序都可以使用该DLL中包含的功能来实现“打开”对话框。这有助于促进代码复用和内存的有效使用。通过使用DLL,程序可以实现模块化,由相对独立的组件组成。例如,一个计帐程序可以按模块来销售。可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。

DLL的优点

  1. 使用较少的资源。当多个程序使用同一个函数库时,DLL可以减少在磁盘和物理内存中加载的代码的重复量。这不仅可以大大影响在前台运行的程序,而且可以大大影响其它在Windows操作系统上运行的程序;
  2. 简化部署和安装。当DLL中的函数需要更新或修复时,只要函数的参数和返回值没有更改,就不需重新编译或重新建立程序与该DLL的链接。此外,如果多个程序使用同一个DLL,那么多个程序都将从该更新或修复中获益;
  3. 支持多语言程序。只要程序遵循函数的调用约定,用不同编程语言编写的程序就可以调用相同的DLL函数。程序与DLL函数在下列方面必须是兼容的:函数期望其参数被推送到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及寄存器中是否传递了任何参数;
  4. 使国际版本的创建轻松完成。通过将资源放到DLL中,创建应用程序的国际版本变得容易得多。可将用于应用程序的每个语言版本的字符串放到单独的DLL资源文件中,并使不同的语言版本加载合适的资源。

DLL的类型(Kinds of DLLs)

Visual C++支持三种类型的DLL,它们分别是Non-MFC DLL、MFC Regular DLL和MFC Extension DLL:

  1. Non-MFC DLL指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被MFC或非MFC编写的客户程序调用;
  2. Extension DLL支持C++接口,也就是说它导出C++函数或者整个类给客户程序。导出函数可以使用C++或MFC的数据形式作为参数或返回值,当导出整个类时,客户程序可以创建此类的对象或者从这些类进行派生。使用Extension DLL的一个问题就是该DLL仅能和MFC客户程序一起工作;
  3. Regular DLL和上述的Extension Dll一样,也是用MFC类库编写的,它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环)。Regular DLL有一个很大的限制就是,它只可以导出C风格的函数,但不能导出C++类、成员函数或重载函数。调用Regular DLL的客户程序不必是MFC应用程序,也可以是在Visual C++、Dephi、Visual Basic等环境下开发的客户程序。

DLL的加载

客户程序使用DLL的方式有两种:隐式链接(静态加载或加载时动态链接)和显式链接(动态加载或运行时动态链接)。

为了隐式链接到DLL,客户程序必须从DLL的提供程序获取下列项:

  1. 包含导出函数或C++类声明的头文件(.h文件);
  2. 要链接的导入库(.lib文件);
  3. 实际的DLL(.dll文件)。

使用DLL的客户程序必须include头文件(此头文件包含每个DLL中的导出函数或C++类),并且链接到此DLL的创建者所提供的导入库。

1
2
3
4
5
// Cacl.cpp
DLLAPI int Sum(int a, int b)
{
    return a + b;
}
1
2
3
4
5
6
7
8
// Cacl.h
#ifdef CACL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

DLLAPI int Sum(int a, int b);
1
2
// Client.cpp
DLLAPI int Sum(int a, int b);

为了显式链接到DLL,客户程序必须在运行时通过函数调显式加载DLL:

  1. 调用LoadLibrary加载DLL和获取模块句柄;
  2. 调用GetProcAddress获取指向客户程序要调用的每个导出函数的函数指针(由于客户程序是通过指针调用DLL的函数,编译器不生成外部引用,故无需与导入库链接);
  3. 使用完DLL后调用FreeLibrary释放资源。
1
2
3
4
5
6
7
8
9
10
11
12
// Client.cpp
HINSTANCE hDLL = LoadLibrary("demo");
if (hDLL != NULL)
{
    LPFNDLLFUNC1 lpfnDllFunc1 = GetProcAddress(hDLL, "Sum");
    if (lpfnDllFunc1 == NULL)
    {
        FreeLibrary(hDLL);
        return SOME_ERROR_CODE;
    }
    return lpfnDllFunc1(dwParam, uParam);
}

客户程序如何找到DLL

如果用LoadLibrary显式链接DLL的话,可以指定DLL的全路径名。如果没有指定路径名,或者使用了隐式链接,则系统将使用下面的搜索序列来定位DLL:

  1. 包含客户可执行文件所在的目录;
  2. 当前目录;
  3. Windows系统目录(可用GetSystemDirectory函数获取);
  4. Windows目录(可用GetWindowsDirectory函数获取);
  5. 在Path环境变量里列出的目录(注意:不使用LIBPATH环境变量)。

导出DLL函数

DLL文件的布局与EXE文件非常相似,但有一个重要差异:DLL文件包含导出表。导出表包含DLL导出到客户程序的每个函数的名称。只有导出表中的函数可由客户程序访问。DLL中的任何其它函数都是DLL私有的。通过使用带/EXPORTS选项的dumpbin工具,可以查看DLL的导出表。有两种方法可以从DLL导出函数:

方法1:在定义中使用__declspec(dllexport)关键词。

1
2
3
4
5
6
7
// 导出函数
__declspec(dllexport) void __cdecl Function();

// 导出类中的所有公共数据成员和成员函数
class __declspec(dllexport) CExampleExport : public CObject
{
}

方法2:在生成DLL时,创建一个后缀名为def的模块定义文件(如果希望按序号而不是按名称从DLL导出函数,则使用此方法)。

实例DLL和客户程序代码:

1
2
3
4
5
6
7
8
// Cacl.h
#ifdef CACL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

DLLAPI int Sum(int a, int b);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Cacl.cpp
#include <windows.h>
#include "Cacl.h"

BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason, LPVOID lpReserved)
{
    switch (reason)
    {
        case DLL_PROCESS_ATTACH:
        case DLL_THREAD_ATTACH:
        case DLL_THREAD_DETACH:
        case DLL_PROCESS_DETACH:
            break;
    }
    return TRUE;
}

DLLAPI int Sum(int a, int b)
{
    return a + b;
}
1
2
3
4
5
6
7
8
9
// Client.cpp
#include <stdio.h>
#include "Cacl.h"

int main(int argc, char* argv[])
{
    printf("Sum = %d\n", Sum(5, 3));
    return 0;
}

如何调试DLL

调试DLL很容易,只要从DLL工程启动调试器即可。第一次这样做的时候,调试器会请求给出客户EXE程序的路径。之后,每次从调试器运行DLL时,调试器会自动装入客户EXE程序,而EXE用搜索序列找到DLL。

Comments