COM(Component Object Model,组件对象模型) 是微软于1993年推出的一种二进制接口标准。它允许不同语言(C++, C#, VB, Delphi等)编写的软件组件在同一个操作系统上进行交互。

虽然现在有更新的技术(如 .NET),但 COM 依然是 Windows 操作系统的基石。你使用的很多核心功能(如 DirectX、Office 自动化、Windows Shell 扩展、音频设备控制)底层都依赖 COM。

以下是 COM 对象的核心基础知识:

1. 核心设计哲学

COM 的核心不是编程语言,而是二进制规范。这意味着你用不同语言编译出来的 DLL,只要遵循 COM 的内存布局约定,就能互相调用,无需源码。

  • 语言无关性:C++ 写的 COM 组件可以被 VB6 或 C# 调用。
  • 位置透明性:调用者不需要知道组件是在当前 DLL(进程内)中,还是在另一个进程(进程外)甚至远程机器上(DCOM)。
  • 版本兼容性:通过接口的不可变性来管理版本(一旦发布接口,永不修改)。

2. 核心概念

接口

这是 COM 中最关键的概念。COM 对象通过接口暴露功能,对象本身被隐藏。

  • 接口是一组纯虚函数的集合(在 C++ 中对应 struct 虚函数表)。
  • 接口不变性:一旦一个接口被发布(如 IMyInterface v1.0),就永远不能修改它。如果需要新功能,必须创建新接口(IMyInterface v2.0)。
  • IUnknown:所有 COM 接口的基类。每个 COM 对象都必须实现 IUnknown
GUID(全局唯一标识符)

COM 使用 128 位的 GUID 来唯一标识:

  • CLSID:标识具体的组件类(如 {00024500-0000-0000-C000-000000000046} 是 Microsoft Word 的类 ID)。
  • IID:标识接口(如 IID_IClassFactory)。
引用计数

COM 没有垃圾回收器。它通过 AddRefRelease 方法(继承自 IUnknown)来管理对象的生命周期。

  • 当客户端从 COM 获取一个接口指针时,内部计数加 1。
  • 当客户端使用完毕后,必须调用 Release
  • 当计数归零时,对象在内存中自我销毁。

3. 三个基础接口:IUnknown

任何 COM 对象都必须实现 IUnknown,它包含三个方法:

  1. QueryInterface:这是 COM 的“多态”机制。给定一个 IID(接口 ID),询问对象:“你支持这个接口吗?”

    • 如果支持,返回该接口指针(并自动调用 AddRef)。
    • 如果不支持,返回 E_NOINTERFACE
      这允许对象在不同的时间提供不同的功能集,而不需要强制继承庞大的基类。
  2. AddRef:增加引用计数。

  3. Release:减少引用计数。

4. 创建对象:类厂

通常不允许直接用 new 关键字创建 COM 对象(因为对象可能在进程外或远程)。COM 使用 类厂 模式:

  • 每个 COM 类都有一个关联的类厂(Class Factory),它实现了 IClassFactory 接口。
  • 客户端通过调用 API(如 CoCreateInstance)告诉 COM 库:“请根据这个 CLSID 创建对象。”
  • COM 库在注册表中查找该 CLSID 对应的 DLL/EXE,加载它,调用类厂的 CreateInstance 方法,最终返回对象指针。

5. 运行环境:套间

COM 必须处理多线程问题。它定义了 套间 模型来管理线程安全:

  • STA(单线程套间)
    • 一个线程拥有一个套间。
    • COM 会自动对调用进行同步(序列化)。如果一个线程试图调用另一个 STA 线程的对象,调用会通过消息队列排队。
    • 常用于 UI 线程。
  • MTA(多线程套间)
    • 多个线程可以同时进入套间。
    • 对象必须自己实现线程安全(同步)。
    • 调用直接进行,无同步。

6. 注册表

COM 严重依赖 Windows 注册表。当你在代码中调用 CoCreateInstance(CLSID_MyComponent) 时,COM 会去 HKEY_CLASSES_ROOT\CLSID\{...} 下查找:

  • InprocServer32:指向 DLL 的路径(进程内组件)。
  • LocalServer32:指向 EXE 的路径(进程外组件)。
  • ProgID:便于阅读的字符串(如 “Word.Application”)。

7. 常用 API

如果你在 Windows 上用 C++ 开发,通常会用到以下 API:

  • CoInitialize / CoUninitialize:初始化 COM 库。必须在当前线程使用 COM 前调用。
  • CoCreateInstance:最常用的创建函数。内部封装了“查找注册表 -> 启动服务器 -> 获取类厂 -> 创建对象”的流程。
  • CoCreateInstanceEx:支持创建远程对象(DCOM)。

8. 演化与现状

你不需要以 1990 年代的方式手动编写所有的 AddRefQueryInterface 逻辑。现代生态系统中 COM 以不同形式存在:

  • Win32 C++:使用 CComPtr(ATL 库)等智能指针来避免手动调用 Release 导致的内存泄漏。
  • .NET (C#)
    • Interop:通过添加 COM 引用,.NET 运行时作为“包装器”自动处理底层接口调用。
    • WinRT:Windows Runtime 是 COM 的现代化演进,它基于 COM 基础,但增加了元数据(.winmd)、异步模式(IAsyncInfo)和更现代的语言投影,使得 API 在 C++/WinRT 或 C# 中调用起来更自然。
  • 系统组件:许多现代 Windows API(如 Windows.Storage)实际上是 WinRT(COM 的继承者)暴露的接口。

总结

理解 COM 的关键在于掌握三点:

  1. 二进制标准:内存布局固定(虚函数表),因此语言无关。
  2. 接口隔离:通过 QueryInterface 动态获取能力,而不是通过对象本身调用方法。
  3. 严格的契约:引用计数(谁创建谁释放)、不可变接口、注册表定位。

虽然直接从头编写原始 COM 组件在当今的常规应用开发中已不常见,但当你进行 Windows 底层开发、编写 Shell 扩展、Office 插件或使用 DirectX 时,深入理解 COM 依然是避免内存泄漏和界面卡顿的必备知识。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐