Programming 版 (精华区)

发信人: lofe ()感激生活(), 信区: Programming
标  题: 组件、COM和ATL(2)
发信站: 哈工大紫丁香 (Sun Sep  3 08:04:05 2000), 转信

发信人: kywu (太阳星辰), 信区: VisualC
标  题: 组件、COM 和 ATL(2)
发信站: BBS 水木清华站 (Mon Mar 22 13:06:59 1999) WWW-POST

第二部分:COM 基本概念
回顾与前瞻
在第一部分,医生探讨了为什么 C++ 这样的语言不能解决用二进制组件构造软件的问题
。这个问题的关键是,C++ 原本不打算解决这个问题;相反,它本来的用途是在单个可执
行程序中,较为容易地重用源代码。C++ 很好地做到了这一点。但是我们希望能够混合和
匹配不同供应商的组件,而在组件每次改变时不必重新连编系统的一部分(或全部)。我
们已经讨论了很多理由,说明为什么 C++ 模型对此无能为力。

那又怎样?您必须抛弃 C++ 吗?不,但是您必须以与以前所熟悉的方法稍有不同的方法
使用它。这就是我们下一步将要讨论的,即如何从 C++ 中使用 COM。

这是否意味着,如果您不是一位 C++ 程序员,就不要阅读下去了?不,因为不管您使用
的是哪种 COM 兼容的语言(Visual Basic、Visual J++、Delphi 等等),这些语言都会
做我们讨论的事情(或许不只是机房中的事情?)。所以,如果您继续读下去,您会对这
些 ActiveX 控件和 COM 组件的工作机制获得有益的了解。

好吧,那么什么是 COM 呢?
Components Object Model (COM) 是软件组件互相通讯的一种方式。它是一种二进制和网
络标准,允许任意两个组件互相通讯,而不管它们是在什么计算机上运行(只要计算机是
相连的),不管各计算机运行的是什么操作系统(只要该操作系统支持 COM),也不管该
组件是用什么语言编写的。COM 还提供了位置透明性:当您编写组件时,其他组件是进程
内 DLL、本地 EXE 还是位于其他计算机上的组件,对您而言都无所谓。(当然会有性能
区别,但是,即使改变了其他组件的位置,您也不必重新编写什么,这是关键所在。)

对象
COM 是基于对象的——但是这种对象概念与您熟悉的 C++ 或 Visual Basic 中的对象不
太一样。(顺便说一下,“对象”和“组件”几乎是同一个东西。GUI 医生在谈论应用程
序的结构时愿意说成“组件”,而在谈论实现时愿意说成是“对象”)

首先,COM 对象被很好地封装起来。您无法访问对象的内部实现细节;您无法知道对象使
用了什么数据结构。实际上,对象的封装是如此的严密,以致于 COM 对象通常被描绘为
盒子。图 1 描绘了一个完全封装的对象。请注意,实现细节是如何向您隐藏的。



图 1 一个完全封装的非 COM 对象。

封装是不错,但是通讯又怎么样呢?在这种状况下,我们无法与这个盒子中的组件通讯。
很明显,这个方法不行。

接口:与对象的通讯
这时就需要接口了。访问 COM 对象的唯一途径是通过接口。我们可以象图 2 所示,在对
象上描绘一个名为 IFoo 的接口。



图 2 带有接口的对象—也不是 COM。

在对象旁边支出来的象棒棒糖的东西就是接口——这里是 IFoo 接口。该接口是与该对象
通讯的唯一途径。医生认为,将接口看成一个插件连接器,比看成棒棒糖会更有用。正是
通过它您才能为对象添加功能,可将它看成是录相机或电视机的天线输入。

接口有两个含义。首先,它是一组可以调用的函数,由此您可以让该对象做某些事情。在
 C++ 中,接口是用抽象基类代表的。例如,IFoo 的定义可能是:

class IFoo {
   virtual void Func1(void) = 0;
   virtual void Func2(int nCount) = 0;
};

我们现在暂时忽略返回值和继承性,但是要注意在接口中可以有多个函数,而且这些函数
都是纯虚函数:它们没有在 Ifoo 类中实现。我们在这里并非要定义行为,而只是要定义
在接口中有什么函数(当然,真正的对象必须要有实现部分,有关的详细内容稍后讲解。


其次,也是更重要的,接口是组件及其客户程序之间的协议。也就是说,接口不但定义了
可用什么函数,也定义了当调用这些函数时对象要做什么。这种语义定义不是以对象的特
定实现来表达的,所以无法用 C++ 代码来表达该定义(虽然我们可以用 C++ 提供一种特
定实现)。相反,该定义是以对象的行为来定义的,所以对该对象和(或)也实现该接口
(协议)的新对象进行修订是可能的。实际上,对象可以按自己选择的任何方式实现该协
议(只要该对象遵守该协议)。也就是说,该协议必须(医生恶狠狠地说)书写在源代码
之外的文档中!因为客户程序无法(也不必)得到源代码,所以这一点尤其重要。

这种特殊协议的观念对 COM 和组件软件都是很至关重要的。没有“坚不可摧”的协议,
就不可能交换组件。

接口协议象钻石一样永久
在 COM 中,一旦您通过发布一个组件来“公布”了一个接口协议,该协议就不能变更了
——不能以任何方式变更。您不能添加、不能删除、不能修改。为什么?因为其他组件依
赖于该协议。如果更改了该协议,您将会破坏那些软件。只要遵守该协议,您可以改进内
部的实现。

如果您忘记了什么怎么办?如果需求发生了变化怎么办?难道整个世界都要永远停滞不前
吗?

答案很简单:编写一个新协议。标准的 OLE 接口列表有很多这样的协议:
IClassFactory 和 IClassFactory2,IViewObject 和 IViewObject2,等等。当然,您也
可以提供一个 IFoo2。(我敢肯定,您已注意到接口名称按约定是以大写字母 I 开头的
。)

如果我编写了一个新协议,那么那些只知道旧协议的软件如何继续使用我的组件呢?这是
否会把新旧组件搞得一团糟?

COM 对象可以支持多接口—它们可以实现多个协议
答案也是不,原因很简单:在 COM 中,一个对象可以支持多个接口。实际上,所有有用
的 COM 对象都至少支持两个接口。(至少包含标准 IUnknown 接口(有关的详细内容稍
后讲解)和一个实现所需功能的接口。)Visual ActiveX 控件都支持十几个接口,大多
是标准接口。要使组件支持一个接口,必须实现该接口中的每个方法程序,所以要进行大
量的工作。这就是 Active Template Library (ATL) 等工具流行的原因:它们提供了所
有接口的实现。

所以为了支持新的 IFoo2 功能,我们将 IFoo2 也添加到该对象。



图 3 2.0 版,它支持 IFoo 和 IFoo2——但仍不是一个 COM 对象。

如果您仍想到插件,可以将 IFoo 想象为电视机的天线输入,将 IFoo2 想象为复合视频
输入。注意,您不能将天线电缆插入到复合视频输入的插孔,反过来也不行。也就是说,
每个接口在逻辑上都是唯一的。

另一方面,这些接口也有共同的地方。为了添加一个与旧接口几乎一样的新接口,是否需
要重新编写全部的实现代码?不,因为 COM 支持接口的继承。只要我们不更改 IFoo 中
已有的函数,我们可以如下定义 IFoo2:

class IFoo2 : public IFoo {
   // 继承了 Func1, Func2
   virtual void Func2Ex(double nCount) = 0;
};
接口回顾
现在,让我们回顾一下我们阅读过的内容。首先,COM 是一个有关软件对象交互的二进制
标准。由于是一个二进制标准,对象不会也不能知道所使用对象的实现细节。所以,对象
就是黑盒子。

我们只能通过对象提供的接口来操作这些黑盒子对象。最后,一个对象可以提供任意多的
接口。

很简单,是不是?

其实,我们忽略了很多细节。如何创建这些对象?如何访问接口?如何调用接口的方法程
序?不管怎么说,这些对象的实现代码在哪里?这个恼人的对象何时最后被破坏?

这些问题很好,但遗憾的是 GUI 医生的手术要迟到了,所以我们将推迟到第三部分回答
。不过医生现在可以处理一个问题:如何调用接口的方法程序?

调用接口的方法程序
您可能认为会很复杂,但其实很简单:COM 方法程序调用就是 C++ 虚函数的调用。我们
将以某种方法(有关的详细内容在第三部分讲解)获得实现接口的对象的指针,然后我们
就调用该接口的方法程序。

首先,假设我们有一个名为 CFoo 的 C++ 类,它实现了 IFoo 接口。注意,我们从
IFoo 继承,以保证我们按正确的顺序实现了正确的接口。

class CFoo : public IFoo {
   void Func1() { /* ... */ }
   void Func2(int nCount) { /* ... */ }
};

我们使用的指针称为接口指针。假设我们可以得到一个接口指针,我们的代码将如下所示


#include <IFOO.H // 不需要 CFoo,只需接口
void DoFoo() {
  IFoo *pFoo = Fn_That_Gets_An_IFoo_Pointer_To_A_CFoo_Object();

  // 调用方法程序。
  pFoo -> Func1();
  pFoo -> Func2(5);
};
就这么简单。

但是,在这些代码的背后到底发生了什么呢?正如您以后要看到的,COM 二进制标准也应
用于方法程序的调用——所以 COM 定义了调用函数时将发生什么。具体地说,所发生的
事情与虚函数调用时的情形相同:

由 pFoo 获得对象的虚函数表指针。


由虚函数表指针和索引获得要调用函数的地址。


调用函数。
有关这几个步骤的情况,请参阅图 4:



图 4 通过接口指针进行的 C++ 虚函数调用

记住,在 C++ 中,每当声明虚函数时,就会生成一个虚函数表,它指向这些函数,并且
,对这些函数的调用都是通过虚函数表和索引进行的。

“哈哈!”您说,“我知道了。实际上,COM 是离不开 C++ 的!它根本算不上一个二进
制标准!”

GUI 医生回答说:“不对。”毕竟,您可以在任何支持函数指针数组的语言中实现这种调
用。例如,在 C 语言中就很容易,通过指针 p 对 Func2 的调用可能会象这样:

(*((*p)+1))(p, 5); // 将 5 传递到数组中第二个函数
注意,我们必须将 p 作为第一个参数来传递——这模拟了 C++ 的 this 指针。(*p) 是
第一次寻址(步骤1),*((*p) + 1) 是用索引进入虚函数表的正确入口(步骤 2),然
后我们用 p 和 5 作为参数调用了函数(步骤 3)。很容易,但是很不雅观——GUI 医生
做这个示范只是为了表明 C 语言是可以做到的(并且使您欣赏 C++)。在 x86 汇编语言
中,调用可能会象这样:

MOV EAX, [pFoo]      ; 步骤 1
MOV EAX, [EAX + 4]   ; 步骤 2,用索引获得第二个指针
CALL [EAX]          ; 步骤 3
GUI 医生知道第二和第三个指令可以合并为 CALL [EAX + 4],如果您不想在 EAX 中保留
函数的地址。

为什么医生演示了所有这些细节?是的,如果能用汇编语言或 C 语言做到,就能用任何
语言做到!其他语言(Visual Basic、Visual J++、Delphi)将对这些调用的支持置入了
它们的运行时模块或虚拟机中——通常是使用与上面相似的汇编语言代码或 C 语言代码


要点是任何 COM 方法程序的调用都必须使用上面所示的数据结构,而不管其原始语言是
什么,也不管 COM 对象位于何处。在将来的栏目中我们将讨论 COM 是如何做到位置透明
的。

回顾与前瞻
好了,我们已经讨论了接口、对象,以及如何调用接口的方法程序。

对象是 COM 的基本单元——它是 COM 所创建的东西。对象要实现一些接口。接口是一组
方法程序和规定这些方法程序做什么的协议。调用接口的方法程序的方式与调用 C++ 虚
函数的方式相同。

在第三部分中,我们将讨论如何创建这些对象,如何获得接口指针,以及对象是如何被破
坏的。


--

                路漫漫兮,其修远。
                吾将上下而求索。
※ 修改:.haojs 于 Sep  3 08:01:37 修改本文.[FROM: bbs.hit.edu.cn]
--
※ 转寄:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: bbs.hit.edu.cn]

--
☆ 来源:.哈工大紫丁香 bbs.hit.edu.cn.[FROM: haojs.bbs@bbs.whnet.]
[百宝箱] [返回首页] [上级目录] [根目录] [返回顶部] [刷新] [返回]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:3.744毫秒