DLL导出类避免地狱问题的完美解决方案

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

    然而事实却没这么简单,导出类的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的时候,从新定义一个类的声明,这个声明能够无论原来的类里的成员变量之类的,只把接口函数列在类的声明里,以下面的例子:post

class ClassInterface
{
privated:
    ClassInterface();
public:
    static ClassInterface * NewInstance();
    int GetXXX();
    void SetXXX();
    void Function();
};

    使用该DLL的应用程序用上面的定义做为ClassInterface的头文件,便不会有任何可能致使的安全问题。
    DLL地狱问是归根结底是由于DLL当初是做为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能作到。ui

参考连接:this

DLL导出类避免地狱问题的完美解决方案url

DLL入门浅析(1)——如何创建DLL

dll 导出函数名的那些事spa

相关文章
相关标签/搜索