1 COM技术体系

1.1 COM的架构

  COM(Component Object Model,组件对象模型) 是由微软公司制定的一种Windows平台下的软件模块复用技术。借助于COM技术,用户可以编写一些具有特定接口的软件模块(称为COM组件 ),它们可以以dll或exe的形式注册到Windows系统中来对外公开自己,其它的应用程序则可以借助于Windows提供的API来调用这些组件,从而在整个系统范围内实现了软件模块的复用。
  COM技术是基于C/S架构的,其如图1-1所示。架构中的服务端就是注册到Windows系统的COM组件,它们是为其他应用程序提供服务的。架构中的客户端就是调用COM组件的应用程序,它可以是dll,可以是exe,可以是C++编写的,也可以是Java编写的,总而言之COM客户端可以是任意一种可运行在Windows平台的可执行程序,它们通过COM组件提供的服务来实现自身的功能。架构中的通信协议就是COM客户端与COM组件之间的通信方法(也即如何开发COM服务端,以及COM客户端如何调用COM组件),这其中主要包括COM组件的开发规范以及一组包含在Windows SDK中的COM API函数(COM库函数)。
在这里插入图片描述

1.2 COM DLL

  大多数COM组件在开发完成后都会被编译成DLL的形式注册在操作系统的注册表中(也有的编译成exe),然后COM客户端即可通过一组特定的函数从注册表中搜索该COM DLL的路径,并将其加载到内存中调用它。一个COM DLL通常就称为一个COM组件(实际上这种说法是笼统的,因为可以在COM DLL中定义多个注册到注册表的COM组件,只不过绝大多数时候一个COM DLL只会导出一个COM组件,因此通常笼统的将一个COM DLL称作为一个COM组件)。一个COM组件可以包含多个可被COM客户端调用的类,这些类都是继承自一组标准COM接口的类(这组接口定义在Windows SDK中,详见“二 COM接口类”),并且这些类通常会相互调用,相互依赖而形成一个树形结构的类关系(这种关系是通过COM技术的包容和聚合实现的,详见“三 COM组件类”)。
一个COM DLL中包含的类通常至少包含以下四种:
(1)COM接口类
  每个COM DLL中都包含一组接口类(它们的类名通常以大写字母I开头),每个接口类都是继承自标准COM接口(IUnkonw或IDispatch接口)的C++抽象类,接口中定义了一些public类型的纯虚函数,这些函数会被若干个COM组件类所实现。
  用户调用COM组件,大多数情况下调用的就是COM组件中的COM接口类,更确切的说是定义在COM接口类中的的函数。然而,COM开发规范要求COM接口必须实现为抽象类,因此用户无法直接实例化COM接口类进行调用。但是,COM接口类在COM DLL中是会被COM组件类所实现的,因此用户通常是采用多态的方式调用COM DLL的,其一般调用流程为:创建COM实现类对象—>将对象传递给其所实现的COM接口类—>调用COM接口类中的函数。
(2)COM实现类
  由于COM接口类是抽象的,因此要想让外部能够调用COM DLL中的COM接口,则必须在COM DLL中定义一个或多个类去实现这些COM接口,这些类就是COM实现类(也有某些场合称为COM组件类),由COM实现类创建出来的对象就称为COM对象。通常情况下,在COM DLL中COM接口类和COM实现类都是一对一的,但也有很多COM DLL会将实现类和接口类组织成多继承或钻石继承的模式。但对调用COM DLL的用户来说,其并不需要关心COM实现类,而只需要关心COM DLL提供了哪些可以调用的COM接口,以及这些COM接口相互之间的依赖关系即可。
(3)COM 工厂类
  正如之前所说,用户调用COM DLL通常是先创建COM实现类对象,然后将对象转型为COM接口类对象进行调用。然而COM实现类并不能像普通C++类那样直接采用new或声明式的方法去创建其对象,而应调用Windows SDK中提供的一组特殊的函数去创建它们的对象(这组函数的用法详见“六 COM库函数”)。
  用户在调用COM库函数创建COM实现类对象时,COM库函数实际上会先调用COM DLL的导出函数创建其所包含的COM工厂类的对象,然后通过它来创建COM实现类的对象并将其返回给用户,也就是说COM对象的创建实际上是在COM DLL的工厂类中完成的,而不是在调用COM DLL的应用程序中完成的。
  COM工厂类是一种继承自标准COM接口IClassFactory的类,COM工厂类类似于工厂设计模式中的工厂类,其主要用于创建COM DLL中所包含的COM实现类的对象。通常情况下,COM DLL中的每个COM实现类都需要定义一个与之对应的工厂类,但也可以只在COM DLL中定义一个工厂类,然后在这个工厂类中去创建所有COM实现类的对象(工厂类用法详见“四 COM工厂类”)。
(4)COM导出函数
  COM导出函数是COM DLL导出给外部调用的一组DLL导出函数,这些函数用于注册/卸载COM组件,以及返回COM工厂类的实例等。COM导出函数都具有固定的名称和参数,其主要包括:组件入口函数、组件注册函数、组件卸载函数、组件工厂类获取函数(COM导出函数用法详见“五 COM导出函数”)。
  一个简单的COM DLL所包含的类实现如下:

//COM接口类
class IMyComInterface:public IUnknow
{
public:
	virtual void func()=0;
}

//COM实现类
class MyComInterface:public IMyComInterface
{
public:
	@override;
	STDMETHODIMP QueryInterface(REFIID riid, void **ppv);		  
	@override;	
	void func()
	{
		cout<<"I'm implementation class of IMyComInterface";
	};
}

//COM工厂类
class MyComInterfaceFactory:public IClassFactory
{
public:
	@override;
	STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
	@override;
	HRESULT CreateInstance(LPUNKNOW pUnk,REFIID riid, void **ppv); 
	@override;
	HRESULT LockServer(Bool fLock);
}

//COM导出函数
DllCanUnloadNow()
DllGetClassObject()
DllRegisterServer()
DllUnregisterServer()

1.3 COM库函数

  COM库函数包含在Windows SDK提供的一个用于调用COM组件的函数库中,其包括GUID查询和转换函数、COM组件对象创建函数等等(COM库函数用法详见“六 COM库函数”)。
  如下演示了在C++环境中使用COM库函数创建并调用COM组件的基本过程。

#include <Windows.h>
int main()
{
	//初始化COM库
	CoInitialize(NULL)
	
	//创建COM对象
	IMyComInterface* pMyComInterface;
    CoCreateInstance(CLSID_MyComInterface, NULL, CLSCTX_INPROC_SERVER, IID_MyComInterface,&pMyComInterface);
	
	//调用COM对象
	pMyComInterface->func();
	
	//卸载COM库
	CoUnInitialize(NULL)
}

1.4 COM客户端

  COM客户端是调用COM组件的一端,COM客户端在调用COM组件时首先需要借助COM库函数的API创建相应的COM组件对象,然后将其转型为相应的COM接口类的对象,然后再调用COM接口类中的函数来实现想要的功能。
  COM客户端与COM组件是环境隔离的,所谓环境隔离是指COM客户端与COM组件之间不存在任何环境依赖,两者的开发语言可以不同,两者所用编译器也可以不同,两者可执行文件的形式都可以不同,例如可以在C++中调用Java编写的COM组件等等。
  

2 COM技术特点

  COM是实现面向组件编程以及构建可复用软件的一种有效的手段,这主要源于COM技术的以下几个特点:

2.1 平台语言无关性

  COM实际上并不局限于Windows平台以及C++语言,因为COM它只是制定了一套通用的组件开发规范,这些规范并没有与具体的语言和平台进行绑定,Windows下的COM只是此标准的一种实现。
  COM是一套跨平台以及跨语言的组件开发规范,不管用户使用何种语言开发组件,也不管该组件运行在何种平台,只要开发的组件符合COM规范的要求,那么其它的应用程序就能够基于COM规范来正确的调用它。

2.2 位置透明性

  在Windows系统上,COM组件通过在注册表记录自身可执行文件的位置以及CLSID来向用户公布它的存在。用户在调用COM组件时只需向COM库函数传入适当的CLSID或PROGID即可,并不需要知道其可执行文件的位置。这使得COM组件的可执行文件既可以存于本地,也可以存在于远程(这种COM组件为分布式COM组件,简称DCOM)。同时还能够随意的调动位置而不影响客户程序的使用(每次调动位置时只需要在系统中重新注册一下即可)。

2.3 版本兼容性

  COM组件中的每个接口都有一个固定的ID(IID)。用户在调用组件接口的某个函数时首先需要向IUnknow::QueryInterface()传入合适的IID来获取该组件的接口实例,这使得COM组件实现版本向后兼容变得十分简单:升级COM组件时,不改变原有已经定义的任何接口类,而是增加新的接口类来实现新的功能。这样一来,需要使用新版COM组件接口的用户直接向IUnknow::QueryInterface()传入新接口的IID即可,而原有使用旧版COM组件接口的用户仍能使用旧接口的IID正常运行。

2.4 即插即用性

  COM组件是以动态链接的方式加载运行的,客户程序可以在运行过程中动态的对其载入或卸出。

2.5 信息封装性

  COM组件是以COM接口或IDispatch接口向外提供服务的,组件的实现完全隐藏在了COM实现类中。这使得一方面COM客户端和COM服务端能够完全隔离开来,另一方面这使得客户端能够与服务端采用一套通用的接口进行通信。这种特性在保证了组件开发灵活性的前提下,又极大的方便了客户端的调用。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐