COM 是什么?
COM 是一个说明如何建立可动态互变组件的规范。它提供了为保证能够互操作, 客户和组件应遵循的一些标准。
COM 组件是什么:COM 组件是以Win32 动态链接库(DLLs )或可执行文件( EXEs ) 的形式发布的可执行代码组成的。遵循COM 规范编写的组件将能够满足对组件架构的所有需求。COM 组件是动态链接的。COM 使用DLL 将组件动态链接起来。但动态链接本身并不能满足对于组件架构的需求。为满足这些需求, 组件还必须是封装的。COM 组件按照一种标准的方式来宣布它们的存在。使用COM 的发布方案, 客户可以动态地找到它所需的组件。COM 组件是一种给其他应用程序提供面向对象的API 或服务的极好方法。对于可用于快速构造应用程序、与语言无关的组件库的建立, COM 组件也是不在话下。
COM 库
COM 具有一个被称作是COM 库(COM Library) 的API , 它提供的是对所有客户及组件都非常有用的组件管理服务。
COM 方法
COM 最值得称道的地方也许就是我们可以将其作为一种编写程序的方法。例如, 可以在任何操作系统上使用任何编程语言按COM 风格进行编程。
COM 的优点:
- COM架构下,人们可以开发出各种功能专一的组件,然后将它们按照需要“搭”起来,构成复杂的应用系统。
- 组件独立可方便的替换升级
- 可以在多个系统中重复利用同一个组件
- 可以方便的将应用系统扩展到网络环境下
- COM 与语言、平台无关的特性,是的所有的Coder 编写出组件模块
组件的概念:组件实际是一些小的二进制可执行程序,它可以给应用程序、操作系统以及其它组件提供一些服务。开发定制的COM 组件就如同开发动态的、面向对象的API。
组件可以在运行时、在不重新链接或编译应用程序的情况下被卸下或替换掉。
组件
组件的优点
1. 应用程序定制
2. 快速应用程序开发,开发人员从某个组件库中取出所需的组件并将其快速组装到一块以构造所需的应用程序,如同搭积木块一样。
3. 分布式组件
为了实现上面的优点,我们需要怎么做
1. 动态链接
否则每次升级、更换组件,都需要重新编译链接整个项目
2. 信息封装
组件和使用它的客户,都应该尽量避免改变它们之间的调用接口。
这种将客户同组件实现相应隔离开来的要求对于组件加上了一些限制:
- 组件必须将其实现所用的编程语言封装起来
- 组件必须以二进制的形式发布
- 组件可以在不妨碍已有客户的情况下被升级
- 组件在网络上的位置必须可以被透明地重新分配
语言无关
在一个与语言无关地架构中,任何人均可编写组件,并且这些组件不会因为编程语言的发展而过时。这种架构将大大的促进软件市场的繁荣。
版本
强行改变某个组件的功能以使之适应新应用程序的需要、同时使其能支持老的应用程序仍应该是可能的。
接口
接口的作用
COM 中接口比实现接口的组件更为重要
可复用应用程序架构
设计可复用的结构要求设计者应具有预测未来的能力。
接口的其它优点
1. 接口可以保护系统免收外界变化的影响
2. 接口使得客户可以用同样的方式来处理不同的组件
COM 接口的实现
COM 与语言无关,对于什么是接口,它有一个二进制的标准。表示一个接口的内存块必须具有一定的结构。当使用纯抽象基类时,许多C++ 编译器将可以生成具有这种结构的内存块。
实际的接口,必须继承IUnknown 接口。
编码约定
#include <objbase.h>
#define interface struct
之所以使用struct,是因为struct 成员自动具有公有的属性
一个完整的例子
- COM 接口在C++ 中是用纯抽象基类实现的
- 一个COM 组件可以提供多个接口
- 一个C++ 类可以使用多重继承来实现一个可以提供多个接口的组件
调用约定使用的是__stdcall
1)参数从右向左压入堆栈,2)函数自身修改堆栈
参数数目可变的函数使用的是C 调用约定。
非接口通信
当客户和组件在动态链接的情况下,需要隔离实现和使用,此时,链接它们的只能是接口。另外,new 和 delete 不应该暴露给客户使用,因为new 和 delete 是C++ 相关的,我们需要一个统一的管理对象生命周期的方法。
实现细节
1. 类并非组件,只是使用类来实现组件比较方便,只要生成的二进制文件符合格式就行
2. 接口并非总是继承的,对接口的继承只不过是一个实现细节,除了可以使用一个类来实现几个不同的接口外,还可以用单个的类来实现每一个接口,再使用指向这些类的指针。
3. 多重接口及多重继承
4. 命名冲突,COM 不关心这个,COM 只关心接口,内部想办法解决这个问题即可。
接口理论:第二部分
接口的不变性
多态
一个组件所支持的接口越多,这些接口就应该越小。较小的接口表示较为简单的行为,而大的接口则表示更多的行为。一个接口所表示的行为越多,它的特定性越强,因此它被其它组件服用的可能性将越小。
开发组件软件的一个最大的问题是如何设计出具有高复用性、适应性、灵活性的接口,并考虑到将来可能会出现的新情况。
接口的背后
虚拟函数表
当定义一个纯抽象基类时,所定义的实际上是一个内存块的结构。纯抽象基类所有实现都是一些具有相同的基本结构的内存块。
Interface IX
{
virtual void — stdcall Fx1() = 0 ;
virtual void — stdcall Fx2() = 0 ;
virtual void — stdcall Fx3() = 0 ;
virtual void — stdcall Fx4() = 0 ;
};
另外,所有的COM 接口都必须继承一个名为IUnknown 的接口,这意味着所有COM 接口的前三项都是相同的。
多重实例
int main() {
// Create first instance of CA .
CA * pA1 = new CA(1 .5) ;
// Create second instance of CA .
CA * pA2 = new CA(2 .75) ;
…
}
不同的类,相同的vtbl
IUnknown 与 QueryInterface
interface IUnknown {
virtual HRESULT __stdcall QueryInterface(const IID& iid,void * * ppv) = 0 ;
virtual ULONG __stdcall AddRef() = 0 ;
virtual ULONG __stdcall Release() = 0 ;
} ;
IUnknown 指针的获取
我们在导出接口的时候,不应该导出接口相关的构造函数,应该有统一的构造函数
IUnknown* CreateInstance();
关于 QueryInterface
IUnknown 中包含一个名为QueryInterface 的成员函数,通过此来查询某个组件是否支持某个特定的接口。支持,则QueryInterface 将返回一个指向此接口的指针,否则返回值将是一个错误代码,然后客户可以接着查询其它接口或将组件卸载。
HRESULT __stdcall QueryInterface(const IDD& idd,void** ppv);
HRESULT 是一个具有特定结构的32 位值,应该使用SUCCEEDED 宏或FAILED 宏。
QueryInterface 的使用
void foo(IUnknown * pI) {
// Define a pointer for the interface .
IX * pIX = NULL ;
// Ask for interface IX .
HRESULT hr = pI - > QueryInterface(IID- IX, (void * * ) &pIX) ;
// Check return value .
if (SUCCEEDED(hr))
{
// Use interface .
pIX - > Fx();
}
}
QueryInterface 的实现
非虚拟继承
注意IUnknown 并不是虚拟基类。IX 和IY 并不能按虚拟方式继承IUnknown , 这是由于会导致与COM 不兼容的vtbl。若IX 和IY 按虚拟方式继承IUnknown , 那么IX和IY 的vtbl 中的头三个函数指向的将不是IUnknown 的三个成员函数。
HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
trace("QueryInterface: Return pointer to IUnknown.") ;
*ppv = static_cast<IX*>(this) ;
}
else if (iid == IID_IX)
{
trace("QueryInterface: Return pointer to IX.") ;
*ppv = static_cast<IX*>(this) ;
}
else if (iid == IID_IY)
{
trace("QueryInterface: Return pointer to IY.") ;
*ppv = static_cast<IY*>(this) ;
}
else
{
trace("QueryInterface: Interface not supported.") ;
*ppv = NULL ;
return E_NOINTERFACE ;
}
reinterpret_cast<IUnknown*>(*ppv)->AddRef() ; // See Chapter 4.
return S_OK ;
}
关于类型转换
关于QueryInterface 的实现规则
- QueryInterface 返回的总是同一IUnknown 指针
- 若客户曾经获取过某个接口,那么它将总能获取此接口
- 客户可以再次获取已经拥有的接口
- 客户可以返回到起始接口
- 若能够从某个接口获取某特定接口,那么可以从任意接口都将可以获取此接口
第一条规则用于判断两个接口是否属于同一个组件
QueryInterface 定义了组件
接口集
COM 提供了一个名为类型库的手段,供客户在运行时确定组件所提供的接口。
在许多情况下,客户可以使用只实现某个特定接口集的组件,创建一个组件,然后一个个查询其组件,以最终找出此组件是否支持某个所需的接口是非常浪费时间的。为节省时间开销,某个特定的接口集可以用一个组件类别来标识。各组件可以声明它是否属于某个特定的组件类别。客户可以在不创建组件的情况下获取此种信息。
新版本组件的处理
何时需要建立一个新版本
- 接口中函数的数目。
- 接口中函数的顺序。
- 某个函数的参数。
- 某个函数参数的顺序。
- 某个函数参数的类型。
- 函数可能的返回值。
- 函数返回值的类型。
- 函数参数的含义
- 接口中函数的含义
不同版本接口的命名
老名称的后面加上一个数字。对于用户自己建立的接口,可以按照自己的喜好指定名称。若老接口是其它人建立的,那么在建立其新版本或指定一个新名称之前应该先询问别人的意见。
隐含合约
隐含合约:客户使用接口中函数的方式定义了它同实现此接口的组件之间的合约。
为了避免违反隐含合约,可以使用两种方法
- 使得接口不论在其成员函数怎么被调用都能正常工作
强制客户按一定的方式来使用此接口并在文档中将这一点说明清楚。比如说,先初始化才能调用之后的一些方法。