DLL除了可以导出函数之外,还可以导出类。
然而,导出类的DLL在维护和修改时有很多地方必需很小心,增加成员变量、修改导出类的基类等操作都可能导致意想不到的后果。当用户更新了最新版本的DLL库后,应用程序可能再也不能工作了。这就是DLL Hell问题。
如导出如下类
Class A
{
public:
int get_a();
private:
int a;
}
当需要再新版本的DLL导出类中作如下修改
Class A
{
public:
int get_a();
private:
int b;
int a;
}
应用程序使用的代码如下
A a;
...
当更新DLL后,由于应用程序编译A a;时已经给a按原始dll中A的大小分配了内存。类内成员变量a的类内偏移地址发生了改变,导致无法正确访问到a.
另一个问题在于,新的类A的内存大小比原始类A大,而调用语句A a;的内存是应用程序根据原始类A分配的。导致新A的多出内容并没有被分配内存!
总结一下,在如下情况下,(可能)会发生DLL hell;
1) 应用程序直接访问类的公有变量,而该公有变量在新DLL中定义的位置发生了变化;
2) 应用程序调用类的一个虚函数,而新的类中,该虚函数的前面又增加了一个虚函数;
3) 新类的后面增加了成员变量,并且新类的成员函数将访问、修改这些变量;
4) 修改了新类的基类,基类的大小发生了变化;
从如上情况可以看出,当
(1)类的大小,
(2)类成员的偏移地址,
(3)虚函数的顺序
以上三者发生变化时,会触发Dll hell.
那么应该如何避免DLL hell呢?
编写dll调用程序时应该:
1,不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态(static)成员函数(如NewInstance())用来生成类的实例。因为 NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。
2,不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。
3,忘了虚函数吧,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有 (privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。
而导出类不要导出除了函数以外的任何内容!!
至于为什么虚函数重排序会导致Dl hell而普通函数不会,这可能和C++中类的内存模型有关系,有待后面补充。