当在线程内初始化 COM 组件 库之后就可以安全的调用 COM 接口了。如果想要使用 COM 接口,你的程序首先创建一个实现该接口的对象。

一般情况下,有两种方式创建 COM 组件对象:

  • 使用模块本身专门提供用来创建对象的函数。

  • 使用 COM 组件自身提供的函数 CoCreateInstance。

1. COM 组件专门的创建接口

依然用 Shape 类举例,该类继承实现了 IDrawable 接口,具备绘图的功能,如果将 Shape 组件给用户使用,你可以专门为此导出一个函数:

// Not an actual Windows function.

HRESULT CreateShape(IDrawable** ppShape);

通过这个函数,你可以创建一个新的 Shape 对象。

IDrawable *pShape;

HRESULT hr = CreateShape(&pShape);
if (SUCCEEDED(hr))
{
    // Use the Shape object.
}
else
{
    // An error occurred.
}

这个函数参数 ppShape 是一个指针的指针。如果你没有见过这种方式,理解起来可能需要一些时间。

考虑一下 CreateShape 函数,这个函数必须返回一个 IDrawable 类型的接口指针对象,但是这个函数需要用返回值来获取错误代码,所以 IDrawable 的指针只能通过参数返回。

函数的使用者将指针变量通过参数传递给 CreateShape 函数内部,函数会生成一个新的 IDrawable 指针,并将指针保存到传入的参数中。在 C++ 中,有两种方式可以将结果传出,一种是引用,一种是指针,COM 使用了后者,这也是一般 C语言中使用的方式。

在 C/C++ 语言中,如果用参数返回结果,就会涉及到形参实参的问题,如果想要正确传出,必须要使用对应类型的指针,所以 IDrawable 指针类型的指针,就是指针的指针,所以参数必须是 IDrawable**

下面是该过程的图示:

指针传参示意图

2. 常规的创建方法 CoCreateInstance

CoCreateInstance 提供一种常规的创建对象的方法。如果想要理解 CoCreateInstance,首先需要记住不同的 COM 组件对象可以拥有相同的接口,而一个 COM 组件对象又可以继承实现多个接口。所以,一个通用的创建对象的函数需要两方面的信息:

  • 要创建什么对象。

  • 要从对象中获取什么接口。

当我在调用函数的时候,如何才能提供这些信息?在 COM 组件 中,对象或者接口都会用一个128位的编号标识,这种标识被称为全球唯一标识(globally unique identifier),也就是常说的 GUID。

GUID 的生成算法能保证它的唯一性。在没有一个中央控制统筹的情况下,GUID 是保证标识唯一性的可靠方式之一。有时候 GUID 也会被称为(universally unique identifiers),即 UUID。在 COM 之前,它们被用在 DCE/RPC(Distributed Computing Environment/Remote Procedure Call) 中。GUID 生成算法并不唯一,不是所有的算法都会确保唯一性,但是重复的几率非常小,可以说基本为零。

对于前面案例中的 Shapes 库来说,可以声明两个GUID 常量:

extern const GUID CLSID_Shape;
extern const GUID IID_IDrawable;

常量 CLSID_Shape 用来标识 Shape 对象,IID_IDrawable 用来标识 IDrawable 接口,它们都是128位的字符串。CLSID 前缀的常量代表类的标识,而 IID 前缀的常量代表接口标识,这个是 COM 组件的标准约定。

通过这些值,你可以创建一个新的 Shape 接口:

IDrawable *pShape;
hr = CoCreateInstance(CLSID_Shape, NULL, CLSCTX_INPROC_SERVER, IID_Drawable,
     reinterpret_cast<void**>(&pShape));

if (SUCCEEDED(hr))
{
    // Use the Shape object.
}
else
{
    // An error occurred.
}

CoCreateInstance 函数包含五个参数。第一个和第四个参数分别代表类和接口的标识。实际上这几个参数的含义是,创建一个 Shape 对象,给我返回一个指向 IDrawable 接口的指针。

第二个参数设置为 NULL。第三个参数接收一组标记,用来确定对象的执行上下文(execution context)。这个执行上下文标识着对象是否和应用程序在同一个进程中运行。可以指定在相同的机器不同的进程中或者在远程计算机中。下面该参数常用的标记:

标记 含义
CLSCTX_INPROC_SERVER 同一个进程
CLSCTX_LOCAL_SERVER 相同计算机,不同进程
CLSCTX_REMOTE_SERVER 不同计算机
CLSCTX_ALL 使用对象支持的最有效选项,(从最高效到最低效的排名是:进程内、进程外和跨计算机。)

在特定组件的文档中可能会告诉你如何指定执行上下文,如果没有说明,可以使用 CLSCTX_ALL。如果你请求的执行上下文 COM 组件不支持,则CoCreateInstance 函数会返回错误代码 REGDB_E_CLASSNOTREG。如果组件没有注册对应的 CLSID 标识,函数也会返回这个错误码。

第五个参数接收一个接口指针变量,因为 CoCreateInstance 函数适用于所有组件,所以无法确认接口的具体类型,因此这个参数的类型是 void**。如果你想得到自己想要的接口指针,你需要使用 reinterpret_cast 强制转换。

检测 CoCreateInstance 函数的返回值至关重要,如果函数返回一个错误码,这个 COM 接口指针是无效的,尝试使用它可能会导致你程序的崩溃。

在 CoCreateInstance 函数的内部会使用多种技术创建一个对象。在简单的案例中,它会在注册表中查找类标识。注册表的入口指向实现了该类的 DLL 或者 EXE 程序。除了注册表,CoCreateInstance 也可以使用 COM+ 中的目录或者 SxS 清单列表。不过这些对于调用者来说都是不可见的。

上面例子中的 Shapes 类并不是很自然,现实中 COM 组件和它还是有很大差别的,下面我们展示一个调用真实 COM 组件 的例子以供学习。