库管易

 找回密码
 立即注册

QQ登录

只需一步,快速开始

扫描二维码登录本站

查看: 10607|回复: 7

使用C++开发Excel插件(3):动态链接库(dynamic-link library)

[复制链接]
  1、为什么我们要讲动态链接库
  一句话就是excel插件就是一个动态链接库,它和普通动态链接库除了功能之外,在形式上几乎没什么大的区别,它们的主要区别是:
  xll文件中必须包含几个excel调用要求的几个函数
  必须导出excel调用的函数,而其它dll不是必须的
  Excel插件扩展名最好为xll,其实扩展名是什么并没什么关系,写成xll也是为了好区别。
  可以这么认为,excel插件就是一种特殊的动态链接库。

  2、什么是动态链接库
  动态链接库在MSDN里面定义是一个文件,一个包含了一个或多个被编译了的函数的文件。其实动态链接库是一个封装了一些可以被其它应用程序共享的程序、数据以及资源的程序库。动态链接库的出现使大型系统更容易被划分为多个模块。动态链接是一种程序调用方法,通过动态链接我们可以使用公用的程序模块,甚至可以调用其它软件中的程序。它提供了一种开发可重用模块的有效方法。通常把一些可能被其它一个或多个应用程序调用的程序模块封装在一起,然后编译链接成库。动态链接库文件的扩展名一般是dll,也有可能是drv和sys等,当然也可以自定义其它扩展名,只要文件的格式在正确即可。在dll中每个函数都含有完整的可执行代码,但它们不可以单独执行,只能在其它应用程序调用时执行。因为它缺少自己的堆栈空间。也正因此,一个dll可以被多个应用程序同时使用,应用程序在调用它时是把它映射到自己的进程空间,并使用应用程序的自己的堆栈空间。
  那既然有动态链接,那是否有静态链接呢,回答是肯定的。动态链接就是相对于静态连接而言的。所谓静态链接是在编译链接时把要调用的程序代码的副本复制到调用程序中,成为调用程序自身的一部分,而在执行时应用程序也不再需要静态链接库了。所以当一个静态库被多个程序使用时,每个程序内部都会包含一个相应函数的副本,导致应用程序文件较大,在执行时也会占用更多内存。而动态链接库并没有把函数代码的副本复制到应用程序的内部,仅仅是保存了函数所在的地址,不会占用多少空间。在执行时应用程序也只是把动态库映射到自己的进程空间,并不会在内存中生成多个副本。那么同一个DLL副本在内存中被多个程序调用会不会像会影响呢?对于普通的开发人员来说大可不必担心,win32系统会为我们处理这个过程。

  通常情况下,当应用程序调用dll时,win32系统首先把dll调入到系统的全局堆栈中,然后通过内存映射文件,让每个调用进程都有该dll的一份影像。
  动态链接库与静态链接库的比较:
  
 动态链接库静态链接库
可否单独执行
调用程序以何种方式调用其中函数通过使用函数的地址复制副本到调用程序
被多个程序调用时,在内存中的存在方式单个副本多个副本
应用程序链接过程在调用时才链接在生成应用程序时链接
通常文件扩展名Dll、sys等LIB
是否可以被其它语言调用可以
  Windows本身就含有大量的dll,下表将说明Win32 API中几个常用的DLL。
  
DLL内容说明
GDI32.dll用于设备输出的图形设备接口(GDI)函数,例如用于绘图和字体管理的函数。
Kernel32.dll用于内存管理和资源处理的低级别操作系统函数。
User32.dll用于消息处理、计时器、菜单和通信的Windows管理函数。
  综上所诉使用动态链接库较静态链接库有许多优点。使用dll不仅可以节省内存,减少交换操作,也节省磁盘空间,试想下如果在win32系统中每个应用程序都包含一个实现windows的基本功能的副本那将是什么样的情景。使用dll可以降低维护以及升级成本。总结起来它有如下一些优点:
  节省内存:dll在内存中只有一个副本,而使用静态链接库的应用程序在内存中都会加载一个代码副本。
  节省磁盘空间:对于使用dll的程序,它自身内部并不保存dll的副本,在磁盘上只要有一个副本就可以了,但是对于使用静态库的程序,每个程序内部都有一个代码副本,那如果你的程序中把gdi32.dll、user32.dll等基本的windows功能实现的代码都包含进程序内部,那你的单个程序就可能大到几百兆。下图显示了一个简单应用程序testapp.exe所调用的dll,正是有这些dll的支持,几K字节的程序就能够拥有丰富的功能。
  

635344663694267578.jpg

635344663694267578.jpg

  可以降低升级维护的成本。在对软件系统进行升级维护时,只要不更改调用接口,修改dll中的代码不会对调用它程序产生任何影响。而静态链接库则没有如此方便,只要它自身代码被改变,那所有调用它的程序都需要重新编译链接。
  Dll可以被多种语言调用:无论是何种语言编写的应用程序,只要它遵循dll的函数的调用约定,就可以调用此dll。关于dll的调用约定在后面章节中将详细介绍。
  可以扩展MFC库的功能:通过创建MFC扩展dll,可以从现有MFC类派生类,创建具有扩展功能的MFC动态库,以供MFC应用程序使用。
  创建纯资源dll:我们可以创建具有某种国家语言的资源dll,通过调用不同的资源dll就可以轻松实现具有多国语言的应用程序。
  动态链接库也是程序,它与可执行应用程序除了在使用时能否单独执行的区别外,它们内部还是有有些本质的区别,下面的表中列出了动态链接库于普通应用程序的一些区别:
  
 动态链接库应用程序
可否单独执行可以
是否可以存在多个实例否,只能有一个实例可以
是否拥有堆栈
是否包含导出表没有
可否被其它程序调用可以自身可以被调用,但其中的函数不可以。
通常文件扩展名Dll、sys等Exe
   

  通过上面的介绍我们知道的了动态链接库是什么,也了解了它和静态库以及普通应用的程序的区别。那我们现在想知道,是不是所有的dll都一样呢。可以这么理解,从文件格式来看它们都是一样的,因为都遵循win32的定义标准,但是,从用途以及生成方式来看它又可以分成四种类型:win32 dll,com dll,mfc规则动态链接库以及MFC扩展动态链接库。下面我们将简单了解一下这几种dll。
  2.1、Win32 dll也就是符合32位windows系统环境的动态链接库,是最普通的动态链接库,它可以由不同语言编写,比如VB,C#等都可以编写win32 dll。大多数语言编写的dll都可以相互调用。通常把一些通用的又经常被其他程序调用的模块封装成win32 dll,就大大方便的程序的调用和代码的重用。本书主要讲的xll插件就使用此类,我们将在下一节讲解如何创建win32 dll。
  2.2、com dll是一类按照com(Component Object Model,组件对象模型)规范编写的dll,由于它的语言无关性,使它的用途广泛。使用com不仅可以编写excel插件,而且可以为所有支持com规范的程序编写插件。但它编写起来比较复杂,在本书中暂不作讲解。
  2.3、MFC规则的动态链接库,是在内部使用了MFC dll并按照MFC编写规则编写的dll。在这类DLL中,必须在所有导出函数的开始处添加AFX_MANAGE_STATE宏,以将当前模块的状态设置为DLL的状态。为此,需将下列代码行添加到从DLL导出的函数的开始处:
  AFX_MANAGE_STATE(AfxGetStaticModuleState( ));

  就像下面的代码:
  1. extern "C" _declspec(dllexport) LPCTSTR __stdcall GetString()
  2. {
  3.        AFX_MANAGE_STATE(AfxGetStaticModuleState());
  4.        return(NULL);
  5. }
复制代码
  动态链接到MFC的规则DLL的主要特点:
  在编译选项中需要定义_AFXDLL和_USRDLL。
  在所有导出函数的开始处添加AFX_MANAGE_STATE宏
  这类DLL必须实例化CWinApp派生类。
  此类型的DLL的入口函数使用MFC提供的DllMain,其他与在标准MFC应用程序中一样。
  应用程序的主消息泵必须调用从DLL导出的例程来调用CWinApp::PreTranslateMessage
  与在标准MFC应用程序中一样,将DLL的初始化操作放到CWinApp::InitInstance成员函数中。卸载DLL前,将从MFC提供的DllMain函数调用CWinApp派生类的CWinApp::ExitInstance成员函数。
  发布时,必须同时包含共享的MFC dll。
  函数通常是通过标准C接口从规则DLL导出。
  规则DLL内的所有内存分配都应在该DLL内进行;DLL不应向调用可执行文件传递或从调用可执行文件接收指向MFC对象的指针和由MFC分配的指向内存的指针,否则需要创建另外一种dll,即扩展MFC DLL。

  2.4、MFC扩展DLL是通常实现从现有Microsoft基础类库类派生的可重用类的DLL。是对现有的MFC类库的扩展,那么使用此dll的程序必须也是MFC规则的应用程序。比如比较流行MFC扩展库有BCG库。由于它和本书所要介绍的内容,无太大关系,创建excel插件也不会使用此方法,所以此处也不作进一步介绍了。
  现在我们知道了几种常见的dll,也对它们各自的创建方法有了一定的了解,那么我们在实际使用中如何确定要使用哪种dll更合适呢。下面列出了几种创建dll的原则供参考:
  尽量不使MFC规则的dll,如果你的程序中不需要调用MFC中类,就创建普通的win32 dll,否则在发布你的DLL是不得不包含庞大的MFC类库。另外在你的程序中尽量使用STL,可以降低使用MFC的几率。
  如果确实需要MFC dll的支持,则尽量使用动态链接到MFC规则DLL,这样一方面可以减小生成的DLL的文件大小,也可以减少调用时的内存占用,而且在编译时也比静态链接MFC库要快。
  如果此dll只可能用于MFC规则的应用程序中,那么可以考虑使用MFC规则DLL或MFC扩展DLL,但是如果DLL实现从现有MFC类派生的可重用类,或如果需要在应用程序和DLL之间传递MFC派生的对象,则必须生成扩展DLL。
  现在我们已对常见的几种动态链接库有了深入的了解,下一步就看看如何编写动态链接库。

  3、如何编写动态链接库
  这一节中我们将介绍如何编写win32 dll,在正式编写程序之前我们需要先了解一些关于在dll编写函数或类的知识。
  3.1调用约定
  所谓调用约定是指在告诉编译器函数将以何种方式被调用,在函数声明过程中,通过使用调用约定关键字显示声明函数的调用方式,调用方式的不同将会影响到函数在被调用过程中的执行方式,主要是指:
  函数调用时参数传递到堆栈上的顺序
  是由谁来清除调用后的堆栈中的参数。
  函数的名称修饰约定。

  最常见的有三种调用方式,它们分别是__cdecl,__stdcall和__fastcall,其中__cdecl是vc++默认的调用约定,在默认情况下,如果函数没有被显示声明为其它调用方式都会被使用c调用方式__cdecl。但是建议不要使用__cdecl调用约定,因为在这种调用约定下,对传递参数的堆栈的清除工作是由调用者完成的,这对于调用不同语言编写的dll时是个很困惑的事情,除非函数中的参数数量不能确定,就像C中的fprint函数一样。尽量使用__stdcall约定,这种约定下函数的参数堆栈由被调用者自己处理,就避免了以后调用函数时还得处理堆栈的麻烦。而且以__stdcall约定修饰的函数,符合大多数程序的调用方式,使用范围也较广,系统中许多函数都是使用的__stdcall,下面是windef.h中的一段定义,是否觉得很面熟呢:
  1. #define CALLBACK __stdcall
  2. #define WINAPI __stdcall
  3. #define WINAPIV __cdecl
  4. #define APIENTRY WINAPI
  5. #define APIPRIVATE __stdcall
  6. #define PASCAL __stdcall
复制代码
  __fastcall调用约定和__stdcall的调用约定差不多,但它使用寄存器传递参数,使调用过程更快些。

  3.2修饰名:
  修饰名是在编译函数定义或函数原型期间由编译器创建的字符串。C和C++程序中的函数在内部通过其修饰名加以识别。对于不同的调用约定,编译器产生的函数的修饰名也不同。对于C函数,__cdecl命名约定使用以下划线(__)开头的函数名,不执行任何大小写转换。而对于__stdcall调用方式时名称修饰用下划线(_)作为符号名的前缀,并在符号的后面追加一个@符号,@符号后是参数列表中的字节数—即所需的堆栈空间。例如声明如下一个函数:
  Int_stdcall func (char *pStr)
  对于__stdcall方式其修饰名为_func@4
  对于__cdecl方式其修饰名为_func。
  注意:在win32系统中所有的指针都是4字节。
  通常情况下,我们并不需要知道函数的修饰名,编译器通常可以识别未加修饰的名称。但也有例外,比如下面两种情况就需要指定修饰形式的名称:
  对于重载函数和特殊成员函数(比如构造函数和析构函数),必须指定修饰名
  当引用C或C++函数名的程序集源文件中也必须使用修饰名。

  C函数的修饰形式取决于其声明中使用的调用约定,如下所示:
  以Intfunc (char *pStr)为例
  
调用约定修饰方式
__cdecl前导下划线(_)_func
__stdcall前导下划线(_)和结尾@符号,后面是表示参数列表中的字节数_func@4
__fastcall与__stdcall相同,但前置符不是下划线而是@符号@func@4
  C++函数的修饰名包含下列信息:
  函数名
  函数所属的类(如果函数是成员函数)。这可能包括封装函数的类的类,等等。
  函数所属的名名空间(如果函数是某个命名空间的组成部分)。
  函数的参数类型。
  调用约定。
  函数的返回类型
  函数名和类名在修饰名中以代码形式存在。修饰名的其余部分仅对编译器和链接器具有内部意义的代码。下面是未修饰的和修饰的C++名称的事例。
  
未修饰名修饰名
int __stdcall func(char *pStr)?func@@YGHPAD@Z
int CTest::func(char *pStr)?func@CTest@@QAEHPAD@Z
  那么我们如何知道一个函数编译后的修饰名是什么样的,这就可以借助DUMPBIN工具,它是个命令行工具,你可以在命令行方式下键入dumpbin /symbols <obj或lib文件名>,下图显示了一个ShowMessage函数的原型和修饰名:
  下面我们来看一下有关三种调用约定及其对修饰名的影响的比较:
  
 __cdecal__stdcall__fastcall
参数传递顺序从右向左压入堆栈从右向左压入堆栈前两个字(DWOR)或更小的参数被放入ECX和EDX寄存器中,其它参数以从右向左的顺序压入堆栈
是否支持可变参数支持不支持不支持
谁来清除堆栈中的参数由调用函数移除堆栈中参数由被调用函数在调用结束时移除堆栈中的参数由被调用函数在调用结束时移除堆栈中的参数
名称修饰对于C函数:
  前导下划线(_)
对于C函数:
  前导下划线(_)和结尾@符号,后面是表示参数列表中的字节数
对于C函数:
  与__stdcall相同,但前置符不是下划线而是@符号
对应的编译器参数/Gz/Gd/Gr

  3.3、导出函数
  DLL和可执行程序最大的区别就是dll文件中包含到处函数表,表中列出了所有可被其它程序调用的函数,只有通过这些函数才能执行dll中代码,而且只有导出表中的函数才能被其它程序调用。同时为了让其它应用程序(比如用vb,pascal等语言写的应用程序)可以调用C/C++DLL中的函数,必须让编译器导出正确的函数,而不做任何名称修饰。现在我们来看一下如何导出dll中的函数,有两种方法可以实现我们的目的:
  在生成dll时,创建一个模块定义(.def)文件并使用此.DEF文件。
  在函数的定义中使用__declspec(dllexport)关键字。
  不过值得注意的是在使用上面两种方法时,确保使用__stdcall调用约定,因为在其它语言的程序中使用堆栈的方法也许和C++的不同。下面我们来分别看一下如何使用这两种方法。

  模块定义(.DEF)文件是一种用来定义dll属性的文本文件,它通过模块定义脚本对dll的属性进行描述,其中也包括对导出函数的定义。如果你不使用__declspec(dllexport)关键字的话就必须使用模块定义(.DEF)文件导出dll函数。下面我们将看到一个简单的.DEF文件:
  1. ; ExcelAddin.def : Declares the module parameters for the DLL.

  2. LIBRARY "ExcelAddin"
  3. DESCRIPTION 'ExcelAddin Windows Dynamic Link Library'

  4. EXPORTS
  5.     ; Explicit exports can go here
  6.        xlAutoOpen
  7.        xlAutoClose
  8.        xlAddInManagerInfo
  9.        MyMotd
  10.        MyFunc
复制代码
  上面文本描述了动态链接库ExcelAddin.dll中的导出函数列表。其中,LIBRARY语句指定了此文件是对ExcelAddin动态库的描述,DESCRIPTION指明了此dll的描述信息,EXPORTS下面列出了所有要导出的函数名。注意模块定义文件中的注释必须以“;”开头,可以有多行,但不能和定义语句在同一行。
  如果使用.DEF文件定义输出函数信息则文件中必须至少包含下面基本的模块定义语句:
  文件中的第一个语句必须是LIBRARY语句。通LIBRARY语句将此.DEF文件标识为所属DLL。所以LIBRARY语句的后面紧跟的是DLL的名称。链接器会将此名称放到DLL的导入库中。
  EXPORTS语句列出导出函数的名称,如果是系统自动生成的,有时还会列出DLL导出函数的序号值。当然也可以手动在函数名的后面加上@符号和一个数字,给函数分配序号值。当指定序号值时,序号值的范围必须是从1到N,其中N是DLL导出函数的个数。
  有关.DEF文件中的模块定义语法在MSDN里有详细的描述,这里就不做解释了。如果你创建的是MFC规则的dll则向导会首先替你创建一个.DEF框架,并添加到项目中。你只需再此文件中添加函数名既可。对于普通win32 dll就需要手动创建.DEF文件并把它添加到项目中。

  另一种方法是使用__declspec(dllexport)关键字,通过设定关键字同样可以从dll中导出数据、函数、类、或类成员函数。如果仅仅是简单的导出函数的话就可以不使用.DEF文件。但它并不能完全替代.DEF文件,因为有许多导出指令(如序号、NONAME等)只能在.DEF文件中使用。在通常情况下使用__declspec(dllexport)关键字较便利,尤其是当你要导出C++修饰符时,你不必了解复杂的修饰方法。所以如果你对导出函数没有具体要求就最好使用__declspec(dllexport)关键字。
  要导出函数,__declspec(dllexport)关键字必须出现在调用约定关键字的左边(如果指定了关键)。例如:
  1. __declspec(dllexport) void __stdcall func(void);
复制代码
  若要导出类中所有公共数据成员和成员函数,关键字必须出现在类名的左边,例如:
  1. Class __declspec(dllexport) CXlTest
  2. {
  3.        ……类的内容
  4. };
复制代码
  为了使代码书写起来更方便,也为了提高代码的可读性,我们可以用一个宏来代替__declspec(dllexport),例如:
  1. #define DllExport __declspec(dllexport)
复制代码
  如果你想让C++编写的DLL可以在C语言程序中使用,则应该让编译器以C链接而不是C++链接来声明这些函数。通常情况下C++编译器使用C++调用约定和C++名称修饰方式。要指定C链接,只需为函数声明指定extern“C”。例如:
  1. Extern “C” __declspec(dllexport) int func(void);
复制代码
  那么反过来,如果是用C编写的DLL如何用在C++语言中呢。此时我们可以使用__cplusplus预处理器宏确定正在编译的语言,然后如果是从C++语言模块使用,则用C链接声明这些函数。如果在为DLL提供的头文件中使用此方法,则这些函数可以原封不同地由C和C++用户使用。下面代码显示可由C和C++客户端应用程序使用的头文件:
  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif

  4. __declspec( dllimport ) void func(void);
  5. __declspec( dllimport ) void GetString(char *pChar);

  6. #ifdef __cplusplus
  7. }
  8. #endif
复制代码
  如果我们现在所使用的C头文件并没有使用上面的方法怎么办呢,是不是需要重新编译那些代码呢,通过另一种方法同样可以达到上面的效果,例如:
  1. extern "C" {
  2. #include "ExcelAddin.h"
  3. }
复制代码

  我们已经知道了两种导出方法,那么我们在实际使用中使用哪种导出方法较合适呢。我们现在来看一下两种方法的优缺点:
  使用.DEF文件的优缺点:
  通过.DEF文件可以控制导出函数的序号,当你把新添加的函数放置到.DEF文件中时如果分配一个更高的序号值,就可以保证现有的应用程序继续正常使用新的DLL。另外值得一提的是MFC DLL中的函数也是用.DEF文件导出的。另外在.DEF文件还可以使用NONAME属性导出函数,该属性仅将序号放到DLL的导出表中。对具有大量导出函数的DLL,使用NONAME属性可以减小DLL文件的大小。
  但使用.DEF文件的主要缺点是:.DEF维护起来比较麻烦,如果需要把修饰名放到.DEF文件中,还需要通过其他方法(可以使用Dumpbin工具)先获得修饰名。而且修饰名随编译器的版本不同而不同,这就需要链接此DLL的应用程序也得使用相同版本的编译器。
  使用__declspec(dllexport)的优缺点:
  使用__declspec(dllexport)非常方便,因为不需要考虑维护.DEF文件和获取导出函数的修饰名。但是,由于无法控制编译器生成的导出序号,当用新导出重新生成DLL,有时还需要重新生成应用程序。

  导入函数
  如果你的程序里要调用dll中的公共符号,那你需要导入这些公共符号。使用__declspec(dllimport)可以导入任何由.DEF文件或由__declspec(dllexport)关键字导出的变量、函数、类等。使用时只需在符号定义前加上次关键字,通常为了提高代码的可读性也为导入关键字定义一个宏:
  1. #define DllImport __declspec( dllimport )

  2. DllImport int iNum;
  3. DllImport void func(void);
复制代码
  但是在上面的声明中如果不使用&shy;&shy;__declspec(dllimport)同样可以使用。那么程序如何知道func函数是在dll中,又是如何执行的呢。其实如果不使用导入关键字声明的话,编译器是通过生成形实转换程序(thunk)来实现的。这样带来直接后果是代码生成的代码变大,而其降低了缓存性能,而使用__declspec(dllimport),编译器会生成间接调用,从编译器生成的代码来看使用关键字不仅生成的代码较少,而且调用效率也比较高,所以在程序中导入dll中的符号时一定要使用__declspec(dllimport)关键字。但注意由于它生成的是间接调用代码,所以导入关键字不要用在dll内部函数上。通常为了使dll和客户端应用程序可以使用相同的头文件,我们可以使用预处理宏来指示是生成dll还是应用程序。例如:
  1. #ifdef _EXPORTING
  2.    #define CLASS_DECLSPEC __declspec(dllexport)
  3. #else
  4.    #define CLASS_DECLSPEC __declspec(dllimport)
  5. #endif

  6. class CLASS_DECLSPEC CTest
  7. {
  8. };
复制代码
  链接dll
  前面我们所讲的导入只是在给出了如何在应用程序中使用dll中的函数、变量等。那么应用程序中具体是如何与dll建立链接的呢。应用程序有两种方法链接到dll的方法,这两种方法一种是隐式链接,另一种是显示链接:
  隐式链接是指在程序中没有明确的使用加载动态链接库的方法(LoadLibrary),那么显示链接就是明确的使用了链接方法。

  隐式链接
  在隐式链接下,是由编译器在生成程序时链接到dll的导入库文件(.LIB),所以要使用隐式链接就必须有对应dll的导入库文件。当然在程序执行时还必须要dll文件。所以如果你要使用隐式连接那么以下几项是必不可少的:
  包含导出函数和C++类的声明的头文件(.H文件)。
  要链接的导入库(.LIB files)。(生成DLL时链接器创建导入库。)
  实际的DLL(.DLL文件)。
  在编写应用程序时,包含dll的导出函数的定义文件。如果你是dll的提供者,你可以定义一个或几个包含导出函数或类的头文件,也可以把dll中的头文件直接提供给用户,文件中的函数定义方法就是我们在上一节导入中所讲的那样。有了头文件使用者才能知道哪些函数是可以使用的。但是一旦包含了这些头文件,那么对这些头文件的中的函数或类的调用就和本地调用没有任何区别了。
  在生成应用程序时,应用程序必须能够链接到dll的导入库文件,因为导入库文件中才真正包含函数或类的描述信息。通常我们可以在编译器中加入我们要链接的库名即可以了。
  对于vc++6,你可以在project->setting->lib属性页中设置。虽然链接了导入库,但应用程序中仍不包含具体的程序代码,所以在执行时还需要让程序能够定位.DLL文件。

  显式链接
  相比隐式链接,显示连接更灵活一些。在显式链接下,应用程序并不需要在程序生成时链接dll的导入库文件,也不需要dll的导入函数文件(.h),在应用程序中通过应用程序需要通过dll加载函数(LoadLibrary)来加载dll文件,并通过函数指针调用dll的导出函数。具体我们需要通过以下方法显示调用dll。
  使用LoadLibrary函数加载dll文件并获取dll的模块句柄。
  使用GetProcAddress获取dll中的导出函数的指针,在应用程序中通过函数指针调用dll中的函数。
  最后通过FreeLibrary函数释放此次调用。
  下面演示了在应用程序中调用test.dll中的导出函数“func”的方法,调用其它函数的方法与此都类似:
  1.   typedef int (__stdcall *dllfunc)(char*) ; //定义函数指针 

  2.        HINSTANCE hDll = LoadLibrary(_T("test.dll")); //获得dll的句柄
  3.        //如果成功载入test.dll则继续获得函数的地址
  4.        if (NULL != hDll)
  5.        {
  6.               dllfunc func = (dllfunc)GetProcAddress(hDll,_T("func"));//获得函数func的地址
  7.               //如果地址获得到了,则执行
  8.               if (NULL != func)
  9.               {
  10.                      int iResult = func(_T("success of calling dll!"));//调用dll中func函数
  11.               }
  12.        }
  13. FreeLibrary(hDll);
复制代码
  确定要使用的链接方法
  同样象前面几节那样,当我们了解了这些链接方法后,需要知道在何时使用何种方法才是最合适的。下面我们来看一下这两种链接方法的优缺点:
  隐式链接
  如果应用程序使用隐式链接,那么应用程序在被编译时,dll函数调用在对象代码中生成一个外部函数引用,此时应用程序与此dll的导入库(.LIB)文件进行链接,并加载调用dll中函数的代码。这些代码包含完整的对函数的调用信息,但不包含函数的执行代码。当应用程序启动时,程序会根据这些信息查找并调用函数。应用程序在启动时必须能够定位要调用的dll文件,如果没有找到则应用程序会提示错误终止执行进程,否则系统将dll模块映射到进程的地址空间中。当然仅仅能够定位到dll也并不代表dll能加载成功,所有DLL都具有入口点函数,通常情况下就是我们看到的winmain,main函数,也可以指定其它函数,这些函数负责着对dll的初始化工作,在dll被加载时首先执行入口点函数,如果入口函数调用不成功,即返回FALSE,同样也会导致dll加载失败。只有dll的初始化成功,此dll才可以被使用。最后在调用dll函数执行时,系统修改进程的可执行代码以提供dll函数的起始地址。
  显式链接
  大部分应用程序都使用隐式链接,因为这种方法使用起来方便。但有时候也需要使用显示链接,因为显示链接也有一些隐式链接无法替代的优点。下面我们来看一些使用显式链接的常见原因:
  在应用程序启动时无法确定dll的名称,比如我们常见到的资源dll,应用程序启动时需要根据配置文件中信息才能知道要载入哪种资源的dll。
  使用隐式链接的程序如果再启动时没有找到dll,则进程就被立刻终止。但是对于显示链接的程序只在显示加载dll时才查找dll文件,而且可以通过编写处理加载失败的代码以避免进程被迫终止。
  如果应用程序使用的dll较多,则如果在启动时加载所有的dll就会导致程序启动较慢,而显示链接只在需要时才加载,而且是需要哪个就加载哪个,就不会出现启动过慢得情况。当然你可以让程序只隐式链接那些在启动时就需要的dll。
  显示链接的一个重要优点是,由于dll不需要与导入库(.LIB)文件链接,当dll被修改后,只要应用程序中调用的dll函数的定义没变,应用程序就不需要重新链接。而隐式链接需要重新链接导入库。
  使用显示链接时一定要注意,在使用完dll时一定要通过FreeLibrary函数释放dll。

  关于LoadLibrary和AfxLoadLibrary
  对于显示链接dll的程序必须使用显示载入函数载入dll文件,windows系统提供了两个API函数来实现此功能,一个是LoadLibrary,通常使用这个函数就可以,对于MFC扩展dll的加载,必须使用AfxLoadLibrary,而不是LoadLibrary。这两个函数通过在指定和默认的的系统路径下搜索指定的dll文件。如果找到则将dll映射到调用进程的地址空间,并返回此dll的模块句柄。否则返回空(NULL),但是也许你会问,如果此dll已经被映射到进程的地址空间中,那调用此函数会不会错误呢,由于dll是通过引用数来控制多次应用的,所以多次对同一dll引用不会出错也不会载入多个dll副本。另外要注意的一点是,这两个函数都是通过一定顺序在系统中寻找dll文件的,所以在调用dll时一定要清楚此时调用的是哪个路径下的dll,否则可能会出现一些让你困惑的问题。下面列出了windows搜索dll的路径顺序:
  当前进程的可执行模块所在的目录。
  当前目录。
  Windows系统目录。GetSystemDirectory函数检索此目录的路径。
  Windows目录。GetWindowsDirectory函数检索此目录的路径。
  PATH环境变量中列出的目录。
  注意:未使用LIBPATH环境变量。

  LoadLibrary和AfxLoadLibrary的使用方法一样,它们的参数中接收dll的文件名,在程序中使用时可以像如下代码:
  1. HINSTANCE hDll = LoadLibrary(_T("test.dll"));
  2. if (NULL != hDll)

  3.        …… //使用dll函数
  4. }
复制代码
  也可以直接指定dll路径,比如:
  1. HINSTANCE hDll = LoadLibrary(_T("d://Mydll/test.dll"));
  2. if (NULL != hDll)
  3. {
  4.        …… //使用dll函数
  5. }
复制代码
  关于FreeLibrary和AfxFreeLibrary
  在显示调用dll的应用程序中,当不再需要DLL模块时,调用FreeLibrary来递减模块的引用数,直到引用数为零,此函数便从进程的地址空间中取消模块的映射。对于MFC扩展dll的卸载是通过AfxFreeLibrary来实现,其它的由FreeLibrary处理。
  FreeLibrary和AfxFreeLibrary的用法也相同,它们的参数是dll的句柄,例如
  1. FreeLibrary(hDll);
复制代码
  通常也可以使用GetModuleHandle来获得dll模块句柄。

  关于GetProcAddress
  GetProcAddress是用来获得显式链接得dll的函数地址,它有两个参数,一个是dll模块句柄,另一个是函数的修饰名,注意不一定和函数名相同。通过此函数返回函数的地址,我们就可以使用函数指针调用函数了,但由于没有经过编译时得类型检查,所以要确保函数至指针的参数要与函数原型保持一致以避免调用错误。通常我们根据导出函数的原型定义一个typedef,然后再定义函数指针。例如:
  1. typedef int (__stdcall *dllfunc)(char*) ;
  2. typedef int(__stdcall *dllfnTest)(void);
  3. ……
  4. dllfunc func = (dllfunc)GetProcAddress(hDll,_T("func"));//函数原型是func
  5. dllfnTest fnTest = (dllfnTest) GetProcAddress(hDll,_T("_fnTest@0"));//函数原型是fnTest
  6. ……
复制代码
回复

使用道具 举报

学习了,谢谢!
回复 支持 反对

使用道具 举报

学习
好复杂啊,感觉
回复 支持 反对

使用道具 举报

值得学习,收藏!
回复 支持 反对

使用道具 举报


值得学习,收藏!
回复 支持 反对

使用道具 举报

真的有点难度,真不是看不懂
回复 支持 反对

使用道具 举报

学习了,支持一下
回复 支持 反对

使用道具 举报

来自手机 显示全部楼层
太专业课,有点难度
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|仓库管理网

GMT+8, 2024-4-24 09:53

Powered by 库管易

KuGuanYi.Com

快速回复 返回顶部 返回列表