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 没有垃圾回收器。它通过 AddRef 和 Release 方法(继承自 IUnknown)来管理对象的生命周期。
- 当客户端从 COM 获取一个接口指针时,内部计数加 1。
- 当客户端使用完毕后,必须调用
Release。 - 当计数归零时,对象在内存中自我销毁。
3. 三个基础接口:IUnknown
任何 COM 对象都必须实现 IUnknown,它包含三个方法:
-
QueryInterface:这是 COM 的"多态"机制。给定一个 IID(接口 ID),询问对象:"你支持这个接口吗?"- 如果支持,返回该接口指针(并自动调用 AddRef)。
- 如果不支持,返回
E_NOINTERFACE。
这允许对象在不同的时间提供不同的功能集,而不需要强制继承庞大的基类。
-
AddRef:增加引用计数。 -
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 年代的方式手动编写所有的 AddRef 和 QueryInterface 逻辑。现代生态系统中 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 的关键在于掌握三点:
- 二进制标准:内存布局固定(虚函数表),因此语言无关。
- 接口隔离 :通过
QueryInterface动态获取能力,而不是通过对象本身调用方法。 - 严格的契约:引用计数(谁创建谁释放)、不可变接口、注册表定位。
虽然直接从头编写原始 COM 组件在当今的常规应用开发中已不常见,但当你进行 Windows 底层开发、编写 Shell 扩展、Office 插件或使用 DirectX 时,深入理解 COM 依然是避免内存泄漏和界面卡顿的必备知识。