非同期性と、C++/WinRT と C++/CX の間の相互運用

ヒント

このトピックは最初から読んでお勧めしますが、「 C++/CX 非同期から C++/WinRT への移植の概要 」セクションの相互運用手法の概要に直接進むことができます。

これは、C++/CX から C++/WinRT に段階的に移植することに関連する高度なトピックです。 このトピックでは、 C++/WinRT と C++/CX の間の相互運用 に関するトピックを取り上げます。

コードベースのサイズや複雑さがプロジェクトを段階的に移植する必要がある場合は、C++/CX と C++/WinRT コードが同じプロジェクトに並んで存在する移植プロセスが必要になります。 非同期コードがある場合は、ソース コードを段階的に移植する際に、並列パターン ライブラリ (PPL) タスク チェーンとコルーチンがプロジェクト内に並んで存在することが必要になる場合があります。 このトピックでは、非同期 C++/CX コードと非同期 C++/WinRT コードの間で相互運用する手法について説明します。 これらの手法は、個別に、または一緒に使用できます。 この手法を使用すると、プロジェクト全体の移植に向けたパスに沿って、段階的で制御されたローカル変更を行うことができます。各変更をプロジェクト全体で制御不能に連鎖させる必要はありません。

このトピックを読む前に、 C++/WinRT と C++/CX の間の相互運用機能を読んでおくことをお勧めします。 このトピックでは、段階的な移植のためにプロジェクトを準備する方法について説明します。 また、C++/CX オブジェクトを C++/WinRT オブジェクト (およびその逆) に変換するために使用できる 2 つのヘルパー関数も導入されています。 非同期に関するこのトピックは、その情報に基づいており、これらのヘルパー関数を使用します。

Note

C++/CX から C++/WinRT に段階的に移植するには、いくつかの制限があります。 Windows ランタイム コンポーネント プロジェクトがある場合、段階的に移植することはできません。プロジェクトを 1 回のパスで移植する必要があります。 また、XAML プロジェクトでは、XAML ページの型は常に、すべて C++/WinRT であるか、または すべて C++/CX である必要があります。 詳しくは、「 C++/CX から C++/WinRT への移動」をご覧ください

トピック全体が非同期コード相互運用専用である理由

C++/CX から C++/WinRT への移植は一般的に簡単ですが、 並列パターン ライブラリ (PPL) タスクからコルーチンへの移行は例外です。 モデルは異なります。 PPL タスクからコルーチンへの自然な 1 対 1 のマッピングはなく、コードを機械的に移植する簡単な方法 (すべてのケースで機能) はありません。

良いニュースは、タスクからコルーチンへの変換が大幅に簡素化されるということです。 開発チームは、非同期コードの移植のハードルを超えると、移植作業の残りの部分は主に機械的であると日常的に報告しています。

多くの場合、アルゴリズムはもともと同期 API に合わせて記述されていました。 そして、それはタスクと明示的な継続処理に置き換えられ、その結果、多くの場合、根底にあるロジックが意図せず分かりにくくなっていました。 たとえば、ループは再帰になります。if-else 分岐は、タスクの入れ子になったツリー (チェーン) に変わります。共有変数が shared_ptrになります。 多くの場合、PPL ソース コードの不自然な構造を分解するには、最初に戻って元のコードの意図を理解することをお勧めします (つまり、元の同期バージョンを検出します)。 次に、適切な場所に co_await (協調的に待機) を挿入します。

そのため、ポートの開始元となる非同期コードの C# バージョン (C++/CX ではなく) がある場合は、より簡単な時間とよりクリーンなポートを提供できます。 C# コードでは、 awaitを使用します。 そのため、C# コードは、基本的に同期バージョンから始めて、適切な場所に await を挿入するという理念に既に従っています。

プロジェクトの C# バージョン がない 場合は、このトピックで説明する手法を使用できます。 また、C++/WinRT に移植すると、必要に応じて非同期コードの構造を C# に移植する方が簡単になります。

非同期プログラミングの一部の背景

非同期プログラミングの概念と用語に関する共通の参照フレームを用意するため、一般的な非同期プログラミングWindows ランタイムに関するシーンを簡単に設定し、2 つの C++ 言語プロジェクションがそれぞれ異なる方法でどのように重なっているかを簡単に設定しましょう。

プロジェクトには非同期的に動作するメソッドがあり、主に 2 つの種類があります。

  • 非同期処理の完了を待ってから、他の作業を行うのが一般的です。 非同期操作オブジェクトを返すメソッドは、待機できるメソッドです。
  • ただし、非同期的に行われた作業の完了を待ちたくない場合や、待機する必要がない場合があります。 その場合、非同期メソッドが非同期操作オブジェクトを返 さない 方が効率的です。 このような非同期メソッド (待機しないメソッド) は、 ファイア アンド フォーゲット メソッドと 呼ばれます。

Windows ランタイム の非同期オブジェクト (IAsyncXxx)

Windows::Foundation Windows ランタイム名前空間には、4 種類の非同期操作オブジェクトが含まれています。

このトピックでは、 IAsyncXxx の便利な短縮形を使用する場合、これらの型をまとめて参照します。または、4 つの型の 1 つについて説明します。どちらを指定する必要もありません。

C++/CX 非同期

非同期 C++/CX コードでは、 並列パターン ライブラリ (PPL) タスクを 使用します。 PPL タスクは、 コンカレンシー::task クラスによって表されます。

通常、非同期の C++/CX メソッドは、コンカレンシー::create_task とコンカレンシー::task::then でラムダ関数を使用して PPL タスクを連結します。 各ラムダ関数はタスクを返します。タスクが完了すると、タスクの 継続のラムダに渡される値が生成されます。

または、タスクを作成するために create_task を呼び出す代わりに、非同期 C++/CX メソッドで concurrency::create_async を呼び出して IAsyncXxx^ を作成することもできます。

したがって、非同期 C++/CX メソッドの戻り値の型は、PPL タスクまたは IAsyncXxx^ にすることができます。

どちらの場合も、メソッド自体は return キーワードを使用して非同期オブジェクトを返します。非同期オブジェクトが完了すると、呼び出し元が実際に必要とする値 (ファイル、バイト配列、ブール型など) が生成されます。

Note

非同期 C++/CX メソッドが IAsyncXxx^ を返す場合、TResult (ある場合) はWindows ランタイム型に制限されます。 たとえば、ブール値はWindows ランタイム型ですが、C++/CX 投影型 (Platform::Array<byte>^など) ではありません。

C++/WinRT 非同期

C++/WinRT は、C++ コルーチンをプログラミング モデルに統合します。 コルーチンと co_await ステートメントは、結果が返るのを協調的に待つための自然な方法です。

IAsyncXxx 型は、winrt::Windows::Foundation C++/WinRT 名前空間内の対応する型に投影されます。 それらを winrt::IAsyncXxx (C++/CX の IAsyncXxx^ と比較して) と見てみましょう。

C++/WinRT コルーチンの戻り値の型は 、winrt::IAsyncXxx または winrt::fire_and_forget です。 また、 return キーワードを使用して非同期オブジェクトを返す代わりに、コルーチンは co_return キーワードを使用して、呼び出し元が実際に必要とする値 (ファイル、バイト配列、ブール値など) を協調的に返します。

メソッドに少なくとも 1 つの co_await ステートメント (または少なくとも 1 つの co_return または co_yield) が含まれている場合、その理由からメソッドはコルーチンになります。

詳細とコード例については、「 C++/WinRT を使用したコンカレンシーと非同期操作」を参照してください。

Direct3D ゲーム サンプル (Simple3DGameDX)

このトピックには、非同期コードを段階的に移植する方法を示す、いくつかの特定のプログラミング手法のチュートリアルが含まれています。 ケース スタディとして機能するために、Direct3D ゲーム サンプル (Simple3DGameDX と呼ばれる) の C++/CX バージョンを使用します。 そのプロジェクトで元の C++/CX ソース コードを取得し、その非同期コードを C++/WinRT に段階的に移植する方法の例をいくつか示します。

  • 上記のリンクから ZIP をダウンロードし、解凍します。
  • Visual Studioで C++/CX プロジェクト (cpp という名前のフォルダーにあります) を開きます。
  • その後、プロジェクトに C++/WinRT サポートを追加する必要があります。 実行する手順については、「 C++/CX プロジェクトの作成と C++/WinRT サポートの追加」で説明されています。 このセクションでは、 interop_helpers.h ヘッダー ファイルをプロジェクトに追加する手順は、このトピックのヘルパー関数に依存するため、特に重要です。
  • 最後に、#include <pplawait.h>pch.hを追加します。 これで、PPL のコルーチン サポートが提供されます (そのサポートの詳細については、次のセクションを参照してください)。

まだビルドしないでください。そうしないと、 バイト があいまいであるというエラーが発生します。 これを解決する方法は次のとおりです。

  • BasicLoader.cppを開き、using namespace std;コメント アウトします。
  • 同じソース コード ファイルで、shared_ptrを std::shared_ptr として修飾する必要があります。 これは、そのファイル内の検索と置換を使用して行うことができます。
  • 次に 、ベクターstd::vector として、 文字列std::string として修飾します。

プロジェクトが再びビルドされ、C++/WinRT がサポートされ、 from_cx とto_cx相互運用ヘルパー関数 含まれます。

これで、Simple3DGameDX プロジェクトを使用して、このトピックのコードの解説を読み進める準備ができました。

C++/CX 非同期から C++/WinRT への移植の概要

簡単に言うと、移植するにつれて、PPL タスク チェーンを co_await の呼び出しに変更します。 メソッドの戻り値を PPL タスクから C++/WinRT winrt::IAsyncXxx オブジェクトに変更します。 また、 IAsyncXxx^ を C++/WinRT winrt::IAsyncXxx に変更します。

コルーチンは、 co_xxxを呼び出す任意のメソッドであることを思い出します。 C++/WinRT コルーチンは、 co_return を使用してその値を協調的に返します。 PPL のコルーチン サポート ( pplawait.h提供) により、 co_return を使用してコルーチンから PPL タスクを返すこともできます。 また、タスクと co_await の両方をすることもできます。 ただし、 co_return を使用して IAsyncXxx^ を返すことはできません。 次の表では、図内の pplawait.h を使用したさまざまな非同期手法間の相互運用のサポートについて説明します。

Method それをco_awaitできますか? co_return をそこからできますか?
メソッドは task<void を返します> Yes Yes
メソッドは タスク<T を返します> No Yes
メソッドは IAsyncXxx を返します^ Yes No. ただし、co_return を使用するタスクを create_async でラップします。
メソッドは winrt::IAsyncXxx を返します Yes Yes

この次の表を使用して、関心のある相互運用手法について説明するこのトピックのセクションに進むか、ここから読み続けます。

非同期相互運用手法 このトピック内のセクション
co_awaitを使用して、fire-and-forget メソッド内またはコンストラクター内からタスク<void> メソッドを待機します。 fire-and-forget メソッド内 でタスク<void> を待機する
task<void> メソッド内から task<void> メソッドの完了を待機するには、co_await を使用します。 task<void> メソッド内で await <void> を待機する
task<T> メソッド内から task<void> メソッドを待機するには、co_await を使用します。 task<T> メソッド内で task<void> を await する
co_awaitを使用して IAsyncXxx^ メソッドを待機します。 タスク メソッドで IAsyncXxx^ を待機します。残りのプロジェクトは変更されません
task<void> メソッド内で co_return を使用します。 task<void> メソッド内で Await <task>void を待機する
task<T> メソッド内で co_return を使用します。 タスク メソッドで IAsyncXxx^ を待機します。残りのプロジェクトは変更されません
co_returnを使用するタスクをcreate_asyncでラップします。 使用するタスクの周囲にcreate_asyncをラップするco_return
concurrency::wait を移植する。 concurrency::waitco_await winrt::resume_after に移植
>の代わりに winrt::IAsyncXxx を返します。 task<void> 戻り値型を winrt::IAsyncXxx に移行する
winrt::IAsyncXxx<T> (T はプリミティブ) をタスク<T>に変換します。 winrt::IAsyncXxx<T> (T はプリミティブ) をタスク<T に変換します>
winrt::IAsyncXxx<T> (T はWindows ランタイム型) をタスク<T^>に変換します。 winrt::IAsyncXxx<T> (T はWindows ランタイム型) をタスクに変換します<T^>

サポートの一部を示す短いコード例を次に示します。

#include <ppltasks.h>
#include <pplawait.h>
#include <winrt/Windows.Foundation.h>

concurrency::task<bool> TaskAsync()
{
    co_return true;
}

Windows::Foundation::IAsyncOperation<bool>^ IAsyncXxxCppCXAsync()
{
    // co_return true; // Error! Can't do that. But you can do
    // the following.
    return concurrency::create_async([=]() -> concurrency::task<bool> {
        co_return true;
        });
}

winrt::Windows::Foundation::IAsyncOperation<bool> IAsyncXxxCppWinRTAsync()
{
    co_return true;
}

concurrency::task<bool> CppCXAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    co_return co_await IAsyncXxxCppWinRTAsync();
}

winrt::fire_and_forget CppWinRTAsync()
{
    bool b1 = co_await TaskAsync();
    bool b2 = co_await IAsyncXxxCppCXAsync();
    bool b3 = co_await IAsyncXxxCppWinRTAsync();
}

Von Bedeutung

これらの優れた相互運用オプションでも、移植は徐々に、プロジェクトの残りの部分に影響を与えない手術的に行うことができる変更の選択に依存します。 私たちは、任意の緩い端で引っ張らないようにし、それによってプロジェクト全体の構造を解明したいと考えています。 そのためには、特定の順序で行う必要があります。 次に、このような非同期関連の移植/相互運用の変更を行う例をいくつか詳しく見ていきます。

タスク<void>メソッドを待機し、プロジェクトの残りの部分は変更しません

task<void> を返すメソッドは非同期的に作業を実行し、非同期操作オブジェクトを返しますが、最終的には値を生成しません。 そのようなメソッドを co_await できます。

したがって、非同期コードの移植を徐々に開始する良い場所は、そのようなメソッドを呼び出す場所を見つけることです。 これらの場所には、タスクの作成や返しが含まれます。 また、各タスクから継続に値が渡されないタスク チェーンの種類も含まれる場合があります。 このような場所では、次に示すように、非同期コードを co_await ステートメントに置き換えることができます。

Note

このトピックが進むにつれて、この戦略の利点がわかります。 特定 のタスク<void> メソッドが co_awaitを介して排他的に呼び出されると、そのメソッドを C++/WinRT に自由に移植でき、 winrt::IAsyncXxx が返されます。

いくつかの例を見つけましょう。 Simple3DGameDX プロジェクトを開きます (Direct3D ゲーム サンプルを参照)。

Von Bedeutung

次の例では、メソッドの実装が変更されていることがわかりますが、変更するメソッドの 呼び出し元 を変更する必要はありません。 これらの変更はローカライズされ、プロジェクトを連鎖しません。

fire-and-forget メソッド内で task<void> を await する

最初に、< メソッド内でタスク>voidを待機してみましょう。これは最も簡単なケースであるためです。 これらは非同期的に動作するメソッドですが、メソッドの呼び出し元はその処理が完了するまで待機しません。 非同期的に完了するという事実にもかかわらず、メソッドを呼び出して忘れるだけです。

プロジェクトの依存関係グラフのルートに目を向け、create_task や、task<void> メソッドのみが呼び出されるタスクチェーンを含む voidメソッドを探します。

Simple3DGameDX では、GameMain::Update メソッドの実装にそのようなコードがあります。 これはソース コード ファイルの GameMain.cppにあります。

GameMain::Update

メソッドの C++/CX バージョンからの抽出を次に示します。非同期的に完了するメソッドの 2 つの部分を示します。

void GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    case UpdateEngineState::Dynamics:
        ...
        m_game->LoadLevelAsync().then([this]()
        {
            m_game->FinalizeLoadLevel();
            m_updateState = UpdateEngineState::ResourcesLoaded;
        }, task_continuation_context::use_current());
        ...
    ...
}

Simple3DGame::LoadLevelAsync メソッド (PPL タスクを返します<void>) の呼び出しを確認できます。 その後、いくつかの同期処理を行う 継続 です。 LoadLevelAsync は非同期ですが、値を返しません。 そのため、タスクから継続に値が渡されません。

この 2 つの場所で、コードに同じ種類の変更を加えることができます。 コードは、以下の一覧の後で説明します。 ここでは、クラス メンバー コルーチンで この ポインターにアクセスする安全な方法について説明します。 ただ、その話は後の節に回しましょう(co_awaitthis ポインターに関する後回しにした説明)。今のところ、このコードは動作します。

winrt::fire_and_forget GameMain::Update()
{
    ...
    case UpdateEngineState::WaitingForPress:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    case UpdateEngineState::Dynamics:
        ...
        co_await m_game->LoadLevelAsync();
        m_game->FinalizeLoadLevel();
        m_updateState = UpdateEngineState::ResourcesLoaded;
        ...
    ...
}

ご覧のように、LoadLevelAsync はタスクを返すので、それを co_await できます。 また、明示的な継続は必要ありません。 co_await に続くコードは 、LoadLevelAsync が完了したときにのみ実行されます。

co_awaitを導入すると、メソッドがコルーチンに変わるので、void返したままにすることはできませんでした。 これは fire-and-forget メソッドなので、 winrt::fire_and_forget を返すように変更しました。

GameMain.hを編集する必要もあります。 そこにある宣言でも、GameMain::Update の戻り値型を void から winrt::fire_and_forget に変更してください。

この変更をプロジェクトのコピーに加えることができますが、ゲームは引き続き同じビルドと実行を行います。 ソース コードは基本的には C++/CX ですが、C++/WinRT と同じパターンを使用しているため、残りのコードを機械的に移植できるようになるのに少し近づいていました。

GameMain::ResetGame

GameMain::ResetGame はもう 1 つのファイア アンド フォーゲット メソッドです。 LoadLevelAsync も呼び出します。 そのため、練習が必要な場合は、同じコード変更を行うことができます。

GameMain::OnDeviceRestored

GameMain::OnDeviceRestored では、no-op タスクも含めて非同期コードの入れ子がより深くなっているため、少し事情が複雑になります。 メソッドの非同期部分の概要を次に示します (省略記号で表されるあまり興味深くない同期コード)。

void GameMain::OnDeviceRestored()
{
    ...
    create_task([this]()
    {
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            ...
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ...
    }, task_continuation_context::use_current());
}

まず、GameMain.h.cpp で、GameMain::OnDeviceRestored の戻り値の型を void から winrt::fire_and_forget に変更します。 また、 DeviceResources.h を開き、 IDeviceNotify::OnDeviceRestored の戻り値の型に同じ変更を加える必要があります。

非同期コードを移植するには、create_taskthen の呼び出しをすべて削除し、それらの波かっこも取り除いて、メソッドを平坦な一連のステートメントに簡略化します。

タスクを返す returnco_awaitに変更します。 何も返さない 1 つの return が残りますので、削除してください。 完了すると、no-op タスクは消え、メソッドの非同期部分のアウトラインは次のようになります。 ここでも、あまり興味深くない同期コードは省略されています。

winrt::fire_and_forget GameMain::OnDeviceRestored()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

ご覧のように、この形式の非同期構造は非常に単純で、読みやすくなりました。

GameMain::GameMain

GameMain::GameMain コンストラクターは非同期的に作業を実行し、その作業が完了するまでプロジェクトの一部は待機しません。 ここでも、この一覧では非同期部分の概要を示します。

GameMain::GameMain(...) : ...
{
    ...
    create_task([this]()
    {
        ...
        return m_renderer->CreateGameDeviceResourcesAsync(m_game);
    }).then([this]()
    {
        ...
        if (m_updateState == UpdateEngineState::WaitingForResources)
        {
            return m_game->LoadLevelAsync().then([this]()
            {
                ...
            }, task_continuation_context::use_current());
        }
        else
        {
            return create_task([]()
            {
                // Return a no-op task.
            });
        }
    }, task_continuation_context::use_current()).then([this]()
    {
        ....
    }, task_continuation_context::use_current());
}

ただし、コンストラクターは winrt::fire_and_forget を返すことはできません。そのため、非同期コードを新しい GameMain::ConstructInBackground fire-and-forget メソッドに移動し、コードを co_await ステートメントにフラット化し、コンストラクターから新しいメソッドを呼び出します。 結果を次に示します。

GameMain::GameMain(...) : ...
{
    ...
    ConstructInBackground();
}

winrt::fire_and_forget GameMain::ConstructInBackground()
{
    ...
    co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
    ...
    if (m_updateState == UpdateEngineState::WaitingForResources)
    {
        ...
        co_await m_game->LoadLevelAsync();
        ...
    }
    ...
}

これで、GameMain のすべての fire-and-forget メソッド (実際には、すべての非同期コード) がコルーチンに変わりました。 もしその気があれば、他のクラスでも fire-and-forget メソッドを探し、同様の変更を加えてみてもよいでしょう。

co_awaitこのポインターに関する遅延ディスカッション

GameMain::Update に変更を加えたときに、このポインターに関する説明を延期しました。 ここでその話し合いをしましょう。

これは、これまでに変更してきたすべてのメソッドに当てはまります。また、これは fire-and-forget 型だけでなく、すべてのコルーチンに当てはまります。 メソッドに co_await を導入すると、 中断ポイントが導入されます。 そのため、 この ポインターに注意する必要があります。もちろん、クラス メンバーにアクセスするたびに中断ポイントの に使用します。

短い話は、ソリューションが implements::get_strong を呼び出すということです。 ただし、問題と解決策の詳細については、「 クラス メンバー コルーチンで この ポインターに安全にアクセスする」を参照してください。

implements::get_strong は、winrt::implements から派生したクラスでのみ呼び出すことができます。

winrt::implements から GameMain を派生させる

最初に行う必要がある変更は、 GameMain.hです。

class GameMain :
    public DX::IDeviceNotify

GameMain は引き続き DX::IDeviceNotify を実装しますが、 winrt::implements から派生するように変更します。

class GameMain : 
    public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
    DX::IDeviceNotify

次に、 App.cppで、このメソッドを見つけます。

void App::Load(Platform::String^)
{
    if (!m_main)
    {
        m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
    }
}

ただし、 GameMainwinrt::implements から派生したので、別の方法で構築する必要があります。 この場合は、 winrt::make_self 関数テンプレートを使用します。 詳細については、「 実装の型とインターフェイスのインスタンス化と返し」を参照してください。

そのコード行をこれに置き換えます。

    ...
    m_main = winrt::make_self<GameMain>(m_deviceResources);
    ...

その変更のループを閉じるには、 m_mainの種類も変更する必要があります。 App.hでは、このコードが見つかります。

ref class App sealed :
    public Windows::ApplicationModel::Core::IFrameworkView
{
    ...
private:
    ...
    std::unique_ptr<GameMain> m_main;
};

m_mainの宣言をこれに変更します。

    ...
    winrt::com_ptr<GameMain> m_main;
    ...

implements::get_strong を呼び出すようになりました

GameMain::Update の場合、およびco_awaitを追加したその他のメソッドについては、コルーチンの先頭でget_strongを呼び出して、コルーチンが完了するまで厳密な参照が存続するようにする方法を次に示します。

winrt::fire_and_forget GameMain::Update()
{
    auto strong_this{ get_strong() }; // Keep *this* alive.
    ...
        co_await ...
    ...
}

task<void><void> メソッド内で Await <task>

次に単純なケースは、それ自体が task<void> を返すメソッド内で task<void> を await することです。 これは、co_awaittask<void>をでき、また1つからco_returnできるためです。

Simple3DGame::LoadLevelAsync メソッドの実装には、非常に単純な例があります。 これはソース コード ファイルの Simple3DGame.cppにあります。

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    return m_renderer->LoadLevelResourcesAsync();
}

同期コードがいくつか存在し、 その後に GameRenderer::LoadLevelResourcesAsync によって作成されたタスクが返されます。

そのタスクを返す代わりに、タスクをco_awaitし、結果のco_returnvoidします。

task<void> Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

それは大きな変化とは見えません。 ただし、を介して co_await を呼び出すようになったので、タスクの代わりに winrt::IAsyncXxx を返すように移植できます。 これについては、「task<void> 戻り値の型を winrt::IAsyncXxx に移植する」セクションで後述します。

task<T> メソッド内で await <void>

Simple3DGameDX には適切な例はありませんが、パターンを示すだけの仮定の例を考えることができます。

次のコード例の最初の行は、task<void>の単純なco_awaitを示しています。 次に、 タスク<T> 戻り値の型を満たすために、 StorageFile^ を非同期的に返す必要があります。 これを行うには、Windows ランタイム API をco_awaitし、結果のファイルをco_returnします。

task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder^ location,
    Platform::String^ filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location->GetFileAsync(filename);
}

このように、より多くのメソッドを C++/WinRT に移植することもできます。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
Simple3DGame::LoadLevelAndRetrieveFileAsync(
    StorageFolder location,
    std::wstring filename)
{
    co_await m_renderer->LoadLevelResourcesAsync();
    co_return co_await location.GetFileAsync(filename);
}

m_renderer データ メンバーは、その例では引き続き C++/CX です。

タスク メソッドで IAsyncXxx^ を待機します。残りのプロジェクトは変更されません

co_await task<void> にする方法を見てきた。 co_await IAsyncXxx を返すメソッドを使用することもできます。これには、プロジェクト内のメソッドや、非同期の Windows API (たとえば、前のセクションで待機した StorageFolder.GetFileAsync) が含まれます。

この種のコードを変更できる例については、 BasicReaderWriter::ReadDataAsync ( BasicReaderWriter.cppで実装されていることがわかります) を見てみましょう。

元の C++/CX バージョンを次に示します。

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
    )
{
    return task<StorageFile^>(m_location->GetFileAsync(filename)).then([=](StorageFile^ file)
    {
        return FileIO::ReadBufferAsync(file);
    }).then([=](IBuffer^ buffer)
    {
        auto fileData = ref new Platform::Array<byte>(buffer->Length);
        DataReader::FromBuffer(buffer)->ReadBytes(fileData);
        return fileData;
    });
}

以下のコード一覧は、co_awaitIAsyncXxx^ を返す Windows API を利用できることを示しています。 それだけでなく、co_return が非同期的に返す値 (この場合はバイト配列) をすることもできます。 この最初の手順では、これらの変更のみを行う方法を示します。実際には、次のセクションで C++/CX コードを C++/WinRT に移植します。

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename
)
{
    StorageFile^ file = co_await m_location->GetFileAsync(filename);
    IBuffer^ buffer = co_await FileIO::ReadBufferAsync(file);
    auto fileData = ref new Platform::Array<byte>(buffer->Length);
    DataReader::FromBuffer(buffer)->ReadBytes(fileData);
    co_return fileData;
}

ここでも、戻り値の型を変更していないため、変更するメソッドの 呼び出し元 を変更する必要はありません。

ReadDataAsync (主に) を C++/WinRT に移植します。残りのプロジェクトは変更されません

さらに一歩進んで、プロジェクトの他の部分を変更しなくても、 メソッドをほぼ完全に C++/WinRT に移植できます。

このメソッドがプロジェクトの残りの部分に対して持つ唯一の依存関係は、 BasicReaderWriter::m_location データ メンバーです。これは C++/CX StorageFolder^ です。 そのデータ メンバーを変更せずに、パラメーター型と戻り値の型を変更せずに残すには、メソッドの先頭と末尾に 1 つずつ、2 つの変換のみを実行する必要があります。 そのために、 from_cx とto_cx相互運用ヘルパー関数 使用できます。

実装を主に C++/WinRT に移植した後の BasicReaderWriter::ReadDataAsync の外観を次に示します。 これは、 段階的に移植する良い例です。 このメソッドは、 いくつかの C++/WinRT 手法を使用する C++/CX メソッドと考え離れ、C++ /CX と相互運用する C++/WinRT メソッドと見なすことができる段階にあります。

#include <winrt/Windows.Storage.h>
#include <winrt/Windows.Storage.Streams.h>
#include <robuffer.h>
...
task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

Note

上記 の ReadDataAsync では、新しい C++/CX 配列を構築して返します。 もちろん、メソッドの戻り値の型を満たすためにそれを行います(プロジェクトの残りの部分を変更する必要はありません)。

自分のプロジェクトでも、移植後にメソッドの終わりまで来た時点で、手元にあるのが C++/WinRT オブジェクトだけという、別の例に遭遇することがあるでしょう。 それを co_return するには、 to_cx を呼び出して変換するだけです。 詳細については、次のセクションの例を参照してください。

winrt::IAsyncXxx<T>タスク<T に変換します>

このセクションでは、非同期メソッドを C++/WinRT (winrt::IAsyncXxx<T> を返すように) に移植したが、タスクを返すかのようにそのメソッドを呼び出す C++/CX コードがある状況について説明します。

  • 1 つのケースは、 T がプリミティブであり、変換を必要としない場合です。
  • もう 1 つは、T がWindows ランタイム型である場合です。その場合は、T^ に変換する必要があります。

winrt::IAsyncXxx<T> (T はプリミティブ) をタスク<T に変換します>

このセクションのパターンは、プリミティブ値を非同期的に返すときに適用されます (ブール値を使用して説明します)。 C++/WinRT に既に移植したメソッドにこのシグネチャがある例を考えてみましょう。

winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
    bool value = ...
    co_return value;
}

そのメソッドの呼び出しを次のようなタスクに変換できます。

task<bool> MyClass::RetrieveBoolTask()
{
    co_return co_await GetBoolMemberFunctionAsync();
}

または、次のようになります。

task<bool> MyClass::RetrieveBoolTask()
{
    return concurrency::create_task(
        [this]() -> concurrency::task<bool> {
            auto result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

ラムダ関数の タスク の戻り値の型が明示的であることに注意してください。これは、コンパイラでは推測できないためです。

このような任意のタスク チェーン内からメソッドを呼び出すこともできます。 ここでも、明示的なラムダ戻り値の型を使用します。

...
.then([this]() -> concurrency::task<bool> {
    co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
    ...
});
...

winrt::IAsyncXxx<T> (T はWindows ランタイム型) をタスクに変換します<T^>

このセクションのパターンは、Windows ランタイム値を非同期的に返すときに適用されます (StorageFile 値を使用して説明します)。 C++/WinRT に既に移植したメソッドにこのシグネチャがある例を考えてみましょう。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
    co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
    (L"MyFile.txt");
}

次の一覧では、そのメソッドへの呼び出しをタスクに変換する方法を示します。 返された C++/WinRT オブジェクトを C++/CX ハンドル (帽子とも呼ばれます) オブジェクトに変換するには、to_cx相互運用ヘルパー関数を呼び出す必要があることに注意してください。

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    winrt::Windows::Storage::StorageFile storageFile =
        co_await GetStorageFileMemberFunctionAsync();
    co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}

より簡潔なバージョンを次に示します。

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

また、そのパターンを再利用可能な関数テンプレートにラップし、通常はタスクを返すのと同じように return することもできます。

template<typename ResultTypeCX, typename Awaitable>
concurrency::task<ResultTypeCX^> to_task(Awaitable awaitable)
{
    co_return to_cx<ResultTypeCX>(co_await awaitable);
}

task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
    return to_task<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}

そのアイデアが必要な場合は、interop_helpers.hを追加できます。

co_return を使用するタスクを create_async でラップする

co_return^ を直接することはできませんが、同様の結果を得ることができます。 値を協調的に返すタスクがある場合は、 コンカレンシー::create_asyncの呼び出し内でラップできます。

Simple3DGameDX からリフトできる例がないため、仮定の例を次に示します。

Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
    return concurrency::create_async(
        [this]() -> concurrency::task<bool> {
            bool result = co_await GetBoolMemberFunctionAsync();
            co_return result;
        });
}

ご覧のように、 co_awaitできる任意のメソッドから戻り値を取得できます。

concurrency::waitco_await winrt::resume_after に移植する

Simple3DGameDXコンカレンシー::wait を使用してスレッドを短時間一時停止する場所がいくつかあります。 次に例を示します。

// GameConstants.h
namespace GameConstants
{
    ...
    static const int InitialLoadingDelay = 2000;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]()
    {
        wait(GameConstants::InitialLoadingDelay);
    }));
    ...
}

C++/WinRT バージョンの コンカレンシー::waitwinrt::resume_after 構造体です。 PPL タスク内でその構造体を co_await できます。 コード例を次に示します。

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto InitialLoadingDelay = 2000ms;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::CreateGameDeviceResourcesAsync(_In_ Simple3DGame^ game)
{
    std::vector<task<void>> tasks;
    ...
    tasks.push_back(create_task([]() -> task<void>
    {
        co_await winrt::resume_after(GameConstants::InitialLoadingDelay);
    }));
    ...
}

私たちが行わなければならなかった他の2つの変更に注意してください。 GameConstants::InitialLoadingDelay の型を std::chrono::duration に変更し、また、コンパイラがその型を推論できなくなったため、ラムダ関数の戻り値の型を明示的にしました。

タスク<void>戻り値の型を winrt::IAsyncXxx に移植する

Simple3DGame::LoadLevelAsync

Simple3DGameDX での作業のこの段階では、Simple3DGame::LoadLevelAsync を呼び出すプロジェクト内のすべての場所で、co_awaitを使用して呼び出します。

つまり、そのメソッドの戻り値の型を task<void> から winrt::Windows::Foundation::IAsyncAction に変更できます (残りの部分は変更されません)。

winrt::Windows::Foundation::IAsyncAction Simple3DGame::LoadLevelAsync()
{
    m_level[m_currentLevel]->Initialize(m_objects);
    m_levelDuration = m_level[m_currentLevel]->TimeLimit() + m_levelBonusTime;
    co_return co_await m_renderer->LoadLevelResourcesAsync();
}

そのメソッドの残りの部分とその依存関係 ( m_level など) を C++/WinRT に移植することは、かなり機械的であるはずです。

GameRenderer::LoadLevelResourcesAsync

GameRenderer::LoadLevelResourcesAsync の元の C++/CX バージョンを次に示します。

// GameConstants.h
namespace GameConstants
{
    ...
    static const int LevelLoadingDelay = 500;
    ...
}

// GameRenderer.cpp
task<void> GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;

    return create_task([this]()
    {
        wait(GameConstants::LevelLoadingDelay);
    });
}

Simple3DGame::LoadLevelAsync、GameRenderer::LoadLevelResourcesAsync を呼び出すプロジェクト内の唯一の場所であり、既に co_await を使用して呼び出しています。

そのため、GameRenderer::LoadLevelResourcesAsync がタスクを返す必要がなくなりました。代わりに winrt::Windows::Foundation::IAsyncAction を返すことができます。 実装自体は、C++/WinRT に完全に移植するのに十分に単純です。 それには、Port concurrency::wait to co_await winrt::resume_after で行ったのと同じ変更を加える必要があります。 また、懸念するプロジェクトの残りの部分に大きな依存関係はありません。

したがって、C++/WinRT に完全に移植した後のメソッドの外観を次に示します。

// GameConstants.h
namespace GameConstants
{
    using namespace std::literals::chrono_literals;
    ...
    static const auto LevelLoadingDelay = 500ms;
    ...
}

// GameRenderer.cpp
winrt::Windows::Foundation::IAsyncAction GameRenderer::LoadLevelResourcesAsync()
{
    m_levelResourcesLoaded = false;
    co_return co_await winrt::resume_after(GameConstants::LevelLoadingDelay);
}

目標 - メソッドを C++/WinRT に完全に移植する

メソッド BasicReaderWriter::ReadDataAsync を C++/WinRT に完全に移植することで、このチュートリアルを最終的な目標の例と共にまとめましょう。

このメソッドを最後に見たのは (ほとんどの場合、 プロジェクトの残りの部分を変更せずに、C++/WinRT に ReadDataAsync を移植するセクションで)、 ほとんどが C++/WinRT に移植されました。 ただし、引き続き Platform::Array<byte>^ のタスクが返されました。

task<Platform::Array<byte>^> BasicReaderWriter::ReadDataAsync(
    _In_ Platform::String^ filename)
{
    auto location_from_cx = from_cx<winrt::Windows::Storage::StorageFolder>(m_location);

    auto file = co_await location_from_cx.GetFileAsync(filename->Data());
    auto buffer = co_await winrt::Windows::Storage::FileIO::ReadBufferAsync(file);
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));

    co_return ref new Platform::Array<byte>(bytes, buffer.Length());
}

タスクを返す代わりに、 IAsyncOperation を返すように変更します。 また、その IAsyncOperation を介してバイト配列を返す代わりに、C++/WinRT IBuffer オブジェクトを返します。 また、呼び出しサイトのコードを少し変更する必要もあります。

C++/WinRT 構文とオブジェクトを使用するために、実装、パラメーター、 およびm_location データ メンバーを移植した後のメソッドの外観を次に示します。

winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::Streams::IBuffer>
BasicReaderWriter::ReadDataAsync(
    _In_ winrt::hstring const& filename)
{
    StorageFile file{ co_await m_location.GetFileAsync(filename) };
    co_return co_await FileIO::ReadBufferAsync(file);
}

winrt::array_view<byte> BasicLoader::GetBufferView(
    winrt::Windows::Storage::Streams::IBuffer const& buffer)
{
    byte* bytes;
    auto byteAccess = buffer.as<Windows::Storage::Streams::IBufferByteAccess>();
    winrt::check_hresult(byteAccess->Buffer(&bytes));
    return { bytes, bytes + buffer.Length() };
}

ご覧のように、 BasicReaderWriter::ReadDataAsync 自体は、バッファーからバイトを取得する同期ロジックを独自のメソッドに組み込んだため、はるかに簡単です。

ただし、C++/CX では、この種の構造体から呼び出しサイトを移植する必要があります。

task<void> BasicLoader::LoadTextureAsync(...)
{
    return m_basicReaderWriter->ReadDataAsync(filename).then(
        [=](const Platform::Array<byte>^ textureData)
    {
        CreateTexture(...);
    });
}

C++/WinRT におけるこのパターンへ。

winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
    auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
    auto textureData = GetBufferView(textureBuffer);
    CreateTexture(...);
}

重要な API