Posted on 2006-07-04 17:59
奔跑的阿甘 阅读(490)
评论(0) 编辑 收藏 引用 所属分类:
COM/ATL
COM的由来
Michael 2006年07月04日
最近,公司的产品在支持SNA网络时出现了一个怪异的问题,终端和主机连接总是无法建立,经过追查源码发现应用客户端在调用SNA网络服务库的接口时莫名其妙的改变了网络服务对象的数据成员,实际上,该数据成员只有在对象构造函数中被初始化过一次,其他地方没有任何写操作。
根据应用客户端对多网络协议的支持代码,我做了以下测试,Client应用调用一个Operate接口,由两个不同的服务端实现:
Client包含IOperator接口文件,调用operate方法:
1 class EXPORIMP IOperator {
2 public:
3 IOperator();
4 ~IOperator();
5
6 long operate(const long var1, const long var2);
7
8 private:
9 int a;
10 int b;
11 };
operate的第一个实现:server1.dll
1class EXPORIMP IOperator {
2 public:
3 IOperator();
4 ~IOperator();
5
6 long operate(const long var1, const long var2);
7
8 private:
9 int a;
10 int b;
11 };
operate的第二个实现:增强的server1.dll
1class EXPORIMP IOperator {
2 public:
3 IOperator(); // Initialize szName, a, b
4 ~IOperator();
5
6 long operate(const long var1, const long var2); //access szName
7
8 private:
9 char szName[256];
10 int a;
11 int b;
12 };
client通过server1.lib来实现接口调用,server1的发布者在发布dll后发现server1中存在某个BUG,或者为了改进operate的效率,因而引入了szName成员并更新了operate接口实现,然后重新发布了增强版的server1 DLL。客户拿到新版本后很高兴,但是,当他兴致勃勃地替换掉老的DLL时,发现自己的客户端再也跑不起来了,令人厌烦的异常!
我们发现两种实现的唯一区别是私有数据成员的组成,但是DLL的PUBLIC接口没有变化为什么会出现异常呢?
原来,客户端在第一次编译时引入老的server1.lib,并没有准备为新的dll分配256个char变量,但是客户端调用的新的dll接口时却对不属于自己的内存块做了操作,其实,客户端在创建IOperator对象时就出错了!
我们称以上的接口定义为“老”的接口定义方式,这种方式下,如果改变了数据成员而且公用接口对数据成员又做了操作,那么在不重新编译客户程序的情况下,客户程序将毫无疑问的出现异常甚至崩溃。
封装-C++的三大特性之一,在这里迷惑了我们的视眼。因为利用PRIVATE和PUBLIC关键字定义的封装是“语法”上的封装,也就是说,在同一工程内是不能够直接访问PRIVATE的成员的,否则编译器会报告语法错误,实际上,编译器在编译重用库的时候还是需要访问重用类的所有成员(包括PRIVATE),以便在客户中构造类对象。这样,“接口”和“实现”实际上是一个东西。
“接口”和“实现”的真正分离,要求C++的“封装”是种“二进制层次”的封装。也就是说,不管重用类的实现如何改变,它提供的接口对于客户来说都是静止的。因此,我们把接口类和实现类分离的时候,要让接口类的二进制布局不会随着实现类的变化而变化。
下述对接口类和实现类的分离是成功的,因为不论实现类如何改进,接口IOperatorItf的内存布局从未改变。但是,一个残酷的问题是,IOperatorItf类必须声明IOperator的所有拥有的接口,对于一个稍微大型的类来说,这是个烦琐的过程,而且,嵌套调用的开销也不可忽略。
1 class EXPORIMP IOperatorItf { //接口类
2 class IOperator;
3 IOperator* m_pThis;
4 public:
5 IOperator();
6 ~IOperator();
7
8 long operate(const long var1, const long var2);
9 };
10
11 class EXPORIMP IOperator { //实现类
12 public:
13 IOperator();
14 ~IOperator();
15
16 long operate(const long var1, const long var2);
17
18 private:
19 int a;
20 int b;
21 };
这里还有个非常关键的问题,上述改进并没有解决编译器/链接器的标识符名字改编问题,这造成严重的编译器/链接器依赖。
编译器之间不可避免的在编译细节上存在多种差异,然而,有一条特性却是所有的编译器都满足的:“某个给定平台上的所有C++编译器都实现了同样的虚函数调用机制”,即对于每个编译器,类的对象在内存中如何表示,以及在运行时虚函数如何被动态调用,都是一样的。这个特性非常漂亮的解决上述问题。
1 //接口类
2 class IOperatorItf {
3 public:
4 vritual long operate(const long var1, const long var2)=0;
5 };
6 extern "C" IOperatorItf* CreateOperatorInstance();
7
8 //实现类
9 class IOperator : public IOperatorItf {
10 public:
11 IOperator() {a=b=1};
12 ~IOperator();
13
14 long operate(const long var1, const long var2) {return (a+b)};
15
16 private:
17 int a;
18 int b;
19 };
20 extern "C" IOperatorItf* CreateOperatorInstance() { return (new IOperator)};
这里,接口类和实现类在定义上是独立的,但是因为继承,实现类的内存布局是接口类布局的二进制超集,这种“二进制层次”的继承解决了我们前面几种方案的所有问题。
“接口”和“实现”的分离是重用组件的核心,当我们学会用虚函数表来表达我们的接口时,COM已经在向我们招手了。
[完]