组件对象模型

发布日期:2025-07-22 16:50:26 分类:beat365官网在线体育 浏览:4893

不同的COM组件类型用类ID(CLSID)标示,这是一种全局唯一标识符(GUID)。每个COM组件用一个或多个接口来暴露其功能。这些接口也采用GUID唯一标识,称为接口ID(IID)。

COM接口与几种编程语言有语言绑定,如C语言、C++、Visual Basic、Delphi语言、Python[4][5]以及Windows平台上的几种脚本语言。它们都是通过接口的方法来访问组件。

接口

编辑

所有COM组件都实现了IUnknown接口,该接口暴露了引用计数实现的对象生命期管理与类型转换,以访问不同的预定义接口。

IUnknown接口以及基于IUnknown的定制接口包括一个指向虚函数表(英语:virtual method table)的指针,虚函数表中包含若干函数指针,分别指向接口所声明的函数实现。对于进程内的COM组件调用,其效率等同于C++的虚函数调用。

除了基于IUnknown的定制接口,COM也支持继承自IDispatch的dispatch接口,从而支持了用于OLE自动化(英语:OLE Automation)的晚绑定(英语:late binding)。不能访问定制接口的编程语言(例如VBS)可以通过dispatch接口访问COM组件。

Windows API提供了C语言定义COM接口的方法:

#include

#undef INTERFACE

#define INTERFACE IClassFactory

DECLARE_INTERFACE_(IClassFactory, IUnknown)

{

// *** IUnknown methods ***

STDMETHOD(QueryInterface) (THIS_

REFIID riid,

LPVOID FAR* ppvObj) PURE;

STDMETHOD_(ULONG,AddRef) (THIS) PURE;

STDMETHOD_(ULONG,Release) (THIS) PURE;

// *** IClassFactory methods ***

STDMETHOD(CreateInstance) (THIS_

LPUNKNOWN pUnkOuter,

REFIID riid,

LPVOID FAR* ppvObject) PURE;

};

// 等效的C++例子:

struct FAR IClassFactory : public IUnknown

{

virtual HRESULT STDMETHODCALLTYPE QueryInterface(

IID FAR& riid,

LPVOID FAR* ppvObj) = 0;

virtual HRESULT STDMETHODCALLTYPE AddRef(void) = 0;

virtual HRESULT STDMETHODCALLTYPE Release(void) = 0;

virtual HRESULT STDMETHODCALLTYPE CreateInstance(

LPUNKNOWN pUnkOuter,

IID FAR& riid,

LPVOID FAR* ppvObject) = 0;

};

// C语言宏扩展后是这样的:

typedef struct IClassFactory

{

const struct IClassFactoryVtbl FAR* lpVtbl;

} IClassFactory;

typedef struct IClassFactoryVtbl IClassFactoryVtbl;

struct IClassFactoryVtbl

{

HRESULT (STDMETHODCALLTYPE * QueryInterface) (

IClassFactory FAR* This,

IID FAR* riid,

LPVOID FAR* ppvObj) ;

HRESULT (STDMETHODCALLTYPE * AddRef) (IClassFactory FAR* This) ;

HRESULT (STDMETHODCALLTYPE * Release) (IClassFactory FAR* This) ;

HRESULT (STDMETHODCALLTYPE * CreateInstance) (

IClassFactory FAR* This,

LPUNKNOWN pUnkOuter,

IID FAR* riid,

LPVOID FAR* ppvObject);

HRESULT (STDMETHODCALLTYPE * LockServer) (

IClassFactory FAR* This,

BOOL fLock);

};

编辑

COM类(coclass)是一个或多个接口的具体实现,它很类似面向对象程序设计语言中的类。类的GUID标识被称作类ID(CLSID);或者programmatic identifier字符串(progid),因为VBS等脚本语言不能使用GUID,只能用字符串查找、使用COM组件。

COM对象不能被直接访问,只能通过COM接口来访问对象。COM也支持同一个接口的多个实现,因此客户程序运行时可以选择实例化接口的哪个实现。

接口定义语言与类型库

编辑

类型库(type library)包含着COM类型的元数据。这些类型采用微软接口定义语言(MIDL)描述。

IDL文件定义了类、接口、结构、枚举与其他用户定义类型。IDL类似于C++的声明,使用了一些额外的关键字如interface、library等。IDL还支持在声明前给出方括號属性(bracketed attribute)以提供额外信息,如接口的GUID、指针参数与长度域之间的关系等。

MIDL编译器用来编译IDL文件,产生编译器独立的头文件。头文件包含了IDL文件中声明的接口对应的结构定义。结构只包含一项成员,即指向在接口中声明函数的地址表的指针(vtbl),以模仿C++对虚函数的实现。头文件还包含了类与接口等的GUID的常量的定义。MIDL编译器也可以产生C++源文件,包含代理模块(proxy module),用以把COM调用转为远程过程调用,以支持跨进程的DCOM通信。

IDL文件也可以被MIDL编译器生成类型库(TLB)文件.TLB,以供其他语言编译器与运行时环境使用,如VB、Delphi、.NET等生成语言相关表示COM类型的结构。C++把TLB转回到IDL表示。

#import 类型信息

编辑

使用C++的预编译directive#import ,可以装入如下格式的类型信息:

包含类型库的文件,如.olb、.tlb、.dll等;

progid

libid

exe文件

dll文件包含着类库资源(如.ocx)

复合文档包含了类库

其他可以被LoadTypeLib函数理解的文件

#import创建两个头文件以用C++源码形式恢复类型库信息:

类型库主头文件(.TLH):类似于MIDL编译器产生的头文件,还有一些额外的代码与数据;

类型库次头文件(.TLI):编译器产生的成员函数。该文件被包含在主头文件中。

两个文件被放在输出目录中。编译器在现场就地#include主头文件。

类型库主头文件(.TLH)包含七部分:

头部常规代码

将要用到的结构的前向引用与typedef

智能指针声明:使用宏语句_COM_SMARTPTR_TYPEDEF建立了COM接口的typedef,实际上是_com_ptr_t的模板特化。

Typeinfo声明:类定义、ITypeLib:GetTypeInfo返回的其他Typeinfo项

可选的旧格式的GUID常量定义,形如CLSID_CoClass、IID_Interface

#include类型库次头文件

尾部常规代码:#pragma pack(pop)

从第2至第6部分都包含在命名空间中,其名字在最初的IDL文件的library语句中给出。改名字在#import语句中可用属性no_namespace抑制掉;也可用rename_namespace属性更名。

COM作为对象框架

编辑

COM是一个运行时框架,类型必须在运行时单独地标识并可指定。为此,使用GUID,每个COM类型被指定了它自己的GUID用于运行时标识。这也解决了C/C++语言的名字修饰导致的链接兼容性问题。

为了使COM类型信息在编译时与运行时都可以访问,COM使用类型库。这使得COM成为对象交互的动态框架。

考虑下述用IDL定义coclass的例子:

coclass SomeClass {

[default] interface ISomeInterface;

};

上述代码框架声明了一个COM类,称为SomeClass,实现了接口ISomeInterface。

这在概念是等价于下述C++类:

class SomeClass : public ISomeInterface {

...

...

};

其中ISomeInterface是一个C++虚基类。

包含COM接口与类的IDL文件被编译为类型库(TLB)文件。客户程序可以在运行时分析类型库文件,以确定对象支持哪些接口,然后调用对象的接口方法。

C/C++程序以类ID(CLSID)与接口ID(IID)作为参数,用CoCreateInstance函数实例化COM对象。SomeClass的实例化代码如下:

ISomeInterface* interface_ptr = NULL

HRESULT hr = CoCreateInstance(CLSID_SomeClass, NULL, CLSCTX_ALL,

IID_ISomeInterface, (void**)&interface_ptr);

在这个例子中,使用COM子系统获取指向ISomeInterface接口的实现对象的指针,用CLSID_SomeClass指示用这个特定的coclass。

引用计数

编辑

所有COM对象采用引用计数管理对象的生命期。客户程序通过所有COM对象都要强制实现的IUnknown接口的AddRef与Release方法来控制引用计数。当引用计数降到0时,COM对象自己负责释放内存。即对动态分配内存创建的COM对象,其Release函数内部引用计数降为0时,就释放自身所占的动态分配内存。有的COM对象(如IClassFactory)往往是静态对象,Release函数内部引用计数降为0时不需做额外的操作。

特定语言(例如Visual Basic)提供了自动引用计数,所以COM对象开发者在源代码中不需要显式维护任何内部的引用计数。C/C++编程者或者执行显式的引用计数,或者使用智能指针(如MFC提供的CComPtr)自动管理引用计数[需要解释]。

下述是如何调用COM对象的AddRef与Release的指引:

函数、方法返回接口的引用(通过返回值或者"out"参数),应当在返回前增加被返回的对象的引用计数。

接口指针被覆盖或超出作用域之前,必须调用接口指针的Release方法。

如果一个接口引用指针被复制,必须调用该指针的AddRef方法。

AddRef与Release必须在被引用的相关接口上调用。因为一个COM对象可能实现了逐个接口上的引用计数,使得仅在相关接口上内部分配资源。

不向远程对象发出引用计数的调用。代理模块保持着远程对象的一个引用,并维持着它自己的本地引用计数。

为简化COM开发,引入了活动模板库(Active Template Library,ATL)用于C++开发。ATL提供了更高层次的COM开发范式。ATL也有益于COM客户应用程序开发摆脱直接维护引用计数,而是用智能指针对象。

其他能直接支持COM的库与语言还包括MFC Visual C++编译器的COM支持[6]、VBScript、Visual Basic、ECMAScript(JavaScript)和Borland Delphi等。

程序设计

编辑

COM是一个语言独立的二进制标准,任何能够理解与实现COM的二进制定义的数据类型与接口的语言都可以开发COM组件。

COM实现负责进入、离开COM环境,实例化与引用计数COM对象,查询对象支持的接口,以及错误处理。

Microsoft Visual C++编译器支持对C++语言的扩展:称作C++ Attributes。[7]这些扩展被设计用于简化COM开发,去除实现COM服务器时大量臃肿的代码。[8]

使用注册表

编辑

参见:regsvr32

在Windows操作系统中,COM类、接口、类型库都会根据其GUID登记到Windows注册表。HKEY_CLASSES_ROOT\CLSID下是COM类;HKEY_CLASSES_ROOT\Interface下是接口。COM类型库注册在每个COM对象的本地库条目下或者远程服务的网络位置处。

不使用注册表的COM

编辑

不使用注册表的COM(RegFree COM)是Windows XP引入的技术,允许COM组件不在注册表中存期激活的元数据与类ID(CLSID),而是在实现类的程序集清单或者存储在可执行文件的资源中或组件安装时的单独文件中。[9]这使得同一组件的不同版本可以安装在不同目录下,用其各自的manifest描述,直接复制安装(英语:XCOPY deployment)。[10]这种技术有限支持EXE COM服务器[11]且不能用于系统范围组件如MDAC、MSXML、DirectX或Internet Explorer。

应用程序装入时,Windows装入器搜索manifest。[12]如果存在,装入器从它增加信息到激活上下文。[10]COM类工厂试图实例化一个类时,激活上下文首先检查这个CLSID的实现是否可以找到。仅当查找失败时,才扫描Windows注册表。[10]

进程与网络透明

编辑

COM对象可以透明地实例化与引用在同一进程、跨进程边界、甚至在网上远程(DCOM)。进程外或远程对象用marshalling序列化方法调用与返回值。这种marshalling对用户是不可见的,就如同访问进程内的COM对象。

线程化与“套间”

编辑

一个进程加载了一个COM的DLL文件后,该DLL可能定义并使用了一些可修改的全局变量或访问共享资源。该进程内的多个线程如何并发访问该DLL并保证是线程安全的,这就是“套间”(apartment)技术需要解决的问题。

COM对象与创建或调用COM对象的线程可以按两种策略来实现并发安全:

按照单线程执行方式写COM对象的代码,完全不考虑并发执行问题。这样的每个COM对象只能由一个线程执行,该线程通过Windows消息队列实现多线程访问该COM对象被串行化从而并发安全。这种策略称作单线程套间(Single-Threaded Apartment,STA)。

COM对象的代码自身实现了并发控制(通过Windows互斥原语,如互斥锁、临界区、事件、信号量等)。因此实际上多线程可以直接调用该COM对象的方法,这是并发安全的。这种策略称作多线程套间(Multi-Threaded Apartment,MTA)。

COM的并发安全的具体实现,提出了套间(apartment)概念。每一种套间类型表示在一个进程内部是多线程情况下,如何同步对COM对象的调用。套间是一个逻辑容器,收纳遵循相同线程访问规则的COM对象与COM线程(创建了COM对象的线程或者调用了COM对象的方法的线程)。套间本质上只是一个逻辑概念而非物理实体,没有句柄类型可以引用它,更没有可调用的API操纵它。套间有两种:

单线程套间(Single-Threaded Apartment,STA):每个进程可以有多个STA套间。每个STA套间只能有一个线程。每个STA性质的COM对象只能属于一个STA套间。一个STA套间可以有零个或多个STA属性的COM对象,这些COM对象的方法只能由该套间的唯一线程执行。STA套间的线程可以直接调用该套间的COM对象的方法。如果STA套间的COM对象被套间外的线程或进程调用,那么该套间的线程必须实现Windows消息队列与消息循环处理机制,其他线程必须通过marshalling与unmarshalling机制,通过给该STA套间的线程发送Windows消息来调用COM对象。每个STA性质的线程自动形成一个STA套间,这个套间容纳了该线程及其创建的所有STA性质COM对象。MTA性质的线程创建STA性质的COM对象时,系统自动把该COM对象放在default STA套间内,由该套间的STA线程来执行该COM对象的方法。每个进程至多有一个default STA套间,该套间与套间内线程是自动生成的。

多线程套间(Multi-Threaded Apartment,MTA):每个进程至多有一个MTA套间。所有MTA性质的线程都属于MTA套间。所有MTA性质的COM对象也都属于这个MTA套间。STA性质的线程创建MTA性质的COM对象时,系统自动创建一些线程以执行这些MTA性质的COM对象,这些线程也属于MTA套间,系统返回安整后的COM对象的描述给STA性质的线程。

中立套间(Thread Neutral Apartment,NA):一个进程可以有一个中立套间。中立套间只包含COM对象,不包含线程。当STA或MTA线程调用同一进程的NA对象,则调用线程临时离开它的套间并执行COM对象的代码,没有任何线程切换。即任何线程都可以直接了当调用COM对象的方法。[13]因此NA可以认为是优化套间之间方法调用的效率。

一个COM对象只能存在于一个套间。COM对象一经创建就确定所属套间,并且直到销毁它一直存在于这个套间。COM对象的套间类型写在Windows注册表相关条目中。

一个COM线程从创建到结束都属于同一个套间。COM线程只有两种套间模式:STA或MTA。[14]线程必须通过调用CoInitializeEx()函数并且设定参数为COINIT_APARTMENTTHREADED或者COINIT_MULTITHREADED,来指明该线程的套间模式。调用了CoInitializeEx()函数的线程即已进入套间,直到线程调用CoUninitialize()函数或者自身终止,才会离开套间。COM为每个STA的线程自动创建了一个隐藏窗口,其Windows class是"OleMainThreadWndClass" 。跨套间调用这个STA套间内的COM对象,实际上是向这个隐藏窗口发送了一条窗口消息,通过消息循环与分派,该窗口过程收到这条窗口消息并调用相应的COM对象的接口方法。

线程访问属于同一套间的COM对象,直接执行方法调用而不需COM设施的辅助。线程跨套间边界去调用COM对象,传递的指针需要marshalling。如果通过标准的COM的API来调用,可以自动完成安整。例如,把一个COM接口指针作为参数传递给另外一个套间的COM对象的proxy的情形。但如果软件编程者跨套间传递接口指针而没有使用标准COM机制,就需要手工完成安整(通过CoMarshalInterThreadInterfaceInStream函数)与反安整(通过CoGetInterfaceAndReleaseStream函数获取COM接口的proxy)。例如,把COM接口指针作为线程启动时的参数传递的情形。

跨进程的调用COM对象类似于同一进程内跨套间的调用COM对象。

COM对象coclass在注册表表示中的子键InProcServer32下的条目中ThreadingModel给出:

ThreadingModel的值

描述

Legacy STA(ThreadingModel=Single或空 )

该COM对象属于进程的第一个STA线程,通常是UI界面的线程。这是在过去单核CPU时代没有遗留下来的。

单线程套间[15](STA),(ThreadingModel=Apartment)

一个单独的线程专门用于执行COM对象的方法。如果是STA的COM线程创建了STA的COM对象,这个COM对象的方法就由该线程执行,该线程调用该COM对象是直接调用。如果MTA的COM线程创建了STA的COM对象,系统在当前进程内自动创建一个default STA线程来执行该STA的COM对象的方法,并把COM对象的proxy返回该MTA的线程。COM对象所在STA套间之外的线程调用该COM对象的方法,需要对COM对象的指针先做marshalling再由操作系统自动排队(通过该COM对象被调用方法所在的线程的标准的Microsoft Windows的訊息迴圈)。这提供了自动同步以确保对象的方法每次调用执行完毕后才能启动方法的新的调用。开发者不需要担心线程加锁(locking)或競態條件。如果跨套间调用STA的COM对象,该对象所在STA的线程必须提供线程消息循环处理机制。

多线程套间[16](MTA),(ThreadingModel=Free)

COM运行时不提供同步,多个MTA线程可以同时调用同一个MTA的COM对象,由各个MTA线程直接执行COM对象的方法,且因为在同一个MTA中因此不需要安整。COM对象需要自己实现同步控制以避免多线程同时访问造成的競態條件或死锁。STA的线程创建MTA的COM对象,系统自动创建一个或多个线程来执行MTA的COM对象。STA线程调用MTA的COM对象也需要marshalling,系统自动分配某个自动创建的线程来执行COM对象。MTA的优点是提高了并发处理性能,同时工作线程不需要有自己的Windows消息循环。

自动选择套间[17],(ThreadingModel=Both)

COM对象的套间类别与创建它的线程的套间类别一致。这避免了很多marshalling开销,例如一个MTA服务器被一个STA线程调用。

Thread Neutral Apartment(NA),(ThreadingModel=Neutral)

一个特殊的套间,没有任何指定的线程。当STA或MTA线程调用同一进程的NA对象,则调用线程临时离开它的套间并执行COM对象的代码,没有任何线程切换。即任何线程都可以直接了当调用COM对象的方法。[13]因此NA可以认为是优化套间之间方法调用的效率。