C++码农日记(全程视频讲解)
上QQ阅读APP看书,第一时间看更新

第14天 可动态加载的DLL

视频讲解

今天要学习的案例对应的源代码目录:src/chapter03/ks03_03。本案例不依赖第三方类库。程序运行效果如图3-3所示。

图3-3 第14天案例程序运行效果

今天的目标是掌握如下内容。

  • 动态加载DLL的含义与作用。
  • 实现动态加载的DLL的方法。

在进行软件开发活动时,软件开发人员可能无法预测未来会碰到什么样的需求,当需求发生变化时,原来的软件有可能难以适应这种变化从而导致无法满足新的需求。但是,通过需求分析和软件设计工作,开发人员可以有效降低这种变化带来的风险,对软件进行插件化设计就是一种非常有效的解决方案。所谓插件化设计,简单来讲,就是为软件中的某项功能制定设计规范,只要新开发的软件遵循这种规范,就可以在不改动原软件的前提下,将新软件提供的功能添加到系统中,从而满足新的需求。在第13天的学习内容中介绍过DLL开发技术,当EXE使用DLL时,需要在构建时链接DLL的lib文件,用这种方式开发的DLL叫静态链接的DLL。本节将介绍动态加载DLL的技术,这种技术可以用来开发插件。动态加载的DLL和静态链接的DLL有什么区别呢?区别有以下两点。

(1)构建过程中对DLL的lib文件的依赖不同。如果使用静态链接的DLL,在构建EXE项目时需要用到DLL的lib文件,以便链接DLL中的符号,如引出类、引出接口;如果使用动态加载的DLL,在构建EXE项目时不需要DLL的lib文件。

(2)在运行过程中,对DLL的依赖时间不同。如果使用静态链接的DLL,EXE整个运行过程中都要依赖DLL库文件(如a.dll);如果使用动态加载的DLL,EXE只有在加载该DLL后才依赖DLL库文件,而在卸载DLL后就不再依赖DLL库文件了,这时即使删除该DLL库文件也不影响EXE的正常运行。

怎样开发可以动态加载的DLL呢?开发动态加载的DLL分为两步,第一步是开发可动态加载的DLL,第二步是动态加载DLL。下面进行详细介绍。

1.开发可动态加载的DLL

可动态加载的DLL的开发技术与用于静态链接的DLL类似,不同之处在于需要把引出的接口用extern "C"进行声明。extern "C"是让C++代码能够调用C代码写的接口而采用的一种语法形式。如代码清单3-14所示,在标号①处,定义该DLL的引出宏KS03_03_DLL_API。在第13天的学习内容中介绍DLL开发技术时,编写了专门定义引出宏的头文件base_export.h,使用本节的写法可以省去这个头文件,因为该头文件中的内容被转移到引出类所在的头文件了。当DLL中有多个头文件需要引出时,本节的这种方法就不适合了,因为仍然需要编写专门用于定义引出宏的头文件,如base_export.h。在标号②处,使用extern "C"声明一个引出接口function_test(int)。如果有多个引出接口,可以将引出接口写到extern "C" {}的花括号内部,如标号③处所示。在标号④处、标号⑤处定义了两个引出接口,可以看出,这些接口跟普通DLL中的引出接口的唯一区别就是被写在了extern "C" {}的花括号内部。

代码清单3-14

下面看一下这几个接口的实现。如代码清单3-15所示,这几个接口的实现见标号①、标号②、标号③处。它们与普通DLL接口的区别在于把接口实现写在了extern "C"后面的花括号中。

代码清单3-15

2.在EXE中动态加载某个DLL

完成DLL的开发后,就可以在EXE项目中加载DLL并调用其中的接口了。这里只介绍与普通DLL开发的不同之处。Windows系统与Linux系统对于动态加载DLL提供了不同的接口,但是都包含加载DLL、查找DLL中的接口、调用DLL中的接口、卸载DLL这四个步骤。下面分别进行介绍。

1)在Windows中动态加载DLL并调用DLL中的接口

(1)如果要调用DLL中的接口,首先需要加载DLL。在Windows中加载DLL的接口为LoadLibrary(),其原型如下。

     HMODULE WINAPI LoadLibrary(_In_ LPCTSTR lpFileName);

其中_In_表示后面的参数是输入参数,也就是在接口内部只会引用传入的参数,而不会修改它。参数lpFileName表示要加载的DLL名称,如果DLL所在路径已经配置到PATH环境变量,就可以不写全路径而只写DLL文件名。HMODULE是返回值类型,它是一个句柄,用来操作打开的DLL。WINAPI宏在WIN32中被定义为_ _stdcall,在第13天的学习内容中已经介绍过。调用LoadLibrary()的示例代码如下。该例子表示加载的DLL为"my_dll.dll"。

     HMODULE  hDll = LoadLibrary("my_dll.dll");

(2)在Windows系统中加载DLL后,可以用GetProcAddress()查找DLL中的接口,其原型如下。

     WINBASEAPI FARPROC WINAPI GetProcAddress(_In_ HMODULE hModule, _In_ LPCSTR
     lpProcName);

WINBASEAPI宏用来表明后面是一个引出接口。FARPROC表示该接口的返回值类型是一个函数地址,也就是DLL中接口的地址。hModule是指向DLL的句柄,hModule可以取LoadLibrary()的返回值。lpProcName表示要查找的接口名。调用GetProcAddress()的示例代码如下。该例子表示在hDll句柄所指向的DLL中查找函数(或称作符号)function_test并将找到的函数地址保存到pFuncAddress中。

     void *pFuncAddress = GetProcAddress(hDll, "function_test");

(3)找到DLL中的接口后,就可以调用它了。function_test()的定义见代码清单3-14中标号②处,调用它的代码见代码清单3-16。在标号①处定义一个函数指针pFunction,为了跟function_test()的定义保持一致,要把它定义成不带参数并且返回值类型为int的函数指针。在标号②处,将GetProcAddress()返回的函数指针转换为期望的函数指针类型,其中int(*)()表示返回值为int类型的函数指针,int(*)后面的()表示该函数不带参数,如果有参数就把参数列表写在int(*)后面的()里。在标号③处,完成了对pFunction()的调用,也就是对DLL中function_test()的调用。

代码清单3-16

(4)完成接口调用后,如果不需要再调用该DLL中的接口,可以在适当的时机卸载DLL。但是,如果仍然需要调用其中的接口,就不能卸载DLL,否则将导致调用异常。在Windows中卸载DLL的接口为FreeLibrary(),其原型如下。

     WINBASEAPI BOOL WINAPI FreeLibrary(In_ HMODULE hLibModule);

其中hLibModule表示DLL的句柄,该句柄可以由LoadLibrary()得到。FreeLibrary()返回BOOL类型的值,用来表示卸载成功与否。调用FreeLibrary()的示例代码如下。

     FreeLibrary(hDll);

本节的案例中,在Windows中加载DLL的完整代码见代码清单3-17。在Windows版的演示代码中,为了演示不同类型的函数调用,调用了DLL中的两个带有不同参数的接口getComputerGeneration()、calculate(int, int)。如标号①处所示,定义两个变量strFunctionName、strFunctionName2,这两个变量用来存储DLL中的函数名称。在标号②处,定义一个void*类型的变量pFuncAddress,用来存放GetProcAddress()返回的函数地址。在标号③处分别针对函数getComputerGeneration()、calculate(int, int)定义了函数指针,在定义函数指针时,它的参数表、返回值类型必须与指向的函数完全一致,如果不明白,可以查看代码清单3-14中getComputerGeneration()、calculate(int, int)的定义。在标号④、标号⑤处为strDllPath赋值,该变量用来表示待加载的DLL名称,可以看出在Windows、Linux系统中DLL的命名方式有所不同。如标号⑥处所示,调用LoadLibrary()加载指定DLL,需要注意将参数strDllPath.c_str()转换为LPCTSTR类型,以便保证跟LoadLibrary()的参数类型一致。在标号⑦处,将GetProcAddress()返回的函数指针转换为和getComputerGeneration()定义一致的函数指针,请注意其具体语法,可以跟标号⑧处指向calculate(int, int)函数的指针进行对比,以便加深理解。在标号⑨处,调用pFunction2(1, 2)就相当于调用calculate(1, 2)。在标号⑩处,当不再使用DLL中的接口时,卸载hDll所指向的DLL。

代码清单3-17

2)在Linux中动态加载DLL并调用DLL中的接口

在Linux中动态加载DLL并调用DLL中接口的过程同Windows一致。

(1)如果要调用DLL中的接口,首先需要加载DLL。在Linux中加载DLL的接口为dlopen(),调用该接口需要包含头文件“#include <dlfcn.h>”,其原型如下。

     void* dlopen(const char *pathName, int mode);

其中pathName表示要加载的DLL名称,如果DLL所在路径已经配置到PATH环境变量,就可以不写全路径而只写DLL文件名。mode表示加载模式,在本案例中取值RTLD_LAZY,表示等需要时再解析DLL中的符号(即函数)。该函数返回值类型为void*。调用dlopen()的示例代码如下。

     void *hDll = dlopen("my_dll.so.1", RTLD_LAZY);

该例子表示加载的DLL为"my_dll.so.1"。需要注意的是Linux中的DLL文件一般会有多个软链接(类似Windows中的快捷方式),在使用前需要确认该文件名与磁盘上DLL的实际文件名是否一致,如果不一致将导致加载失败。

(2)在Linux系统中加载DLL后,可以用dlsym()查找DLL中的接口,其原型如下。

     void* dlsym(void *handle, const char *symbol);

void*指向返回的函数地址。handle是指向DLL的指针,可以取dlopen()的返回值。symbol表示要查找的接口名。调用dlsym()的示例代码如下。该示例表示在hDll所指向的DLL中查找函数function_test并将找到的函数地址保存到pFuncAddress中。

     void *pFuncAddress = dlsym(hDll, "function_test");

(3)找到DLL中的接口后,就可以调用它了。关于函数指针的使用方式见Windows部分的描述,见代码清单3-16。

(4)完成接口调用后,如果不需要再调用该DLL中的接口,可以在适当的时机卸载DLL。但是,如果仍然需要调用其中的接口,就不能卸载DLL,否则将导致接口调用异常。在Linux中卸载DLL的接口为dlclose(),其原型如下。

     int dlclose (void *handle);

其中handle表示DLL的句柄,该句柄可以由dlopen()得到。只有当DLL的使用计数为0时,DLL才会真正被系统卸载。调用dlclose()的示例代码如下。

     dlclose(hDll);

注意:如果要使用dlopen()、dlclose()等接口,需要在项目的pro中添加对Linux库dl的引用,否则会导致编译错误“undefined reference to symbol 'dlclose@@GLIBC_2.2.5'”。pro配置如下。

本节的案例中,在Linux中加载DLL的完整代码见代码清单3-18。在Linux版的演示代码中,为了演示不同类型的函数调用,同样调用了DLL中的两个带有不同参数的接口getComputerGeneration()、calculate(int, int)。如在标号①处,调用dlopen()加载指定DLL。在标号②处,调用dlsym()查找DLL中的指定接口。在标号③处,将返回的函数指针转换为和getComputerGeneration()定义一致的函数指针,请注意具体语法,可以与标号⑤处指向calculate(int, int)函数的指针进行对比,以便加深理解。在标号④、标号⑥处,分别调用pFunction()、pFunction2(1, 2),相当于调用getComputerGeneration ()和calculate(1, 2)。在标号⑦处,当不再使用DLL中的接口时,卸载hDll所指向的DLL。

代码清单3-18

本节介绍了动态加载DLL的技术,在下节案例中,将会把这些接口调用封装到自定义类中以方便使用。