Widget 工厂从何而来?
为了能够构造 Widget 对象,我们构建了一个 WidgetFactory,因此为了能够构造 WidgetFactory 对象,我们将构建一个 WidgetFactoryFactory。然后,为了能够构造那些……哈!开玩笑的。😉
每个可激活的运行时类都在一个模块 (DLL) 中定义。定义一个或多个可激活的运行时类的每个模块都必须导出一个名为 DllGetActivationFactory 的入口点。它声明如下:
HRESULT WINAPI DllGetActivationFactory(HSTRING activatableClassId,
IActivationFactory** factory);
从某种意义上说,此函数是激活工厂的工厂:它将运行时类的名称 (activatableClassId) 作为参数,并通过 out 参数工厂返回指定类型的激活工厂实例。如果模块没有指定类型的激活工厂,则会返回失败错误代码。附言:HSTRING 是 Windows 运行时字符串类型,我们将在以后的文章中讨论。
从概念上讲,我们可以认为该函数的实现方式如下:
HRESULT WINAPI DllGetActivationFactory(HSTRING activatableClassId,
IActivationFactory** factory)
{
// Convert the HSTRING to a C string for easier comparison:
wchar_t const* className = WindowsGetStringRawBuffer(activatableClassId, nullptr);
// Are we being asked for the Widget factory? If so, return an instance:
if (wcscmp(className, L"WidgetComponent.Widget") == 0)
{
*factory = Make<WidgetFactory>().Detach();
return S_OK;
}
// If our module defines other activatable types, we'd check for them here.
// Otherwise, we return that we failed to satisfy the request:
*factory = nullptr;
return E_NOINTERFACE;
}
实际上,我们根本不需要做太多工作来实现此功能。使用 C++/CX 构建组件时,如果定义了 _WINRT_DLL 宏,默认情况下,此宏在 Visual Studio 中的 Windows 运行时组件项目模板中定义,编译器将自动实现此功能。使用 WRL 需要做一些工作,但非常简单。必须使用其中一个 ActivatableClass 宏向 WRL 注册每个可激活类。例如,要将我们的 Widget 类型注册到其 WidgetFactory 激活工厂,我们可以使用 ActivatableClassWithFactory 宏:
ActivatableClassWithFactory(Widget, WidgetFactory)
由于许多类型仅允许默认构造,并且默认构造使用 IActivationFactory 接口并且不需要任何自定义的类型特定逻辑,因此 WRL 还提供了此宏的有用形式 ActivatableClass。此宏生成一个允许默认构造的简单激活工厂,并注册生成的激活工厂。在第 1 部分中,我们将 Number 类从 C++/CX 转换为 WRL 时使用了这个宏。
如果所有可激活的运行时类都已在 WRL 中注册,我们可以简单地让 DllGetActivationFactory 委托给 WRL,并让 WRL 完成所有艰苦的工作。
HRESULT WINAPI DllGetActivationFactory(HSTRING activatibleClassId,
IActivationFactory** factory)
{
auto &module = Microsoft::WRL::Module<Microsoft::WRL::InProc>::GetModule();
return module.GetActivationFactory(activatibleClassId, factory);
}
此时,我们已经拥有了使运行时类可构造所需的一切:我们有一个可以构造运行时类实例的工厂,并且我们有一个定义明确的方法来获取任何可激活运行时类的工厂,只要我们知道该运行时类定义的模块即可。
创建实例
我们已经完成了可激活运行时类的实现;现在让我们看看 ref new 以及创建 Widget 实例时会发生什么。在本文的开头,我们从以下内容开始:
Widget^ widget = ref new Widget(42);
我们可以将其转换为以下使用 WRL 而不是 C++/CX 的 C++ 代码:
HStringReference classId(RuntimeClass_WidgetComponent_Widget);
ComPtr<IWidgetFactory> factory;
RoGetActivationFactory(
classId.Get(),
__uuidof(IWidgetFactory),
reinterpret_cast<void**>(factory.GetAddressOf()));
ComPtr<IWidget> widget;
factory->CreateInstance(42, widget.GetAddressOf());
实例化是一个两步过程:首先,我们需要获取 Widget 类型的激活工厂,然后我们可以使用该工厂构造 Widget 实例。这两个步骤在 WRL 代码中非常清晰。RoGetActivationFactory 是 Windows 运行时本身的一部分。它将会:
- 查找定义命名运行时类型的模块;
- 加载模块(如果尚未加载);
- 获取指向模块的 DllGetActivationFactory 入口点的指针;
- 调用该 DllGetActivationFactory 函数以获取激活工厂的实例;
- 调用工厂上的 QueryInterface 以获取指向请求接口的指针,并返回生成的接口指针;
其中大部分都很简单,不需要进一步说明。例外是第一项:Windows 运行时究竟如何确定要加载哪个模块来实例化特定类型?虽然要求在名称与类型名称相似的 WinMD 文件中定义类型的元数据,但对模块的命名没有这样的要求:Widget 类型可以在任何模块中定义。
每个 Windows 应用商店应用都包含一个名为 AppXManifest.xml 的文件。此清单包含有关应用的各种重要信息,包括其身份、名称和徽标。清单还包含一个包含扩展的部分:此部分包含定义可激活类型的所有模块的列表以及每个模块定义的所有可激活类型的列表。例如,以下条目类似于我们在 Widget 类型中找到的条目:
<Extension Category="windows.activatableClass.inProcessServer">
<InProcessServer>
<Path>WidgetComponent.dll</Path>
<ActivatableClass ActivatableClassId="WidgetComponent.Widget" ThreadingModel="both" /> </InProcessServer>
</Extension>
该列表仅包含应用包中包含的模块定义的类型;Windows 提供的类型(即 Windows 命名空间中的类型)在注册表中全局注册,不包含在 AppXManifest.xml 清单中。
对于大多数项目,此清单是作为应用构建后运行的应用打包任务的一部分创建的。扩展部分的内容通过检查任何引用组件的 WinMD 文件和任何引用扩展 SDK 的清单自动填充。当我们的应用调用 RoGetActivationFactory 时,Windows 运行时会使用此列表查找它需要为 Widget 类型加载的模块。
应该注意的是,为了提高性能,激活工厂可能会缓存在 ABI 边界的两侧:定义 Widget 类型的组件实际上只需要创建 WidgetFactory 的单个实例;它不需要在每次要求工厂时都创建一个新实例。类似地,我们的应用可以缓存从 RoGetActivationFactory 返回的工厂,以避免每次需要构造 Widget 时都必须往返于运行时。如果我们的应用创建了大量 Widget,这可能会产生巨大的差异。WRL 和 C++/CX 在这种缓存方面都非常智能。
总结
可以说,C++/CX 语法隐藏了大量的复杂性!我们从实际上只有四行 C++/CX 的代码开始:两行用于声明构造函数,两行用于演示这些构造函数的用法。我们最终得到的远不止这些。对于这两个构造函数,我们有一个特定于 Widget 的工厂接口、一个实现该接口和 IActivationFactory 接口的激活工厂,以及一个创建工厂的模块入口点。对于 ref new 表达式,我们通过 Windows 运行时基础结构进行往返。
请注意,此处描述的所有内容都适用于在一个模块中定义可构造类型并从另一个模块实例化该类型的一般情况。也就是说,这是通过 ABI 构造对象的机制。如果类型与实例化类型的代码在同一个模块中定义,则编译器能够避免跨 ABI 边界调用所需的大部分开销。未来的文章将讨论单个模块内的工作原理,但要知道在这种情况下事情通常会简单得多。