Programming 版 (精华区)

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

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

要注意一些事情。首先,在 IDL 中有各种属性包含在方括号中。属性总是应用于后面紧
跟的内容,所以上面的 UUID 属性应用于接口—它是接口的 IID。(UUID 或通用唯一标
识符,是 GUID 的同义词。)[in, out] 属性应用于指针,并且告诉 COM 当调用 Func3
时,必须让单个的 int 在函数内外排队(如果需要排队)。如果 int 指针引用一个数组
,它会有一个附加属性(size_is 带一个参数)。也有 IDL 代码定义对象,定义我们的
对象的代码段可能会象这样:

[ uuid(E312522E-A7B7-11D1-A52E-0000F8751BA7) ]
coclass Foo
{
   [default] Interface IFoo;
};

这就是 CLSID 如何与类相关的,也是类实现的接口如何定义的。注意,虽然这些代码很
象 C++,并且带有一些附加属性,但是这些代码并不象接口定义那样严格地对应于 C++
代码。

对象的创建
一旦我们的 CLSID 与一个对象类型相关(将在稍后详细探讨),就可以创建一个对象。
正如所证明的,这很简单—只是一个函数的调用:

IFoo *pFoo = NULL;
HRESULT hr = CoCreateInstance(CLSID_Foo, NULL, CLSCTX_ALL,
               IID_IFoo, (void **)&pFoo);

如果 CoCreateInstance 成功了,它会创建 CLSID GUID CLSID_Foo 标识的对象的一个实
例。注意,没有“对象的指针”这回事;相反,我们总是通过一个接口指针引用对象。所
以我们必须指定我们需要哪个接口 (IID_IFoo),并将一个指针传递到某处,以便让
CoCreateInstance 存储该接口指针。

我们还没有讨论的两个参数目前还不重要。

一旦我们调用了一个函数,需要进行检查,以确保调用成功,并且接下来使用该对象:

if (SUCCEEDED(hr)) {
   pFoo->Func1();   //调用方法程序。
   pFoo->Func2(5);
   pFoo->Release();   // 当处理完之后,必须释放接口。
}
else // 创建失败...
CoCreateInstance 返回一个 HRESULT,以表明它是否成功。因为非负数表示成功,我们
总是使用 SUCCEEDED 宏来检查结果。实际上,最普通的成功代码 S_OK 是零,所以象
"if (hr) // Success" 这样的检查根本不起作用。一旦成功地创建了该对象,您可以如
上所示,使用接口指针来调用接口的方法程序。

当您处理完接口指针后,需要通过调用 Release 来释放该接口指针,这是至关重要的。
注意,由于所有接口都是从 IUnknown 导出的,所以所有接口都支持 Release。当您告诉
 COM 对象您已经处理完它时,由它自己释放自己,但是它需要您告诉它您何时处理完。
如果您忘记了调用 Release,该对象会被泄漏(并且被锁在内存中,至少要等到关闭应用
程序,甚至要等到系统重新启动)。弄乱对象生命期是非常普遍的 COM 编程问题,而且
难于发现。所以,要从现在开始小心谨慎。注意,如果我们真正创建了某接口,就只能释
放该接口。

图 5 是我们新创建对象的图例。按约定,IUnknown 没有标识;它总是画在对象的右上角
。所有其他接口画在左边。



图 5 第一个简单的 COM 对象,带有无标识的 IUnknown。

由于我们实现了 IUnknown,所以就有了一个 COM 对象。(就象画一个连接器一样简单!


如果将一个 IFoo2 接口添加到该对象,总共就有了三个接口,如图 6 所示。



图 6 理论版 2.0,支持 IFoo 和 IFoo2。

GUID 和注册表
那么 COM 是如何找到对象的代码,以便创建对象的呢?很简单:它在注册表中找。当安
装了一个 COM 组件时,它必须在注册表中建立注册项。对于我们的 Foo 类,注册项可能
会是这样:

HKEY_CLASSES_ROOT
  CLSID
    {E312522E-A7B7-11D1-A52E-0000F8751BA7}="Foo Class"
      InprocServer32="D:\\ATL Examples\Foo\\Debug\\Foo.dll"

大多数对象会有一些附加项,但是我们现在暂时忽略这些项。

在 HKEY_CLASSES_ROOT\CLSID,有一个我们的类的 CLSID 的注册项。这就是
CoCreateInstance 如何查找组件的 DLL 名称的。当您为 CoCreateInstance 提供了
CLSID,它会找到 DLL 名称,加载这个 DLL,并且创建该组件(稍后将详细讨论)。

如果服务程序是线外的或远程的,该注册项会有所不同,但是重要的是这些信息存在,所
以 COM 可以启动服务程序并且创建该对象。

如果您知道对象的名称 (ProgID),但不知道它的 CLSID,就可以在注册表中查找 CLSID
。对于我们的对象,有这样一个注册项:

HKEY_CLASSES_ROOT
  Foo.Foo="Foo Class"
    CURVER="Foo.Foo.1"
    CLSID="{E312522E-A7B7-11D1-A52E-0000F8751BA7}"
  Foo.Foo.1="Foo Class"
    CLSID="{E312522E-A7B7-11D1-A52E-0000F8751BA7}"

"Foo.Foo" 是独立版本的 ProgID,Foo.Foo.1 是 ProgID。如果您从 Visual Basic 中创
建一个 Foo 对象,可使用其中一个 ProgIDs 查找 CLSID。(注意,在当前版本,ATL 向
导还不能完全正确地创建注册项:它会漏掉上面所示的前两个 CLSID 关键字。不要忘记
为独立版本的 ProgID 复制 CLSID。)

模块、组件类和接口
注意,一个模块(DLL 或 EXE)有可能(实际上是通常)实现多个 COM 组件类。如果是
这样,会有多个 CLSID 注册项引用相同的模块。

这样我们现在可以定义模块、类和接口之间的关系。一个模块(您连编和安装的基本单元
)可以实现一个或多个组件,每个组件在注册表中都有自己的 CLSID 和指向模块的文件
名的注册项,并且每个组件至少实现两个接口:IUnknown 和一个提供组件功能的接口。
图 7 表明了这点。



图 7 模块 Oo.DLL 包含三个对象(Foo、Goo 和 Hoo)的实现细节。每个对象实现了
IUnknown 和一个或多个附加接口。

使用 QueryInterface 获得其他接口
可以说,我们有了一个新的改进了的 Foo2 对象,它实现了两个自定义接口:IFoo 和
IFoo2。我们已经知道了如何使用 CoCreateInstance 创建这样一个对象,也知道了如何
获得一个指向三个接口(不要忘记 IUnknown)之一的指针。

当我们得到这样的接口指针之后,怎样才能得到该对象的其他接口的接口指针呢?不能再
调用 CoCreateInstance—这会创建一个新对象,这并不是我们希望的,我们只需要现有
对象的另一个接口。

这就需要 IUnknown::QueryInterface 来解决了。记住,由于所有的接口都从 IUnknown
继承,所以它们都执行 QueryInterface。这样,我们只需使用第一个接口指针来调用
QueryInterface,以便得到第二个接口指针:

IFoo *pFoo = NULL;
HRESULT hr = CoCreateInstance(CLSID_Foo2, NULL, CLSCTX_ALL,
               IID_IFoo, (void **)&pFoo);
if (SUCCEEDED(hr)) {
   pFoo->Func1();   //调用 IFoo::Func1
   IFoo2 *pFoo2 = NULL;
   hr = pFoo->QueryInterface(IID_IFoo2, (void **)&pFoo2);
   if (SUCCEEDED(hr)) {
      int inoutval = 5;
      pFoo2->Func3(&inoutval);   // IFoo2::Func3
      pFoo2->Release();
   }
   pFoo->Release();
}
我们向 QueryInterface 传递了所需接口的 IID,还有一个指向 QueryInterface 保存新
接口指针的位置的指针。一旦 QueryInterface 返回成功,我们就可以使用该接口指针来
调用该接口的函数。

一定要注意,当我们处理完两个接口指针之后必须释放这两个指针。如果释放其中的一个
指针失败,就会泄漏该对象。由于我们只能通过接口指针引用该对象,所以必须释放每个
得到的接口指针,这样才能作为一个整体释放该对象。

IUnknown 的其他函数
IUnknown 有其他两个函数:AddRef 和 Release。我们已经看到,您使用 Release 告诉
一个对象,您已经处理完一个接口指针。那么您什么时候使用 AddRef 呢?

引用计数,并且当一个对象可以释放的时候
大多数 COM 对象都保留一个引用计数 ( reference count)—也就是说,它们需要跟踪有
多少个该对象的接口指针正在使用。如果所有对象接口的引用计数变成零时,该对象就可
以释放。我们不必明确地释放该对象;只需释放对象的所有接口指针,对象会在合适的时
候释放自己。

AddRef 增加引用计数,而 Release 减少引用计数。所以,如果没有调用 AddRef,为什
么必须调用 Release 呢?

每当 QueryInterface 为一个对象分配一个新指针时,QueryInterface 有责任在返回该
指针前调用 AddRef。这就是为什么不必为得到的指针调用 AddRef。QueryInterface 为
我们做了。(注意,CoCreateInstance 调用 QueryInterface,而 QueryInterface 调用
 AddRef,所以对象的第一个接口指针也是这样。)

对于调用了 AddRef 的相同接口指针,也需要调用 Release。如果对象需要,它们可以一
个接口一个接口地跟踪引用。上面的代码小心地做到了这一点,对应 Release 调用的正
确配对的隐含 AddRef 调用—每个接口指针一个 Release 调用。

如果您复制了一个接口指针,则需要调用 AddRef,这样该接口的引用计数才准确。至于
何时需要何时不需要有点复杂,但是各种 COM 参考书都有详细的介绍。有关细节请查看
这些参考书。

各种巧妙的指针类使得处理 IUnknown 变得容易多了(实际上是自动的)。在 ATL 和
Visual C++ 5.0 中有几个这样的类。如果您使用另一种语言,例如 Visual Basic 或
Java,该语言对 COM 的实现会正确地处理 IUnknown 方法程序。

回顾与前瞻
我们已经讨论了如何创建对象,以及如何破坏它们(不是真的破坏,只是释放它们的所有
接口指针),还有如何调用接口的方法程序和切换接口。同时,也介绍了用于标识对象和
接口的各种 GUID 的概念,还有所需的注册项,这样 COM 才能知道如何创建您的对象。


在第四部分,我们将特别详细讨论如何创建进程内对象,以及使创建过程更高效的方法。
如果有时间,还将探讨实现一个对象的本质,包括创建一个对象和 IUnknown 的代码。

--

                路漫漫兮,其修远。
                吾将上下而求索。
※ 修改:.haojs 于 Sep  3 08:02:05 修改本文.[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.957毫秒