COM 技术概述

本主题概述 Microsoft 组件对象模型 (COM):

COM 简介

Microsoft 组件对象模型 (COM) 定义了二进制互操作性标准,可利用此标准创建在运行时交互的可重用软件库。 无需将 COM 库编译到应用程序中即可使用 COM 库。 COM 是许多 Microsoft 产品和技术的基础,例如 Windows Media Player 和 Windows Server。

COM 定义适用于许多操作系统和硬件平台的二进制标准。 对于网络计算,COM 定义了标准线路格式和协议,用于在不同硬件平台上运行的对象之间交互。 COM 独立于实现语言,这意味着可以使用不同的编程语言(如 C++ 和 .NET Framework 中的编程语言)创建 COM 库。

COM 规范提供了启用跨平台软件重用的所有基本概念:

  • 组件之间的函数调用的二进制标准。
  • 用于将函数强类型分组到接口中的预配。
  • 提供多态性、功能发现和对象生存期跟踪的基接口。
  • 唯一标识组件及其接口的机制。
  • 从部署创建组件实例的组件加载程序。

COM 有许多部件协同工作,从而支持创建从可重用组件生成的应用程序:

  • 提供符合 COM 规范的运行时环境的主机系统
  • 定义功能协定的接口和实现接口的组件
  • 向系统提供组件的服务器和使用组件提供的功能的客户端
  • 用于跟踪组件在本地和远程主机上的部署位置的注册表
  • 用于找到本地和远程主机上的组件并将服务器连接到客户端的服务控制管理器
  • 定义如何在主机文件系统上导航文件内容的结构化存储协议。

启用跨主机和平台的代码重用是 COM 的核心。 可重用接口实现命名为组件组件对象COM 对象。 一个组件实现一个或多个 COM 接口。

通过设计库实现的接口,可定义自定义 COM 库。 库的使用者无需了解库的部署和实现详细信息即可发现和使用其功能。

对象和接口

COM 对象通过接口公开其功能,接口是成员函数的集合。 COM 接口定义组件的预期行为和职责,并指定一个强类型协定,该协定提供一小组相关操作。 COM 组件之间的所有通信都通过接口进行,并且通过组件接口公开组件提供的所有服务。 调用方只能访问接口成员函数。 除非在接口中公开内部状态,否则调用方无法使用内部状态。

接口是强类型。 每个接口都有自己的唯一接口标识符(名为 IID),这样就消除了与人类可读名称可能发生的冲突。 IID 是全局唯一标识符 (GUID),此标识符与开放软件基金会 (OSF) 分布式计算环境 (DCE) 定义的通用唯一 ID (UUID) 相同。 创建新接口时,必须为该接口创建新的标识符。 当调用方使用接口时,它必须使用唯一标识符。 此显式标识可消除会导致运行时失败的命名冲突,从而提高稳健性。

定义新接口时,可以使用接口定义语言 (IDL) 创建接口定义。 从此接口定义中,Microsoft IDL 编译器会生成标头文件供使用接口的应用程序使用,并生成源代码来处理远程过程调用。 Microsoft 提供的 IDL 基于 DCE IDL 的简单扩展,这是基于远程过程调用 (RPC) 的分布式计算的行业标准。 IDL 是可方便接口设计人员的工具,不是 COM 互操作性的核心。 使用 IDL 时,无需为每个编程环境手动创建标头文件。 有关详细信息,请参阅定义 COM 接口

在 COM 接口中很少使用继承。 COM 仅支持接口继承以重用与基接口关联的协定。 COM 不支持选择性继承;因此,如果一个接口继承自另一个接口,则它包括基接口定义的所有函数。 此外,接口仅使用单重继承而不是多重继承从基接口获取函数。

接口实现

不能单独创建 COM 接口的实例。 而是创建实现接口的类实例。 在 C++ 中,COM 接口建模为抽象基类,这意味着接口是仅包含纯虚拟成员函数的 C++ 类。 C++ 库会从一个或多个接口继承成员函数签名、覆盖每个成员函数并为每个函数提供实现,从而实现 COM 对象。

可以使用任何支持函数指针概念的编程语言来实现 COM 接口。 例如,在 C 中,接口是一个结构,其中包含指向函数指针表的指针,此结构用于接口中的每个方法。

实现接口时,类必须为接口中的每个函数提供实现。 如果类在接口函数中无事可做,则实现可以是单个 return 语句。

COM 类可使用唯一的 128 位类 ID (CLSID) 标识,该 ID 将类与文件系统中的特定部署相关联,对于 Windows 而言这是 DLL 或 EXE。 CLSID 是 GUID,这意味着没有其他类具有相同的 CLSID。 使用唯一类标识符可防止类之间发生名称冲突。 例如,两个不同的供应商可以编写名为 CStack 的类,但两个类都具有唯一的 CLSID,因此可避免出现任何冲突。

可以使用 CoCreateGuid 函数或使用在内部调用此函数的 COM 创作工具(如 Visual Studio)获取新的 CLSID。

IUnknown 接口

所有 COM 接口都继承自 IUnknown 接口。 IUnknown 接口包含基本 COM 操作,可实现多态性和用于实例生存期管理。 IUnknown 接口有三个成员函数,名为 QueryInterfaceAddRefRelease。 要实现 IUnknown 接口,需要所有 COM 对象。

QueryInterface 成员函数为 COM 提供多态性。 调用 QueryInterface 以在运行时确定 COM 对象是否支持特定接口。 如果 COM 对象实现请求的接口,则返回 ppvObject 参数中的接口指针,否则返回 NULLQueryInterface 成员函数支持在 COM 对象支持的所有接口之间导航。

COM 对象实例的生存期由其引用计数控制。 IUnknown 成员函数 AddRefRelease 控制计数。 AddRef 会递增计数,而 Release 会递减计数。 当引用计数达到零时,Release 成员函数可能会释放实例,因为没有调用方在使用它。

客户端/服务器模型

一个 COM 类可实现许多 COM 接口。 实现由调用方与 COM 类实例交互时运行的二进制文件组成。 COM 支持在不同应用程序中使用类,包括在不了解特定类的情况下编写的应用程序。 在 Windows 平台上,类存在于动态链接库 (DLL) 或其他应用程序 (EXE) 中。

在其主机系统上,COM 维护系统上安装的 COM 对象的所有 CLSID 的注册数据库。 注册数据库是每个 CLSID 与存放相应类的 DLL 或 EXE 的位置之间的映射。 只要调用方想要创建 COM 类的实例,COM 就会查询此数据库。 调用方只需知道 CLSID 即可请求类的新实例。

COM 对象与其调用方之间的交互建模为客户端/服务器关系。 客户端是从系统请求 COM 对象的调用方,而服务器是存放向客户端提供服务的 COM 对象的模块。

COM 客户端是将 CLSID 传递给系统以请求 COM 对象实例的任何调用方。 创建实例的最简单方法是调用 COM 函数 CoCreateInstance

CoCreateInstance 函数会创建指定 CLSID 的一个实例,并返回客户端所请求类型的接口指针。 客户端负责管理实例的生存期,方法是在客户端使用完实例后调用其 Release 函数。 若要根据单个 CLSID 创建多个对象,请调用 CoGetClassObject 函数。 若要连接到已创建且正在运行的对象,请调用 GetActiveObject 函数。

COM 服务器向系统提供 COM 实现。 服务器将 CLSID 与 COM 类相关联,存放类的实现,实现用于创建类实例的类工厂,并可用于卸载服务器。

注意

COM 服务器与提供给系统的 COM 对象不同。

 

若要启用创建 COM 对象,COM 服务器必须提供 IClassFactory 接口的实现。 客户端可以调用 CreateInstance 方法,以请求 COM 对象的新实例,但通常将此类请求封装在 CoCreateInstance 函数中。

可以将 COM 服务器部署为在运行时加载到客户端进程中的共享库(Windows 平台上的 DLL),或部署为可执行模块(Windows 平台上的 EXE)。 有关详细信息,请参阅注册 COM 应用程序

服务控制管理器

服务控制管理器 (SCM) 会处理 COM 对象实例的客户端请求。 以下列表显示了事件序列:

  • 客户端会使用 COM 对象的 CLSID 调用 CoCreateInstance 等函数,以从 COM 库中请求指向 COM 对象的接口指针。
  • COM 库会查询 SCM,以查找与所请求的 CLSID 对应的服务器。
  • SCM 会找到服务器,并从服务器提供的类工厂请求创建 COM 对象。
  • 如果成功,COM 库将返回指向客户端的接口指针。

COM 系统将服务器对象连接到客户端后,客户端和对象将直接通信。 通过中间运行时调用不会增加开销。

将向主机系统注册 COM 服务器时,可以为要激活的服务器指定不同的方式。 以下列表显示了 SCM 可用于激活 COM 服务器的三种方法:

  • 进程内:SCM 返回包含对象服务器实现的 DLL 的文件路径。 COM 库会加载 DLL 并在其中查询其类工厂接口指针。
  • 本地:SCM 启动本地可执行文件,该可执行文件会在启动时注册类工厂,并且其接口指针可供系统和客户端使用。
  • 远程:本地 SCM 从正在远程计算机上运行的 SCM 获取类工厂接口指针。

当客户端请求 COM 对象时,COM 库会联系本地主机上的 SCM。 SCM 会找到可能为本地或远程的相应 COM 服务器,并且服务器将返回指向服务器的类工厂的接口指针。 当类工厂可用时,COM 库或客户端可以使用类工厂创建请求的对象。 有关详细信息,请参阅实现 IClassFactory

可重用性

COM 支持黑盒可重用性,这意味着不会将可重用组件的实现详细信息向客户端公开。 为了实现黑盒可重用性,COM 支持两种机制,通过这两种机制一个对象可以重用另一个对象。 这两种重用形式名为包含聚合。 按照约定,要重用的对象名为内部对象,而在使用内部对象的对象名为外部对象

在包含中,外部对象充当内部对象的客户端。 外部对象是内部对象的逻辑容器,并且当外部对象使用内部对象的服务时,外部对象会将实现委托给内部对象的接口。 这意味着,外部对象是在内部对象的服务中实现的。 外部对象可能不支持与内部对象相同的接口,并且外部对象可以使用内部对象的接口,帮助实现外部对象上不同接口的各个部分。

在聚合中,外部对象会公开来自内部对象的接口,就像在外部对象上实现接口一样。 当外部对象始终将其接口上的每次调用委托给内部对象中的相同接口时,这种方法就非常有用。 聚合是一种方便的方法,使外部对象能够避免额外的实现开销。

有关详细信息,请参阅重用对象

存储和流对象

COM 对象使用结构化存储将状态保存到文件,这是一种持久存储形式,支持使用文件系统语义导航文件内容。 以这种方式处理文件内容时,可启用进程之间的增量访问、事务和共享等功能。

COM 持久存储规范提供两种类型的存储元素:存储对象和流对象。 这些对象将由 COM 库实现,用户应用程序很少实现这些存储元素。 存储对象实现 IStorage 接口,而流对象实现 IStream 接口。

流对象包含数据,在概念上类似于文件系统中的单个文件。 每个流都具有访问权限和单个寻道指针。 通过 IStream 接口,可以读取、写入流基础数据,以及为这些数据进行寻道和执行其他操作。 流是使用文本字符串命名的。 它可以包含任何内部结构,因为它是一个平面字节流。 此外,IStream 接口中的函数类似于基于标准文件句柄的函数,例如 ANSI C 运行时库中的函数。

存储对象在概念上类似于文件系统中的目录。 每个存储都可以包含任意数量的子存储对象和任意数量的流。 每个存储都有自己的访问权限。 通过 IStorage 接口,可以执行枚举、移动、复制、重命名、创建和删除元素等操作。 存储对象不存储应用程序定义的数据,但会隐式存储包含的元素(存储和流)的名称。

根据主机平台上的 COM 规范实现存储和流对象时,可在进程之间共享这些对象。 这使进程内或进程外正在运行的对象能够对其文件存储进行同等的增量访问。 由于单独将 COM 加载到每个进程中,因此它使用操作系统支持的共享内存机制,传达打开的元素的状态及其在进程之间的访问模式。

结构化文件中的每个存储和流对象都有一个名称来标识。 该名称是遵循特定约定的字符串。 有关详细信息,请参阅存储对象命名约定。 会将该名称传递给 IStorage 函数,以指定要在其上运行的存储中元素。 根存储对象的名称与基础文件系统中的文件名相同,并且这些名称必须遵循文件系统的约定和限制。 传递给命名文件的存储相关函数的字符串将传递到文件系统,而无需解释或更改。

存储对象中包含的元素的名称可通过实现相关特定存储对象来进行管理。 存储对象的所有实现都必须支持长度为 32 个字符的元素名称,并且某些实现可能支持更长的名称。 名称以保留大小写的形式存储,但比较时不区分大小写。 定义存储元素名称的应用程序必须选择适用于任一情况的名称。

可以使用 COM 实现的函数和接口访问结构化存储文件中的每一个元素。 这意味着,其他应用程序可以使用提供类似目录的服务的 IStorage 接口函数进行导航,籍此浏览文件。 此外,其他应用程序可以使用文件数据,而无需运行写入文件的应用程序。 当 COM 应用程序访问另一个应用程序的结构化存储文件时,标准 Windows 访问权限适用,并且应用程序必须具有足够的特权。

COM 对象可以读取和将自身写入持久存储。 客户端会查询 COM 对象上的其中持久性相关接口,具体取决于操作上下文。 COM 对象可以实现以下接口的任意组合:

  • IPersistStorage:COM 对象会读取并将其持久状态写入存储对象。 客户端通过此接口为对象提供 IStorage 指针。 这是唯一包含增量访问语义的持久性接口。
  • IPersistStream:COM 对象会读取并将其持久状态写入流对象。 客户端通过此接口为对象提供 IStream 指针。
  • IPersistFile:COM 对象直接读取并将其持久状态写入基础系统上的文件。 除非通过这些接口访问基础文件,否则此接口不涉及 IStorageIStream,但 IPersistFile 接口没有存储和流的语义。 客户端会为对象提供文件名,并调用 SaveLoad 函数。

数据传输

结构化存储为 COM 对象和进程之间的数据交换提供了基础,此数据交换称为统一数据传输。 在 OLE 2 中实现 COM 之前,Windows 上的数据传输通过传输协议(例如剪贴板和拖放协议)指定。 每个传输协议都有自己的函数集,这些函数将协议绑定到查询,并且需要特定的代码来处理每个不同的协议和交换过程。 统一数据传输表示使用 IDataObject 接口进行的所有数据传输,该接口会将通用数据交换操作与传输协议分开。

IDataObject 接口会封装有关数据、查询和枚举以及检测对象中数据更改的通知的标准 get 和 set 操作。 统一数据传输支持数据格式的丰富描述,以及使用不同的存储介质进行数据传输。

在统一数据传输期间,所有协议都会交换指向 IDataObject 接口的指针。 服务器是数据源并实现一个数据对象,该对象在任何数据交换协议中都可用。 客户端从任何协议接收 IDataObject 指针时,都会使用数据并从数据对象请求数据。 指针交换完成后,双方通过 IDataObject 接口以统一的方式处理数据交换。

COM 定义了两个支持统一数据传输的数据结构。 FORMATETC 结构表示通用剪贴板格式,而 STGMEDIUM 结构将传输介质表示为内存句柄。

客户端会创建 FORMATETC 结构,以指示它从数据源请求的数据类型,而数据源使用它来描述提供的格式。 客户端会请求数据源的 IEnumFORMATETC 接口,以在数据源中查询可用格式。 有关详细信息,请参阅 FORMATETC 结构

客户端会创建 STGMEDIUM 结构并将其传递给 GetData 方法,而数据对象会采用提供的 STGMEDIUM 结构返回数据。

STGMEDIUM 结构使客户端和数据源都能够选择最高效的交换介质。 例如,如果要交换的数据非常大,则数据源可以指示将基于磁盘的介质作为其首选格式,而不是主内存。 这种灵活性会使得数据交换非常高效,就像将指针传递给 IStorageIStream 那样快。 有关详细信息,请参阅 STGMEDIUM 结构

当数据更改时,数据源的客户端可能需要通知。 COM 使用建议接收器对象处理数据更改通知,该对象将实现 IAdviseSink 接口。 建议接收器对象和 IAdviseSink 接口由客户端实现,客户端会将 IAdviseSink 指针传递给数据源。 当数据源检测到基础数据更改时,它会调用 IAdviseSink 方法来通知客户端。 有关详细信息,请参阅数据通知

远程处理

COM 支持远程和分布式计算。 接口远程处理使成员函数能够返回指向位于不同进程或不同主计算机的 COM 对象的接口指针。 执行接口远程处理的基础结构对客户端和对象服务器都是透明的。 客户端和服务器都不需要彼此的部署详细信息,即可通过远程接口进行通信。 客户端调用同一接口上的成员函数,以与本地主机或远程计算机上的进程内和进程外 COM 对象通信。 客户端无法区分同一接口上的本地和远程调用。

若要与 COM 对象通信,客户端始终将调用进程内实现。 如果 COM 对象是进程内对象,则调用是直接调用。 如果 COM 对象是进程外或远程对象,COM 提供了一个代理实现,该实现将使用远程过程调用 (RPC) 协议将调用转发到对象。

COM 对象始终通过进程内实现从客户端接收调用。 如果调用方是进程内调用方,则调用是直接调用。 如果调用方是进程外或远程调用方,COM 会提供一个存根实现,用于从客户端进程中的代理接收远程过程调用。

封送处理是打包要从代理传输到存根的调用堆栈的过程。 取消封送是在接收端发生的解包。 返回值将被封送处理,并在从存根到代理时取消封送。 这种通信也称为通过线路发送调用。

每种不同的数据类型都有封送处理规则。 接口指针还具有封送处理协议,该协议封装在 CoMarshalInterface 函数中。 在大多数情况下,由系统提供的标准接口封送处理已足够,但 COM 对象可能会根据需要实现自定义接口封送处理,以控制其远程对象代理的创建。 有关详细信息,请参阅对象间通信

安全性

COM 提供两种形式的应用程序安全性。 一种是激活安全性,这种安全性会指定如何创建新对象、客户端如何连接到新对象和现有对象,以及如何保护某些公共服务(如类表和正在运行的对象表)。 另一种是调用安全性,这种安全性会指定安全如何在客户端与 COM 对象之间的已建立连接中运行。

服务控制管理器 (SCM) 会自动应用激活安全性。 当 SCM 收到检索 COM 对象的请求时,它会根据注册表中存储的安全信息检查请求。

SCM 实现通常提供注册表驱动的配置,用于管理部署的类和主机上的特定用户帐户。 有关详细信息,请参阅激活安全性

应用程序会自动应用或强制实施调用安全性。 如果应用程序提供设置信息,COM 将执行必要的检查,以保护应用程序。

自动机制会检查进程的安全性,但不会检查单个对象或方法的安全性。 如果应用程序需要更精细的安全性,COM 会提供应用程序可用于执行自己的安全检查的函数。

可以将自动机制和自定义机制结合使用,因此应用程序可能会要求 COM 执行自动安全检查,然后执行自己的安全检查。

COM 调用安全性服务分为以下几个类别:

通常,客户端会查询 IClientSecurity 接口的 COM 对象,该接口由远程处理层在本地实现。 客户端使用此接口来控制 COM 对象上各个接口代理的安全性,然后再对其中一个接口进行调用。

当调用到达服务器时,服务器可能会调用 CoGetCallContext 函数,以检索 IServerSecurity 接口,这样服务器就可以检查客户端的身份验证,并在必要时模拟客户端。 IServerSecurity 对象在调用期间有效。

调用 CoInitializeSecurity 函数以初始化安全层,并将指定的值设置为安全默认值。 如果进程未调用 CoInitializeSecurity,则 COM 会在首次封送或取消封送接口时自动调用它,从而注册系统默认安全性。 CoInitializeSecurity 函数允许客户端为进程建立默认调用安全性,从而避免在单个代理上使用 IClientSecurityCoInitializeSecurity 函数使服务器能够为进程注册自动身份验证服务。 有关详细信息,请参阅使用 CoInitializeSecurity 设置进程范围安全性

COM 客户端和服务器

定义 COM 接口

注册 COM 应用程序

COM 中的安全性

进程、线程和单元