65.9K
CodeProject 正在变化。 阅读更多。
Home

基于多重继承的 COM 框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (4投票s)

2001年7月17日

5分钟阅读

viewsIcon

67760

downloadIcon

1425

一个简单的 COM 服务器, 基于多重继承。

引言

我相信,任何想深入了解 COM 的程序员都必须用纯 C++ 编写一个 COM 组件。在本文中,我将介绍一种创建精简且高度可重用的 COM 组件的方法。我还将解释注册 COM 组件所需的最低步骤。

《Professional COM Applications with ATL》(作者 Sing Li 和 Panos Economopoulos)一书中描述了一种从头开始创建 COM 组件的方法。作者使用了嵌套类方法而不是多重继承方法。这种技术有几个缺点,因为随着接口的增加,代码会变得复杂。多重继承在一定程度上消除了这个问题,因为接口更加独立。其次,由于 C++ 类通过多重继承公开多个接口,因此对于 C++ 程序员来说,将 COM 服务器视为从多个接口多重继承而来更为直观。

类设计

CMyComClass 是实现类,它继承自三个接口:IMyInterfaceIAnotherInterfaceIYetAnotherInterface。所有这些接口都派生自 IUnknown。每个接口都实现了一个方法,因此 CMyComClass 的声明将是

class CMyComClass : public IMyInterface , public IAnotherInterface, public IYetAnotherInterface
{
public:
//Declared in IMyInterface 
	HRESULT _stdcall AddNumbers( long First, long Secong, long* Result );
//Declared in IAnotherInterface 
	HRESULT _stdcall SubtractNumbers( long First, long Second, long* Result );
//Declared in IYetAnotherInterface
	HRESULT _stdcall MultiplyNumbers( long First, long Second, long* Result );
//Must implement QueryInterface
	HRESULT _stdcall QueryInterface( REFIID riid, void** ppObj );
//Must also include this macro in class declaration
	DECLARE_IUNKNOWN_METHODS
};

如类图所示,概念设计基于装饰器设计模式,这意味着您的实现类夹在 CComPtr 类和接口之间。CComPtr 通过 AddRefRelease 实现引用计数。它总是通过类桥接机制从您的实现类派生。

template< class T >
class CComPtr : public T
{
// Reference count
	long m_nRefCount;	
public:		
// Increments reference count	
	virtual ULONG _stdcall AddRef()
// Decrements reference count and deletes if necessary
	virtual ULONG _stdcall Release();
	
	... // Constructor and Destructor omitted	
};

因此,当我写 CComPtr<CMyComClass> 时,CComPtr 实际上继承自 CMyComClass,并且可以执行对 CMyComClass 透明的任务。唯一未实现的方法是 QueryInterface。您必须在 CMyComClass 中提供 QueryInterface 的实现。我包含了一些宏,有助于实现 QueryInterface

HRESULT _stdcall CMyComClass::QueryInterface( REFIID riid, void** ppObj )
{
	QIIUNKNOWN( riid, ppObj )               // Check for IUnknown

	QI( riid, IMyInterface, ppObj )         // Check for IMyInterface
	QI( riid, IAnotherInterface, ppObj )    // Check for IAnotherInterface
	QI( riid, IYetAnotherInterface, ppObj ) // Check for IYetAnotherInterface

	return E_NOINTERFACE;			
}

如果 riid 等于 IID_IUnknownQIIUNKNOWN 会将 *ppObj 强制转换为此类型并返回 S_OKQI 执行类似的操作,但针对其他接口。为了使 QueryInterface 正确工作,您必须只编写一次 QIIUNKNOWN,并为类继承的所有接口编写 QI。由于 MyComClass 继承自这三个接口,所以我为每个接口都编写了 QI

CComPtrFactory 是将用于创建 COM 服务器实例的类工厂。CComPtrFactory 也应使用 CComPtr 的引用计数机制来进行内存管理。实现此目的的一种方法是将 CComPtrFactoryCComPtr 派生(就像 IClassFactoryIUnknown 派生一样)。但是 CComPtr 只能从实现 QueryInterface 的类派生。这就是为什么我们需要一个类 CComPtrFactoryQIImpl,它位于 CComPtrIClassFactory 之间,并为类工厂实现 QueryInterface

class CComPtrFactoryQIImpl : public IClassFactory
{
public:
//  QueryInterface implementation for the class factory class
	HRESULT _stdcall QueryInterface( REFIID riid, void** ppObj )
	{	
		if( riid == IID_IClassFactory )
		{ // Check for IID_IClassFactory 
			*ppObj = static_cast< IClassFactory* >(this);
			AddRef();			
			return S_OK;
		}
		else if( riid == IID_IUnknown )
		{ // Check for IID_IUnknown		
			*ppObj = static_cast< IUnknown* >(this);
			AddRef();			
			return S_OK;
		}		
		else
		{ // no interface created
			*ppObj = NULL;
			return E_NOINTERFACE;		
		}	
	}	
	
	... // constructor and destructor omitted	
};

然后,CComPtrFactoryCComPtr 派生(它继承自 CComPtrFactoryQIImpl),因此它不必实现 AddRefQueryInterfaceRelease。现在,只剩下实现 CreateInstanceLockServer,我们的类工厂就可以创建 COM 对象了。这些函数的实现非常简单,我相信您能够理解。

template< class T >
class CComPtrFactory : public CComPtr< CComPtrFactoryQIImpl >
{
public:
// Creates CComPtr object and retrieves an interface pointer to this object.
	virtual HRESULT _stdcall CreateInstance( IUnknown* pUnknown, REFIID riid, void** ppObj );

// Calling LockServer allows a client to hold onto a class factory so that 
// multiple objects can be created quickly.		
	virtual HRESULT _stdcall LockServer( BOOL bLock );

	... // constructor and destructor omitted
};

现在我将描述注册 COM 服务器所需的最低步骤。为了正确注册 COM 服务器,必须在系统注册表中安装以下条目:

  1. HKEY_CLASSES_ROOT\(组件的 progid)
  2. HKEY_CLASSES_ROOT\(组件的 progid)\Default: (组件的 progid)
  3. HKEY_CLASSES_ROOT\(组件的 progid)\CLSID
  4. HKEY_CLASSES_ROOT\(组件的 progid)\CLSID\Default: (组件的 CLSID)

有了这些条目,您只能访问默认接口。因此,为了访问其他接口,您还必须安装接口的信息。这可以通过为每个接口在注册表中定义以下条目来实现:

  1. HKEY_CLASSES_ROOT\Interface\(接口 ID)
  2. HKEY_CLASSES_ROOT\Interface\(接口 ID)\Default: (接口名称)
  3. HKEY_CLASSES_ROOT\Interface\(接口 ID)\NumMethods
  4. HKEY_CLASSES_ROOT\Interface\(接口 ID)\NumMethods\Default: (接口公开的方法数)

CRegistryManager 类完成了上述所有任务。它使用注册表 API 来注册和注销服务器的接口。其声明是:

class CRegisteryManager
{
public:
// Enters progid, clsid, path and threading model entries in the registry
	static HRESULT RegisterServer( TCHAR tcProgID[], TCHAR tcCLSID[], TCHAR tcThreadingModel[], 
	                               TCHAR tcPath[] );
// Removes server entries from the registry
	static HRESULT UnregisterServer( TCHAR tcProgID[], TCHAR tcCLSID[] );
// Registers interface name and id in the registry
	static HRESULT RegisterInterface( TCHAR tcInterfaceName[], TCHAR tcInterfaceID[], 
	                                  TCHAR tcNumMethods[] );
// Removes interface entries from the registry
	static HRESULT UnregisterInterface( TCHAR tcInterfaceID[] );
};

您必须在 DllRegisterServerDllUnregisterServer 中调用这些函数。应该为类型库中的所有接口调用 RegisterInterfaceUnregisterInterface。从 idl 文件复制接口名称和 ID。还要指定接口公开的方法数。请记住,这还包括接口继承的方法。由于所有这些都继承自 IUnknown 并实现了一个方法,因此所有方法的数量都应该是 4。

// Instructs an in-process server to create its registry entries for all classes supported
STDAPI DllRegisterServer()
{ 
	TCHAR tcProgID[] = _T("Adeel");
	TCHAR tcCLSID[] = _T("{D27733A2-F516-11d4-B219-0080C84499A8}");
	TCHAR tcThreadingModel[] = _T("Apartment");
	TCHAR tcPath[MAX_PATH];
// Get path of the dll
	GetModuleFileName( ( HMODULE )g_hInstance, tcPath, sizeof(tcPath) / sizeof(TCHAR) );
	
// Call for all the interfaces in the type library
	CRegisteryManager::RegisterInterface( _T("IMyInterface"), 
                      _T("{D27733A0-F516-11d4-B219-0080C84499A8}"), _T("4") );
	CRegisteryManager::RegisterInterface( _T("IAnotherInterface"), 
                      _T("{5159BAC6-C5F2-457e-8FA7-5725A8B2A6AD}"), _T("4") );
	CRegisteryManager::RegisterInterface( _T("IYetAnotherInterface"), 
                      _T("{C2C02F3A-53D9-4cc0-991B-33A37AAB1DC3}"), _T("4") );

	return CRegisteryManager::RegisterServer( tcProgID, tcCLSID, tcThreadingModel, tcPath );
}

// Instructs an in-process server to remove 
// only those entries created through DllRegisterServer.
STDAPI DllUnregisterServer()
{
	TCHAR tcProgID[] = _T("Adeel");
	TCHAR tcCLSID[] = _T("{D27733A2-F516-11d4-B219-0080C84499A8}");

// Call for all the interfaces in the type library	
	CRegisteryManager::UnregisterInterface( _T("{D27733A0-F516-11d4-B219-0080C84499A8}") ); 
	CRegisteryManager::UnregisterInterface( _T("{5159BAC6-C5F2-457e-8FA7-5725A8B2A6AD}") ); 
	CRegisteryManager::UnregisterInterface( _T("{C2C02F3A-53D9-4cc0-991B-33A37AAB1DC3}") ); 
	
	return CRegisteryManager::UnregisterServer( tcProgID, tcCLSID ); 
} 

问题

基于 MI 的设计总是有一个明显的问题,那就是名称冲突。因此,如果您想拥有两个具有同名方法但实现不同的接口,本文对此无能为力。另一个问题是,COM 接口不允许使用虚拟继承,因此会有多个 IUnknown 的副本。但是,由于 IUnknown 只有三个纯 virtual 方法,所以只有当您的组件公开数千个接口时,这才会成为一个严重的问题。

如何使用它

  1. 创建一个新的 Win32DLL 项目。
  2. 通过定义以下函数并在 .def 文件(包含在源代码中)中编写它们来导出它们:
    BOOL APIENTRY DllMain( HANDLE hDllHandle, DWORD dwReason, LPVOID lpReserved )
    STDAPI DllGetClassObject( REFCLSID rClsid, REFIID riid, void** ppObj )
    STDAPI DllCanUnloadNow()
    STDAPI DllRegisterServer()
    STDAPI DllUnregisterServer()
  3. DllRegisterServerDllUnregisterServer 中实现注册和注销功能。
  4. DllGetClassObject 将创建 CComPtrFactory<YourClass> 的实例,并对其调用 QueryInterface
  5. 创建 idl 文件来定义所有接口,并将该文件包含在项目中。
  6. FrameworkClasses.cpp 包含在项目中。
  7. 让您的类继承自接口,并为所有方法以及 QueryInterface 提供实现。

恭喜!您已从头开始创建了一个基于多重继承的 ActiveX 控件。

历史

  • 2001 年 7 月 17 日:初始版本

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.