Michael's Space

Technology changes the world, serves the people.
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

COM的由来

Posted on 2006-07-04 17:59 奔跑的阿甘 阅读(478) 评论(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已经在向我们招手了。

[完]

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理