2017 年 1 月

第 32 卷,第 1 期

C++ - 介绍 C++/WinRT

作者 Kenny Kerr | 2017 年 1 月

Windows 运行时 (WinRT) 是为新型 Windows API 提供支持的技术,并且是通用 Windows 平台 (UWP) 的核心。相同 API 可用于所有 Windows 设备,不管是 PC、平板电脑、HoloLens、手机、Xbox 或任何其他运行 Windows 的设备。

WinRT 基于组件对象模型 (COM),但它的 COM API 不设计为直接使用,你可以在 DirectX 等中使用经典 COM API。相反,它旨在在所谓的“语言投影”中使用。 语言投影囊括与 COM API 协同工作的不愉快细节,并为特定编程语言提供更自然的编程体验。例如,WinRT API 可以很自然地用于 JavaScript 和 .NET,无需过多担心 COM 基础。

直到最近,还没有一个很好的语言投影可供 C++ 开发人员使用: 必须在笨拙的 C++/CX 语言扩展或冗长、繁琐复杂的 WinRT C++ 模板库 (WRL) 之间进行选择。

这时候出现了 C++/WinRT。C ++ / WinRT 是 WinRT 的标准 C ++ 语言投影,在头文件中完全实现—顶尖 C++ 库。它旨在支持对使用任何与标准兼容的 C++ 编辑器的 WinRT API 进行编写和使用。C++/WinRT 最终为 C++ 开发人员提供对新型 Windows API 的优先访问权限。

补救性 Windows 元数据文件

C++/WinRT 基于 Windows 运行时项目 (moderncpp.com) 的新型 C++,该项目在我加入 Microsoft 前便已经开始。它转而基于另一个为尝试实现 DirectX 现代化编程而创建的项目 (dx.codeplex.com)。WinRT 的出现解决了实现现代化 COM API 的首要问题,这通过所谓的 Windows 元数据 (.winmd) 文件提供一个标准的方式描述 API 面来完成。考虑到 Windows 元数据文件集,C / WinRT 编译器 (cppwinrt.exe) 可以生成一个标准 C++ 库,该库完全面对 Windows API 或任何其他 WinRT 组件进行介绍或计划,以便开发人员可以同时使用和生成 API 实现。后者之所以重要是因为它意味着 C++/WinRT 不仅用于调用或使用 WinRT API,同时非常适合 WinRT 组件的实现。Microsoft 中的团队已经开始使用 C++/WinRT 来构建适用于 OS 本身的 WinRT 组件。

C++/WinRT 于 2016 年 10 月在 GitHub 上首次公开发布,现在可以立即试用。也许最简单的方法是克隆 git 存储库,虽然它也可以作为一个 zip 文件下载。只需发出以下命令:

git clone https://github.com/Microsoft/cppwinrt.git

Cloning into 'cppwinrt'..

现在你应该有一个名为“cppwinrt”的文件夹,其中包含你自己的本地存储库副本。该文件夹包含 C++/WinRT,以及一些文档和入门指南。库本身包含在一个文件夹内,该文件夹反映 Windows SDK 基于其构建的版本。在撰写本文时,Windows SDK 基于最新的 10.0.14393.0 版本构建,以支持 Windows 10 周年更新 (RS1) 的发展。本地文件夹可能如下所示:

dir /b cppwinrt

10.0.14393.0
Docs
Getting Started.md
license.txt
media
README.md

版本化文件夹可能包含 2 个文件夹:

dir /b cppwinrt\10.0.14393.0

Samples
winrt

winrt 文件夹正是你所需的。如果你只是想将此文件夹复制到你自己的项目或源控件系统中,那很好。它包含所有所需的头文件,因此你可以快速开始编写代码。但具体代码是什么样的呢? 让我们从一个控制台应用入手,你可以使用 Visual C++ 工具命令提示符进行编译。不需要将其用于 Visual Studio 或复杂的 .msbuild 或 .appx 文件,它们是典型 UWP 应用的主要文件。从 图 1 中将代码复制到源文件并将其命名为 Feed.cpp。

图 1 你的第一个 C++/WinRT 应用

#pragma comment(lib, "windowsapp")
#include "winrt/Windows.Foundation.h"
#include "winrt/Windows.Web.Syndication.h"
using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;
int main()
{
  initialize();
  Uri uri(L"http://kennykerr.ca/feed");
  SyndicationClient client;
  SyndicationFeed feed = client.RetrieveFeedAsync(uri).get();
  for (SyndicationItem item : feed.Items())
  {
    hstring title = item.Title().Text();
    printf("%ls\n", title.c_str());
  }
}

我将花上一点时间来介绍代码的意义。现在,你可以确保它使用以下 Visual C++ 编译器选项进行生成:

cl Feed.cpp /I cppwinrt\10.0.14393.0 /EHsc /std:c++latest

Microsoft (R) C/C++ Optimizing Compiler...

/std:c++latest 编译器标志将在 Visual C++ 2015 Update 3 或更高版本在使用 C++/WinRT 时用到。但是请注意,不需要 Microsoft 特定的扩展名。事实上,你可能还希望包含 /permissive- 选项来进一步约束代码,以此提高标准兼容性。如果一切顺利的话,编译器将调用链接器并同时生成一个名为 Feed.exe 的可执行对象,以显示我最近的博客文章的标题:

Feed.exe

C++/WinRT: Consumption and Production
C++/WinRT: Getting Started
Getting Started with Modules in C++
...

好戏正在上演

祝贺您! 你已经生成你的第一个带有 C++/WinRT 的应用。那么,接下来会发生什么呢? 请看一下 图 1。第一行告知链接器可以在哪里找到由任何语言投影使用的几个 WinRT 功能。这些并没有具体到 C++/WinRT。它们仅仅是使应用或组件初始化线程单元上下文、操纵 WinRT 字符串、激活工厂对象,传播错误信息等的 Windows API。

接下来是一组 #include 指令,它将包含来自计划拟使用的命名空间的类型。一开始,C++/WinRT 在一个单独的头文件中定义所有内容,但是,鉴于 Windows API 的庞大规模,这被证明不利于生成吞吐量。在未来,我们可能会转向使用我在 2016 年 4 月 MSDN 杂志文章“Microsoft Pushes C++ into the Future”(msdn.com/magazine/mt694085) 中所述的 C++ 模块。当这一天到来时,可能不再需要首先包含这些头文件,因为模块格式非常高效,并且早期的试验表明,生成吞吐量问题可能很大程度上会消失。

使用命名空间指令为可选,但鉴于 Windows API 使用命名空间相当严重,它们确实相当方便。所有类型及其封闭命名空间均置于称为 WinRT 的 C++/WinRT 根命名空间。这有点遗憾,但这是与现有代码的互操作性的要求: C++/CX 和 Windows SDK 在名为“Windows”的根命名空间中声明类型。 一个单独的命名空间可以让开发人员慢慢从 C++/CX 中转移,同时保留这些旧技术中的现有投资。

图 1 中的应用仅创建一个 Uri 对象来指示下载 RSS 源。Uri 对象在 Windows.Foundation 命名空间中定义。然后此 Uri 对象通过 SyndicationClient 对象方法来检索源。SyndicationClient 在 Windows.Web.Syndication 命名空间中定义。很快你会看到,WinRT 在很大程度上依赖于异步,所以应用需要等待 retrievefeedasync 方法的结果,而这是 trailing get 方法的工作。在具备 Syndication­Feed 对象的条件下,源的项可能通过项方法返回的集合一一列出。然后每个生成的 Syndication­Item 可能会被解包来检索文章标题中的文本。结果是封装 WinRT HSTRING 类型的类型 hstring,但提供了一个类似于 std::wstring 的接口的接口,以便为 C++ 开发人员提供熟悉的体验。

可以看到,WinRT 旨在通过在众多 Windows OS 核心经过试验和测试的 COM 探测来展示高级类型系统。C++/WinRT 如实地达到到这个目标。没有必要调用 CoCreateInstanceEx 等函数或处理 COM 指针或 HRESULT 错误代码。当然,幕后仍然存在这些 COM 样式基元的道德等效。例如, 图 1 中主要函数开始时调用的初始化功能在内部调用 RoInitialize 函数,这是经典 COM 中传统 CoinitializeEx 函数的 WinRT 等效项。C++/WinRT 库还负责 HRESULT 和异常之间的转换,为开发人员提供自然、高效的编程模型。此操作通过避免代码膨胀和提高内联能力的方式完成。

我曾提到过这个事实,WinRT 在异步中有着深远的结构投资,并且你已经在 图 1 例子中看到了这一点。RetrieveFeedAsync 函数可能需要一些时间来完成,这是其本质使然,而且 API 设计者不希望方法调用被阻止,因此并非直接返回一个 Syndicationfeed,而是返回一个代表操作的 IAsyncOperationWithProgress 类型,当这些结果可用时,最终该操作可能导致 SyndicationFeed。

并发

WinRT 提供了四种类型来表示不同的异步对象,并且 C++/WinRT 提供一些方法来创建和使用这些对象。 图 1 说明如何阻止调用线程直到结果可用,但 C++/WinRT 还集成了 C++ 协同例程深入到编程模型,以此提供一种自然的方式来协同等待结果,而无需在其他有用的工作中阻止 OS 线程。可以编写 co_await 语句取代 get 方法来等待运行结果,如下所示:

SyndicationFeed feed = co_await client.RetrieveFeedAsync(uri);

你也可以通过按 图 2 中所述编写协同例程来生成你自己的 WinRT 异步对象。然后,由此生成的 IAsync­Action - 在 Windows.Foundation 命名空间中找到的另一个 WinRT 异步类型 - 可能会被聚合到其他协同例程中,调用方可能会决定使用 get 方法来阻止和等待结果,甚至还可以传递给另一种编程语言来支持 WinRT。

图 2 使用 C++/WinRT 的协同例程

IAsyncAction PrintFeedAsync()
{
  Uri uri(L"http://kennykerr.ca/feed");
  SyndicationClient client;
  SyndicationFeed feed = co_await client.RetrieveFeedAsync(uri);
  for (SyndicationItem item : feed.Items())
  {
    hstring title = item.Title().Text();
    printf("%ls\n", title.c_str());
  }
}
int main()
{
    initialize();
    PrintFeedAsync().get();
}

正如我所提到的,C++/WinRT 并非仅关乎调用 WinRT API。实现 WinRT 接口并生成其他对象可能对其进行调用的实现很容易。关于这一点,一个很好的示例是 WinRT 应用程序模型的构建方法。一个最小的应用可能如下所示:

using namespace Windows::ApplicationModel::Core;
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  IFrameworkViewSource source = ...
  CoreApplication::Run(source);
}

这就是传统的 WinMain 入口点函数,它提醒你,即使你现在可以使用标准 C++ 编写程序,你仍需负责编写特定平台上出色的应用程序。CoreApplication 的静态 Run 方法希望 IFrameworkViewSource 对象创建应用的第一个视图。IFrameworkViewSource 只是一个方法单一的接口,至少在概念上如下所示:

struct IFrameworkViewSource : IInspectable
{
  IFrameworkView CreateView();
};

因此,考虑到 IFrameworkViewSource,你可能会以如下方式对其进行调用:

IFrameworkViewSource source = ...
IFrameworkView view = source.CreateView();

当然,对其进行调用的是操作系统而不是应用开发人员。而你,作为应用开发人员,需要实现此接口以及 CreateView 返回的 IFrameworkView 接口。C++/WinRT 使该操作变得非常简单。再次声明,无需执行 COM 样式编程。无需动用令人生畏的 WRL 或 ATL 模板和宏。你只需使用 C++/WinRT 库以实现接口,如下所示:

struct Source : implements<Source, IFrameworkViewSource>
{
  IFrameworkView CreateView()
  {
    return ...
  }
};

implements 类模板轻松基于可变参数模板方法来实现 WinRT 接口。我的 2014 Special Connect(); 文章“Visual C++ 2015 Brings Modern C++ to the Windows API”中介绍了这一点 (msdn.com/magazine/dn879346)。

第一个类型参数是派生类的名称,旨在实现作为后续类型参数列出的接口。如何实现 IFrameworkView 接口? 其实它只是另一个稍微复杂一点的接口。如下所示:

struct IFrameworkView : IInspectable
{
  void Initialize(CoreApplicationView const & applicationView);
  void SetWindow(Windows::UI::Core::CoreWindow const & window);
  void Load(hstring_ref entryPoint);
  void Run();
  void Uninitialize();
};

鉴于 IFrameworkView 实例,你可以按照此处所述的内容自由调用这些方法,但是需要再次声明的是,你正在谈论的是应用期望实现以及 OS 将调用的一个接口。此外,你可以简单地使用 C++/WinRT 来实现此接口,如 图 3 所述。这展示的是一个最小的应用,它将启动一个窗口并在桌面上运行,即使它可能不会执行任何令人兴奋的操作。我所介绍的这一切都不是为了说明如何编写下一个出色的应用,而是展示如何使用自然的 C++ 代码来使用和生成 WinRT 类型。

图 3 实现 IFrameworkView

struct View : implements<View, IFrameworkView>
{
  void Initialize(CoreApplicationView const & view)
  {
  }
  void Load(hstring_ref entryPoint)
  {
  }
  void Uninitialize()
  {
  }
  void Run()
  {
    CoreWindow window = CoreWindow::GetForCurrentThread();
    window.Activate();
    CoreDispatcher dispatcher = window.Dispatcher();
    dispatcher.ProcessEvents(CoreProcessEventsOption::ProcessUntilQuit);
  }
  void SetWindow(CoreWindow const & window)
  {
    // Prepare app visuals here
  }
};

然后你可以更新 IFrameworkViewSource 实现来生成和返回此实现:

struct Source : implements<Source, IFrameworkViewSource>
{
  IFrameworkView CreateView()
  {
    return make<View>();
  }
};

同样,你可以更新 WinMain 函数以使用 IFrameworkViewSource 实现,如下所示:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  CoreApplication::Run(make<Source>());
}

如果将 implements 类模板作为可变参数模板设计,它可以轻松实现多个接口。你可能会决定在一个类中实现 IFrameworkViewSource 和 IFrameworkView,如 图 4 所示。

图 4 使用 C++/WinRT 实现多个接口

struct App : implements<App, IFrameworkViewSource, IFrameworkView>
{
  // IFrameworkViewSource method...
  IFrameworkView CreateView()
  {
    return *this;
  }
  // IFrameworkView methods...
  void Initialize(CoreApplicationView const & view);
  void Load(hstring_ref entryPoint);
  void Uninitialize();
  void Run();
  void SetWindow(CoreWindow const & window);
};

如果你曾花时间使用 ATL 或 WRL 等库实现 COM 对象,这应该是一个可喜的改进。它不仅更加方便、高效且使用起来更加愉快,事实上,它还会为你提供最小的二进制文件和任何 WinRT 语言投影的最佳性能。它甚至会超过直接使用 ABI 接口的手写代码。这是因为抽象的目的是利用 Visual C++ 编译器旨在优化的新型 C++ 用语。这包括奇妙的静态、空基类、strlen 省略,以及最新版本 Visual C++ 中许多新的优化,旨在提高 C++/WinRT 的性能。

作为一个例子,WinRT 介绍了所需的接口概念。一个给定的运行时类或接口可能会额外期望实现一些其他的接口集。这使得基于 COM 接口的 WinRT 无法更改、支持版本控制。例如,前面所述的 Uri 类要求使用单个 ToString 方法的 IStringable 接口。典型的应用开发人员不知道 IStringable 接口的方法实际上是由不同的 COM 接口和函数提供,并且可以简单地按如下方式对其进行使用:

Uri uri(L"http://kennykerr.ca/feed");
hstring value = uri.ToString();

幕后,C++/WinRT 必须先使用 IUnknown QueryInterface方法查询 URI 对象中的 IStringable 接口。一切都很顺利,直到你碰巧调用这样一个需要闭环模式的接口方法:

Uri uri(L"http://kennykerr.ca/feed");
for (unsigned i = 0; i != 10'000'000; ++i)
{
  uri.ToString();
}

你没有看到的是,此代码导致这样的事情发生:

Uri uri(L"http://kennykerr.ca/feed");
for (unsigned i = 0; i != 10'000'000; ++i)
{
  uri.as<IStringable>().ToString();
}

C++/WinRT 对 as 方法注入必要调用,进而调用 QueryInterface。现在,从 COM 的角度来看,QueryInterface 是一个纯粹的调用。如果它成功一次,那么针对指定对象必须一直成功。幸运的是,现已优化 Visual C++ 编译器以检测这一模式,并且与 C++/WinRT 的协作将提升此调用脱离循环,使代码最终如下所示:

Uri uri(L"http://host/path");
IStringable temp = uri.as<IStringable>();
for (unsigned i = 0; i != 10'000'000; ++i)
{
  temp.ToString();
}

该代码最终会是一个相当重要的优化,因为 Windows API 中所需的接口数量相当可观,特别是在诸如 XAML 开发等区域。这只是一些令人难以置信的优化的一个示例,将 C++/WinRT 用于为应用和组件开发时可以使用这些优化。其中存在进一步的优化,该优化将分摊访问激活工厂的成本,从而显著改进实例激活(构造函数)和静态方法调用性能,给你带来尽可能多的 C++/CX 40x 性能改进。

标准 C++ 为任何尝试生成 WinRT 语言投影的人员带来独特挑战,这就是为什么 Microsoft 的 Visual C++ 团队最初想出了一个非标准解决方案。革命尚未成功,我们会继续努力探究 C++/WinRT,以此实现为系统程序员、应用开发人员以及任何对编写吸引人且快速的 Windows 代码感兴趣的开发人员提供不打折的一流语言投射的目标。

总结

James McNellis 和我在 2016 年的 CppCon 进行了两次谈话,并正式公开推出了 C++/WinRT。你可以在此找到两次谈话的视频:

这些都是“under the hood”演示文稿下技术性比较强的内容。对于高层次的介绍,可以在 bit.ly/2fwF6bx 听取我的 CppCast 采访。

最后,可以在: github.com/microsoft/cppwinrt 下载并立即试用 C++/WinRT。

还有很多工作要做,随着标准 C++ 的演进可能会有一些变化,并且我们会寻找其他方法来改善和简化编程模型。请将你的想法告知我们。欢迎你的反馈。


Kenny Kerr 是 C++ 系统程序员、C++/WinRT 创建者、Pluralsight 作者,同时也是 Microsoft Windows 团队的一名工程师。他的博客网址是 kennykerr.ca,您可以通过 Twitter @kennykerr 关注他。

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