Programming 版 (精华区)
发信人: lofe ()感激生活(), 信区: Programming
标 题: 组件、COM和ATL(3)
发信站: 哈工大紫丁香 (Sun Sep 3 08:04:28 2000), 转信
发信人: kywu (太阳星辰), 信区: VisualC
标 题: 组件、COM 和 ATL(3)
发信站: BBS 水木清华站 (Mon Mar 22 13:12:39 1999) WWW-POST
第三部分:获取对象和接口
在第二部分,我们讨论了 COM 中两个非常基本的概念:对象和接口。我们还演示了一个
对象如何实现多个接口。最后,我们讨论了 COM 方法程序调用的本质,并且认识到它们
与 C++ 虚函数的调用方式相同。(但是,我们也注意到,您可以在任何支持函数指针数
组的指针的语言中(或者通过调用汇编语言来支持)调用 COM 的方法程序。)
有关 COM 的详细讨论:对象的创建和破坏;获得接口指针
但是,您可能会发现这些讨论有些不能令人满意:我们从来没有讨论如何创建对象,也没
有讨论如何获得接口指针,以调用对象接口的方法程序。而且,我们从来没有讨论如何删
除您不再需要的对象,或者如何切换接口。
医生将在本周的讨论中涉及这些主题。但是,首先,我们需要稍微整理一下思路。您是否
有过这样的经历:想要解释什么,但是突然想起来您忘记了一些重要的东西。现在就是这
种情况,医生忘记的事情是,为了创建对象,需要有一种能引用它们的方法,还要有一种
能明确定义接口的方法。所以,我们先讨论被遗忘的东西,然后再涉及我们真正感兴趣的
内容。(而且,这也是为什么这次的栏目这么长,并且这么晚与大家见面。)
COM 中的标识符
如果您考虑得比较超前,您会知道,我们需要一些标识符来表示 COM 世界中的各种实体
。首先,对象类型(或称“类”)需要一个标识符。其次,各接口也需要标识符。但是,
我们应该用什么作为标识符呢?32 位整数?64 位整数?我们可以用它们作标识符。但是
有一个问题:在所有的计算机上,标识符都必须是独一无二的,因为无法知道会在什么计
算机上安装组件。对象和接口需要在所有的计算机上都使用相同的标识符,这样才能使任
何客户程序都可以使用该组件。另外,不能有其他对象或接口使用这个标识符,不管其他
对象或接口来自何处。也就是说,这些标识符必须是全球唯一的。
幸运的是,创建这种标识符的算法和数据格式是存在的。通过使用计算机的唯一网络卡
ID、当前时间和其他数据,一个称为 GUIDGEN.EXE 的程序就可以创建这种标识符,称为
GUID(全球唯一标识符)。GUID 是按 16 字节(128 位)结构存储的,这样就可以有
2128 个可能的 GUID。但是,不要担心会用完所有的 GUID:虽然医生不知道整个宇宙中
的原子的确切数目,即使搜索了 Web 也未能找到答案,但是他相信那个数目一定比
2128 少得多。所以,GUID 不会缺乏,也没必要保留 GUID,不用理会 COM 从业者的玩笑
。
在 C++ 中,COM 头文件为 GUID 定义了数据类型,即 CLSID(类标识符 GUID)和 IID(
接口标识符 GUID)。由于这些 16 字节结构有些大,因而不能按值传递,所以当传递
GUID 时,需要使用 REFCLSID 和 REFIID 作为参数的数据类型。您需要为每个对象类型
创建一个 CLSID,为每个自定义接口创建一个 IID。
标准接口
COM 定义了大量的标准接口及其相关的 IID。例如,所有接口的母辈 IUnknown 的 IID
是 "00000000-0000-0000-c000-000000000046"(连字符是书写 GUID 的标准方式)。这
个 IID 是由 COM 定义的,您永远也不必直接引用它;而应使用 IID_IUnknown 常量,这
是在头文件中定义的。
IUnknown 接口有三个函数:
HRESULT QueryInterface(REFIID riid, void **ppvObject);
ULONG AddRef();
ULONG Release();
稍后我们将详细讨论这些函数的功用。
使用标准接口可以完成大量 COM 编程工作——其中的大部分工作就是提供标准接口的实
现细节,这样,其他的 COM 客户程序和对象就可以使用您的对象了。
顺便说一句,在 COM 中用到了一些宏,以说明函数的返回值和调用约定。几乎所有的
COM 方法程序都返回一个 HRESULT 类型的量,所以 STDMETHODIMP 宏就采用这种类型。
STDMETHODIMP_() 宏带有一个参数——方法程序的返回类型。(只有当您使用纯虚函数定
义接口时,才会用到 STDMETHOD 宏——并且将由 IDL 编译器为您编写代码,有关的详细
内容稍后讲解。)
使用这些宏,上面的声明就会是这样:
STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
STDMETHODIMP_(ULONG) AddRef();
STDMETHODIMP_(ULONG) Release();
以后我们将一直使用这些宏。这样做,就可以容易地将代码移植到其他不同的 COM 平台
(例如 Macintosh 和 Solaris)上。
自定义接口
自定义接口是您创建的接口。您要为这些接口创建您自己的 IID,并且定义您自己的函数
。我们的 IFoo 接口就是一个自定义接口。通过运行我计算机上的 GUID 生成器,我已经
定义了一个 IID,称为 IID_Ifoo(它的值是 "13C0205C-A753-11d1-A52D-0000F8751BA7"
)。
回忆一下,原来的类声明是:
class IFoo {
virtual void Func1(void) = 0;
virtual void Func2(int nCount) = 0;
};
我们将略作修改,就将它变成 COM 兼容的:
Interface IFoo : IUnknown {
virtual HRESULT STDMETHODCALLTYPE Func1(void) = 0;
virtual HRESULT STDMETHODCALLTYPE Func2(int nCount) = 0;
};
使用上面所说的宏,就变成:
Interface IFoo : IUnknown {
STDMETHOD Func1(void) PURE;
STDMETHOD Func2(int nCount) PURE;
};
"Interface" 并不是 C++ 中的关键字,而是在相应的 COM 头文件中用 #define 定义为
"struct" 的。(回忆一下,在 C++ 中,类和结构是相同的,只是在默认情况下,结构使
用公共继承和访问,而不是私有的。)STDMETHOD 使用 STDMETHODCALLTYPE,它定义为
__stdcall,这表明编译器要为这些函数生成标准的函数调用序列。记住,我们使用这些
宏是因为将我们的代码移植到不同的平台上时,它们的定义会改变。
所有的 COM 函数(几乎毫无例外)都返回一个 HRESULT 类型的错误代码。这个
HRESULT 是一个 32 位的数值,它使用符号位代表成功或失败,其余 31 位中的域表明“
功能”和与功能对应的错误代码,还有一些保留位。通常情况下要返回成功代码 S_OK,
但是如果在方法程序中遇到了问题,也可以返回错误代码——或者是标准的,或者是您自
己构造的。
最后,注意我是从标准 COM 接口 IUnknown 派生出 IFoo 的。这意味着,任何实现
IFoo 的类也需要同时实现 AddRef、Release 和 QueryInterface 这三个函数。另外,在
IFoo 的虚函数表中,这三个函数的指针将位于 Func1 和 Func2 函数的指针之前。在虚
函数表中有五个函数,而这五个函数都需要实现。所有的 COM 接口都是从 IUnknown 派
生的,所以所有的 COM 接口除包含其他函数外,都包含这三个函数。
MIDL 又怎样?
您不用自己动手编写上面的声明 — 它是由 MIDL 编译器为您生成的。为什么?正如事实
表明的,C++ 不能表达需要在一个接口中表达的所有东西。回忆一下,COM 对象可以是进
程内使用的 DLL,意味着位于相同的地址空间。所以,如果您将某些数据的一个指针传递
到一个进程内服务程序,该服务程序会直接废弃该指针。
但是,也要记住,您的 COM 对象也可以是一个本地(线外)服务程序,位于单独的 EXE
地址空间,甚至可以远程访问。每当您向这样一个对象的 COM 方法程序中传递一个指针
时,都会遇到问题:在任何其他地址空间,该指针都是毫无意义的,有意义的是该指针所
指向的数据。该数据必须复制到其他地址空间—甚至复制回去。这个复制正确数据的过程
称为排队 (marshalling)。谢天谢地,在大多数情况下 COM 为您进行排队。但是,为此
您不仅需要告诉 COM 指针所指数据的类型,您还需要告诉 COM 该指针是如何使用的。例
如,该指针是否指向一个数组?指向一个字符串?该参数只是一个输入参数?是一个输出
参数?还是两者都是?您会看到,无法在 C++ 中表达这些。
所以,我们需要另一种语言,称为 IDL (Interface Definition Language),以便定义接
口。IDL 很象 C++,但是将方括号中的“属性”添加到与 C++ 类似的代码中。MIDL.EXE
编译您(或 Visual Studio)编写的 IDL 文件,以便生成各种输出。到目前为止,我们
关心的唯一输出是我们的接口的头文件,我们将在我们的代码中包含这个头文件。
在我们的示例中,并没有多少区别,因为我们只是按值传递参数,所以 IDL 代码很眼熟
—主要区别是没有 "virtual" 一词。但是,如果我们创建一个新的接口 IFoo2,除了其
他两个方法程序,还添加了一个方法程序 Func3(int *),IDL 会是这样:
[ uuid(E312522F-A7B7-11D1-A52E-0000F8751BA7) ]
Interface IFoo2 : IUnknown
{
HRESULT Func1();
HRESULT Func2(int in_only);
HRESULT Func3([in, out] int *inout);
};
--
路漫漫兮,其修远。
吾将上下而求索。
※ 修改:.haojs 于 Sep 3 08:01:51 修改本文.[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)
页面执行时间:8.800毫秒