次の方法で共有



January 2017

Volume 32 Number 1

C++ - C++/WinRT の紹介

Kenny Kerr | January 2017

Windows ランタイム (WinRT) は、最新の Windows API を支えるテクノロジで、ユニバーサル Windows プラットフォーム (UWP) の中核を成します。PC、タブレット、HoloLens、スマートフォン、Xbox など、Windows が実行されているデバイスであれば、同じ API をすべての Windows デバイスで使用できます。

WinRT はコンポーネント オブジェクト モデル (COM) を基盤としています。ですが、WinRT の COM API は直接使用するようには設計されていません。DirectX の COM API など、従来の COM API の中には直接使用するものもあります。WinRT の COM API は、「言語プロジェクション」という機能を利用するように設計されています。 言語プロジェクションとは、COM API を操作する際の細々としたやっかいな要素をカプセル化し、どのプログラミング言語でも自然なプログラミング エクスペリエンスを可能にするものです。たとえば、WinRT API は JavaScript からでも .NET からでもごく自然に使用できます。COM の基盤部分に頭を悩ませる必要はありません。

最近まで、C++ 開発者が使用できる優れた言語プロジェクションはありませんでした。 扱いにくい C++/CX 言語拡張か、冗長で手間のかかる複雑な WinRT C++ テンプレート ライブラリ (WRL) のどちらかを選ばなければなりませんでした。

ここで登場するのが C++/WinRT です。C++/WinRT は、ヘッダー ファイルだけで実装される WinRT 向け標準 C++ 言語プロジェクションで、C++ ライブラリとしては最高の部類に入ります。C++/WinRT は、標準に準拠する任意の C++ コンパイラを使って、WinRT API を作成および使用できるように設計されています。C++/WinRT によって、C++ 開発者はようやく最新 Windows API に最適にアクセスできるようになりました。

Windows メタデータ ファイルの登場

C++/WinRT が基礎に置いているのは Windows ランタイム プロジェクト用の Modern C++ です (moderncpp.com、英語)。このプロジェクトは、筆者がマイクロソフトの一員になる前に始めました。また、このプロジェクト自体は、DirectX プログラミングを近代化しようとして作った別のプロジェクトに基づいています (dx.codeplex.com、英語)。WinRT が登場したことで、Windows メタデータ (.winmd) と呼ばれるファイルを介して、API サーフェイスを記述する標準的な方法が用意され、COM API を近代化する際の最大の問題が解決されました。Windows メタデータ ファイルが一式あれば、C++/WinRT コンパイラ (cppwinrt.exe) は、Windows API や他の任意の WinRT コンポーネントを完全に記述またはプロジェクションする標準 C++ ライブラリを生成することができます。その結果、開発者はそうした API の実装を使用および作成できるようになります。重要なのは後者です。このことは C++/WinRT が WinRT API を呼び出したり使用するためだけではなく、WinRT コンポーネントを実装するにも最適であることを表しています。マイクロソフトのチームは、既に C++/WinRT を使用して、OS 自体を対象とする WinRT コンポーネントのビルドを始めています。

C++/WinRT は 2016 年 10 月に初めて GitHhub で一般にリリースされています。そのためすぐにでもお試しいただけます。おそらく、最も簡単な方法は 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 SDK では、Windows 10 Anniversary Update (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 や、通常の UWP アプリに欠かせない複雑な .msbuild や .appx ファイルは不要です。図 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 Studio C++ コンパイラ オプションを使用してアプリがビルドされていることを確認します。

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

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

/std:c++latest コンパイラ フラグは、C++/WinRT を使用する場合 Visual C++ 2015 Update 3 以降が必要になることを示す手掛かりになります。ただし、マイクロソフト固有の拡張機能が要求されることはありません。実際、コードをさらに制限して標準準拠を促す /permissive- オプションを含めてもかまいません。問題がなければ、コンパイラによってリンカーが呼び出され、この 2 つにより Feed.exe という実行可能ファイルが作成されます。この実行可能ファイルは、筆者が最近投稿したブログ記事のタイトルを表示します。

Feed.exe

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

ショーの始まり

これで、 C++/WinRT を使った最初のアプリをビルドしました。では、次は何をすればよいでしょう。 図 1 をもう一度ご覧ください。1 行目で、言語プロジェクションで使用されるごく一部の WinRT 機能を探す場所をリンカーに指示しています。これらは C++/WinRT 固有ではありません。こうした機能は、スレッドのアパートメント コンテキストの初期化、WinRT 文字列の操作、ファクトリ オブジェクトのアクティブ化、エラー情報の伝播などをアプリやコンポーネントで可能にする Windows API にすぎません。

ここからは、プログラムで使用するために、名前空間の型をインクルードする #include ディレクティブについて説明します。本来、C++/WinRT は 1 つのヘッダー ファイルですべてを定義します。しかし、大規模な Windows API が渡された場合、このことがビルドのスループットに悪影響を及ぼすことになります。2016 年 4 月の MSDN マガジンのコラム「Visual C++ - C++ の将来に向けたマイクロソフトの後押し」(msdn.com/magazine/mt694085)で説明したように、今後は C++ モジュールを使用するように切り替える可能性があります。その日が来たら、最初にこれらのヘッダーをインクルードする必要はなくなるかもしれません。モジュール形式は非常に効率的なため、ほとんどのビルドのスループット問題が解消される可能性が初期の試行で示されています。

using namespace ディレクティブは省略できます。ただし、Windows API が名前空間を非常に頻繁に使用することを考えると、指定する方が便利です。すべての型とそれを含む名前空間は winrt という名前の C++/WinRT ルート名前空間内部に含まれています。これは少々残念ですが、既存のコードとの相互運用には必要なことです。 C++/CX と Windows SDK はどちらも Windows というルート名前空間で型を宣言します。 名前空間を別に用意することで、開発者は、以前のテクノロジへの既存の投資を維持しながら、徐々に C++/CX から移行できます。

図 1 のアプリは、単純にダウンロードする RSS フィードを示す Uri オブジェクトを作成します。Uri オブジェクトは Windows.Foundation 名前空間で定義されています。次に、この Uri オブジェクトを SyndicationClient オブジェクトのメソッドに渡し、フィードを取得します。SyndicationClient は Windows.Web.Syndication 名前空間で定義されています。後ほど説明しますが、WinRT は非同期に大きく依存しています。そのため、アプリは RetrieveFeedAsync メソッドの結果を待機する必要があります。この待機に使用するのが、末尾に get が付くメソッドです。Syndication­Feed オブジェクトを使用すると、Items メソッドから返されるコレクションを使ってフィードの複数のアイテムが列挙されることがあります。その後、作成された各 Syndication­Item を展開して、記事タイトルのテキストを取得します。結果は型 hstring です。この型は WinRT HSTRING 型をカプセル化します。ただし、この型により、std::wstring のインターフェイスに似たインターフェイスが提供されるため、C++ 開発者にとっては使い慣れたエクスペリエンスになります。

WinRT の目的は、多くの Windows OS の中核に組み込まれた実績のある COM コードを利用して、洗練された型システムを用意することです。C++/WinRT は忠実にその理想に従っています。CoCreateInstanceEx のような難解な関数を呼び出したり、COM ポインターや HRESULT エラー コードを処理する必要はありません。当然、背後では事実上 COM スタイルのプリミティブに相当する要素がこれまでどおり存在します。たとえば、図 1 の main 関数で最初に呼び出される initialize 関数は、内部的には RoInitialize 関数を呼び出します。これは、従来の COM の CoInitializeEx 関数に相当する WinRT の関数です。C++/WinRT ライブラリは HRESULT と例外との変換も処理するため、開発者にとっては自然で生産性の高いプログラミング モデルが実現されます。この変換は、コードの膨張を防ぎながらインライン化を促進する方法で行われます。

WinRT がアーキテクチャ上非同期を多用していることは前に説明しました。これについては図 1 の例でも既に確認できます。RetrieveFeedAsync 関数は必然的に完了までにある程度の時間がかかる可能性があり、API の設計者はそのメソッド呼び出しをブロックすることを望みません。そのため、SyndicationFeed を直接返すのではなく、最終的に結果が利用可能になった時点で SyndicationFeed が返す処理を持つ IAsyncOperationWithProgress 型を返しています。

同時実行

WinRT には、さまざまな種類の非同期オブジェクトを表す 4 つの型が用意されており、C++/WinRT ではそのような非同期オブジェクトを作成および使用する方法をいくつか提供しています。図 1 では、結果が利用可能になるまで呼び出し元のスレッドをブロックする方法を示しています。ただし、C++/WinRT はプログラミング モデルに C++ コルーチンを密接に統合することによって、OS スレッドによる他の有益な作業を妨げることなく、自然な方法で協調的に結果を待機できるようにしています。get メソッドを使用する代わりに、次のように co_await ステートメントを記述して処理の結果を待機できます。

SyndicationFeed feed = co_await client.RetrieveFeedAsync(uri);

また、独自の WinRT 非同期オブジェクトを作成することも可能です。それには、図 2 に示すようにコルーチンを記述するだけです。結果の IAsync­Action (Windows.Foundation 名前空間に存在するもう 1 つの WinRT 非同期型) は、その後、別のコルーチンに集約されます。呼び出し元の判断により、get メソッドを使ってブロックしながら結果を待機することも、WinRT をサポートしている別のプログラミング言語に IAsync­Action を渡すこともできます。

図 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);
}

これは、標準 C++ で記述できるようになるにしても、相変わらず関与するのは特定のプラットフォームで真価を発揮するアプリを記述することだということを想起させる従来の WinMain エントリ ポイント関数にすぎません。CoreApplication の静的 Run メソッドでは、アプリの最初のビューを作成するために IFrameworkViewSource オブジェクトを必要とします。IFrameworkViewSource は 1 つのメソッドを使用するただのインターフェイスなので、少なくとも概念的には次のようになります。

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

そのため、IFrameworkViewSource が用意されている場合は次のように呼び出します。

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

当然、これを呼び出すのはアプリ開発者ではなくオペレーティング システムです。またアプリ開発者は、CreateView を返す IFrameworkView インターフェイスと共に、この IFrameworkViewSource インターフェイスを実装することを求められます。C++/WinRT を使うと、これを非常に簡単に行うことができます。ここでも、COM スタイルのプログラミングは必要ありません。おっかない WRL テンプレートや ATL のテンプレート、またマクロに頼る必要もありません。次のように C++/WinRT ライブラリを使用するだけでインターフェイスを実装できます。

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

implements クラス テンプレートは、広い意味では、WinRT インターフェイスを実装する可変個引数テンプレート アプローチを基礎にしています。これについては、2014 年の Special Connect(); 号のコラム「Windows API で最新の C++ を使えるようにする Visual C++ 2015」(msdn.com/magazine/dn879346)で説明しています。

最初の型パラメーターは派生クラスの名前で、2 番目以降の型パラメーターとして示されているインターフェイスの実装を目的としています。IFrameworkView インターフェイスはどのように実装するのでしょう。 これは、もう 1 つのインターフェイスが少し複雑になっただけで、次のようになります。

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 参照)。これはデスクトップでウィンドウを開いて実行する最小のアプリを表しており、特に興味深いことは実行されません。こうしたことをすべて示しているのは、次のすばらしいアプリの記述方法を説明するためではなく、WinRT 型を使用および生成する自然な C++ コードを用いる方法を示すためです。

図 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 クラス テンプレートが可変個引数テンプレートとして設計されていることを考えると、implements クラスを使用することで複数のインターフェイスを非常に簡単に実装できます。図 4 に示すように、IFrameworkViewSource と IFrameworkView を 1 つのクラスの内部に実装する場合もあります。

図 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 オブジェクトを実装した経験がある方なら、この改善を歓迎することでしょう。また、C++/WinRT は便利で、生産的で、使用するのが楽しいというだけではありません。実際、あらゆる WinRT 言語プロジェクションの中でも、最小のバイナリ サイズで、可能な限り最大のパフォーマンスを実現します。そのパフォーマンスは、ABI インターフェイスを直接使用した手書きのコードさえ上回ります。これは、最新の C++ 手法を活用するように抽象化が設計されており、Visual C++ コンパイラもそのような C++ 手法を最適化するように設計されているためです。このような最適化には、マジック静的変数、空ベースのクラス、strlen の省略のほか、C++/WinRT のパフォーマンス向上を特にねらいとした、最新バージョンの Visual C++ の新しい最適化も多数含まれます。

一例として、WinRT には必須インターフェイスの概念が導入されています。特定のランタイム クラスまたはインターフェイスは、他の一連のインターフェイスを追加で実装する必要がある場合があります。これにより、変更されることのない COM インターフェイスに基づいている WinRT でバージョン管理がサポートされます。たとえば、前述の Uri クラスでは ToString メソッドを 1 つ指定した IStringable インターフェイスが必要です。一般的なアプリ開発者は、IStringable インターフェイスのメソッドが実際には個別の COM インターフェイスと vtable から提供されていることを意識せずに、このメソッドを簡単に次のように利用できます。

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 メソッドに挿入され、さらに as メソッドにより QueryInterface が呼び出されます。ここで、COM の観点から見ると、QueryInterface は純粋な呼び出しです。1 回成功すれば、特定のオブジェクトについては常に上手く行きます。さいわい、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 と比較して 40 倍ものパフォーマンス向上が実現します。

標準 C++ は、WinRT 言語プロジェクションを生成しようとする開発者全員に対して、一部の特有の問題を突き付けています。その一端は、マイクロソフトの Visual C++ チームが最初に非標準のソリューションを考案したことが原因です。作業はまだ終わっていませんが、システム プログラマやアプリ開発者など、Windows 向けに高速で美しいコードを記述することに関心のあるすべての開発者のために、優秀かつ妥協のない言語プロジェクションを提供することを目標に掲げ、マイクロソフトは引き続き C++/WinRT に熱心に取り組んでいきます。

まとめ

James McNellis と筆者は、C++/WinRT を公式発表した 2016 年の CppCon で 2 つの講演を行いました。その 2 つの講演のビデオは以下でご覧いただけます。

  • 「Windows ランタイム向け標準 C++ の採用」(bit.ly/2ePwbyz、英語)
  • 「Windows ランタイムと連携するコルーチンの配置」(bit.ly/2fMLZqy、英語)

これらはやや技術的な「内幕を探る」プレゼンテーションです。大まかな概要については、bit.ly/2fwF6bx (英語) で CppCast インタビューをお聞きください。

最後に、C++/WinRT を github.com/microsoft/cppwinrt (英語) からダウンロードして今すぐ試すことができます。

やるべき作業はまだ多く残っており、標準 C++ の進歩に応じて、またプログラミング モデルを改善および単純化する他の方法を探る中で、さらに変更がいくつか加えられる可能性があります。皆様のご意見をお聞かせください。フィードバックを心待ちにしています。


Kenny Kerr は、C++ システム プログラマーであり、C++/WinRT の作成者です。また、Pluralsight の執筆者で、マイクロソフトの Windows チームのエンジニアでもあります。彼のブログは kennykerr.ca (英語) で、Twitter は @kennykerr (英語) でフォローできます。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの James McNellis に心より感謝いたします。