September 2015

第 30 卷,第 9 期

借助 C++ 进行 Windows 开发 - Windows 运行时中的高级类型

作者 Kenny Kerr | September 2015

Kenny Kerr借助 Windows 运行时 (WinRT),组件开发者可以向应用开发者呈现高级类型系统。由于 Windows 运行时是完全使用 COM 接口进行实现,因此熟悉 C++ 和经典 COM 的开发者认为这似乎非常荒谬。毕竟 COM 是一种以接口(而不是类)为主的编程模型。借助 COM 激活模型,可以使用 CoCreateInstance 和友元来构造类,但这似乎只支持与默认构造函数类似的函数。WinRT RoActivateInstance 对这样的困境也丝毫没有影响。那么,具体的工作方式如何呢?

实际上,并不像第一次听起来那样不协调。COM 激活模型已经提供了支持高级类型系统所需的基本抽象层。它只是缺少向使用者进行描述所需的元数据。COM 激活模型定义了类对象和实例。某类的实例从不直接由使用者进行创建。即使应用开发者调用 CoCreateInstance 创建类的某个实例,操作系统也仍必须获取类对象,才能检索所需的实例。

虽然 Windows 运行时按新名称调用类对象,但运作方式是一样的。激活工厂是使用者调用组件的第一个端口。组件会导出 DllGetActivationFactory 函数,而不是实现 DllGetClassObject。虽然类的标识方式已改变,但结果只是一个可能已针对给定类进一步查询过的对象。传统的类对象(以下简称“激活工厂”)可能已实现了 IClassFactory 接口,便于调用方创建上述类的实例。在新的体系中,Windows 运行时中的激活工厂必须实现新的 IActivationFactory 接口,这可有效地提供同一服务。

正如我在我的上两期专栏中所探讨的一样,Windows 运行时中使用可激活的特性修饰的运行时类能够生成元数据来指示语言投影允许默认构造。其假定是,特性类的激活工厂将通过 IActivationFactory 的 ActivateInstance 方法提供此类默认构造对象。下面用 IDL 描述了一个简单的运行时类,这是一个具有默认构造函数的类:

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

其他构造函数也可以用 IDL 进行描述,其中包含封装了一系列方法的接口(如您所猜),这些方法代表了具有不同参数集的其他构造函数。就像 IActivationFactory 接口定义了默认构造一样,组件接口和类接口可能描述了参数化构造。下面是一个简单的工厂接口,可用于创建发出特定次数的咯咯声的 Hen 对象:

runtimeclass Hen;
[version(1)]
[uuid(4fa3a693-6284-4359-802c-5c05afa6e65d)]
interface IHenFactory : IInspectable
{
  HRESULT CreateHenWithClucks([in] int clucks,
                              [out,retval] Hen ** hen);
}

与 IActivationFactory 类似,IHenFactory 接口必须毫无理由地直接继承自 IInspectable。然后,此工厂接口的每个方法会投影为具有给定参数的其他构造函数,每个方法最终必须具有与类同类型的逻辑返回值。接下来,通过指明接口名称的其他可激活特性,明确关联此工厂接口和运行时类:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

如果默认构造函数对您的类没有任何意义,那么您只需省略原始可激活特性即可,类的语言投影同样也会缺少默认构造函数。如果我寄送我的一个禽类组件版本,随后意识到它不具备使用大区创建母鸡的功能,该怎么办? 我现在当然不能更改已寄送给客户的 IHenFactory 接口,因为 COM 接口始终必须采用二进制语义协定。没关系,我只需定义另一个封装了其他所有构造函数的接口即可:

[version(2)]
[uuid(9fc40b45-784b-4961-bc6b-0f5802a4a86d)]
interface IHenFactory2 : IInspectable
{
  HRESULT CreateHenWithLargeComb([in] float width,
                                 [in] float height,
                                 [out, retval] Hen ** hen);
}

然后,我可以将这第二个工厂接口与 Hen 类相关联(和先前一样):

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
runtimeclass Hen
{
  [default] interface IHen;
}

语言投影负责合并,应用开发者只是看到大量的构造函数重载,如图 1 所示。

C# 中的 IDL 定义工厂方法
图 1:C# 中的 IDL 定义工厂方法

组件开发者会谨慎地实现这些工厂接口,以及必备的 IActivationFactory 接口:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2>
{
};

此时,我再次依赖的是我的 2014 年 12 月专栏 (msdn.microsoft.com/magazine/dn879357) 中介绍的“实现类”模板。即使我选择禁用默认构造,母鸡的激活工厂也仍需实现 IActivationFactory 接口,因为组件必须实现的 DllGetActivationFactory 函数已经过硬编码,可以返回 IActivationFactory 接口。也就是说,如果应用需要的不是默认构造,则语言投影必须先在生成的指针上调用 QueryInterface。仍必须实现 IActivationFactory 的 ActivateInstance 虚拟函数,但 no-op(如下所示)就足够了:

virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance) noexcept override
{
  *instance = nullptr;
  return E_NOTIMPL;
}

可以按照任意方式来实现其他构造方法,只要对给定实现有意义即可,而简单的解决方案是只需将自变量转发到内部类本身即可。类似如下输出:

virtual HRESULT __stdcall CreateHenWithClucks(int clucks,
                                              ABI::Sample::IHen ** hen) noexcept override
{
  *hen = new (std::nothrow) Hen(clucks);
  return *hen ? S_OK : E_OUTOFMEMORY;
}

此操作假定 C++ Hen 类不包含引发构造函数。我可能需要在这个构造函数中分配一个 C++ cluck 向量。在这种情况下,使用简单的异常处理程序就足够了:

try
{
  *hen = new Hen(clucks);
  return S_OK;
}
catch (std::bad_alloc const &)
{
  *hen = nullptr;
  return E_OUTOFMEMORY;
}

静态类成员怎么样? 这也是使用 COM 接口在激活工厂上进行实现,对此您不应该感到惊讶。回到 IDL,我可以定义一个接口来封装所有的静态成员。能不能报告后院中产蛋鸡的数量:

[version(1)]
[uuid(60086441-fcbb-4c42-b775-88832cb19954)]
interface IHenStatics : IInspectable
{
  [propget] HRESULT Layers([out, retval] int * count);
}

然后,我需要将此接口与具有静态特性的同一运行时类相关联:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
[static(IHenStatics, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

C++ 实现同样也相当简单。我将按以下方式更新“实现可变参数”模板:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2,
                               ABI::Sample::IHenStatics>
{
};

同时,按照适当的方式实现 get_Layers 虚拟函数:

virtual HRESULT __stdcall get_Layers(int * count) noexcept override
{
  *count = 123;
  return S_OK;
}

我将把更准确地统计头数的任务留给大家...我是指母鸡计数。

对于使用者而言,在应用内,所有内容都变得十分简单明了。如图 1 所示,IntelliSense 体验相当有帮助。我能够使用 CreateHenWithClucks 构造函数,就像是使用 C# 类上的其他构造函数一样:

Sample.Hen hen = new Sample.Hen(123); // Clucks

当然,为了让此函数运行起来,C# 编译器和公共语言运行时 (CLR) 有大量的工作要做。在运行时,CLR 会调用 RoGetActivationFactory,这会依次调用 LoadLibrary 和 DllGetActivationFactory 来检索 Sample.Hen 激活工厂。然后,它会调用 QueryInterface 来检索工厂的 IHenFactory 接口实现,并且只有在此之后,它才能调用 CreateHenWithClucks 虚拟函数来创建 Hen 对象。更不用说 CLR 坚持的对 QueryInterface 的许多额外调用,因为它促使进入 CLR 的每个对象发现各种特性和上述对象的语义。

调用静态成员同样也很简单:

int layers = Sample.Hen.Layers;

不过,在这里再次强调一下,CLR 必须调用 RoGetActivationFactory 才能获取激活工厂,后面需要调用 QueryInterface 检索 IHenStatics 接口指针,然后才能调用 get_Layer 虚拟函数检索此静态属性的值。然而,CLR 确实会缓存激活工厂,因此成本多少会分摊到多次此类调用上。另一方面,它还会尽各种努力尝试在组件内缓存工厂,确切地说,这毫无意义,并会带来不必要的复杂性。但这是下次要讨论的主题了。下个月请与我一起继续探讨 C++ 中的 Windows 运行时。


Kenny Kerr是加拿大的计算机程序员,是 Pluralsight 的作者,也是一名 Microsoft MVP。他的博客网址是 kennykerr.ca,您可以通过 Twitter twitter.com/kennykerr 关注他。


衷心感谢以下 Microsoft 技术专家对本文的审阅: Larry Osterman