dll、com 二进制模块封装内幕

DLL动态链接库是程序复用的重要方式,DLL可以导出函数,使函数被多个程序复用,DLL中的函数实现可以被修改而无需重新编译和连接使用该DLL的应用程序。作为一名面向对象的程序员,希望DLL可以导出类,以便在类的层次上实现复用。所幸的是,DLL确实也可以导出类。

然而事实却没这么简单,导出类的DLL在维护和修改时有很多地方必需很小心,增加成员变量、修改导出类的基类等操作都可能导致意想不到的后果,也许
用户更新了最新版本的DLL库后,应用程序就再也不能工作了。这就是著名的DLL Hell(DLL地狱)问题。

DLL地狱问题是怎么产生的呢?看下面的例子,假设DLL有一个导出类ClassD1:
class ClassD
{
       public:
int GetInt();
              private:
int m_i;
};
int ClassD::GetInt()
{
       return m_i;
}

应用程序使用现在的代码来使用这个类:
ClassD d;
printf(“%d”, d.GetInt());
 
程序进行正正常,没有什么问题。后来DLL需要升级,对ClassD进行了修改,增加了一个成员变量,如下:
class ClassD // 修改后
{
       public:
int GetInt();
              private:
                     int m_i2;
int m_i;
};

把新的DLL编译连接完成后,复制到应用程序目录,这个倒楣的应用程序调用GetInt方法恐怕再也无法得正确的值了。事实上它还算幸运的,如果GetInt的实现改成如下这样,那么它马上就要出错退出了。
int ClassD::GetInt() // 修改后
{
       return m_i++;
}

这样的事情,称它是个地狱(Hell)一点也不夸张。为什么会出错呢?我们要先从类实例的创建开始,看看使用一个类的工作过程。

首先,程序语句“ClassD d;”为这个类申请一块内存。这块内存保存该类的所有成员变量,以及虚函数表。内存的大小由类的声明决定,在应用程序编译时就已经确定。

然后,当调用“d.GetInt()”时,把申请的这一块内存做为this指针传给GetInt函数,GetInt函数从this指向的位置开始,加上m_i应有的偏移量,计算m_i所在的内存位置,并从该位置取数据返回。m_i相对this的偏移量是由m_i在类中定义的位置决定的,定义在前的成员变量在内存中也更靠前。这个偏移量在DLL编译时确定。

当ClassD的定义改为修改后的状态时,有些东西变了。

第一个变的是内存的大小。因为修改后的ClassD多了一个成员变量,所以内存也变大了。然而这一点应用程序并不知道。

第二个变的是m_i的偏移地址。因为在m_i之前定义了一个m_i2,m_i的实现偏移地址实际已经靠后了。所以d.GetInt()访问的将是原来m_i后面的那个位置,而这个位置已经超出原来那块内存的后部范围了。

很显然,在更换了DLL后,应用程序还按原来的大小申请了一块内存,而它调用的方法却访问了比这块内存更大的区域,出错再在所难免。

同样的情形还会发生在以下这些种情况中:

1) 应用程序直接访问类的公有变量,而该公有变量在新DLL中定义的位置发生了变化;
2) 应用程序调用类的一个虚函数,而新的类中,该虚函数的前面又增加了一个虚函数;
3) 新类的后面增加了成员变量,并且新类的成员函数将访问、修改这些变量;
4) 修改了新类的基类,基类的大小发生了变化;

等等,总言而之,一不小心,你的程序就会掉进地狱。通过对这些引起出错的情况进行分析,会发现其实只有三点变化会引起出错,因为这三点是使用这个DLL的应用程序在编译时就需要确定的内容,它们分别是:
1) 类的大小;
2) 类成员的偏移地址;
3) 虚函数的顺序。

要想做一个可升级的DLL,必需避免以上三个问题。所以以下三点用来使DLL远离地狱。

1,不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态(static)成员函数(如NewInstance())用来生成类的实例。因为NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。

2,不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。

3,忘了虚函数吧,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有(privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。

如果导出的类能遵循以上三点,那么以后对DLL的升级将可以认为是安全的。

如果对一个已经存在的导出类的DLL进行维护,同样也要注意:不要改动所有的成员变量,包括导出类的父类,无论定义的顺序还是数量;不要动所有的虚函数,无论顺序还是数量。

总结起来,其实是一句话:导出类的DLL不要导出除了函数以外的任何内容。听起来是不是有点可笑呢!

事实上,建议你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里,如下面的例子:
class ClassInterface
{
       privated:
              ClassInterface();
       public:
              static ClassInterface * NewInstance();
              int GetXXX();
              void SetXXX();
              void Function();
};

使用该DLL的应用程序用上面的定义作为ClassInterface的头文件,便不会有任何可能导致的安全问题。

DLL地狱问是归根结底是因为DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。 



1.为什么要使用delete this,以及如何使用。
(1)考查这样的情况:
有两个对象A,B需要访问同一个多态对象C。
因为C一般是通过new 操作构造的,一定要自己释放,但是对象A,B都需要使用它,并且B不知道A什么时候使用完成C,A也不知道B什么时候使用完成C(当然可以用函数通信的方法通知了,不过是一个比较丑陋的实现方法),所以不能在A/B中间来delete,
一个折中的办法是在程序退出的时候来delete,但是这样做不到资源的立即释放,假如有多个A/B/C会有比较大的运行开销。
解决的办法就是在C中增加引用计数,并且自己决定什么时候来释放自己:

下面是代码
struct c{
C(){nRef=1;};
int AddRef(){
return ++nRef;
}
int Release(){
if(!--nRef)
{
 delete this;
 return 0;
}
return nRef;
}
protected:
virtual~c()//donot allow delete from outsides
{
}
int nRef;
};

就是通过C内部的计数来判断什么时候释放自己的资源。当然需要A/B在适当时候调用AddRef/Release了。
下面是一个简单的实现。
struct a{
a(c* pc)
{
m_pc=pc;
pc->AddRef();
}
void do_Sth(){
;//...使用完成m_pc了
m_pc->Release();
m_pv=NULL;
}
protected:
c* m_pc;
}
class b的结构和a也差不多,就不写了
下面是main函数:
void main()
{
c* pc=new c;
a clsA(pc);
b clsB(pc);
pc->Release();//ok pc was hold by both clsA and clsB,so we don't need it.release.
pc=NULL;
a.do_sth();
b.do_sth();
;//should no memory leak 
}


注意事项
因为是在类成员函数里面delete this的,所以在此语句以后,不能访问任何的成员变量及虚函数,否则一定非法。


另外的一个用处将在后面说明。

2.dll导出类的几种方法。
其实我就知道两种啦,还请大家补充。
先看M$使用的方法。
struct exports{
__declspec(dllexport)exports();
__declspec(dllexport)~exports();
};
exports::exports(){}
exports::~exports(){}
优点:完全和本地的类一样使用,可以直接new/delete.
缺点:只能使用自动连接(就是连lib的),自由度不高。
不能通过选择dll的方式来转换插件(可以通过替换dll文件实现,但是不能保证对所有的修改后的类通用)
继承exports后没有办法通过同一个接口调用子类函数。(因为你永远new的都是父类,除非重新编译原来的main程序)

另外一个方法:
这样声明exports类:
;//exports.h
struct exports{
friend exports* CreateExports(LPVOID lpparm);
virtual int AddRef();
virtual int Release();//前面讲过的方法,如果不清楚请看第一篇delete this
virtual do_sth()=0;//所有成员函数均为虚函数,基类可以纯虚。
protected:
exports(){}
virtual ~exports(){}
}
extern "C" CreateExports函数是dll的输出函数。
编译exports.cpp成exports.dll
然后在main.cpp:
#include "windows.h"
#include "exports.h"
void main()
{
LoadLibrary("exports");....
PCreateExports=GetProcAddress("CreateExports"....);...
exports* pex=pCreateExports(NULL);
pex->do_sth();
pex->Release();
}
 

Q:为什么要使用protected的构造函数。
A:因为我不希望exports类被直接使用(这样就没有办法实现多态了,因为new不到子类),所以protected它的构造函数。
这样在程序里面你只能通过CreateExports来得到Exports类的指针。而CreateExports返回的类是通过lpparm来确定的,这样就实现了多态性。


Q:为什么要使用delete this 的方法(AddRef/Release)。
A:因为exports类是从dll里面new出来的,所以一定需要在dll里面delete,所以使用这个方法,否则会内存泄漏。


Q:为什么所有成员函数均为虚函数。
A:如果不是虚函数,其实再连接的时候成员函数是会连接代码到.exe里面去的,这样就根本call不到dll里面的函数,也就没有实现我们的原意。
而虚函数是通过函数指针调用的,exports类是再dll里面构造,所以可以保证虚函数是call到dll里面,而且.exe只需要dll的头文件即可编译连接通过。

 

如有缺点、错误及遗漏还请大家指教

3.实现plugins。
有了前两章的基础,这个就比较好办了。
先了解一点点基本概念:
Q:what is plugins
A:就是通过加载不同的dll实现不同的功能,或者附加的功能,典型的如模拟器的各种插件(显示,input...)。
Q:why we use plugins
A:主要为了使程序模块化,并且容易扩充。升级组件时不需要重新发布整个产品。
Q:how to do it?
A:使用LoadLibrary来加载具有相同接口(一般是函数,不过我们这里会使用C++类实现)的不同dll,实现/(扩充)功能。
例子:
D:/ePsxe/plugins>dumpbin spuseal.dll /exports
Microsoft (R) COFF Binary File Dumper Version 6.00.8447
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

Dump of file spuseal.dll
File Type: DLL
 Section contains the following exports for spuSeal.dll
      0 characteristics
  36A22A22 time date stamp Sun Jan 17 10:21:22 1999
    0.00 version
      2 ordinal base
     22 number of functions
     22 number of names
  ordinal hint RVA    name
      3  0 00001700 PSEgetLibName
      2  1 00001710 PSEgetLibType
      4  2 00001720 PSEgetLibVersion
      6  3 00001800 SPUabout
     11  4 00001EE0 SPUclose
      5  5 00001730 SPUconfigure
     18  6 00002090 SPUgetOne
      7  7 00001910 SPUinit
     10  8 00001B90 SPUopen
     23  9 00002270 SPUplayADPCMchannel
     12  A 00002250 SPUplaySample
     17  B 00002050 SPUputOne
     19  C 000020B0 SPUsetAddr
     20  D 000020E0 SPUsetPitch
     21  E 00002110 SPUsetVolumeL
     22  F 00002150 SPUsetVolumeR
      8  10 00001B80 SPUshutdown
     13  11 00002190 SPUstartChannels1
     14  12 000021C0 SPUstartChannels2
     15  13 000021F0 SPUstopChannels1
     16  14 00002220 SPUstopChannels2
      9  15 00001880 SPUtest
 Summary
    1000 .rsrc
    96000 UPX0
    5000 UPX1
D:/ePsxe/plugins>

这个是epsxe的一个sound插件的输出表。可以看到epsxe就是通过这些函数与dll通信的。
进入正题:
从一个dll里面输出这么多函数实在是一个很烦的事情,而且一个个的去GetProcAddr也很麻烦,
能不能用简单一些的方法呢?
当然是可以的了,使用我们前面介绍的dll输出类技术2就可以了。
还是看代码,我们做一个input的plugins类。
;//input.h
class input{
virtual int  Addref()=0;
virtual int  Release()=0;
virtual DWORD  QueryClsID()=0;//identify this is a input plugins;
virtual DWORD  IsSupport(DWORD flags)=0;
virtual bool  GetXY(int&x,int&y)=0;
virtual DWORD  GetButtonStat()=0;
virtual bool AddEffect(DWORD id,DWORD level)=0;
virtual bool QueryInterface(DWORD clsid,LPVOID* ppVoid);//留待扩充。越来越像COM了。
virtual bool DoConfig();
protected:
input();
virtual ~input();
};
--------------------------------------------------------
;//mouseinput.h
class MouseInput
 :public input
{
protected:
friend input* CreateInput(LPVOID pVoid);
;//...............
};
--------------------------------------------------------
;//mouseinput.cpp
input* CreateInput(LPVOID pVoid){return new mouseinput;};
 
--------------------------------------------------------
;//keyinput.h
class KeyInput
 :public input
{
protected:
friend input* CreateInput(LPVOID pVoid);
;//...............
};
--------------------------------------------------------
;//keyinput.cpp
input* CreateInput(LPVOID pVoid){return new keyinput;};
 
编译后就得到了mouseinput.dll和keyinput.dll
他们分别输出mouseinput类和keyinput类,而外部程序是通过input的接口来调用他们,从而实现了plugins的功能。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值