【COM API】基础知识与使用方法

本文最后更新于:7 天前

初识 COM API

和 WinAPI 类似,COM API 也提供了一系列函数供应用程序调用来实现功能。两者都属于 Windows API 范畴,但在调用方式上有着很大的不同。

WinAPI COM API
调用 DLL 中导出的函数 通过接口指针调用虚函数
指定函数名就可以调用 指定 CLSID 和 IID 进行初始化
调用完不需要释放函数 调用完需手动释放以及销毁接口指针

从现在开始,本文将向大家讲述有关 COM API 的基础知识以及使用模板,我们将使用 C++ 将 COM API 封装为 WinAPI 供 C# 或其他程序调用 (C# 本来也可以初始化和使用 COM API,但不是本文重点)。

HRESULT

HRESULT 几乎是所有 COM API 函数的返回类型,用于表示函数是否执行成功。可以把它类比为 BOOL,因为它们本质上都是整型 (long 和 int)。但两者在真假判定上也有区别。

BOOL HRESULT 真假
!= 0 >= 0 true
== 0 < 0 false

那我们怎么在编程中判断两者的真假呢?

BOOL:直接写在 if 表达式里就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BOOL b = ...;

if (b)
{
// true
}
else
{
// false
}

if (!b)
{
// false
}
else
{
// true
}

HRESULT:使用 SUCCEEDED 或 FAILED 宏,两者分别用于判断是否执行成功或失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
HRESULT hr = ...;

if (SUCCEEDED(hr))
{
// true
}
else
{
// false
}

if (FAILED(hr))
{
// false
}
else
{
// true
}

初始化/取消初始化 COM 运行时

在初始化 COM API 前,我们需要初始化 COM 运行时。一旦初始化之后且离开作用域,我们必须取消初始化 COM 运行时。

也就是两个步骤必须成对进行,在哪里初始化的,就必须要在哪里取消初始化,二者缺一不可。

1
2
3
4
5
6
7
8
9
10
11
#include <combaseapi.h>

void InitializeCom()
{
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
}

void UninitializeCom()
{
CoUninitialize();
}

这里仅供演示需要引入 combaseapi.h,实际开发中引入接口所在的头文件就行。

我们调用了 CoInitializeEx 函数来初始化 COM 运行时,COINIT_APARTMENTTHREADED 表示单线程单元模型(STA), 与之相对的还有 COINIT_MULTITHREADED 多线程单元模型 (MTA)。我们不用清楚两者有什么区别,只需要知道几乎所有 COM API 的运行环境都是 STA 就行了。

如果你开发过 WinForms 或者是 WPF,在 Main 方法上,你都能看到被标注了 [STAThread],这个属性的背后就是 CLR 运行时在此初始化了 COM 运行环境为 STA。也就是说,对于 WinForms 或 WPF 应用程序,我们在调用 COM API 之前就可以不用初始化 COM 运行时了,因为 CLR 已经完成了初始化,重复初始化可能会发生潜在的问题。

CoUninitialize 表示取消初始化 COM 运行环境。同样的,如果是 WinForms 或 WPF 的话,我们也无需再手动取消初始化,CLR 会帮我们完成。

初始化 COM API 接口指针

如果我们已经初始化了 COM 运行时并获取到了 COM 接口实体,以及它的 CLSID、IID (接口 ID),接下来就可以进行初始化,成功后便可以调用其中的虚函数了。

我们通常使用接口指针来表示要初始化的 COM 对象,因为 COM 本身就是一堆虚函数,我们只有通过指针来调用它们。

接着调用 CoCreateInstance,传入 CLSID、IID、指向接口指针的指针,并使用 SUCCEEDED 宏判断是否成功:

1
2
3
4
5
6
7
8
9
10
11
12
static IExample* pExample = nullptr;

void InitializeExample()
{
// 我们可以根据具体需求在此处初始化 COM 运行时
// CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);

if (SUCCEEDED(CoCreateInstance(CLSID_Example, nullptr, CLSCTX_ALL, IID_IExample, (LPVOID*)&pExample)))
{
pExample->Init(); // 一般来说,COM API 自己还有一个具有初始化作用的函数,通常也在此处完成调用
}
}

调用 COM API

当运行时和接口指针都初始化成功后,我们就可以调用我们想要的函数了

1
2
3
4
5
6
7
void SayHello()
{
if (pExample) // 确保指针有效
{
pExample->Hello();
}
}

释放 COM API 接口指针

1
2
3
4
5
6
7
8
9
10
11
void ReleaseExample()
{
if (pExample) // 确保指针有效
{
pExample->Release(); // 调用接口提供的 Release
pExample = nullptr; // 置空
}

// 我们可以根据具体需求在此处取消初始化 COM 运行时
// CoUninitialize();
}

封装为 WinAPI

很简单,将我们上面提到的函数导出就行,注意保持调用约定一致 (StdCall)。

为了方便导出,我们可以添加一个宏定义:

1
#define cexport(type) extern "C" __declspec(dllexport) type WINAPI
1
2
3
cexport(void) InitializeExample();
cexport(void) SayHello();
cexport(void) ReleaseExample();

可能的疑惑

  • 为什么导出后,函数类型是 void 而不是 HRESULT?
    这个我只能说是个人习惯吧,如果你想在调用端获取执行状态的话,可以导出为 HRESULT 返回类型。但是要知道这些 COM API 同 WinAPI 一样,只要我们符合规范没写错的话,出错率是很小的,很多情况出现异常通常都是我们自己的问题,肯定是哪里漏写之类的,不太可能是系统的问题。就算真不是我们的问题,那到时候再来调试也行,故返回值就没太大必要了。

【COM API】基础知识与使用方法
https://wanghaonie.github.io/posts/921e4b4f8e14/
作者
WangHaonie
发布于
2025-07-24 16:40:12
更新于
2025-07-24 23:34:29
许可协议