深入了解 COM






3.47/5 (83投票s)
2004年7月13日
18分钟阅读

184507
COM 的复杂性得到简化。简单来说,COM 内幕为你而写。
COM 无需复杂
微软的组件对象模型 (COM) 已成为一项至关重要的工具。它是微软分布式计算方法的基础。它是一种强大的方法,用于定制当前和未来的应用程序。它是 OLE 和 ActiveX 的基础。简而言之,COM 有助于为对组件设计感兴趣的开发者以及希望在 COM 移植到 UNIX、MVS 和其他环境时使用的程序员解锁未来的开发。简单来说,基于 COM 的接口正在迅速普及,你也可以掌握它。
引言
为您提供的 COM/DCOM 技术:COM 代表组件对象模型,DCOM 代表分布式对象模型。简单地说,DCOM 就是更长距离的 COM。我们将通过了解组件技术的发展历程来开始我们的 COM 和 DCOM 理解之旅。故事始于自定义控件的引入。它只不过是一个导出函数集的 DLL。与 DLL 不同,自定义控件可以操作属性,并响应用户或程序输入来处理事件的触发。不幸的是,由于 DLL 无法让 VB 查询控件有关控件支持的属性和方法的信息,因此这些 C 语言 DLL 无法被 Visual Basic 使用。Visual Basic 扩展 (VBX) 包含可重用软件组件。它们能够提供广泛的功能,从简单的标签列表到复杂的多媒体控件。因此,VBX 提供了否则在 VB 应用程序中无法实现的各种功能。VBX 是用 C 和 C++ 编写的。
VBX 取得了成功,并且对将它们移植到非 Intel 平台的需求日益增长。不幸的是,VBX 是建立在 16 位架构上的。因此,它们无法轻易移植到 32 位环境。因此,微软引入了一个名为 OLE1.0 的新规范。OLE 代表对象链接与嵌入。OLE 1.0 提供了一种处理复合文档的方法。复合文档是一种在单个文档中以多种格式(如文本、图形、视频和声音)存储数据的方式。OLE 1.0 的成功有限,因为其复杂的 API 使其难以编程。
尽管 OLE 1.0 的成功有限,但微软在 1993 年发布了 OLE 2.0。它提供了可扩展或定制的面向对象服务的体系结构。由于这种可扩展的体系结构,将不会有 OLE 3.0 或任何其他新版本的 OLE 模型。OLE 2.0 不仅提供了编写组件软件的规范,还实现了一些服务来简化这项技术的使用。这些服务包括 OLE 拖放、OLE 自动化、OLE 控件等。MFC 将 OLE API 封装在 C++ 类库中,从而使程序员更容易使用 OLE 体系结构。意识到 OLE 这个术语未能充分传达该技术的全部潜力,微软已将其更名为 ActiveX。如果应用程序由单个单体二进制文件组成,那么对操作系统、硬件或客户需求的任何更改都将需要重新编译该应用程序。相反,如果将应用程序分解为单独的块(组件),则可以轻松地用新组件替换现有组件。
为什么选择 COM?
当今计算的整个理念是从可重用的、可维护的代码(最好是由他人编写的)快速地组装应用程序。面向对象的语言有助于开发人员编写可重用、可维护的代码。面向对象技术的优势显而易见。然而,面向对象的语言不足以实现广泛的重用。这是因为世界各地的开发人员使用不同的语言进行编程。通过单一编程语言开发所有应用程序是一个永远不会实现的梦想。原因如下。
- 某些语言比其他语言更适合特定的问题领域。
- 一些程序员对某种特定语言有天然的偏好,因为这种语言符合他们的思维方式。
- 某些语言比其他语言更能利用某些硬件的功能。
- 可能会出现新的语言来利用新的处理器功能。
教训!我们使用并且将继续使用多种语言编写程序。尽管使用多种语言有其好处,但在集成用不同语言开发的 कोड 时,会遇到严重的问题。例如,Java 类对 C++ 开发人员几乎没有用。同样,一段 Visual Basic 代码对 COBOL 程序员也无济于事。此外,还有五年后可能会出现的系统语言。
所有这些都导致需要一个系统,该系统允许开发人员用他们选择的语言编写代码,并且仍然可以使代码在任何其他语言中重用。这正是 COM 所允许我们做的。COM 提供了一个语言中立的二进制标准来构建组件。
COM 是唯一的解决方案吗?
COM 不是重用已编译代码的第一个也不是唯一的方法。DLL 在 Windows 编程中已经使用了很长时间。然而,DLL 存在一些缺点。它们是:
- 虽然大多数语言都可以调用 DLL,但只有极少数语言允许我们创建它们。
- 当安装一个 DLL 的新版本时,如果对向后兼容性问题不够关注,则存在破坏现有应用程序的非零概率。由于没有制定新版本的标准规则或程序,因此事情出错的频率比不高的。
从好的方面来看,DLL 具有以下优点:
- 它允许在无需重新编译的情况下升级应用程序的某些部分。
- DLL 可以按需加载,因此它不会不必要地占用内存。
- DLL 可以被多个应用程序共享,将 COM 组件打包成 DLL 可以利用 DLL 的这些优点。
DLL 作为 COM 组件
为了利用 DLL 的优点,COM 组件可以打包成 DLL。但是有一个小问题。传统上,DLL 是通过文件名加载的;这意味着如果 DLL 的位置或名称发生更改,应用程序将无法加载该 DLL。这种对客户端对 DLL 文件名的依赖也使得无法在同一系统上提供不同版本的 DLL,并可能导致不同供应商产品之间的冲突。COM 通过在系统注册表中以标识符 (ID) 注册 COM 组件的位置来解决此问题。COM 组件的 ID 保证是唯一的,因此避免了与不同产品的冲突,并且客户端无需了解组件的物理位置。
COM 是什么
程序员经常能够编写 COM 组件,但仍然对 COM 的真正含义感到模糊。所以,让我们给 COM 下一个单行定义。COM 是一套规范和一套服务,允许我们创建语言独立、模块化、面向对象、分布式、可定制和可升级的应用程序。
现在让我们逐字拆解这个定义。
COM 是一项规范
COM 规范由一套服务或 API 支持。在 Win32 平台上,这些服务作为操作系统的一部分以 COM 库的形式提供。在其他操作系统中,它们作为单独的包提供。
COM 组件是语言独立的
COM 是一个二进制标准。只要组件遵循 COM 规定的标准内存布局,就可以用任何编程语言编写。在撰写本文时,支持 COM 的语言和工具数量包括 C、C++、Java、JScript、Visual Basic、VBScript、Delphi、PowerBuilder、COBOL 等。
COM 允许模块化编程
COM 组件可以作为 DLL 或可执行文件分发。不同模块中的组件可以通过 COM 提供的通信机制相互通信。
COM 是面向对象的
COM 组件就像普通对象一样。它们具有标识、状态和行为。COM 组件以及 COM 接口支持封装和多态的概念。
COM 易于定制
COM 组件是动态链接在一起的。此外,还有一种标准的定位组件的方法。因此,我们可以在不重新编译整个应用程序的情况下替换现有组件。
COM 支持分布式应用程序
组件可以透明地重新定位到远程计算机,而不会影响客户端,即本地和远程组件以相同的方式处理。这使得分布式计算非常容易。
COM 不是什么
- COM 不是一种计算机语言。事实上,COM 可以通过多种语言来实现。
- COM 不会取代 DLL。事实上,组件经常被实现为 DLL,以利用 DLL 的动态链接能力。
- COM 不代表 OLE 控件(那是 ActiveX)、复合文档(那是 OLE DB 和 ADO)或游戏和图形编程(那是 DirectX)。然而,所有这些技术都基于 COM。
标准的需求
为了使软件可定制,有必要重用已编写的软件片段。随着软件复杂性的增加,有必要将开发工作分配给不同的人/组织。每个人都可以并行开发不同的组件。除非这些组件是根据某种标准编写的,否则很难将它们集成到程序中。如果软件组件可以以统一的方式处理,那么开发和组装它们将很容易。要构建此类组件,我们需要像 OLE 这样的软件集成标准。OLE 组件可以以标准方式创建。它可以动态链接,因此可以以二进制格式分发。它可以用任何语言进行编程。在不重新编译应用程序的情况下,新组件可以替换现有组件。
COM 的好处
- 由于所有 COM 组件的行为方式都相同,因此使用现成的组件库可以非常容易地组装新应用程序。
- 为不同需求定制应用程序也非常容易。
COM 接口
COM 接口是一组相关函数,它们构成了客户端和服务器之间通信的手段。DLL 通过导出的函数发布其服务,而 COM 组件通过实现一个或多个 COM 接口来提供其服务。客户端通过 COM 接口连接到组件(服务器)。如果组件在不更改接口的情况下更改,客户端根本不需要更改。同样,如果客户端在不更改接口的情况下更改,组件也不需要更改。
COM 接口定义为从另一个名为 IUnknown
的接口派生的 C++ 抽象基类。接口支持的每个函数都是纯虚函数。接口本身不提供其成员函数的实现。如果你想在应用程序中使用这些函数,你必须找到一个实现该接口的 COM 组件。在这种情况下,你的应用程序将成为 COM 组件的客户端,通过接口与组件通信。
COM 接口有什么特别之处?接口中的函数与 DLL 导出的函数有何不同?COM 接口遵循以下使其非常特别的规则:
- 每个接口都由一个唯一的 128 位标识符(称为接口标识符或 IID)标识。
- 接口是不可变的。一旦发布了接口,就不能更改它。对现有接口的任何更改都会产生一个新接口,并为其分配一个新的 IID。这确保了新接口永远不会与旧接口冲突。它还使版本管理变得简单明了。
- 每个接口都必须遵循标准的内存布局。此要求对于使 COM 接口的实现语言独立至关重要。只要所使用的特定编译器遵循 COM 指定的内存布局,程序员就可以自由地使用任何编程语言(C、C++、Java 等)来实现函数。VC 编译器生成的内存布局遵循规范。
什么是 ATL?
ATL 代表 Active Template Library。但是,这个缩写并不完全说明 ATL 包含什么。ATL 中的 Active 这个词实际上是微软旧营销策略的遗留物,当时与 COM 相关的一切都是 ActiveX。如今,ActiveX 的含义仅限于控件。可以使用 ATL 开发这些控件。但是,ATL 的功能远不止构建 ActiveX 控件。
可以使用 MFC 开发 COM 组件。但是,这些组件存在两个限制:
- COM 组件的大小通常会变得异常大。这是因为大多数使用 MFC 开发的 COM 组件都需要 mfc42.dll 等运行时库来执行。将此库与 COM 对象捆绑在一起会导致文件大小非常大。当通过网络使用此类组件时,性能会受到影响。
- 更改组件的默认功能变得非常困难。
与使用 MFC 相比,如果我们使用 Active Template Library (ATL),我们可以轻松构建小型、自包含、可分发的 COM 对象。ATL 使用 C++ 模板库,专门用于促进 COM 对象的创建。除了 COM 组件之外,使用 ATL 我们还可以创建自动化服务器和 ActiveX 控件。
ATL 通过提供以下内容促进组件开发:
- 处理数据类型(如接口指针、
VARIANT
s、BSTR
s 和HWND
s)的类。 - 提供基本 COM 接口(如
IUnknown
、IClassFactory
、IDispatch
等)实现的类。 - 用于管理 COM 服务器的类,即用于公开类对象、自注册和服务器生存期管理。
- 用于简化 COM 开发的向导。
使用 ATL
废话少说,现在让我们尝试使用 ATL 开发一个 COM 服务器。在 ATL 中创建组件的过程包括三个步骤:
- 创建模块
- 向模块添加组件
- 向组件添加方法
我们将尝试创建一个名为 AtlServer 的模块(通常是 DLL)。然后,我们将在此服务器中创建一个名为 MyMath
的组件。最后,我们将向其添加两个方法 MyAdd()
和 MySubtract()
。第一个方法将对客户端传递给它的两个整数进行相加,并返回结果整数。同样,第二个方法将相减两个数字并返回结果。现在让我们执行这些步骤。
创建模块
要创建模块,Developer Studio 提供了 ATL COM AppWizard。创建模块是最容易的工作之一。执行以下步骤:
- 从“文件”菜单中选择“新建”。将弹出如下所示的对话框。选择“ATL COM AppWizard”作为项目。键入 AtlServer 作为项目名称,然后单击“确定”继续。
- 选择模块类型为“动态链接库”(参考下图)并单击“完成”。
现在,将显示“新建项目信息”对话框,其中列出了向导将创建的文件名。单击“确定”。
如果我们使用 F7 构建项目,将创建一个 AtlServer.dll 文件,但由于我们尚未向其中添加任何组件,因此它将什么都不做。下一步将显示如何执行此操作。
向模块添加组件
要向模块添加组件,我们可以使用“ATL 对象向导”。执行以下步骤,使用此向导添加名为 MyMath
的组件:
- 选择“插入 | 新 ATL 对象...”菜单项。这将显示如下所示的“ATL 对象向导”对话框。
- 从各种对象类别中选择“简单对象”,然后单击“下一步”。
- 将显示如下所示的“ATL 对象向导属性”对话框。
属性表包含两个选项卡:名称和属性。名称选项卡分为两个部分。第一部分显示“C++ 名称”,第二部分显示“COM 名称”。输入“短名称”为 MyMath。一旦您这样做,所有其他编辑控件将自动填充。
“C++ 名称”编辑控件下填充的名称表示类
CMyMath
将在 MyMath.h 和 MyMath.cpp 文件中实现对象MyMath
。“COM 名称”编辑控件下填充的名称表示
CoClass
名称(组件类)与短名称相同。接口名称将是ImyMath
。类型是类的描述。ProgID 将是 <项目工作区名称>.<CoClass 名称>。单击“确定”按钮后,将创建类
CMyMath
和接口IMyMath
。可以从类视图选项卡中查看它们。
向组件添加方法
已添加的组件不包含任何功能。要提供功能,我们应该添加两个方法 MyAdd()
和 MySubtract()
,如下所示:
- 切换到类视图选项卡并展开树。选择 按钮。在弹出的菜单中,选择“添加方法”。请注意,如果展开
CMyMath
树,您会发现另一个IMyMath
。可以使用任何一个来添加方法。 - 在“向接口添加方法”对话框中,将方法名称指定为
MyAdd
,并将参数指定为[in] int n1, [in] int n2, [out, retval] int *n3
。单击“确定”。这里,[in]
指定要传递给方法的参数,[out, retval]
指定返回值。 - 对
MySubtract
方法执行类似的操作,该方法的参数与MyAdd
相同。
添加 MyAdd
方法会在 MyMath.cpp 中创建一个函数定义。要查看代码,请切换到文件视图选项卡,展开树,展开“源文件”树,然后双击 MyMath.cpp 打开它。(通过展开视图选项卡并双击方法名称可以查看相同的方法)。下面显示了 MyAdd
方法:
STDMETHODIMP CMyMath::MyAdd(int n1,int n2, int *n3) { // TODO: Add your implementation code here return S_OK; } //Add the following line to the MyAdd() method: *n3=n1+n2; //and add the following line to the MySubract() method: *n3=n1-n2;
现在,如果我们构建项目,将创建一个 DLL 文件,其中包含具有 MyAdd()
和 MySubtract()
方法的组件。现在让我们构建一个客户端来调用这些方法。
创建 COM 客户端
我们将把客户端开发成一个简单的基于对话框的应用程序。构建客户端涉及的步骤如下:
- 创建项目
- 使用 AppWizard(EXE) 创建一个基于对话框的项目 AtlClient。
- 向对话框添加三个编辑框和三个按钮,如下图所示:
导入类型库
将服务器的类型库导入到客户端。这可以通过在最后一条 #endif
语句之前,将以下两条语句添加到 StdAfx.h 文件中来完成。
#import "..\AtlServer\AtlServer.tlb" using namespace ATLSERVERLib;
请注意,客户端应在与服务器相同的父目录中创建。这是必要的,因为在 #import
语句中,我们指定了 "..\\AtlServer\AtlServer.tlb";这意味着在 AtlServer 目录中查找,如果它位于其他位置,请在 #import
语句中指定完整路径。
当我们编译客户端项目时,#import
关键字会在一个扩展名为 '.tlh' 的文件中创建包装器类的头文件,并在一个扩展名为 '.tli' 的文件中创建实现文件。这些文件位于客户端项目工作区的 Debug 目录中。包装器类包含服务器的信息,这些信息在客户端访问服务器中定义的成员函数时使用。此包装器类嵌入在名为 ATLSERVERLib
的命名空间中。
初始化 COM 库
初始化 COM 库允许应用程序调用 COM 函数。客户端可以通过 COM 运行时库与服务器进行交互。要初始化 COM 库,请调用 CoInitialize()
函数,如下所示:
BOOL CAtlClientDlg::OnInitDialog() { //AppWizard generated code //TODO:Add extra initialization here CoInitialize(NULL); }
检索服务器的 CLSID
为了能够实例化服务器,我们需要检索其 CLSID
。由于很难记住 CLSID
,因此可以通过 ProgID 使用 CLSIDFromProgID()
函数检索它,如下所示:
BOOLCAtlClientDlg::OnInitDialog() { //AppWizard generated code // TODO: Add extra initialization here CoInitialize(NULL); CLSID clsid; HRESULT hr; hr= CLSIDFromProID(OLESTR(“AtlServer.MyMath”),&clsid); if(FAILED(hr)) MessageBox(“Unable to access server”); } //Here OLESTR() converts the character string to the ProgID format.
创建组件对象
使用组件的 CLSID
,创建 COM 服务器组件的实例。CoCreateInstance()
函数用于此目的,如下所示:
BOOL CAtlClientDlg::OnINitDialog() { //AppWizard generated code //TODO:Add extra initialization here //calls to CoInitialize and CLSIDFromProgID() hr=CoCreateInstance(clsid, NULL, CLSCTX_INPROC_SERVER, __uuidof(IMyMath),(void **)&math); if(FAILED(hr)) MessageBox(“Cocreate failed”); }
调用 CoCreateInstance()
会创建一个 IMyMath
组件实例,并在指针 math 中返回接口地址。参数 CLSCTX_INPROC_SERVER
表示我们的服务器是一个 DLL。__uuidof()
函数已用于获取接口的 ID。将 math
添加为 IMyMath *
的私有变量。
使用 COM 对象
现在我们已经获得了接口指针,客户端应用程序可以在“Add”和“Subtract”按钮的处理程序中调用 COM 服务器对象的成员函数,然后在出现的对话框中单击“确定”。它们的代码如下:
Void CAtlClientDlg::OnAdd() { int n1,n2,n3; n1=GetDigItemInt(IDC_EDIT1); n2=GetDigItemInt(IDC_EDIT2); n3=math->MyAdd(n1,n2); SetDlgItemInt(IDC_EDIT3,n3); } Void CAtlClientDlg::OnSubtract() { int n1,n2,n3; n1=GetDigItemInt(IDC_EDIT1); n2=GetDigItemInt(IDC_EDIT2); n3=math->MySubtract(n1,n2); SetDlgItemInt(IDC_EDIT3,n3); }
请注意,COM 服务器对象的成员函数 MyAdd()
和 MySubtract()
是使用在调用 CoCreateInstance()
中获得的接口指针 math 来调用的。
取消初始化 COM 对象
这可能是最简单的工作。只需在 InitInstance()
函数的末尾调用 CoUninitialize()
函数,如下所示:
BOOL CAtlClientApp::InitInstance()
{
// AppWizard generated code
CoUninitialize();
}
这样,我们就完成了客户端的创建。现在,您可以编译并执行客户端,并检查它是否能够与服务器中的成员函数进行交互。
一个警告!一旦运行客户端,它就会将服务器 DLL 加载到内存中。现在,如果您想对服务器进行任何更改,则必须关闭客户端,以便客户端使用的服务器 DLL 将从内存中卸载。如果您不这样做,DLL 将保留在内存中,任何尝试修改服务器的操作都将导致编译错误“无法写入 Debug\AtlServer.Dll”。
还有另一篇文章介绍了如何在 VC++ 中编写 COM 程序并在 VB 中使用它。了解其重要性 [ComCalCulator]。这个简单的程序可以帮助您理解 COM 编码并在 VB 中使用它。