COM 技术概述

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

COM 简介

Microsoft 组件对象模型 (COM) 定义了一个二进制互操作性标准,用于创建在运行时交互的可重用软件库。 可以使用 COM 库,而无需将它们编译到应用程序中。 COM 是许多 Microsoft 产品和技术的基础,例如 Windows 媒体播放器 和 Windows Server。

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

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

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

COM 具有许多协同工作的部件,用于创建基于可重用组件构建的应用程序:

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

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

通过设计库实现的接口来定义自定义 COM 库。 库的使用者可以在不了解库的部署和实现详细信息的情况下发现和使用其功能。

对象和接口

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

接口是强类型。 每个接口都有其自己唯一的接口标识符(名为 IID),这可以消除与人类可读名称可能发生的冲突。 IID 是 GUID) (全局唯一标识符,它与 Open Software Foundation (OSF) 分布式计算环境 (DCE) 定义的通用唯一 ID (UUID) 相同。 创建新接口时,必须为该接口创建新的标识符。 当调用方使用接口时,它必须使用唯一标识符。 此显式标识通过消除会导致运行时失败的命名冲突来提高可靠性。

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

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

接口实现

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

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

实现接口时,类必须为接口中的每个函数提供实现。 如果类在接口函数中没有工作要做,则实现可能是单个返回语句。

COM 类通过使用唯一的 128 位类 ID (CLSID) 来标识,该类将类与文件系统中的特定部署相关联,对于 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 成员函数可能会释放实例,因为没有调用方使用它。

客户端/服务器模型

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 接口封装了对数据、查询和枚举以及检测对象中的数据更改时间的通知的标准获取和设置操作。 统一数据传输支持对数据格式的丰富描述,以及使用不同的存储媒体进行数据传输。

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

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

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

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

STGMEDIUM 结构使客户端和数据源能够选择最高效的交换介质。 例如,如果要交换的数据非常大,则数据源可以指示基于磁盘的媒体作为其首选格式,而不是main内存。 这种灵活性可实现高效的数据交换,速度与将指针传递到 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 设置Process-Wide安全性

COM 客户端和服务器

定义 COM 接口

注册 COM 应用程序

COM 中的安全性

进程、线程和单元