COM的真相
COM是一种软件体系结构,这种体系结构允许用不同的软件商提供的组件来构造应用程序和系统。它是一套二进制的和网络标准,允许任何软件相互通信而不管硬件、操作系统OS和用于开发的编程语言。COM不是一种编程语言而是一套规范,它定义了组件怎么样可以相互通信。
每个COM组件被编写成满足由COM规定的二进制标准。这一些标准是:
n 组件要求注意它们自己(即组件对象[3])的创建和销毁
n 组件要求以标准方式提供它们的功能(即通过接口[4])
n 组件的位置要求对用户透明
使用组件的优点在于具有将它们动态装载或卸出应用程序系统的能力。为了达到这一目的,组件必须满足两个需求:
n 组件必须动态链接[5]。这允许它们在运行时改变。如果应用程序是由每次组件改变时静态链接的组件组成,那么应用程序就等同于一个单一的应用程序。
n 在实现时组件必须隐藏。每个组件具有唯一识别自己的ID作为标识。这些ID出现在系统注册表HKEY_CLASSES_ROOT目录里。
动态链接对于组件而言是一个至关重要的要求,而实现细节隐藏则是动态链接的一个必要条件。
COM的工作方式
到目前为止,显然COM并不仅仅是书面形式的规范。它也包含系统级的代码,即它自身的实现。COM规则出现在COM运行库里。
组件对象库,即COM运行时间库是一个系统组件,这个系统组件提供能够在进程内、进程外或通过网络进行调用的COM。
COM核心,简单地讲,是组件对象和客户端使用二进制标准如何交互的规范。COM在操作系统内的实现为COM运行时间库。COM运行时间库包括:
n API函数
n 服务
n 允许应用程序在进程内控制内存分配的标准机制
n …
组件对象库是通过Windows3.1中的COMPOBJ.DLL以及Windows NT和Windows 9X中的OLE32.DLL实现的。
接口
n 概述
n 组件对象高度地封装。组件对象的内部实现对用户完全隐藏,根本无法知道组件对象在使用何种数据结构和这些数据是如何被函数处理的。每个组件有一个接口,这个接口是一种且唯一的访问组件对象功能的方式。接口是由一组虚函数的声明组成。它使得预期的行为和响应清淅化。接中并不具有这些函数的实现。函数是通过组件类(CoClass)实现的。组件类实例化时生成组件对象。
n 接口是用称为虚函数表(VTable)的内存结构实现的。无论何时创建组件对象,组件对象也在内存中创建一张对应的虚函数表。虚函数表是由一系列指针组成,这些指针存储了由组件对象实现的成员函数的地址。
n 客户端创建一个接口指针,接口指针指向一个虚指针,虚指针指向虚函数表。使用接口指针和虚指针,客户端就可以访问组件对象实现的成员函数。
VTable是一个函数指针数组的内存结构。每一个数组元素包含的是一个由组件所实现的函数地址。对于COM而言,接口也就是此内存结构,其他东西,均是COM不关心的实现细节。
n 特征
n 接口是集合在同一个名称(是一个系统唯一的ID值,称IID)下的相关函数(/方法)的集合。这说明接口必须是全球唯一的。
n 如前所述,当组件类实例化时产生组件对象。接口是由组件对象实现的函数的集合。接口不可以被实例化,因为它没有实现。
n 组件之间的通讯是基于接口的。接口是组件和其客户之间严格类型化的契约。实现相同接口的两个对象就被认为是多态的,这里的多态不包含诸如基类指针指向派生类对象的意义,这里是指同一个接口可以由多个组件对象以不同方法实现。
n 组件对象可以实现多个接口。例如,银行交易组件(BankTransaction)支持两个接口,IDebit和ICredit,即借和贷;又例如,Microsoft SQL SERVER数据库服务器组件(SqlImplementation)支持两个接口,一个是维护数据处理,另一个是维护数据定义。数据处理接口提供增加、更新和删除数据方法,然而,数据定义接口提供创建数据库、表和视图的方法。
n 接口没有版本化并且是不变的,如果功能必须改变以适应一个接口,则将创建一个有唯一标识的完全新接口。组件对象实现这两个接口,因而解决了版本问题。为较早版本的接口制作的客户端仍旧可以访问原来的接口。
n 注意
n 原来的旧接口是可以改变的,但是接口中的函数说明不能改变,即接口中的函数的函数名、参数形式不能改变,因为这样改变就需要使得客户端调用服务的代码进行修改和进行重新编绎。但是接口中的函数的函数体是可以修改的。这就是接口不变性的本质,也是COM规范。
COM标识符
因为全球都在开发和使用组件,因此需要标识唯一地标识每个组件。
开发软件基金会(OSF)研究出一种能产生唯一标识符的算法,称之为全球唯一标识符(UUID)。在COM中,UUID被称之为全局唯一标识符(GUID)。GUID是能分配给接口、组件类和类型库的128位或16字节数。GUID唯一地标识组件。
生成GUID的算法根据以下几个方面:
n 当前日期和时间。
n 网络适配器卡地址。
n 时针序。
n 自动递增计数器。
COM使用的128位的接口标识符使得我们可能创建大约340282366920900000000000000000000000000个独立的接口,足够为将来10782897524560000000年每秒创建一万亿个接口。
网卡地址是相互不同的,对没有网卡的机器,地址对使用中的机器保持唯一性。
GUID可能通过执行UUIDGEN.EXE或GUIDGEN.EXE产生。GUIDGEN是装载Microsoft Visual Studio中的一个工具,它通常在C:\Program Files\Microsoft Visual Studio\Common\Tools\GUIDGEN.EXE。
GUID分为三类,具体见下:
n CLSID 是唯一地标识类或组件的GUID,传统地,CLSID的一般形式为CLSID_<unique identifier>,在本例中:
在MyProj_i.c中表示为
const CLSID CLSID_MyCom = {0xFEB7BDEF,0xFB6F,0x446B,{0xBE,0x31,0xDF,0x0A,0x3A,0xD3,0x91,0xBA}};
在MyProj.idl中表示为
[
uuid(FEB7BDEF-FB6F-446B-BE31-DF0A3AD391BA),
helpstring("MyCom Class")
]
coclass MyCom
{
[default] interface IMyCom;
};
在MyCom.rgs中表示为
HKCR
{
MyProj.MyCom.1 = s 'MyCom Class'
{
CLSID = s '{FEB7BDEF-FB6F-446B-BE31-DF0A3AD391BA}'
}
MyProj.MyCom = s 'MyCom Class'
{
CLSID = s '{FEB7BDEF-FB6F-446B-BE31-DF0A3AD391BA}'
CurVer = s 'MyProj.MyCom.1'
}
NoRemove CLSID
{
ForceRemove {FEB7BDEF-FB6F-446B-BE31-DF0A3AD391BA} = s 'MyCom Class'
{
ProgID = s 'MyProj.MyCom.1'
VersionIndependentProgID = s 'MyProj.MyCom'
ForceRemove 'Programmable'
InprocServer32 = s '%MODULE%'
{
val ThreadingModel = s 'Apartment'
}
'TypeLib' = s '{FE651184-11DE-4D01-BD69-B07DDFA12D0C}'
}
}
}
n ProgID 显然,上面的CLSID难以记忆且难以使用。ProgID是分配给对象的用户友好名。ProgID不可能是单一的。每个ProgID映射到CLSID。命名习惯可以是<program>.<component>.<verson>。在本例中:
在MyCom.rgs中可以看到,有这么两句
…
ProgID = s 'MyProj.MyCom.1'
VersionIndependentProgID = s 'MyProj.MyCom'
…
通过分别地调用函数ProgIDFromCLSID和CLSIDFromProgID可以将ProgID转换为CLSID和将CLSID转换为ProgID。
如在本示例中的客户端程序中
HRESULT hr=CoInitialize(NULL);
CLSID clsid;
hr=CLSIDFromProgID(OLESTR("MyProj.MyCom"),&clsid);
if(FAILED(hr))
{
AfxMessageBox("COM Failed");
return;
}
调用函数CLSIDFromProgID把作为第一个参数的ProgID的CLSID存放到第二个参数中。
n IID 是唯一标识接口的GUID。按照惯例,IID的一般形式为IID_<unique identifier>,在本例中:
在MyProj_i.c中表示为
const IID IID_IMyCom = {0x65460F9C,0x3BAB,0x4055,{0x88,0x5A,0x8E,0xD5,0x9F,0x5F,0xA9,0xB0}};
在MyProj.idl中表示为
[
object,
uuid(65460F9C-3BAB-4055-885A-8ED59F5FA9B0),
dual,
helpstring("IMyCom Interface"),
pointer_default(unique)
]
interface IMyCom : IDispatch
{
[id(1), helpstring("method MyF1")] HRESULT MyF1();
[id(2), helpstring("method MyF2")] HRESULT MyF2([in] BSTR str,[out, retval] int* val);
[id(3), helpstring("method MyF3")] HRESULT MyF3([in] BSTR str,[out, retval] BSTR* retstr);
[id(4), helpstring("method MyF4")] HRESULT MyF4([in] int x,[out, retval] int* val);
};
n TypeLibID 是标识系统上的类型库。按照惯例,TypeLibID的一般形式为LIBID_<组件工程名>Lib,在本例中:
在MyProj_i.c中表示为
const IID LIBID_MYPROJLib = {0xFE651184,0x11DE,0x4D01,{0xBD,0x69,0xB0,0x7D,0xDF,0xA1,0x2D,0x0C}};
在MyProj.idl中表示为
[
uuid(FE651184-11DE-4D01-BD69-B07DDFA12D0C),
version(1.0),
helpstring("MyProj 1.0 Type Library")
]
library MYPROJLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
};
在MyCom.rgs中也可以看到这么一句
'TypeLib' = s '{FE651184-11DE-4D01-BD69-B07DDFA12D0C}'