基于多重继承的 COM 框架






4.25/5 (4投票s)
2001年7月17日
5分钟阅读

67760

1425
一个简单的 COM 服务器,
引言
我相信,任何想深入了解 COM 的程序员都必须用纯 C++ 编写一个 COM 组件。在本文中,我将介绍一种创建精简且高度可重用的 COM 组件的方法。我还将解释注册 COM 组件所需的最低步骤。
《Professional COM Applications with ATL》(作者 Sing Li 和 Panos Economopoulos)一书中描述了一种从头开始创建 COM 组件的方法。作者使用了嵌套类方法而不是多重继承方法。这种技术有几个缺点,因为随着接口的增加,代码会变得复杂。多重继承在一定程度上消除了这个问题,因为接口更加独立。其次,由于 C++ 类通过多重继承公开多个接口,因此对于 C++ 程序员来说,将 COM 服务器视为从多个接口多重继承而来更为直观。
类设计
CMyComClass
是实现类,它继承自三个接口:IMyInterface
、IAnotherInterface
和 IYetAnotherInterface
。所有这些接口都派生自 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
通过 AddRef
和 Release
实现引用计数。它总是通过类桥接机制从您的实现类派生。
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_IUnknown
,QIIUNKNOWN
会将 *ppObj
强制转换为此类型并返回 S_OK
。QI
执行类似的操作,但针对其他接口。为了使 QueryInterface
正确工作,您必须只编写一次 QIIUNKNOWN
,并为类继承的所有接口编写 QI
。由于 MyComClass
继承自这三个接口,所以我为每个接口都编写了 QI
。
CComPtrFactory
是将用于创建 COM 服务器实例的类工厂。CComPtrFactory
也应使用 CComPtr
的引用计数机制来进行内存管理。实现此目的的一种方法是将 CComPtrFactory
从 CComPtr
派生(就像 IClassFactory
从 IUnknown
派生一样)。但是 CComPtr
只能从实现 QueryInterface
的类派生。这就是为什么我们需要一个类 CComPtrFactoryQIImpl
,它位于 CComPtr
和 IClassFactory
之间,并为类工厂实现 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
};
然后,CComPtrFactory
从 CComPtr
派生(它继承自 CComPtrFactoryQIImpl
),因此它不必实现 AddRef
、QueryInterface
和 Release
。现在,只剩下实现 CreateInstance
和 LockServer
,我们的类工厂就可以创建 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 服务器,必须在系统注册表中安装以下条目:
- HKEY_CLASSES_ROOT\(组件的 progid)
- HKEY_CLASSES_ROOT\(组件的 progid)\Default: (组件的 progid)
- HKEY_CLASSES_ROOT\(组件的 progid)\CLSID
- HKEY_CLASSES_ROOT\(组件的 progid)\CLSID\Default: (组件的 CLSID)
有了这些条目,您只能访问默认接口。因此,为了访问其他接口,您还必须安装接口的信息。这可以通过为每个接口在注册表中定义以下条目来实现:
- HKEY_CLASSES_ROOT\Interface\(接口 ID)
- HKEY_CLASSES_ROOT\Interface\(接口 ID)\Default: (接口名称)
- HKEY_CLASSES_ROOT\Interface\(接口 ID)\NumMethods
- 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[] );
};
您必须在 DllRegisterServer
和 DllUnregisterServer
中调用这些函数。应该为类型库中的所有接口调用 RegisterInterface
和 UnregisterInterface
。从 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
方法,所以只有当您的组件公开数千个接口时,这才会成为一个严重的问题。
如何使用它
- 创建一个新的
Win32DLL
项目。 - 通过定义以下函数并在 .def 文件(包含在源代码中)中编写它们来导出它们:
BOOL APIENTRY DllMain( HANDLE hDllHandle, DWORD dwReason, LPVOID lpReserved ) STDAPI DllGetClassObject( REFCLSID rClsid, REFIID riid, void** ppObj ) STDAPI DllCanUnloadNow() STDAPI DllRegisterServer() STDAPI DllUnregisterServer()
- 在
DllRegisterServer
和DllUnregisterServer
中实现注册和注销功能。 DllGetClassObject
将创建CComPtrFactory<YourClass>
的实例,并对其调用QueryInterface
。- 创建 idl 文件来定义所有接口,并将该文件包含在项目中。
- 将 FrameworkClasses.cpp 包含在项目中。
- 让您的类继承自接口,并为所有方法以及
QueryInterface
提供实现。
恭喜!您已从头开始创建了一个基于多重继承的 ActiveX 控件。
历史
- 2001 年 7 月 17 日:初始版本
许可证
本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。
作者可能使用的许可证列表可以在此处找到。