文章出处
导出函数,使函数被多个程序复用,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不要导出除了函数以外的任何内容。听起来是不是有点可笑呢!
另外需要补充的是对于导出函数,如果想用LoadLibray+GetProcAddress的灵活加载方式,务必使用
extern "C" { ... }
声明之。否则导出函数经过C++的编译器函数名会变成经过修饰的含有很多随机符号的C++方法名,这种情况下使用GetProcAddress("原函数名")是不可能加载成功的。
事实上,建议你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里,如下面的例子:
class ClassInterface
{
privated:
ClassInterface();
public:
static ClassInterface * NewInstance();
int GetXXX();
void SetXXX();
void Function();
};
使用该DLL的应用程序用上面的定义作为ClassInterface的头文件,便不会有任何可能导致的安全问题。
DLL地狱问是归根结底是因为DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。
分享到:
相关推荐
本文将深入探讨MFC扩展DLL(Dynamic Link Library)中的导出类技术,帮助你理解如何创建和使用这类DLL。 MFC扩展DLL与常规DLL的主要区别在于它允许直接使用MFC类。这使得DLL可以包含MFC的成员函数,从而能够更方便...
本示例重点讲解如何创建一个C++ DLL并导出类,以及如何在控制台程序中调用这些类的方法。我们将探讨以下几个关键知识点: 1. **DLL定义与作用**: DLL是动态链接库的缩写,它包含可由多个程序同时使用的函数和数据...
"编写DLL导出类"是创建DLL时的关键步骤,它涉及到如何设计和实现DLL的核心功能,并提供一个清晰、稳定的接口供其他程序调用。下面我们将深入探讨这个主题。 1. **DLL的基础概念** - DLL是Windows操作系统中的一个...
文档"doc\MFC导出类设计步骤.doc"可能会提供更详细的步骤和注意事项。项目路径"CTestExportClassDlg\CTestExportClassDlg.dsp"可能包含一个示例项目的源代码,供学习者参考。 总结,MFC扩展DLL使得我们可以创建与...
7. **注意事项**: - 异步问题:由于DLL和主程序可能在不同的线程上下文中运行,需要注意线程安全问题。 - ABI兼容性:确保DLL和客户端应用程序使用相同的编译器和编译设置,以保持二进制兼容性。 - 资源管理:...
注意事项: 1. **版本控制**: 如果DLL有多个版本,确保调用程序与DLL的版本兼容,否则可能导致运行时错误。 2. **进程和线程安全**: DLL在多个线程甚至进程中都可能被调用,因此需要考虑线程安全问题,特别是在...
5. 注意事项: - 动态链接库的版本匹配:确保QT和MFC使用的库版本一致,避免兼容性问题。 - 环境设置:确保所有的环境变量如`PATH`、`QTDIR`等已正确设置,使得程序能找到DLL文件。 - 错误处理:添加适当的错误...
注意事项: 1. 动态库中的类和函数应尽可能避免依赖于特定的全局状态或静态数据,以防止意外的依赖性和并发问题。 2. 使用`extern "C"`来确保C++的名称修饰不影响跨模块调用。 3. 如果动态库需要使用到其他库,确保...
#### 七、注意事项 - 当IDL类的函数名或参数发生变化时,需要重新导出对象。 - 修改IDL文件内容时,只需更改`ITT\IDL64\lib`目录下的IDL文件,无需重新导出。 - 保持IDL许可证的有效性和版本兼容性,避免出现授权...
4. **注意事项**: - 如果你的DLL同时包含了C++类,那么在导出类时,必须导出其构造函数、析构函数以及所有虚函数,否则可能会遇到链接错误或运行时问题。 - 使用`__stdcall`调用约定通常更适合于DLL,因为它确保...
- `MY_DLL_EXPORTS`是一个预处理器宏,用于在构建DLL时导出类,而在使用DLL时导入类。 2. **实现DLL**: - 在对应的.cpp文件中,实现接口中的函数和类。 - 编译DLL项目,确保编译器设置为生成DLL而不是可执行...
6. **注意事项**: - DLL的导出函数和类必须保持二进制兼容性,即在不同版本的DLL之间,导出的函数和类的布局不能改变,否则可能导致运行时错误。 - 尽量避免在DLL中使用全局变量,因为这可能引起多线程同步问题和...
5. 使用方法.txt:这个文本文件应该包含了关于如何使用Dll2inc工具的详细步骤和注意事项。 使用Dll2inc工具的一般步骤可能包括以下几步: 1. 运行Dll2inc.exe,或者使用BuildDll.bat批处理文件,指定待转换的Dll...
6. **注意事项**: 使用DLL时,需要注意版本兼容性问题,确保客户端和DLL之间的API保持一致。此外,DLL加载和卸载的顺序也很关键,错误的操作可能导致资源泄露或其他运行时问题。 7. **调试技巧**: Visual ...
5. **跨语言通信注意事项** - 数据类型兼容性:C++和C#的数据类型可能不完全相同,如C++的`int`可能是4字节,而C#的`int`可能是8字节。因此,需要确保参数和返回值的数据类型匹配,或者使用平台无关的类型如`int32`...
这种方式下,编译器会检查DLL导出的函数和类,并在可执行文件中插入相应的调用。在使用隐式链接时,需要在客户端项目中包含DLL的头文件,并链接到对应的导入库。当程序运行时,操作系统自动加载DLL并解析其导出符号...
7. **注意事项**: - 确保主应用程序和DLL使用相同的MFC库版本,否则可能会遇到运行时错误。 - 如果DLL包含MFC的全局对象,如CWinApp派生类,需要特别注意初始化和清理顺序,以避免竞态条件和资源冲突。 通过以上...
7. **注意事项**: - 确保DLL和应用程序之间的API兼容性,因为不同的Windows版本可能支持不同级别的API。 - 考虑线程安全问题,尤其是当多个线程可能同时尝试访问或显示对话框时。 8. **示例代码**: - DLL...
5. **导出类和函数**: - 对于MFC类,使用`DECLARE_DYNCREATE`宏声明动态创建,并使用`IMPLEMENT_DYNCREATE`宏实现,以便在运行时创建类的对象。 - 对于导出函数,使用`__declspec(dllexport)`和`__stdcall`修饰符...
10. **安全注意事项** 确保DLL只从可信任的源加载,防止恶意DLL替换或注入。此外,对DLL进行版本控制和签名验证可以提高系统的安全性。 综上所述,DLL库在Windows编程中扮演着至关重要的角色,理解和熟练掌握DLL的...