一、 COM组件需要遵循的规范
COM组件满足组件架构的需要,即它也是使用DLL来提供运行时的动态加载或卸载特性。
COM规范可以保证组件充分利用动态链接所带来的好处:
(1)、提供了一个所有组件都应遵守的标准。
(2)、允许使用组件的多个不同版本。
(3)、使得可以按相同的方式来处理类似的组件,即COM组件的处理方式都是类似的。
(4)、定义了一个与语言无关的架构。
(5)、支持对远程组件的透明链接。
为了实现上述的好处,COM定义规范强制将客户和组件隔离开。
规范的具体实现方法是通过接口连接客户和组件,以达到信息的隐藏。
Interfaces是客户与组件之间的协议,组件实现接口,客户利用接口。
结论:COM规范定义了特定的接口,所有COM组件必须实现这种接口,需要和组件交互的客户必须通过这种接口和组件交互。
二、 COM编程中涉及的基本概念
接口就是COM编程的重中之重。
COM规范定义了几个基本接口(最基本的接口为IUnknown和IClassFactory接口),所有组件都必须直接或者间接从IUnknown继承,所有客户都必须通过IUnknown和组件交互。这样就任何一个组件就可以被任何一个客户以类似的方式调用。
这个接口必须跨编译器、跨语言、跨平台、跨区域等必须满足组件的要求。
例如:如下为C++的IUnknown定义
class IUnknown
{
public:
virtual HRESULT__stdcall QueryInterface(const IID& iid, void **ppv) = 0 ;
virtual ULONG __stdcall AddRef() = 0;
virtual ULONG __stdcall Release() = 0;
};
COM组件是跨区域的,即开发COM组件的人可能位于世界的各个不同角落,因此为确保每个人开发的COM组件标识符不会和其他人开发的COM组件或者接口标识符出现冲突,为每个COM组件分配一个全球唯一的标识符是必须的,即一个身份证。接口是组件和客户之间连接的桥梁,因此也必须确保每个接口有一个全球唯一的标识符。
从上一段看需要一个统一的授权机构来管理程序所需要的接口标识符,但又考虑到开发COM组件的人位于世界上不同的区域,成立一个统一发放接口标识符的机构来管理所有程序的接口标识符几乎是一件不可能的事情。
一中更好的解决方案,用编程的方法来生成具有全球唯一性的标示符,此类标识符成为GUID(Global Unique Identifier),是一个128位的数。
COM组件是寄宿在DLL上的,因此每个DLL也有一个GUID;DLL是COM组件的服务器,因此每一个COM组件也需要一个GUID;每个COM组件和客户交互需要接口,即一个COM组件是有一个或者多个接口构成,因此每个接口也需要一个GUID。
为了便于区分不同种类的GUID,为每一类GUID去一个名字。
GUID仍然用来表示DLL的标识符。
标示组件的GUID用新名字CLSID来表示。
标示接口的GUID用新名字IID来表示。
从组件必须满足的条件的来看,被调用的组件可能位于网络的不同位置,因此组件的返回值就会有很多,可能会返回多个成功代码及失败代码而不是一个成功代码,这就需要一个特殊的返回值类型HRESULT(Here is the Result),表示这就使返回结果。
HRESULT是一个可以分成三个域的32位值,
// +---+-+-+-------------+----------------+
// |Sev|C|R| Facility| Code |
// +---+-+-+-------------+----------------+
// Sev - is the severity code
// 00 - Success
// 01 - Informational
// 10 - Warning
// 11 - Error
// C - is the Customer code flag
// R - is a reserved bit
// Facility - is the facility code
// Code - is the facility's status code
三、 COM规范的应用
对于客户来说,遵循COM规范的组件就是一个接口集。
COM组件的所有接口必须从COM规范制定的接口IUnknown直接继承或者间接继承。
所有继承了IUnknown的接口必须实现该接口的函数。
(1)、QueryInterface函数可以实现接口查询。
由于COM组件的信息隐藏特性,而且一个COM组件是有许多接口组成的,因此它的客户不需要知道该组件提供的全部接口,这就需要运行时发现COM对象提供什么接口。
COM规范为规定IUnknown接口定义一个函数用来动态查询COM组件提供那些接口。并且规定这个函数需要满足如下的标准:
<1>、QueryInterface必须是对称的,即如果从A接口能查询到B接口,那么也能从B接口查询到A接口。
<2>、QueryInterface必须满足可传递性,即如果从A接口能查询到B接口,从B接口能查询到C接口,那么也能从A接口能查询到C接口。
<3>、QueryInterface必须满足自反性,即可以通过A接口查询到A接口。
总结:通过该函数,客户可以向组件提问是否支持某一个接口,组件通过该函数的返回值回答客户是否支持指定的接口,从而达到运行时查询COM对象所支持接口的功能。
(2)、AddRef和Release函数实现组件的生命期控制。
由于一个组件的不同接口可以被客户的不同接口指针访问,这就会有一个何时可以安全释放组件的问题。若组件的生命周期由使用它的客户控制,那么当客户的一个接口指针不再使用该接口指针所指向的组件时,它就会释放掉该组件,如果此时该客户的其他接口指针仍然在使用该组件的其它接口。那么就会产生问题。由于很难决定两个接口指针是否指向同一个组件,因此决定何时可以安全的释放掉一个组件将是非常复杂的。解决这种问题最简单的一种解决方法为在程序运行期间一直加载组件,这种方法的效率非常低非常。
一种好的解决方法是告诉组件何时开始使用组件接口何时用完接口,而不是释放接口。一般使用组件的客户知道开始使用接口和用完接口的准确时间。AddRef和Release就是给客户提供一种指示何时开始使用接口何时使用完接口的手段。
AddRef和Release实现的是一种名为引用计数的内存管理计数。
<1>、客户通过调用AddRef,告诉组件开始使用组件的某一接口。
<2>、客户通过调用Release,告诉组件不再使用组件的某一接口。
总结:引用计数是使组件能够自己将自己删除的最简单同时也是效率最高的方法。
AddRef的调用时机为:指针赋值时需要调用AddRef函数。
总结:
COM规范规定了客户和组件之间的基本接口IUnknown和IClassFactory。
IUnknown接口的函数可以使客户对接口进行完全的控制。
QueryInterface可以获取组件所支持的函数。
AddRef和Release可以控制组件的生命周期。