Basic Sample Senario :
Client 需要一种组件提供一种 FastString类, 此类具有 int length() 方法
解决方案:
静态链接 : 组件厂商将源代码交给 client ,客户将组件代码与client 代码编译连接运行。 如果组件代码需要fix bug or update ,则client 端代码需要重新编译连接。 而且client软件的不同实例使用各自的组件内存对象。
动态链接 : 组件厂商使用DLL形式发放组件,此时不同的client实例可以共享组件在内存中的代码段。
DLL的问题:1.导出名称的问题 : 不同的compiler可以使用不同的mangle name 用来区分 c++的函数,那么使用不同的compiler的client和组件无法链接 (可以使用extern “C”解决全局函数名的问题,使用 .DEF文件解决导出成员函数的问题)
2.升级的问题 :如果组件最初定义为
class FastString
{
char* m_psz;
public:
FastString(const char* psz){strcpy(m_psz, psz);}
};
而后厂商更改了组件的实现
class FastString
{
int newmember;
char* psz;
public:
FastString(const char* psz){strcpy(m_psz, psz);}
}
原来FastString对象大小为4字节,现在变为8字节,但是client端按照4字节分配对象, dll却要向后面的4个字节存入一个指针,行为不可预料!
解决这一问题的一种方法便是每次发布便更改dll的名字,即1.0, 2.0, x.0 等等。但这样比较弱啊!!
这种问题根本原因是啥呢?
class 的关键在于封装了其中的实现细节,即用户知道类提供了哪些服务( public方法)就行了,不需要管类的内部到底使用了哪些成员变量。这样一来,只要接口没变(类提供功能),user就可以安心的使用任意版本的实现了。C++怎么不行呢?C++告诉用户接口的同时,也告诉了用户类的实现(对象布局)。比如类对象有多大啊,每个成员的偏移啊(具体可以看Inside c++ object model)。知道了这些,客户端使用接口的代码就和DLL中的具体实现紧密的耦合起来了,杯具 啊~
咋办呢? 只要不让client直接创建FastString就行了,这样client的代码就不会受到FastString实现变化的影响了。给FastString加一个Wrapper类,内部嵌套一个FastString,所有对FastString的调用都foward给内部的FastString member, 创建FastString 的任务在dll方面完成,client只知道Wrapper大小为4个字节--指向FastString的指针。这样问题解决了,但是太麻烦了,所有的接口都要包一层!! 而且多了一层调用!
还有啥办法么? 为了保证c++接口类实现二进制级别的兼容只能使用编译器无关的特性:1.假设复合类型表现形式相同(struct) 2. 传参顺序相同,可以使用指示符指定3.虚函数调用机制相同,即基于 vtbl 和 vptr. 基于这些假设,我们创建的c++接口类所有函数设置为虚函数,那么不同compiler将为客户端方法调用产生相同的机器代码。定义了接口,便规定了所有继承于他的类的内存结构一定与它兼容。但此时不能告诉用户类的定义,否则重回上面的老路上了。怎么办,只有接口客户无法创建类的定义,只有export一个创建类对象的函数客户了。 同上面的wrapper一样,创建类的操作仅仅在dll内部调用,这意味着实际建造类对象大小和布局的代码与编译实现类的方法的代码使用同样的编译器创建 (即ctor和call ctor的代码由同一编译器同时编译)。由于虚析构函数在vtbl的位置与compiler相关,所以不能把它设置为虚函数,只有显示增加一个Delete函数完成析构工作。
OK,当前我们得到的DLL中只有创建类对象的函数需要用extern “C”export 给客户,其他的接口中的虚函数是通过虚表访问的,无需靠符号名字链接。
进一步的,如果我们要给接口增加一个功能呢? 如果直接在现有接口中方法声明后加入新的方法,那么此方法会出现在vtbl的最后一栏,旧的client不会调用新方法,但是如果新的client访问老的对象呢? 不幸的事情发生了! 这样做的问题在于,修改公开的接口就打破了对象的封装性。
那么增加接口功能只能通过设计一个接口继承另一个接口,或者让类继承多个接口来实现了。客户可以在运行时通过RTTI来询问对象,支持这个功能不,亲?然而 ,RTTI也是一个compiler相关的东东,好吧,我们让每个类自己实现RTTI,也就是实现一个dynamic_cast 方法, 用来将自己cast成为自己实现的接口,如果不支持则返回 0 。
例如:
void* CFastString::Dynamic_Cast(const char* pszTypename)
{
void * pRev;
if(strcmp(pszTypename, "IFastString") == 0)
{
pRev = static_cast<IFastString*>(this);
}
else if(strcmp(pszTypename , "IOut") == 0)
{
pRev = static_cast<IOut*>(this);
}
else if(strcmp(pszTypename , "IExtent") == 0)
{
pRev = static_cast<IFastString*>(this);
}
else
{
return 0;
}
return pRev;
}
注意cast到IExtent的时候用了IFastString,因为IFastString 和 IOut都是从IExtent继承的,写IExtent的话不知道用哪个,用虚拟继承可以使CFastString对象只有一份IExtent,为啥不用呢? 你懂得。。。跟前面答案一样,编译器相关。
最后一个问题是delete的问题,用户需要记得为每一个对象调用一次delete方法,而指针cast来cast去,想记得对象被delete没有很难啊! 怎么办? 用引用计数吧,把每个指针当做具有生命周期的实体,创建时候计数++,销毁时候--,等到0的时候就delete对象。
大功告成,通过vptr和vtbl的二进制防火墙,我们做到了可重用的二进制组件,组件变化客户无需重新编译 。