从零开始的 COM - 第二部分






4.85/5 (41投票s)
2004年4月18日
14分钟阅读

256479

5998
关于 COM 库的文章。
引言
在第一部分中,我们解释了有关 COM 技术的一些背景信息,并通过一个简单的例子展示了客户端如何通过接口使用组件的功能。在第二部分中,我将指导读者将组件的实现与其客户端分离,以便客户端不再与组件绑定,并能够通过类工厂创建组件。
第二部分 - 打破依赖
提供组件(分发)
软件组件服务器提供了一种更方便重用功能的方式,此外,当多个应用程序同时使用相同功能时,它们可以减少内存开销,因为尽管每个应用程序都会获得自身的数据副本,但它们可以共享代码。通过将组件放入 DLL 中,可以实现一种组件分发形式,DLL 成为组件的服务器,并包含组件所支持接口的实现。
构建组件的服务器(DLL)
在示例中,客户端和组件都在同一个文件中。现在应该将它们分开,客户端将是一个 .exe 文件,它将组件加载到其地址空间以便使用它,而组件将由 DLL 提供服务。客户端需要在获取接口指针之前将 DLL 加载到其进程中并创建组件。如果客户端链接到 DLL 中的 CreateInstance()
函数,则可以通过接口指针访问组件的所有其他函数。因此,解决方案就是从 DLL 导出 CreateInstance()
函数,以便客户端可以在运行时显式地链接到它。DLL 将使用 Microsoft 的命令行工具之一从命令提示符进行构建。
下图显示了用于创建 DLL 的服务器文件。
步骤 1
创建一个源文件(Component.cpp),并将组件类的定义和实现放入其中。
第二步
通过在文件末尾添加以下代码段来导出 CreateInstance()
函数。
步骤 3
链接器应被告知 CreateInstance
函数将被导出,这可以通过使用模块定义文件来完成。模块定义文件是一个扩展名为“def”的文件,其中包含有关导出、属性以及链接具有导出(或 DLL)的 .EXE 文件所需其他信息。在 .def 文件中,CreateInstance
函数的导出序号被选择为 1。下面的部分显示了此文件的内容。
;component.def
; Component module-definition file
; LIBRARY Component.dll
DESCRIPTION 'Components windows dynamik library'
EXPORTS ; Explicit exports can go here
CreateInstance @1 PRIVATE
步骤 4
接口标识符和接口定义对于客户端和组件都应该是已知的,并且可以将它们放入两个独立的文件中,这两个文件在客户端和组件之间共享。创建另一个源文件(GUID.cpp),其中可以包含接口 ID。
// // GUID.cpp - Interface ID // #include "objbase.h" extern "C" { extern const IID IID_IComponent = { 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3 { 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4 // The extern is required to allocate memory for C++ constants. }
步骤 5
创建一个头文件(interface.h),其中包含以下内容:
// // Interface.h // interface IComponent : IUnknown { virtual void __stdcall Print(const char* msg) = 0 ; } ; // Forward references for GUID extern "C" { extern const IID IID_IComponent ; }
步骤 6
创建一个“make”文件,其中包含以下内容用于创建 DLL 的选项:
#Makefile ################################################################################ # Compiler options: # /c compile without linking # CL cl.exe is a 32-bit tool that controls the Microsoft C # and C++ compilers and linker. # The compilers produce Common Object File Format # (COFF) object (.obj) files. # The linker produces executable (.exe) files # or dynamic-link libraries (DLLs). # ################################## # Linker options: # # /DEF Passes a module-definition (.def) file to the linker # /DEBUG Creates debugging information # /DLL Builds a DLL CPP_FLAGS=/c /MTd /Zi /Od /D_DEBUG EXE_LINK_FLAGS=/DEBUG DLL_LINK_FLAGS=/DLL /DEBUG LIBS=UUID.lib ############################################# # Targets: # CodeProject is just a pseudotarget # CodeProject : component component : Component.dll ######################################### # Shared source files: # GUID.obj : GUID.cpp Cl $(CPP_FLAGS) GUID.cpp ########################################## # Component source files: # Component.obj : Component.cpp Interface.h Cl $(CPP_FLAGS) Component.cpp ######################################## # Link component: # Component.dll : Component.obj GUID.obj Component.def link $(DLL_LINK_FLAGS) Component.obj GUID.obj $(LIBS) /DEF:Component.def
步骤 7
使用 Microsoft Program Maintenance Utility (NMAKE.EXE) 从命令行构建 DLL。此程序是一个工具,可以根据描述文件中的命令构建项目。
- 打开命令窗口(单击“开始”,选择“运行”菜单项,然后在对话框中输入 cmd)。
- 在命令提示符下,更改到包含服务器文件的目录。
- 在命令提示符下,输入:nmake /f makefile。
NMAKE 工具将在同一文件夹中创建 DLL。
构建客户端
下图显示了用于构建客户端的文件。客户端将在 Visual C++ 开发环境中构建。
步骤 1
使用 AppWizard,创建一个简单的 Win32 控制台应用程序,并选择一个空项目。
第二步
创建一个新的源文件(Create.cpp),并创建一个接受 DLL 名称作为参数的函数,加载“DLL”,然后调用导出的 CreateInstance()
函数。函数的返回值将是 CreateInstance()
函数的返回值,这是一个 IUnknown
接口指针。为了显式链接到“DLL”,该函数调用 GetProcAddress
函数来获取导出函数的地址。GetProcAddress
函数接受两个参数。第一个参数是“DLL”模块的句柄,第二个参数是“DLL”的名称。通过调用 LoadLibrary
函数,可以获得模块句柄。
// Create.cpp #include "iostream.h" #include "unknwn.h"//IUnknown definition file. #include "Create.h" typedef IUnknown* (*CREATEFUNCPTR)(); ////////////////////////////////////////// IUnknown* CallCreateInstance(char* dllname) { //-----------------------------------------------------------------// // Load dynamic link library into client's process. //Loadlibrary maps a DLL module and return a handle //that can be used in GetProcAddress //to get the address of a DLL function //-----------------------------------------------------------------// HMODULE hm = ::LoadLibrary(dllname); if (hm ==NULL) return NULL; // Get the address of CreateInstance function. CREATEFUNCPTR Function = (CREATEFUNCPTR)::GetProcAddress(hm, "CreateInstance"); if (Function == NULL) return NULL; return Function(); }
步骤 3
创建一个新的头文件(Create.h),其中包含以下内容:
// Create.h IUnknown* CallCreateInstance(char* dllname) ;
步骤 4
创建一个新的头文件(interface.h),其中包含以下内容:
// // Interface.h // interface IComponent : IUnknown { virtual void __stdcall Print(const char* msg) = 0 ; } ; // Forward references for GUID extern "C" { extern const IID IID_IComponent ; }
步骤 5
创建另一个源文件(GUID.cpp),其中可以包含接口 ID。
// GUID.cpp - Interface ID #include "objbase.h" extern "C" { extern const IID IID_IComponent = { 0x853b4626, 0x393a, 0x44df, //Data1,Data2,Data3 { 0xb1, 0x3e, 0x64, 0xca, 0xbe, 0x53, 0x5d, 0xbf } }; //Data4 // The extern is required to allocate memory for C++ constants. }
步骤 6
创建一个源文件(Client.cpp)并实现 main
函数。调用第 2 步中创建的函数,以实例化组件并使用其方法。
//--------// // Client //--------// int main() { HRESULT hr ; // Get the name of the component to use. char dllname[20]; cout << "Enter the filename of component's server [component.dll]:"; cin >> dllname; ... // calling the CreateInstance function in the // DLL in order to create the component. TRACE("Getting an IUnknown interface pointer...") ; IUnknown* pIUnknown = CallCreateInstance(dllname) ; ... IComponent* pIComponent ; hr = pIUnknown->QueryInterface(IID_IComponent, (void**)&pIComponent); if (SUCCEEDED(hr)) { ... pIComponent->Print("COM from scratch.") ; //using the component's functionality pIComponent->Release() ; ... } ... return 0 ; }
步骤 7
将组件服务器(Component.dll)放在客户端的同一目录下。现在,客户端能够将其 DLL 加载到其地址空间,并通过 LoadLibrary
和 GetProcAddress
函数获取 CreateInstance 函数
的地址。构建并运行客户端程序。
下面的屏幕截图显示了客户端应用程序,在加载 DLL 并调用组件的 Print
方法后。
结论:通过服务器分发 COM 组件,使客户端可以轻松地重用组件的功能。
在不重新构建客户端的情况下扩展组件功能
COM 组件的一个优点是可以在不重建的情况下轻松扩展应用程序的功能。只要接口不变,客户端应用程序就可以继续使用该组件,即使其功能已通过对方法的新更改进行了扩展。为了展示 COM 组件的这一优势,最好通过一个简单的例子来考察客户端应用程序的重建问题。在下面,一个 DLL 被链接到一个客户端应用程序,您可能会注意到,每当对 DLL 进行更改时(例如,通过向 DLL 中的类添加新成员变量并修改成员函数),如果未重新构建,客户端应用程序将无法运行。
步骤 1:创建 DLL
- 使用 AppWizard,创建一个新的项目,类型为 Win32 动态链接库。
- 在向导的第二步中,选择“一个简单的 DLL 项目”并单击“完成”。
- 创建一个新的头文件,并定义一个包含成员变量和可以从 DLL 导出的成员函数的类。
//myclass.h class CMyclass { long m_cRef; public: _declspec(dllexport) void Print(const char*msg); };
- 创建一个源文件(myclass.cpp)并实现成员函数。
- 构建 DLL。
步骤 2:创建客户端并加载 DLL
- 创建一个新的空项目,类型为 Win32 控制台应用程序。
- 创建一个新的源文件以加载和测试 DLL(client.cpp)。
- 包含包含 DLL 中类定义的头文件。
//client.cpp #include"iostream.h" #include"..\DLL\myclass.h" ///////////////////////////////// void main() { CMyclass classObj; classObj.Print("COM from scratch."); }
- 将 DLL 项目中的 DLL.lib 文件添加到客户端项目中(项目 -> 添加到项目 -> 文件,然后选择库文件(.lib)作为文件类型)。
- 将 DLL 复制到客户端可执行文件的同一文件夹中。如果构建并运行客户端应用程序,屏幕上将显示“COM from scratch.”,并且 DLL 将没有问题地加载。
步骤 3:查看重建问题
- 返回
CMyclass
类的实现,并添加一个新的成员变量。//myclass.h class CMyclass { long m_cRef; int m_i; // a new member variable public: _declspec(dllexport) void Print(const char* msg); };
- 修改
Print
成员函数的实现。 - 重新构建 DLL 并将其复制到客户端可执行文件的同一文件夹中。
- 在不重建的情况下执行新版本的 DLL 的客户端应用程序,如果您运行客户端应用程序,您将遇到问题,而重建客户端应用程序可以解决此问题。客户端应用程序的重建是一个大问题,因为它需要源代码。现在,如果您对第一部分示例中的组件类进行相同的更改,并重建 DLL 并将其与客户端应用程序(而不重建它)进行测试,您会注意到客户端运行正常,尽管组件类中添加了一个新的成员变量并且
Print
方法的实现已更改。由于 COM 组件中的方法调用是通过接口间接进行的,因此如果方法被修改,将不会有问题。
结论:COM 在不重建的情况下扩展应用程序的功能。
改进示例
在示例中,尽管客户端和组件已分离,但客户端与组件的实现密切相关,并且应该知道 DLL 的名称,更改 DLL 的名称会影响客户端。一种改进方法是将组件从一个 DLL 移动到另一个 DLL 或其他目录。解决方案是将 CallCreateInstance
函数替换为名为 CoCreateInstance
的 COM 库函数。COM 运行时库是 Windows 操作系统的一个组成部分,它为客户端提供查找和实例化 COM 对象的方法。COM 类对象可以通过 CLSID(全局唯一标识符)来标识,这些标识符用于定位和创建对象的实例。一旦获得 CLSID,客户端应用程序就会将 CLSID 提交给 COM 运行时库来加载 COM 对象并检索接口指针。使用 CLSID 和注册表,CoCreateInstance
会定位指定的对象,创建该对象的实例,并返回指向该对象的接口指针。为了使用 CoCreateInstance
创建对象,必须将该对象注册到系统中。
CoCreateInstance
COM 库包含此函数。创建组件最简单的方法是使用 CoCreateInstance
函数。CoCreateInstance
在创建组件时使用类工厂。它接受一个 CLSID,创建相应组件的实例,并返回该组件实例的接口。CoCreateInstance
接受 4 个 in
参数和 1 个 out
参数(IUnknown*
)。通过将 IID 传递给 CoCreateInstance
,客户端在创建组件后无需调用 QueryInterface
。
CoCreateInstance
的参数
- 第一个参数是对象的 CLSID。
- 第二个参数用于将对象聚合为另一个对象的一部分。
- 第三个参数指定对象的执行上下文。
- 第四个参数是要请求的接口的 IID(接口 ID)。
- 最后一个参数是一个
out
参数,是指向所创建对象的接口指针。
组件注册
可以使用 CoCreateInstance
创建的对象也必须在系统中注册。注册将 CLSID 映射到对象所在的自动化组件文件(.dll 或 .exe)。如果客户端希望在运行时获取 CLSID,则必须有一种方法可以动态地定位和加载可访问对象的 CLSID。此外,COM 库必须有一些系统范围内的方法可以将给定的 CLSID(无论客户端如何获取它)与实现该类的服务器代码相关联。换句话说,COM 库需要一些 CLSID 到服务器的持久化映射,它使用这些映射来实现其定位器服务。Microsoft Windows 上的 COM 实现使用 Windows 系统注册表作为此类信息的存储。在该注册表中,有一个名为“CLSID”的根键,服务器负责在该键下创建指向其模块的条目。通常,这些条目是在安装时由应用程序的设置代码创建的,但也可以根据需要动态创建。当服务器安装在 Windows 下时,安装程序将为主机服务器支持的每个类在 CLSID 下创建一个子键,使用 CLSID 的标准字符串表示形式作为键名。因此,CLSID 的主要条目是 CLSID 键下的子键,该子键是包含在花括号中的十六进制数字表示的 CLSID。我们还可能希望将 CLSID 与所谓的编程标识符或 ProgID 相关联,它实际上标识了同一个类。ProgID 是一个不带空格的文本字符串,可以代替 CLSID 字符串使用。标准的 ProgID 格式为 <Vendor>.<Component>.<Version>,例如 Codeproject.Cmpnt1.1。此格式相当独特,如果每个人都遵循它,通常不会发生冲突。还有一个“VersionIndependentProgID”,其格式与 ProgID 相同,但不包含版本号。ProgID 和 VersionIndependentProgID 都可以注册,并在根键下方以人类可读的名称作为值。VersionIndependentProgID 映射到 ProgID,ProgID 映射到 CLSID。要创建注册表条目,您可以编写代码,或者创建一个 REG 文件并简单地运行它以将其条目与注册表合并。下图显示了 Demo Application 中 Component1 的注册表条目。
类工厂
CoCreateInstance
函数不直接创建 COM 组件。相反,它创建一个称为类工厂的组件,然后由类工厂创建所需的组件。类工厂是一个创建其他组件的组件。特定的类工厂仅创建对应于单个、特定 CLSID 的组件。客户端使用类工厂支持的接口来控制类工厂如何创建每个组件。用于创建组件的标准接口是 IClassFactory
接口。IClassFactory
与其他 COM 接口一样,派生自 IUnknown
接口并具有两个方法:
CreateInstance
,它创建一个指定 CLSID 的未初始化对象。LockServer
,它将对象的服务器锁定在内存中,从而可以更快地创建新对象。
下面定义了一个类工厂,用于创建示例中的 COM 组件。
/////////////////////////////////////////////////////////// // // Class factory // class CFactory : public IClassFactory { public: // IUnknown virtual HRESULT __stdcall QueryInterface(const IID& iid,void** ppv) ; virtual ULONG __stdcall AddRef() ; virtual ULONG __stdcall Release() ; // IClassFactory virtual HRESULT __stdcall CreateInstance(IUnknown* pUnkOuter, const IID& iid, void** ppv) ; virtual HRESULT __stdcall LockServer(BOOL bLock) ; // Constructor CFactory() : m_cRef(1) {} // Destructor ~CFactory() {} private: long m_cRef ; } ; // // Class factory IUnknown implementation ////////////////////////////////////////////////////////////////////// HRESULT __stdcall CFactory::QueryInterface(const IID& iid,LPVOID* ppv) { if ((iid == IID_IUnknown) || (iid == IID_IClassFactory)) *ppv = static_cast<IClassFactory*>(this) ; else { *ppv = NULL ; return E_NOINTERFACE ; } reinterpret_cast<IUnknown*>(*ppv)->AddRef() ; return S_OK ; } /////////////////////////////////// ULONG __stdcall CFactory::AddRef() { return ::InterlockedIncrement(&m_cRef) ; } //////////////////////////////////// ULONG __stdcall CFactory::Release() { if (::InterlockedDecrement(&m_cRef) == 0) { delete this ; return 0 ; } return m_cRef ; } // // IClassFactory implementation /////////////////////////////////////////////////////////////// HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnkOuter, const IID& iid,void** ppv) { HRESULT hr; if (pUnkOuter != NULL) { return CLASS_E_NOAGGREGATION ; } CComponent* pComponent = new CComponent ; if (pComponent == NULL) { return E_OUTOFMEMORY ; } // Get the requested interface. hr = pComponent->QueryInterface(iid,(void**) ppv) ; if(FAILED(hr)) pComponent->Release() ; return hr ; } //-----------------------------------------------------------------------// // LockServer // Called by the client of a class object to keep a server open in memory, // allowing instances to be created more quickly. //-----------------------------------------------------------------------// /////////////////////////////////////////////////// HRESULT __stdcall CFactory::LockServer(BOOL bLock) { return S_OK ; }
在深入探讨更多细节之前,最好先概述一下通过 COM 库创建组件。
- 客户端调用
CoCreateInstance
,该函数在 COM 库中实现。 CoCreateInstance
是使用CoGetClassObject
函数实现的。CoGetClassObject
调用DllGetClassObject
,该函数在 DLL 服务器中实现,其工作是为组件创建类工厂。DllGetClassObject
查询类工厂的IClassFactory
接口,该接口将返回给CoCreateInstance
函数。CoCreateInstance
使用IClassFactory
接口调用其CreateInstance
方法。IClassFactory::CreateInstance(...)
使用new
操作符创建组件,并查询组件的接口。- 获取组件的接口后,
CoCreateInstance
释放类工厂并将接口指针返回给客户端。 - 客户端使用接口指针来调用组件的
Print
方法并使用其功能。
下图说明了这些步骤。
因此,为了改进示例,我们需要:
- 实现
CFactory
方法。 - 在组件服务器或 DLL 中实现
DllGetClassObject
,而不是CreateInstance
函数。 - 编写必要的代码(或使用注册文件)以便在 Windows 注册表系统中注册组件。
此外,在 Visual C++ 开发环境中制作 DLL 会更容易。下面将实现这些步骤。
步骤 1
- 使用 AppWizard,为 DLL 创建一个新项目(名称为“Component”),并选择 MFC AppWizard (DLL)。请注意,DLL 现在将驻留在与客户端不同的目录(C:\CodeProject)中。
- 在第一步中,选择“MFC Extension DLL”并单击“Finish”按钮。
- 打开 Component.cpp 文件,并用以下部分替换其内容。
// Component.cpp : Defines the initialization routines for the DLL. // #include "stdafx.h" #include <afxdllx.h> #include "interface.h" #include <objbase.h> #include "iostream.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif // // Component.cpp ///////////////////////////////////////////// BOOL APIENTRY DllMain(HINSTANCE InsModule, DWORD dwReason, void* lpReserved) { return TRUE; }
- 将组件类和类工厂的定义及其实现复制粘贴到源文件 Component.cpp 中,并忽略
CreateInstance
函数的导出,因为类工厂将创建组件。
步骤 2:获取类工厂 - DllGetClassObject
当类上下文为 DLL 时,CoGetClassObject
函数将调用 DllGetClassObject
函数,如前所述,其作用是为组件创建类工厂。在 Component.cpp 文件中实现此函数。
///////////////////////////////////////////////////////////////////////// STDAPI DllGetClassObject(const CLSID& clsid, const IID& iid, void** ppv) { if (clsid != CLSID_Component) return CLASS_E_CLASSNOTAVAILABLE; // Create class factory. CFactory* pFactory = new CFactory ; if (pFactory == NULL) return E_OUTOFMEMORY; // Get requested interface. HRESULT hr = pFactory->QueryInterface(iid, ppv); pFactory->Release(); return hr; }
编译并构建 DLL(Component.dll)。
步骤 3:注册
使用 GUIDGEN.EXE 为 Component 类创建一个 CLSID。
{49BF12F1-5041-48da-9B44-AA2FAA63AEFB} static const GUID CLSID_Component = { 0x49bf12f1, 0x5041, 0x48da, { 0x9b, 0x44, 0xaa, 0x2f, 0xaa, 0x63, 0xae, 0xfb } };
创建一个扩展名为“.reg”的文件(component.reg),以便为组件创建注册表条目(使用 CLSID)。
REGEDIT HKEY_CLASSES_ROOT\Codeproject.Component.1 = Codeproject Component Version 1.0 HKEY_CLASSES_ROOT\Codeproject.Component.1\CLSID = {49BF12F1-5041-48da-9B44-AA2FAA63AEFB} HKEY_CLASSES_ROOT\Codeproject.Component = Codeproject Component HKEY_CLASSES_ROOT\Codeproject.Component\CurVer = Codeproject.Component.1 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB} = Codeproject Component 1.0 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\InprocServer32 = c:\codeproject\component.dll HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\ProgID = Codeproject.Component.1 HKEY_CLASSES_ROOT\CLSID\{49BF12F1-5041-48da-9B44-AA2FAA63AEFB}\ VersionIndependentProgID = Codeproject.Component
通过单击注册表文件来激活它。运行注册表文件后,条目将存储在 Windows 注册表系统中。下图显示了这些条目。
就是这样,依赖关系现在已完全打破。用客户端进行检查。
客户端
现在,尽管组件服务器(component.dll)位于 C:\codeproject 目录中,客户端仍然可以通过类工厂和 COM 库轻松加载它并使用其功能,这就是 COM 组件通常由其客户端创建和使用的方式。下面显示了客户端如何通过 COM 库使用组件。
//-----------// // Client //-----------// void main() { HRESULT hr; IUnknown* pIUnknown; IComponent* pIComponent; IClassFactory* pIClassFactory; ::CoInitialize(NULL); /* //Once the CoCreateInstance is called, the component //will be created and the client can not //control it, that's why CoCreateInstance is inflexible //and the solution is to call CoGetClassObject function hr = ::CoCreateInstance(CLSID_Component,NULL, CLSCTX_INPROC_SERVER,IID_IUnknown,(void**)&pIUnknown) ; if (SUCCEEDED(hr)) { hr=pIUnknown->QueryInterface(IID_IComponent,(void**)&pIComponent); if(SUCCEEDED(hr)) pIComponent->Print("COM from scratch."); } */ //-------------------------------// // improvement of the client code //------------------------------// // By calling the CoGetClassObject function, the client can control // creation of the component hr=CoGetClassObject(CLSID_Component,CLSCTX_INPROC_SERVER, NULL,IID_IClassFactory,(void**)&pIClassFactory); if (SUCCEEDED(hr)) { hr=pIClassFactory->CreateInstance(NULL, IID_IComponent,(void**)&pIComponent); if(SUCCEEDED(hr)) pIComponent->Print("COM from scratch."); } ::CoUninitialize (); }
第三部分将在下一篇文章中介绍。