理解代码的二进制级别重用
在软件开发中,经常提到源代码重用,Dll重用等概念,而代码的二进制级别重用则相对晦涩。本文将从软件发布的角度一步一步讲解二进制级别重用的内涵,希望对大家有帮助。需要说明的是,在行文过程中,默认使用了C++作为程序开发语言。
一、源代码重用
源代码重用是指软件厂商提供包含头文件和源文件的完整代码供客户使用。比如,一软件厂商发布一个类库源代码(CMath四则运算)。此时,在开发客户应用程序时,需要引用CMath的头文件和源文件,编译得到的应用程序中包含CMath对应的二进制代码。若CMath类所产生的代码占用4MB的空间,那么当三个客户应用程序都使用CMath库时,每个可执行文件都包含4MB的类库代码。其示意图如图1。源代码重用的问题主要有两个:一是增加了客户端程序大小;二是更新发布时需要客户应用程序重新编译。
图 1 源代码重用
二、DLL重用
要解决上述问题,一种众所知之技术是将CMath类做成动态链接库。但是,一旦确定要以DLL形式发布一个C++类,将面临C++的基本弱点之一:缺少二进制一级的标准。比如名字改编导致各编译器不兼容,虽然模块定义文件可以减轻这个问题,但还有其他与最终产生的代码相关的更多问题。除了最简单的语言结构外,所有的编译器厂商都会选择自己专有的方法来实现语言的其他特性,从而使其他的编译器无法使用它所产生的函数。异常就是这些语言特性的一个典型例子。Microsoft编译器编译产生的函数,它所抛出的一场不能被Watcom编译器编译生成的客户程序捕捉到。C++通过private和public关键字确实支持语法上的封装性,但是C++草案标准病没有定义二进制层次上的封装性。C++编译模型要求编译器能够访问与对象内存布局有关的所有信息,这样才能够构造实例,或调用类的非虚成员函数。这些信息包括私有成员和公共成员的大小和顺序。
如果上述阐述过于抽象,那么下面将引用《COM本质论》中的例子进行解释。假设一软件厂商以DLL方式提供FastString类供客户使用,具体代码如下:
class __declspec(dllexport) FastString{ char *m_psz; public: FastString(const char *psz); ~FastString(void); int Length(void) const; // 返回字符数目 int Find(const char *psz) const; // 返回偏移量 };
此时,客户应用程序与厂商提供的DLL之间的关系如图2所示。
图 2 DLL重用
DLL重用虽然解决了增加客户应用程序体积的问题,但是并没有完全解决需要客户应用程序需要重新编译的问题。现在假设厂商对FastString进行了升级,添加了一个私有成员变量,提供给客户2.0版本的DLL、DLL引入库和头文件。其中头文件为:
class __declspec(dllexport) FastString{ const int m_cch; // 字符数 char *m_psz; public: FastString(const char *psz); ~FastString(void); int Length(void) const; // 返回字符数目 int Find(const char *psz) const; // 返回偏移量 };
此时,假设客户应用程序没有重新编译,而FastString.dll升级成了2.0版本。那么客户应用程序还是认为FastString对象占用4字节空间,而实际上新的FastString对象占用了8字节空间。但客户应用程序调用了Find函数去访问成员变量m_psz时,由于客户应用程序并没有分配相应的空间(Find函数认为前4个字节是成员变量m_cch),从而导致函数返回错误的结果或者应用程序崩溃,也就是说DLL重用没有实现二进制级别的重用。
图 3 DLL重用存在的问题
三、二进制级别重用
在《COM本质论》中阐述了两种二级制级别的重用方案:句柄类和抽象类。上述两种方法都能够确保DLL内部实现的变化不会对客户应用程序产生影响,开发者无需重新编译程序。因为客户应用程序编译运行所需的对象的内存布局信息没有改变,实现了二进制层次上的封装。
1、句柄类
还是上述的FastString类为例,提供给客户的不再是FastString实体类,而是包含一个实体类指针的句柄类FastStringItf头文件,其对应的函数实现就是调用实体类FastString的同名函数,就不给出具体代码了。FastStringItf.h如下:
// faststringitf.h class __declspec(dllexport) FastStringItf { class FastString; FastString *m_pThis; public: FastStringItf(const char *psz); ~FastStringItf(void); int Length(void) const; int Find(const char *psz) const; };
使用句柄类之后,DLL重用一节中FastString的1.0版和2.0版提供给客户的都是同样的句柄类,1.0版应用程序所认知到的FastStringItf类与2.0版本的FastString.dll是一致的(占用4字节,相同的函数布局),因此客户应用程序无需重新编译。
2、抽象类
继续以FastString类为例,定义一个抽象类IFastString:
// ifaststring.h class IFastString { public: virtual int Length(void) const = 0; virtual int Find(const char *psz) const = 0; }
FastString 1.0版本的代码为:
class FastString:IFastString { char *m_psz; public: FastString(const char *psz); ~FastString(void); int Length(void) const; // 返回字符数目 int Find(const char *psz) const; // 返回偏移量 };
FastString 2.0版本的代码为:
class FastString:IFastString { const int m_cch; char *m_psz; public: FastString(const char *psz); ~FastString(void); int Length(void) const; // 返回字符数目 int Find(const char *psz) const; // 返回偏移量 };
不管是1.0版本还是2.0版本,提供给客户的只是抽象类的定义IFastString,该抽象类的内存空间布局没有随着DLL的升级而改变,因此客户应用程序也无需重新编译。
PS:对于一些大型的软件系统,都会考虑到二进制级别的重用。比如QT就广泛使用了句柄类模式的二进制重用技术,其对外提供服务的类都是句柄类,真正实现具体功能的是对应的private类(也就是上文所说的实体类),比如QWebEnginePage对应的private类是QWebEnginePagePrivate。对二进制重用有一定的了解,就不难理解QT源代码中Q_D、Q_Q等宏的意图和实现方法了。
参考
《COM本质论》
《C++编程思想》
本文出自 “IT技术分享” 博客,请务必保留此出处http://watertoeast.blog.51cto.com/8489855/1950954
原文地址:http://watertoeast.blog.51cto.com/8489855/1950954