一、COM编程基础知识介绍:
1、COM的定义
所谓COM(Componet Object Model)即组件对象模型,是一种说明如何建立可动态互变组件的规范,此规范提供了为保证能够互操作,客户和组件应遵循的一些二进制和网络标准。通过这种标准将可以在任意两个组件之间进行通信而不用考虑其所处的操作环境是否相同、使用的开发语言是否一致、以及是否运行于同一台计算机。因此呢,COM客户端和COM服务器就出现了。
COM的优点:
1>、COM对象可跨进程和跨计算机运行
2>、COM方法可通过网络调用
3>、COM服务器可以使用多种不同的语言和在不同的操作系统上编写,
4>、COM可以跨进程运行(在另一个进程内或者另一个进程上)
5>、COM方法可以通过网络调用(必须要有相应的权限,并且那台机器上已经进行了正确的设置。)
6>、COM最重要的一条规定是你只可通过接口来访问一个COM对象。通过接口,客户端的程序与服务器的执行完全隔离开来。
2、COM规范
显然,在COM规范下将能够以高度灵活的编程手段来开发、维护应用程序。可以将一个单独的复杂程序划分为多个独立的模块进行开发,这里的每一个独立模块都是一个自给自足的组件,可以采取不同的开发语言去设计每一个组件。在运行时将这些组件通过接口组装起来以形成所需要的应用程序。构成应用程序的每一个组件都可以在不影响其他组件的前提下被升级。这里所说的组件是特指在二进制级别上进行集成和重用而能够被独立生产获得和配置的软件单元。
COM规范所描述的即是如何编写组件,遵循COM标准的任何一个组件都是可以被用来组合成应用程序的。至于对组件采取的是何种编程语言则是无关紧要的,可以自由选取。作为一个真正意义上的组件,应具备如下特征:
1>、实现了对开发语言的封装。
2>、以二进制形式发布。
3>、能够在不妨碍已有用户的情况下被升级。
4>、在网络上的位置必须能够被透明的重新分配。
3、COM组件和DLL组件的联系和区别
COM组件的特征使的COM组件具有很好的可重用性,这种可重用性与DLL一样都是建立在二进制基础上的代码重用。但是COM在多个方面的表现均要比DLL的重用方式好的多。
在DLL组件中存在缺陷如:函数重名问题、各编译器对C++函数名称修饰的不兼容问题、路径问题以及与可执行程序的依赖性问题等。在COM中通过使用虚函数表、查找注册表等手段均被很好的解决。
其实COM组件在发布形式上本身就包扩DLL,只不过通过制订复杂的COM规范,使COM本身的机制改变了重用的方法,能够以一种新的方法来利用DLL并克服DLL本身所固有的一些缺陷,从而实现了更高层次的重用,如: 一个动态链接库(DLL)中的COM对象(进程内服务器)。
一个进程内的服务器就是一个会在运行时载入到你程序中的COM类,就是一个动态链接库(DLL)中的COM对象。用传统的观点来看,一个DLL并不是一个真正的服务器,因为它会直接载入到客户的地址空间中。通常在调用LoadLibrary()时,DLL就会被载入。在COM中,你无需显式调用LoadLibrary()。在客户端的程序调用CoCreateInstance()时,所有的处理都会自动启动。CoCreateInstance需要的其中一个参数是你要使用的COM类的GUID。当服务器在编译时创建时,它就会登记了所有它支持的COM对象。当客户端需要该对象时,COM找到服务器DLL,并且自动装载它。一旦载入,DLL就拥有了创建COM对象的类库。
CoCreateInstance() 返回一个指向COM对象的指针,它是用来调用方法的。COM的一个便利之处是DLL可以在不需要的时候被自动卸载。在对象被释放和CoUninitialize()被调用后,FreeLibrary()将会被调用来卸载服务器DLL。有经验的编程者会将所有他们的DLL都写成为进程内的COM服务器。COM处理所有涉及载入、卸载的杂事,而输出DLL函数和COM函数调用只有很少的系统开销。
4、进程内COM服务器的优点和缺点
进程内的COM服务器有优点也有缺点。如果动态链接是你的系统设计中的重要一环,那么你将发现COM可提供一个极好的方式来管理DLL。选择一个进程内服务器更简单,不必关心如何启动远程的服务器(EXE或者服务),因为我们的服务器将会在需要的时候自动载入。也无需建立一个proxy/stub DLL来做marshalling的工作。
缺点是,由于进程内的服务器与我们的客户绑定很紧密,因此COM许多重要的“分布”特性没有展现出来。一个DLL服务器和它的客户共享内存,而一个分布的服务器将令客户端更加隔离开来。在一个分布的客户和服务器间传送数据的处理被称为marshaling。marshaling在COM上的利用是受到限制的,而在进程内的服务器中无需关心这些。
5、COM组件中涉及到的各种ID 的含义
1>、GUID(globally unique identifier) 全球唯一标识符(128位):完成对象的创建和初始化工作。GUID的作用是:对COM对象进行标识,保证对该对象标识的全球唯一性,因此若用人工构造此GUID将存在与已有COM对象的GUID发生冲突的可能。用COM库提供的CoCreateGuid产生(API函数)。
2>、CLSID(class identifier)类标识符:ClSID是GUID一个具体的类型的名称,代表COM对象的类别,代表COM对象的CoClass。
3>、ProgID(program identifier)程序标识符:除了CLSID可以唯一标识一个COM对象外,也支持通过组件对象名对COM对象的标识。此标识信息称为ProgID,有意义的字符串。
4>、IID(interface identifier)接口标识符:来用标识COM接口的。
5>、APPID(Application Indentifier): 应用程序ID;
6>、CATID(Category Identifier): COM组件实现的组件类型;
7>、LIBID(Library Identifier): COM对象实现的Type Library(类型库)代表的ID;
(类型库:在类型库中以二进制的形式描述了一个或多个对象的类型信息。使用ClassWizard时,将可以根据类型库中的描述信息建立相应的包装类。通过此包装类将能很方便的在客户程序中使用组件对象提供的属性和方法。在组件程序中,类型库的创建是根据.odl文件中的描述进行编译的。)
6、COM组件在注册表中
每一个注册了的COM对象在系统注册表的HKEY_CLASSES_ROOT\CLSID子键下均对应一个以CLSID的字符串形式命名的子键。在此子键下,通过COM库可以得到所需要的信息并完成对象的创建。在Windows环境下,除了CLSID可以唯一标识一个COM对象外,也支持通过组件对象名ProgID对COM对象的标识。通常在以CLSID的字符串形式命名的子键下存在有ProgID子键,而在HKEY_CLASSES_ROOT键下可以找到以此子键键值命名的子键,该子键下亦包含有CLSID子键,通过ProgID子键的CLSID值和CLSID子键的ProgID值可以将CLSID与ProgID建立起联系。在程序中也可以通过CLSIDFromProgID()和ProgIDFromCLSID()进行相互转换。
6、客户程序与COM组件的交互
客户程序在与COM组件进行交互时,只需知道与哪个COM对象进行交互即可,而不必关心组件模块的具体名称和位置,即COM对象的位置对客户是透明的。客户将通过GUID完成对象的创建和初始化工作。对于COM对象,此全局标识符也被称作CLSID。此全局标识符是根据一定的算法产生出唯一的GUID值。
二、COM接口的定义:
1、接口和抽象类的联系和区别:
抽象基类:只包含一个或多个虚函数声明而未包括虚函数的具体实现的类。抽象基类不能被实例化,而只能用作基类使用,并要求其派生类完成其所有虚函数的实现。
接口 :被申明为interface类型,只有方法的声明,也没有实现功能。
一般情况下,接口名从字母I开始,类类型名从T开始。抽象基类本身由于没有实体函数与变量,所以并不分配内存。通常只是用来为派生类指定内存结构。只有在派生类实现此抽象基类时,指定的内存才会被分配。
对于接口,通常是采用抽象基类来定义,并利用类的多重继承来实现该组件。实现了接口的类,都有一个指向该类的的虚拟函数表的指针。该指针存放于所有的数据成员之前,接口的方法在虚拟函数表中有唯一的索引,编译器只需根据索引从虚拟函数表中找到函数地址即可。这样子的话,客户只要获取得到了接口指针,就可以使用此COM对象的实际功能。
虚拟函数表:能够为实例数据提供一个方便保存的位置,并能够在同一类的多个实例间共享。
2、COM接口的介绍:
COM接口是COM规范中最重要的部分,COM规范的核心内容就是对接口的定义,甚至可以说“在COM中接口就是一切”。组件与组件之间、组件与客户之间都要通过接口进行交互。接口成员函数将负责为客户或其他组件提供服务。与标识COM对象的CLSID类似,由IID进行标识。
COM接口实际限定了组件与使用该组件的客户程序或其他组件所能进行的交互方式,任何一个具备相同接口的组件都可对此组件进行替换,而不影响其他组件。只要接口不发生变化,就可以在不影响系统的情况下自由的更换组件。因此,在程序设计阶段需要将接口设计的尽可能完美,以减少在开发阶段对COM接口的更改。尽管如此,在实际应用中是很难做到这一点的,往往需要在现有接口基础上对其做进一步的发展。与C++中对类的继承有些类似,对COM接口的发展也可以通过接口继承来实现。
注意: COM接口的继承只能是单继承而不允许从多个基接口进行派生,而且派生接口只是继承了对基接口成员函数的说明而没有继承其实现。
3、定义COM接口需要遵循的规则:
由抽象基类指定的内存结构是符合COM规范的,因此抽象基类可以认为是一个COM接口,但这还不是一个严格意义上的COM接口。对于一个真正意义上的COM接口,在设计时还有以下要求:
1) 接口必须直接或间接地从IUnknown继承。
2) 接口必须具有唯一的标识(IID)。
3) 一旦分配和公布了IID,有关接口定义的任何因素都不能被改变。
4) 接口成员函数应具有HRESULT类型的返回值。
5) 接口成员函数的字符串参数应采用Unicode类型。
6) 不能创建接口实例。
7) 不能在接口中指定范围指示。所有的方法都是公有型(public),不能在接口中申明包括范围指示。
8) 不能声明变量;接口只能决定提供什么样的功能,对于如何完成功能没有限制。
9) 接口中声明的所有函数和过程,概念上都是虚抽象函数和过程;因此生明时不能带Virtual。
10)接口是不变的。
这几条规则中,最基本的是第一条,如果一个对象没有至少实现一个最小程度为IUnknown的接口,那么该对象也就不是一个严格的COM对象。IUnknown接口是COM的核心接口,从上述规则可以得知,任何一个COM接口都必须从IUnknown接口继承。客户在组件之间的通信是通过接口来实现的。组件可以不提供其他接口,但是必须提供IUnknown接口以使客户能够对组件其他接口进行查询。
三、COM接口的实现:
1、申明一个接口
1>、CreateComObject函数:
function CreateComObject(const ClassID: TGUID): IUnknown;
在内存中创建一个进程内COM对象的实体,并返回样例的IUnknown接口(也可以使用CreateInstance得到IUnknown接口指针);
2>、CLSIDFromProgID和ProgIDFromCLSID函数:实现ProgID值和CLSID值的相互转换。
例如:应用程序在内存中建立了COM对象的实例;
var
iRoot: IUnknown;
begin
// 建立了COM对象的实体样例,并返回样例的IUnknown接口。
iRoot := CreateComObject(ProgIDToClassID('Project1.Interface1'));
end;
2、 实现接口(IUnknown接口)
IUnknown接口提供有成员函数QueryInterface()、_AddRef()和_Rlease(),分别用于查询组件中的其他接口(及判断对象是否支持一个接口)和进行生存期控制。由于任何COM接口都是从IUnknown接口派生,因此在所有COM接口虚拟函数表中保存的前三个成员函数指针一定是指向QueryInterface()、AddRef()和Release()的指针。这样,任何一个COM接口都可以被当作IUnknown接口来处理。在创建接口的时候,可以使用TInterfaceObject来自动实现Iunknown,否则的话自己要实现上面的方法。
1>、QueryInterface成员函数:查询组件是否支持某个特定的接口
HRESULT QueryInterface( REFIID iid, void ** ppvObject );
参数说明:
iid: 为一个IID结构,指出了客户所要查询的接口
ppvObject: 查询到的接口指针将存放在ppv所指向的变量中。
HRESULT: 函数的成功执行与否将返回S_OK或E_NOINTERFACE。
COM规范允许使用多接口,QueryInterface()成员函数可以用来查询组件是否支持某个特定的接口。如果支持,QueryInterface()将返回此接口的指针。但是,在使用时不能简单的将QueryInterface函数的返回值与其进行比较,而应使用SUCCEEDED或FAILED宏(SUCCEEDED(hResult))。例如:(C++码)
IUnknow* pI = CreateInstance(); // 得到IUnknown接口指针。
IX* pIX = NULL;
HRESULT hResult = pI->QueryInterface(IID_IX, (void**)&pIX);
if (SUCCEEDED(hResult))
pIX->Func1();
COM对象: TCOMObject继承(TInterfacedObject不提供实现COM对象的必要功能)
Hresult: 特殊类型的返回值,意味着函数调用成功还是失败。
OleCheck: 检查函数调用可能产生的错误;当调用返回HResult的COM函数时应使用该函数;
Safecall: 调用协议可使编码;指示Delphi自动把所有方法包括在try...except模块中;
在客户端safecall导致客户检查是否有HResult类型的返回失败码;
注意事项:
由于QueryInterface函数过于灵活,为避免由此引发的冲突在COM规范中定义了QueryInterface函数所有实现都必须遵循的一些规则:
1)IUnknow接口的唯一性:通过同一对象各个接口指针所查询得到的IUnknown接口指针必须是指向同一个IUnknown接口的。
2)接口与查询时间的无关性:如果某接口曾经被成功查询过,那么此后任何时间对该接口的查询也必定会成功。
3)接口的自反性: 对于已经获取到的接口仍可对其进行再次查询,并且必定会成功。
4)接口的对称性:客户能够从任何接口查询到另外一个接口,而且能够返回到起始接口。
5)接口的传递性:如果能够从某接口获取到某特定接口,那么从任意接口都可以得到此接口。
2>、AddRef成员函数和Release成员函数:对对象的生存期进行了控制
IUnknown接口的另两个成员函数AddRef()和Release()对对象的生存期进行了控制。每个COM对象都记录有一个引用计数,该引用计数表示了当前引用了此COM对象的有效指针的个数。AddRef()和Release()实现的即是这种引用计数的内存管理技术:引用计数初始为0,客户每得到一个指向此对象的接口指针即通过AddRef()将引用计数加1;在每用完此接口指针后,调用Release()函数将引用计数减1。如果引用计数减到0,则从内存卸载掉此COM对象。
注意事项:关于引用计数的使用,在COM规范中也设置了以下几条简单的规则:
1) 任何能够返回接口指针的函数(如QueryInterface()、CreateInstance()等)在返回接口指针之前,必须用相应的指针调用AddRef()函数。
2) 在使用完任何一个接口后,应及时调用该接口的Release()函数。
3) 在进行接口指针赋值操作后,应调用AddRef()函数。
3、高级多级接口问题
在一个类中实现多个接口,多个接口并不是多重继承。其有且只有一个基类TInterfacedObject;
例如:
TXY = class(TInterfacedObject, IX, IY): //类TXY 实现了IX和IY接口的所有方法。
procedure IXX.pxy = pxy1 ;
procedure IYY.pxy = pxy2 ; // 当接口方法在类中实现时,可以改变它的名称
几个函数的介绍:
接口授权:一个接口的实现授权给另一个类:一个类包含针对另一个类的指针。
内部类: 实现一个或多个接口的功能性;
外部类: 简单的将这些方法传递给内部类,而不是重新实现接口;
接口属性:可以定义只读、只写、或者读写属性;
但是所有访问都必须通过访问函数,因为接口不能定义存储。
#include "..\BeepServer\BeepServer.h" // 进程内的COM服务器。 else
// 该函数拆除了HRESULT的值,并且打印出它的所有成员,包括ErrorMessage。 BOOL ShowStatus(HRESULT hr) _com_error e(hr); }
|