COM原理

本文深入探讨了COM(Component Object Model)的核心概念,包括编码约定、标准调用、类与组件的区别、接口继承与多态性。文章指出,COM接口并非总是继承,而是通过接口不变性和命名冲突管理。此外,详细解释了多态性、虚拟函数表、QueryInterface的使用,以及类厂在组件创建中的角色。通过对GUID和Windows注册表的讨论,阐述了COM组件的标识和注册机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

编码约定

在所有的前面都加有一个字母I, 如:IX表示的“接口X”。

而在类名称的前面所加的前缀则为C, 如CA表示“类A”。

Microsoft Win32软件开发工具(SDK)中OBJBASE.H头文件的定义:#define interface struct

使用struct的原因在于struct的成员将自动具有我公有的属性,因此不需要另外定义中加上public关键字。

 

标准调用

Microsoft平台上COM接口所提供的所有函数使用的均是标准的调用约定.参数数目可变的函数使用的则是C调用约定.一般人们希望接口的实现使用这些约定.但要说明的是这并不是COM的绝对需要.

在WINDEF.H中pascal的定义如下:

#define pascal __stdcall

如果读者认为将pascal这个词放在代码中会让人莫名其妙,那么可以使用OBJBASE.H中所定义的如下宏:

#define STDMEFHODCALLTYPE __stdcall

 

类并非组件

用C++开发组件时不一定非用类不可。组件也可以用C来实现。一个组也可以由多个类来实现。

 

接口并非总是继承的

COM没有要求实现某个接口的类必须从那个接口继承,这是客户并不需要了解COM组件的继承关系。对接口的继承只不过是一种实现细节而已。

 

多重接口及多重继承

一个接口是一个函数集合,一个组件则是一个接口集,而一个系统则是一系列组件的集合。

 

接口不变性

一旦分布了一个接口,那么它将永远保持不变。当对组件进行升级时,一般不会修改已有的接口,面是加入一些新接口。

命名冲突

对于一个支持多个接口的组件,接口函数的名字出现冲突是经常会遇到的。这些种情况下,改变某个发生冲突的函数名称即可。COM对此不关心。COM接口是一个二进制标准,客户同接口的连接并不是通过其名称或其成员函数的名称完成的,而是通过它在表示它的内存块的位完成的。

解决命名冲突的另外一种 方法是不使用多重继承

实现组件的类并不需要继承每一个接口,而可以使用指向实现某些接口的类的指针。

接口名称之间出现冲突的情况也是可能的。如果在接口和函数名称的前面加上公司名称或产品名称可以减少此种 可能性。

多态

多态指的是可以按同一种方式来处理不同的对象。若两个不同的组件支持同一接口,那么客户将可以使用相同的代码一处理其中的任何一个组件。就是说,客户可以按照相同的方式来处理不同的组件。

虚拟函数表

当定义一个纯抽象类时,所定义的实际上是一个内存块的结构.纯抽象类所有实现都是一些具有相同的基本结构的内存.


所有的COM接口都继承了IUnknown,每个接口的vtbl中的前三个函数都是QueryInterfaceAddRefRelease

Interface IUnknown

{

            virtual HRESULT __stdcall QueryInterfaceREFIID riid, void ** ppvObject) = 0;

            virtual ULONG __stdcall  AddRefvoid) = 0;

            virtual ULONG __stdcall  Releasevoid) = 0;

}

QueryInterface

IUnknown的一个成员函数QueryInterface,客户可以通过此函数来查询某个组件是否支持某个特定的接口。若支持QueryInterface将返回一个指向些接口的指针,不支持返回值将是一个错误代码。

 

QueryInterface 有两个参数,和一个HRESULT返回值

HRESULT __stdcall QueryInterface( REFIID riid, void ** ppvObject);

第一个参数:接口标识符(IID

第二个参数:存放所请求接口指针的地址。

返回值:查询成功返回S_OK,如果不成功则返回相应错误码。

QueryInterface实现

根据某个给定的IID返回指向相应接口的指针。若组件支持客户指定的接口,那么应返回S_OK以及相应的指针。若不支持返回测返回E_NoINTERFACE并将相应的指针返回值置成NULL

QueryInterface的实现要求可以将一种类型映射成另外一种类型的结构。如:if else 、数组、哈希表或者是树来实现,但是case语句是无法用的。因为接口标识符是一个结构而不是个数。

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();

    return S_OK;

}
接口标准化,在 COM 中有两方面的内容:一是接口基本功能的标准化,二是接口内存结构的标准化


HRESULT

HRESULT(Here's the RESULT)值分成32位值,

 

HRESULT值中16到30这15个比特位包含的是设备代码。设备代码标识的是可以返回HRESULT返回代码的操作系统部分。由于Windows操作系统是由Microsoft开发的。因此它保留有定义设备代码的权力。

 

HRESULT值的查找

获取与某个特定HRESULT值相应的错误消息并将其显示出来的方法。以显示标准COM(以及AcitveX)错误消息,可以使用Win32 API中的FormatMessage函数

 

HRESULT值的使用

注意:

成功的代码有多个,失败的代码也有多个

失败代码可能会发生变化。

1多状态代码

一个函数在各种情况下返回的状态代码通常将包含多个成功代码及多个失败代码。这就是我们为什么要使用SUCCEEDED及FAILED宏的原因。一般不能直接将HRESULT值同某个成功代码(如S_OK)进行比较以决定某个函数是否成功。


GUID

 它实际上是组件和接口的标识号

static const IID IID_IX = {

       0x1ee235bd, 0x2e73, 0x49c2, 0xa5, 0x7c, 0x8b, 0xe5, 0x96, 0x92, 0x7e, 0xa2};

实际上IID是一个128比特(16字节)的一个GUID结构。GUID是英文Globally Unique Identifier(全局唯一标识符)的首字母缩写.

 

GUID 的声明和定义

之前是两个文件(Iface.h和GUDIS.cpp)来声明和定义GUID的。若希望用一条语句来声明并定义GUID,可以用使用OBJBASE.H中的定义的DEFINE_GUID宏。

如下:

// {6EF6377B-6150-462e-84FF-5B3FD43A9954}

DEFINE_GUID(IID_IX,

0x6ef6377b, 0x6150, 0x462e, 0x84, 0xff, 0x5b, 0x3f, 0xd4, 0x3a, 0x99, 0x54);

 

在文件guiddef.h可以看到这样一个定义:

#ifdef INITGUID

#define DEFINE_GUID(name, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8) \

        EXTERN_C const GUID DECLSPEC_SELECTANY name \

                = { l, w1, w2, { b1, b2,  b3,  b4,  b5,  b6,  b7,  b8 } }

#else

#define DEFINE_GUID(namelw1w2b1b2b3b4b5b6b7b8) \

    EXTERN_C const GUID FAR name

#endif // INITGUID

可以看出如果定义了INITGUID DEFINE_GUID 将是一个定义,否则只是声明。

或者直接包含 #include <InitGuid.h> 它的主要内容是:

  
  
#define INITGUID #include <guiddef.h>

 


GUID作为组件标识符

CoCreateInstance将使用一个GUID而不是一个串来标识组件。在COM中用以标识组件的GUID被称作是类标识符。为将类标识符同IID区别开来,与类标识符相应的类型为 。

 

若不想输入const IID&则可以使用等价的REFIID。类似地,在传递类标识符时,可以使用REFCLSID,而在传递GUID值时,可以使用REFGUID



Windows注册表

const char * g_RegTable[][3]={
						   {"CLSID\\{4BCFE27E-C88D-453C-8C94-F5F7B97E7841}",0,"TestCOM"},
						   {"CLSID\\{4BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\InprocServer32",0,(const char * )-1},
						   {"CLSID\\{4BCFE27E-C88D-453C-8C94-F5F7B97E7841}\\ProgID",0,"genii.TestCOM.1"},
						   {"genii.TestCOM.1",0,"TestCOM"},
						   {"genii.TestCOM.1\\CLSID",0,"{3BCFE27E-C88D-453C-8C94-F5F7B97E7841}"},
};
 

/*********************************************************************
 * Function Declare : DllUnregisterServer
 * Explain : self-unregistration routine
 * Parameters : 
 * void -- 
 * Return : 
*********************************************************************/
STDAPI DllUnregisterServer(void)
{
	HRESULT hr=S_OK;
	char szFileName [MAX_PATH];
	::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH);

	int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable);
	for(int i =0;SUCCEEDED(hr)&&i<5;i++)
	{
		const char * pszKeyName=g_RegTable[i][0];
		long err=::RegDeleteKey(HKEY_CLASSES_ROOT,pszKeyName);
		if(err!=ERROR_SUCCESS)
					  hr=S_FALSE;
	}

	return hr;
}
 
/*********************************************************************
 * Function Declare : DllRegisterServer
 * Explain : self Registration routine
 * Parameters : 
 * void -- 
 * Return : 
**************************************/
STDAPI DllRegisterServer(void)
{
	HRESULT hr=S_OK;
	char szFileName [MAX_PATH];
	::GetModuleFileName(g_hinstDll,szFileName,MAX_PATH);

	int nEntries=sizeof(g_RegTable)/sizeof(*g_RegTable);
	for(int i =0;SUCCEEDED(hr)&&i<5;i++)
	{
		const char * pszKeyName=g_RegTable[i][0];
		const char * pszValueName=g_RegTable[i][1];
		const char * pszValue=g_RegTable[i][2];

		if(pszValue==(const char *)-1)
		{
					  pszValue=szFileName;
		}

		HKEY hkey;
		long err=::RegCreateKey(HKEY_CLASSES_ROOT,pszKeyName,&hkey);
		if(err==ERROR_SUCCESS)
		{
			  err=::RegSetValueEx( hkey,
								pszValueName,
								0,
								REG_SZ,
								( const BYTE*)pszValue,
								( strlen(pszValue)+1 ) );
			 ::RegCloseKey(hkey);
		}
		if(err!=ERROR_SUCCESS)
		{
			 ::DllUnregisterServer();
			 hr=E_FAIL;
		}
	}
	return hr;
}

组件的自注册

为把组件注册到注册表,在DLL一定要输出如下两个函数:

STDAPI DllRegisterServer();     // 注册

STDAPI DllUnregisterServer();   // 反注册

我们使用REGSVR32.exe注册某个组件或反注册某个组,其实就是调用这两个函数的。



CoCreateInstance实际上并没有直接创建COM组件 ,而是创建了一个被称作是类厂的组件。而所需的组件正是由些类厂创建的。类厂组件的唯一功能就创建其他的组件。创建组件的标准接口是IClassFactory,用CoCreateInstnce创建的组件实际上是通过IClassFactory创建的。

类厂只是创建其它组件的一个简单组件

为了创建同某个CLSID相应的类厂,需要一个与CoCreateInstance等价的,也可以接收一个CLSID作为参数并把回相应类厂中某个接口指针的函数。这个函数就是COM库中的CoGetClassObject。

 

CoGetClassObject声明:

STDAPI CoGetClassObject(REFCLSID rclsid,

              DWORD dwClsContext,

              COSERVERINFO * pServerInfo,

              REFIID riid,

              LPVOID * ppv);

区别1CoGetClassObject同CoCreatInstance是非常相似的。只一个参数不同,CoCreatInstance将接收一个Iunknown指针,而CoGetClassObject则将接收一个COSERVERINFO指针。

区别2CoGetClassObject返回的是指向类厂中某个接口的指针(客户可以用这个指针来创建所需的组件),而CoCreateInstance返回的则是指向组件中某个接口的指针。

 

IClassFactory

类厂所支持的用于创建组件的标准接口是IClassFactory。大多数组件均可使用IClassFactory接口来创建。

interface IClassFactory : IUnknown

{

    HRESULT _stdcall CreateInstance(IUnknownpUnknownOuter,

                        const IIDiid,

                        void** ppv);

    HRESULT _stdcall LockServer(BOOLbLock);

};

 

CreateInstance

第一个参数为指向某个Iunknown接口的指针。在聚合再介绍。

其它两个参数同传给QueryInterface的参数是相同的。

CreateInstance并没有接收一个CLSID参数。这意味着些函数只能创建同某个CLSID(即传给CoGetClassObjectCLSID)相应的组件。

 

IClassFactory2

除了IClassFactory之外,Microsoft还定义了另外一个创建接口IClassFactory2,接口在IClassFactory的基础上增加了许可或权限功能。此时,为使类厂能够创建所需的组件,客户必须通过IClassFactory2厂提供正确的关键字或许可。通过使用IClassFactory2,类厂可以保证客户只能获得它能合法访问的组件,并具有对此组件的访问授权。

 

CoCreateInstanceCoGetClassObject 的比较

在每次创建组件时,先创建相应的组件的类厂,然后用所获取的IClassFactory指针来创建所需的接口需要完成的工作显然比直接调用 CoCreateInstance来创建所需的组件要复杂一些。所以很多时候用CoCreateInstance来创建组件。但CoCreateInstance是能过CoGetClassObject实现的。

HRESULT CoCreateInstance(const CLSIDclsid,

              IUnknownpunkonwnDuter,

              DWORD dwClsContext,

              const IIDiid,

              void** ppv)

{

    // Set the out paameter to NULL

    *ppv = NULL;

 

    // Create the class factory

// and get an IClassFactroy interface pointer.

    IClassFactorypIFactory = NULL;

    HRESULT hr = CoGetClassObject(clsid,

                     dwClsContext,

                     NULL,

                     IID_IClassFactory,

                     (void**)&pIFactory);

    if (SUCCEEDED(hr))

    {

       // create the component.

       hr = pIFactory->CreateInstance(punkonwnDuteriidppv);

       pIFactory->Release()();

    }

    return hr;

}

多数情况用CoCreateInstance,何时使用CoGetClassObject

第一种情况:若想用不同于IClassFactory的某个创建接口创建组件,则必须使用CoGetClassObject。因此如果想使用IClassFactory2来创建组件,就应使用CoGetClassObject

第二种情况:若需要创建同一个组件的多个实例,那么使用CoGetClassObject将可以获得更高的效率。因为这样只需要创建相应的类厂一次,而CoCreateInstance使得客户可以对组件的创建过程进行更多控件。

 

类厂的若干特性

类厂的一个实例将只能创建同某个CLSID相应的组件。

类厂和它所创建的组件是由同一个开发人员实现的。

类厂就是需要知道如何创建相应的组件并将这一点封装起来,以便客户能够尽可能地同组件所具有的特殊需要分开。

 

类厂的实现

DllGetClassObject的使用

CoGetClassObject需要DLL中一个特定函数来创建组件类厂。此函数的名称的为DllGetClassObject。

STDAPI DllGetClassObject(const CLSID & rclsid

              const IID & riid,     

              void ** ppv);

 

组件的创建过程

 





实例:

参考     COM原理实例。。。


附源码 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值