基于VC6.0的DLL开发

最近在开发一个C/S模式的通讯软件,通讯底层采用DLL方式进行开发,上层应用调用DLL进行通讯操作,因此在此学习了一下DLL的开发,一下是引用别人的文章,文章写的比较易懂,大家一起学习吧。

一、前言

  自从微软推出16位的Windows操作系统起,此后每种版本的Windows操作系统都非常依赖于动态链接库(DLL)中的函数和数据,实际上Windows操作系统中几乎所有的内容都由DLL以一种或另外一种形式代表着,例如显示的字体和图标存储在GDI DLL中、显示Windows桌面和处理用户的输入所需要的代码被存储在一个User DLL中、Windows编程所需要的大量的API函数也被包含在Kernel DLL中。

  在Windows操作系统中使用DLL有很多优点,最主要的一点是多个应用程序、甚至是不同语言编写的应用程序可以共享一个DLL文件,真正实现了资源"共享",大大缩小了应用程序的执行代码,更加有效的利用了内存;使用DLL的另一个优点是DLL文件作为一个单独的程序模块,封装性、独立性好,在软件需要升级的时候,开发人员只需要修改相应的DLL文件就可以了,而且,当DLL中的函数改变后,只要不是参数的改变,程序代码并不需要重新编译。这在编程时十分有用,大大提高了软件开发和维护的效率。

  既然DLL那么重要,所以搞清楚什么是DLL、如何在Windows操作系统中开发使用DLL是程序开发人员不得不解决的一个问题。本文针对这些问题,通过一个简单的例子,即在一个DLL中实现比较最大、最小整数这两个简单函数,全面地解析了在Visual C++编译环境下编程实现DLL的过程,文章中所用到的程序代码在Windows98系统、Visual C++6.0编译环境下通过。

  二、DLL的概念

  DLL是建立在客户/服务器通信的概念上,包含若干函数、类或资源的库文件,函数和数据被存储在一个DLL(服务器)上并由一个或多个客户导出而使用,这些客户可以是应用程序或者是其它的DLL。DLL库不同于静态库,在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为*.LIB),Visual C++的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为"静态链接",此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。

  在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出,DLL和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。

  微软的Visual C++支持三种DLL,它们分别是Non-MFC Dll(非MFC动态库)、Regular Dll(常规DLL)、Extension Dll(扩展DLL)。
 Non-MFC DLL: 指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被非MFC或MFC编写的应用程序所调用。
 Regular DLL: 和下述的Extension Dlls一样,是用MFC类库编写的,它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环),被导出的函数是C函数、C++类或者C++成员函数(注意不要把术语C++类与MFC的微软基础C++类相混淆),调用常规DLL的应用程序不必是MFC应用程序,只要是能调用类C函数的应用程序就可以,它们可以是在Visual C++、Dephi、Visual Basic、Borland C等编译环境下利用DLL开发应用程序。

   常规DLL又可细分成静态链接到MFC和动态链接到MFC上的,这两种常规DLL的区别将在下面介绍。与常规DLL相比,使用扩展DLL用于导出增强MFC基础类的函数或子类,用这种类型的动态链接库,可以用来输出一个从MFC所继承下来的类。
 
   扩展DLL:是使用MFC的动态链接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。例如你已经创建了一个从MFC的CtoolBar类的派生类用于创建一个新的工具栏,为了导出这个类,你必须把它放到一个MFC扩展的DLL中。扩展DLL 和常规DLL不一样,它没有一个从CWinApp继承而来的类的对象,所以,开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码。

三、动态链接库的创建

  在Visual C++6.0开发环境下,打开FileNewProject选项,可以选择Win32 Dynamic-Link Library或MFC AppWizard[dll]来以不同的方式来创建Non-MFC Dll、Regular Dll、Extension Dll等不同种类的动态链接库。

  1. Win32 Dynamic-Link Library方式创建Non-MFC DLL动态链接库

  每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。在Non-MFC DLL中DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。如果应用程序的DLL需要分配额外的内存或资源时,或者说需要对每个进程或线程初始化和清除操作时,需要在相应的DLL工程的.CPP文件中对DllMain()函数按照下面的格式书写。  

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

  参数中,hMoudle是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);ul_reason_for_call是一个说明动态库被调原因的标志,当进程或线程装入或卸载动态链接库的时候,操作系统调用入口函数,并说明动态链接库被调用的原因,它所有的可能值为:DLL_PROCESS_ATTACH: 进程被调用、DLL_THREAD_ATTACH: 线程被调用、DLL_PROCESS_DETACH: 进程被停止、DLL_THREAD_DETACH: 线程被停止;lpReserved为保留参数。到此为止,DLL的入口函数已经写了,剩下部分的实现也不难,你可以在DLL工程中加入你所想要输出的函数或变量了。

  我们已经知道DLL是包含若干个函数的库文件,应用程序使用DLL中的函数之前,应该先导出这些函数,以便供给应用程序使用。要导出这些函数有两种方法,一是在定义函数时使用导出关键字_declspec(dllexport),另外一种方法是在创建DLL文件时使用模块定义文件.Def。需要读者注意的是在使用第一种方法的时候,不能使用DEF文件。下面通过两个例子来说明如何使用这两种方法创建DLL文件。

  1)使用导出函数关键字_declspec(dllexport)创建MyDll.dll,该动态链接库中有两个函数,分别用来实现得到两个数的最大和最小数。在MyDll.h和MyDLL.cpp文件中分别输入如下原代码:  

//MyDLL.h
extern "C" _declspec(dllexport) int Max(int a, int b);
extern "C" _declspec(dllexport) int Min(int a, int b);

//MyDll.cpp
#include
#include"MyDll.h"
int Max(int a, int b)
 {
   if(a>=b)
    return a;
   else
    return b;
 }

 int Min(int a, int b)
 {
  if(a>=b)
   return b;
  else
   return a;
 } 

  该动态链接库编译成功后,打开MyDll工程中的debug目录,可以看到MyDll.dll、MyDll.lib两个文件。LIB文件中包含DLL文件名和DLL文件中的函数名等,该LIB文件只是对应该DLL文件的"映像文件",与DLL文件中,LIB文件的长度要小的多,在进行隐式链接DLL时要用到它。读者可能已经注意到在MyDll.h中有关键字"extern C",它可以使其他编程语言访问你编写的DLL中的函数。

  2)用.def文件创建工程MyDll

  为了用.def文件创建DLL,请先删除上个例子创建的工程中的MyDll.h文件,保留MyDll.cpp并在该文件头删除#include MyDll.h语句,同时往该工程中加入一个文本文件,命名为MyDll.def,再在该文件中加入如下代码:

LIBRARY MyDll
EXPORTS
 Max
 Min

  其中LIBRARY语句说明该def文件是属于相应DLL的,EXPORTS语句下列出要导出的函数名称。我们可以在.def文件中的导出函数后加@n,如Max@1Min@2,表示要导出的函数顺序号,在进行显式连时可以用到它。该DLL编译成功后,打开工程中的Debug目录,同样也会看到MyDll.dll和MyDll.lib文件。

  2.MFC AppWizard[dll]方式生成常规/扩展DLL

  在MFC AppWizard[dll]下生成DLL文件又有三种方式,在创建DLL是,要根据实际情况选择创建DLL的方式。一种是常规DLL静态链接到MFC,另一种是常规DLL动态链接到MFC。两者的区别是:前者使用的是MFC的静态链接库,生成的DLL文件长度大,一般不使用这种方式,后者使用MFC的动态链接库,生成的DLL文件长度小;动态链接到MFC的规则DLL所有输出的函数应该以如下语句开始:   

AFX_MANAGE_STATE(AfxGetStaticModuleState( )) //此语句用来正确地切换MFC模块状态 

  最后一种是MFC扩展DLL,这种DLL特点是用来建立MFC的派生类,Dll只被用MFC类库所编写的应用程序所调用。前面我们已经介绍过,Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,编译器默认了一个DLL入口函数DLLMain()作为对DLL的初始化,你可以在此函数中实现初始化,代码如下:  

BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll,DWORD reason ,LPVOID flmpload)
{
 switch(reason)
 {
   ……………//初始化代码;
 }
 return true;

  参数hinstDll存放DLL的句柄,参数reason指明调用函数的原因,lpReserved是一个被系统所保留的参数。对于隐式链接是一个非零值,对于显式链接值是零。

  在MFC下建立DLL文件,会自动生成def文件框架,其它与建立传统的Non-MFC DLL没有什么区别,只要在相应的头文件写入关键字_declspec(dllexport)函数类型和函数名等,或在生成的def文件中EXPORTS下输入函数名就可以了。需要注意的是在向其它开发人员分发MFC扩展DLL 时,不要忘记提供描述DLL中类的头文件以及相应的.LIB文件和DLL本身,此后开发人员就能充分利用你开发的扩展DLL了。

四、动态链接库DLL的链接

  应用程序使用DLL可以采用两种方式:
 一种是隐式链接,另一种是显式链接。
 在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VCin目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。
 Windows系统将遵循下面的搜索顺序来定位DLL:
  1.包含EXE文件的目录,
  2.进程的当前工作目录,
  3.Windows系统目录,
  4.Windows目录,
  5.列在Path环境变量中的一系列目录。

  1.隐式链接

  隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数。首先生成一个项目为TestDll,在DllTest.h、DllTest.cpp文件中分别输入如下代码:  

//Dlltest.h
#pragma comment(lib,"MyDll.lib")
extern "C"_declspec(dllimport) int Max(int a,int b);
extern "C"_declspec(dllimport) int Min(int a,int b);

//TestDll.cpp
#include
#include"Dlltest.h"

void main()
{
 int a;
 a=min(8,10)
 printf("比较的结果为%d ",a);
}   

  在创建DllTest.exe文件之前,要先将MyDll.dll和MyDll.lib拷贝到当前工程所在的目录下面,也可以拷贝到windows的System目录下。如果DLL使用的是def文件,要删除TestDll.h文件中关键字extern "C"。TestDll.h文件中的关键字Progam commit是要Visual C+的编译器在link时,链接到MyDll.lib文件,当然,开发人员也可以不使用#pragma comment(lib,"MyDll.lib")语句,而直接在工程的Setting->Link页的Object/Moduls栏填入MyDll.lib既可。

  2.显式链接

  显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。下面是通过显式链接调用DLL中的Max函数的例子。  

#include 
#include

void main(void)
{
 typedef int(*pMax)(int a,int b);
 typedef int(*pMin)(int a,int b);
 HINSTANCE hDLL;
 PMax Max ;
 HDLL=LoadLibrary("MyDll.dll");//加载动态链接库MyDll.dll文件;
 Max=(pMax)GetProcAddress(hDLL,"Max");
 A=Max(5,8);
 Printf("比较的结果为%d ",a);
 FreeLibrary(hDLL);//卸载MyDll.dll文件;

  在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用FreeLibrary()卸载DLL文件。在编译程序之前,首先要将DLL文件拷贝到工程所在的目录或Windows系统目录下。

  使用显式链接应用程序编译时不需要使用相应的Lib文件。另外,使用GetProcAddress()函数时,可以利用MAKEINTRESOURCE()函数直接使用DLL中函数出现的顺序号,如将GetProcAddress(hDLL,"Min")改为GetProcAddress(hDLL, MAKEINTRESOURCE(2))(函数Min()在DLL中的顺序号是2),这样调用DLL中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。

    DLL的显式链接在某些时候比隐式链接具有更大的灵活性。比如,如果在运行时发现DLL无法找到,程序可以显示一个错误信息并能继续运行。当你想为你的程序提供插件服务时,显式链接也很有用处。

显式链接到全局C/C++函数非常简单。假设你想调用DLL中的一个函数ExportedFn,你可以像这样很简单地导出它:

extern "C" _declspec(dllexport)

 void ExportedFn(int Param1, char* param2);

必须使用extern "C"链接标记,否则C++编译器会产生一个修饰过的函数名,这样导出函数的名字将不再是ExportedFn,而是一个形如"??ExportedFn@QAEX”的名字。假设这个函数从DLL1.dll导出,那么客户端可以像这样调用这个函数:

HMODULE hMod = LoadLibrary("Dll1.dll");

typedef void (*PExportedFn)(int, char*);

PExportedFn pfnEF = (PExportedFn)GetProcAdress("ExportedFn");

pfnEF(1, "SomeString");

如果你想导出并显式链接一组C++成员函数又该怎么办呢?这里有两个问题。第一是C++成员函数名是经过修饰的(即使指定extern "C"标记也是这样);第二是C++不允许将指向成员函数的指针转换成其它类型。这两个问题限制了C++类的显式链接。下面介绍两种方法来解决这个问题:①用虚函数表的方法,这也是COM使用的方法;②用GetProcAddress直接调用。我将以下面这个类为例进行讲解:

class A

{

private:

 int m_nNum;   

public:  

 A();

 A(int n);

 virtual ~A();

 void SetNum(int n);

 int GetNum();

};

一.用虚函数表进行显式链接

这个方法是COM的基础。当我们定义一组虚函数的时候,编译器会创建一个虚函数表,将各虚函数的地址按声明的顺序放入其中。当一个类对象被创建时,它的前四个字节是一个指针,指向这个虚函数表。如果我们将A的定义修改成这样:

class A

{

private:

 int m_nNum;         

public:        

 A();

 A(int n);

 virtual ~A();

 virtual void SetNum(int n);

 virtual int GetNum();

};

那么一个虚函数表将被编译器创建出来,其中包含三个函数的地址:析构函数,SetNum和GetNum。现在类对象要在dll中创建。既然我们要显式链接,就需要一些全局导出函数来调用operator new以创建对象。因为A有两种构造函数,所以我们定义两个函数CreateObjectofA()和CreateObjectofA1(int)并将其导出。客户可以这样来使用类对象:

typedef A* (*PFNCreateA1)();
PFNCreateA1 pfnCreateA1 = 
 (PFNCreateA1)GetProcAddress(hMod, TEXT("CreateObjectofA1"));
A* a = (pfnCreateA1)();
a->SetNum(1);
 _tprintf(TEXT("Value of m_nNum in a is %d/n"),a->GetNum());
delete a;

要注意的是CreateObjectofA必须使用operator new来创建对象这样客户端才可以安全地调用operator delete来销毁对象:

extern "C" __declspec(dllexport) A* CreateObjectofA1()

{

 return new A();

}

这个方法的使用得用户可以很容易地为你的程序制作插件。它的缺点是创建对象的内存必须在dll中分配。

二.直接使用GetProcAddress进行显式链接

这个方法的关键在于将GetProcAddress函数返回的FARPROC类型转化为C++中指向成员函数的指针。幸运的是,通过C++的unio和模板机制,这个目标可以很容易地实现。我们要做的只是定义如下的函数:

template<class Src , class Dest>

Dest force_cast(Src src){

 union{

  Dest d;

  Src s;

 } convertor;

convertor.s = Src;

 return convertor.d;

}

上面的函数允许我们在任何类型间进行转换,比reinterpret_cast更加有效。例如,我们定义一种指针类型:

typedef void (A::*PSetNum)(int);

我们可以将FARPROC类型的指针fp转化成PSetNum:

PSetNum psn = force_cast<PSetNum>(fp);

找到了将FARPROC转化成成员函数指针的方法以后,我们要考虑如何将C++成员函数以更加友好的名字导出。这可以通过一个.def文件来实现。

第一步是找到待导出函数经过修饰的函数名,这可以通过查看map file或者汇编代码来实现。然后在.def文件中指定导出函数的新的函数名:

EXPORTS

 ConstructorOfA1 = ??0A@@QAE@XZ        PRIVATE

 ConstructorOfA2 = ??0A@@QAE@H@Z       PRIVATE

 SetNumOfA       = ?SetNum@A@@UAEXH@Z  PRIVATE

 GetNumOfA       = ?GetNum@A@@UAEHXZ   PRIVATE          

 DestructorOfA   = ??1A@@UAE@XZ        PRIVATE

下面是调用这些成员函数的方法:

typedef void (A::*PfnConstructorOfA1)();

typedef void (A::*PfnConstructorOfA2)(int);

typedef void (A::*PfnDestructorOfA)();

typedef void (A::*PfnSetNumOfA)(int);

typedef int  (A::*PfnGetNumOfA)();

A* a1 = (A*)_alloca(sizeof(A));

 

PfnConstructorOfA1 pfnConsA =

     force_cast<PfnConstructorOfA1>(GetProcAddress(hMod, TEXT("ConstructorOfA1")));

(a1->*pfnConsA)();

 

PfnSetNumOfA pfnSetNumA =

          force_cast<PfnSetNumOfA>(GetProcAddress(hMod, TEXT("SetNumOfA")));

(a1->*pfnSetNumA)(1);

           

PfnGetNumOfA pfnGetNumA =

          force_cast<PfnGetNumOfA>(GetProcAddress(hMod, TEXT("GetNumOfA")));

_tprintf(TEXT("Value of m_nNum in a is %d/n"),(a1->*pfnGetNumA)());

 

PfnDestructorOfA pfnDestA = 

          force_cast<PfnDestructorOfA>(GetProcAddress(hMod, TEXT("DestructorOfA")));

(a1->*pfnDestA)();

注意这里使用了alloca从栈中分配内存,你也可以使用malloc从堆中分配内存。但是不能使用C++的new操作符,因为能过new来分配内存编译器会自动插入对constructor的调用。但我们要的是显式链接,所以必须避免这种情况。随之产生的结果是我们只能显式地去调用构造函数和析构函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值