Assincronia e interoperabilidade entre o C++/WinRT e o C++/CX

Dica

Embora seja recomendável que você leia este tópico desde o início, você pode ir diretamente para um resumo das técnicas de interoperabilidade na seção Visão geral da portabilidade de assincronia do C++/CX para o C++/WinRT.

Trata-se de um tópico avançado relacionado à portabilidade gradual do C++/WinRT para o C++/CX. Este tópico continua de onde o tópico Interoperabilidade entre C++/WinRT e C++/CX parou.

Se o tamanho ou a complexidade de sua base de código obrigar você a portar seu projeto gradualmente, será necessário um processo de portabilidade no qual, por um momento, os códigos C++/CX e C++/WinRT coexistem no mesmo projeto. Se você tiver código assíncrono, talvez seja necessário ter cadeias de tarefas PPL (Biblioteca de Padrões Paralelos) e corrotinas coexistindo em seu projeto conforme você porta seu código-fonte gradualmente. Este tópico se concentra em técnicas para interoperação entre o código C++/CX assíncrono e o código C++/WinRT assíncrono. Você pode usar essas técnicas individualmente ou em conjunto. As técnicas permitem que você faça alterações graduais, controladas e locais ao longo do caminho da portabilidade de todo o seu projeto, sem que cada alteração se propague em cascata sem controle por todo o projeto.

Antes de ler este tópico, convém ler Interoperabilidade entre C++/WinRT e C++/CX. Este tópico mostra como preparar seu projeto para portabilidade gradual. Ele também apresenta duas funções auxiliares que podem ser usadas para converter um objeto C++/CX em um objeto C++/WinRT (e vice-versa). Este tópico sobre a assincronia baseia-se nessas informações e usa essas funções auxiliares.

Observação

Há algumas limitações para fazer a portabilidade gradual do C++/CX para C++/WinRT. Se você tiver um projeto de componente do Windows Runtime, a portabilidade gradual não será possível e você precisará portar o projeto em um passo. E, para um projeto XAML, a qualquer momento, os tipos de página XAML devem estar ou inteiramente em C++/WinRT ou inteiramente em C++/CX. Para obter mais informações, confira o tópico Migrar do C++/CX para o C++/WinRT.

O motivo pelo qual um tópico inteiro é dedicado à interoperabilidade de código assíncrono

A portabilidade de C++/CX para C++/WinRT é geralmente simples, com a única exceção da migração de tarefas PPL (Biblioteca de Padrões Paralelos) para corrotinas. Os modelos são diferentes. Não há um mapeamento um a um natural de tarefas PPL para corrotinas e não há uma forma simples (que funcione para todos os casos) de portar mecanicamente o código.

A boa notícia é que a conversão de tarefas em corrotinas resulta em simplificações significativas. E as equipes de desenvolvimento normalmente relatam que, depois que elas concluem a etapa de portabilidade do código assíncrono, o restante do trabalho de portabilidade é, em grande parte, mecânico.

Geralmente, um algoritmo era escrito originalmente para se adequar às APIs síncronas. Depois era convertido em tarefas e em continuações explícitas, sendo o resultado geralmente uma ofuscação inadvertida da lógica subjacente. Por exemplo, os loops se tornam recursão; branches if-else são convertidos em uma árvore aninhada (uma cadeia) de tarefas; as variáveis compartilhadas se tornam shared_ptr. Para desconstruir a estrutura geralmente não natural do código-fonte da PPL, recomendamos que você primeiro retroceda e entenda a intenção do código original (ou seja, descubra a versão síncrona original). Em seguida, insira co_await (await cooperativo) nos locais apropriados.

Por esse motivo, se você tiver uma versão em C# (em vez de em C++/CX) do código assíncrono do qual iniciar sua portabilidade, isso poderá facilitar o processo e propiciar uma portabilidade mais limpa. O código C# usa await. Portanto, o código C# já segue essencialmente uma filosofia de começar com uma versão síncrona e, em seguida, inserir await nos locais apropriados.

Se você não tem uma versão do C# do seu projeto, use as técnicas descritas neste tópico. E, depois de portar para o C++/WinRT, a estrutura do seu código assíncrono será mais fácil de portar para o C#, se você quiser.

Alguma experiência em programação assíncrona

Para termos um quadro de referência comum para conceitos e terminologia de programação assíncrona, vamos preparar brevemente o terreno com relação à programação assíncrona do Windows Runtime em geral e também a como as duas projeções de linguagem C++ são, nas diversas maneiras, colocadas umas sobre as outras.

Seu projeto tem métodos que funcionam de maneira assíncrona, e há dois tipos principais.

  • É comum aguardar a conclusão do trabalho assíncrono antes de fazer outra coisa. Um método que retorna um objeto de operação assíncrona é aquele que você pode aguardar.
  • Mas, às vezes, você não deseja nem precisa aguardar a conclusão do trabalho de maneira assíncrona. Nesse caso, é mais eficiente para o método assíncrono não retornar um objeto de operação assíncrona. Um método assíncrono como, aquele que você não aguarda, é conhecido como um método fire-and-forget.

Objetos assíncronos do Windows Runtime (IAsyncXxx)

O namespace do Windows Runtime Windows::Foundation contém quatro tipos de objeto de operação assíncrona.

Neste tópico, quando usamos a abreviação conveniente IAsyncXxx, estamos nos referindo a esses tipos coletivamente ou estamos falando de um dos quatro tipos sem a necessidade de especificar qual deles.

Assincronia do C++/CX

O código C++/CX assíncrono usa as tarefas PPL (Biblioteca de Padrões Paralelos). Uma tarefa PPL é representada pela classe concurrency::task.

Normalmente, um método C++/CX assíncrono encadeia tarefas PPL usando funções lambda com concurrency::create_task e concurrency::task::then. Cada função lambda retorna uma tarefa que, quando concluída, produz um valor que é passado para o lambda da continuação da tarefa.

Como alternativa, em vez de chamar create_task para criar uma tarefa, um método C++/CX assíncrono pode chamar concurrency::create_async para criar um IAsyncXxx^.

Portanto, o tipo de retorno de um método C++/CX assíncrono pode ser uma tarefa PPL ou um IAsyncXxx^.

Em ambos os casos, o próprio método usa a palavra-chave return para retornar um objeto assíncrono que, quando concluído, produz o valor que o chamador realmente deseja (talvez um arquivo, uma matriz de bytes ou um booliano).

Observação

Se um método C++/CX assíncrono retornar um IAsyncXxx^, o TResult (se houver) estará limitado a ser um tipo do Windows Runtime. Um valor booliano, por exemplo, é um tipo do Windows Runtime; mas um tipo C++/CX projetado (por exemplo, Platform::Array<byte>^) não é.

Assincronia do C++/WinRT

O C++/WinRT integra as corrotinas do C++ ao modelo de programação. As corrotinas e a instrução co_await fornecem uma forma natural de aguardar um resultado de modo cooperativo.

Cada um dos tipos IAsyncXxx é projetado em um tipo correspondente no namespace C++/WinRT winrt::Windows::Foundation. Vamos nos referir a eles como winrt::IAsyncXxx (em comparação com o IAsyncXxx^ do C++/CX).

O tipo de retorno de uma corrotina do C++/WinRT é um winrt::IAsyncXxx ou um winrt::fire_and_forget. Em vez de usar a palavra-chave return para retornar um objeto assíncrono, uma corrotina usa a palavra-chave co_return para retornar o valor que o chamador realmente quer (talvez um arquivo, uma matriz de bytes ou um booliano) de maneira cooperativa.

Se um método contém pelo menos uma instrução co_await (ou pelo menos uma co_return ou co_yield), o método é uma corrotina por esse motivo.

Para saber mais e obter exemplos de código, confira Simultaneidade e operações assíncronas com C++/WinRT.

O exemplo de jogo Direct3D (Simple3DGameDX)

Este tópico contém guias passo a passo sobre várias técnicas de programação específicas que ilustram como portar gradualmente um código assíncrono. Para servir como um estudo de caso, usaremos a versão do C++/CX do exemplo de jogo Direct3D (que é chamado de Simple3DGameDX). Mostraremos alguns exemplos de como você pode usar o código-fonte do C++/CX original nesse projeto e, gradualmente, portar o código assíncrono dele para C++/WinRT.

  • Baixe o ZIP do link acima e descompacte-o.
  • Abra o projeto do C++/CX (ele está na pasta chamada cpp) no Visual Studio.
  • Em seguida, você precisará adicionar suporte do C++/WinRT ao projeto. As etapas a serem seguidas para você fazer isso são descritas em Adicionar suporte do C++/WinRT a um projeto C++/CX. Nessa seção, a etapa sobre como adicionar o arquivo de cabeçalho interop_helpers.h ao seu projeto é particularmente importante, pois vamos depender dessas funções auxiliares neste tópico.
  • Por fim, adicione #include <pplawait.h> ao pch.h. Isso oferece suporte de corrotina para a PPL (há mais informações sobre esse suporte na seção a seguir).

Ainda não compile; caso contrário, você receberá erros sobre o byte ser ambíguo. Veja como resolver isso.

  • Abra BasicLoader.cpp e comente using namespace std;.
  • Nesse mesmo arquivo de código-fonte, você precisará qualificar shared_ptr como std::shared_ptr. Faça isso com Localizar e Substituir dentro desse arquivo.
  • Em seguida, qualifique vector como std::vector e string como std::string.

O projeto agora é criado novamente, tem suporte do C++/WinRT e contém as funções auxiliares de interoperabilidade from_cx e to_cx.

Agora seu projetoSimple3DGameDX está pronto para você acompanhar com os guias passo a passo de código neste tópico.

Visão geral da portabilidade da assincronia do C++/CX para C++/WinRT

Resumindo, conforme portamos, alteraremos as cadeias de tarefas PPL para chamadas a co_await. Vamos alterar o valor retornado de um método de uma tarefa PPL para um objeto winrt::IAsyncXxx do C++/WinRT. Vamos alterar também qualquer IAsyncXxx^ para um winrt::IAsyncXxx do C++/WinRT.

Você se lembrará de que uma corrotina é qualquer método que chama co_xxx. Uma corrotina do C++/WinRT usa co_return para retornar o valor dela de modo cooperativo. Graças ao suporte da corrotina para PPL(cortesia de pplawait.h), você também pode usar o co_return para retornar uma tarefa PPL de uma corrotina. E você também pode co_await as tarefas e IAsyncXxx. Mas você não pode usar co_return para retornar um IAsyncXxx^. A tabela abaixo descreve o suporte para interoperabilidade entre as várias técnicas assíncronas com pplawait.h na imagem.

Método Você pode fazer co_await dele? Você pode fazer co_return com base nele?
O método retorna task<void> Sim Sim
O método retorna task<T> Não Sim
O método retorna IAsyncXxx^ Sim Não. Mas você encapsula create_async em torno de uma tarefa que usa co_return.
O método retorna winrt::IAsyncXxx Sim Sim

Use a tabela a seguir para ir diretamente para a seção deste tópico que descreve uma técnica de interoperabilidade de interesse ou simplesmente continue lendo daqui.

Técnica de interoperabilidade assíncrona Seção deste tópico
Use co_await para aguardar um método tarefa<nulo> de dentro de um método fire-and-forget ou de um construtor. Aguardar task<void> dentro de um método fire-and-forget
Use co_await para aguardar um método task<void> de dentro de um método task<void>. Aguardar task<void> de dentro de método task<void>
Use co_await para aguardar um método task<void> de dentro de um método task<T>. Aguardar task<void> de dentro de método task<T>
Use co_await para aguardar um método IAsyncXxx^. Aguardar um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado
Use co_return dentro de um método task<void>. Aguardar task<void> de dentro de método task<void>
Use co_return dentro de um método task<T>. Aguardar um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado
Encapsule create_async em torno de uma tarefa que usa co_return. Encapsular create_async em torno de uma tarefa que usa co_return
Porte concurrency::wait. Portar concurrency::wait para co_await winrt::resume_after
Retorne winrt::IAsyncXxx em vez de task<void>. Portar um tipo de retorno task<void> para winrt::IAsyncXxx
Converter um winrt::IAsyncXxx<T>(T é primitivo) em um task<T>. Converter um winrt::IAsyncXxx<T>(T é primitivo) em um task<T>
Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>. Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>

Veja um breve exemplo de código que ilustra uma parte do suporte.

#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();
}

Importante

Mesmo com essas ótimas opções de interoperabilidade, a portabilidade gradual depende da escolha das alterações que podemos fazer cirurgicamente que não afetam o restante do projeto. Não queremos puxar uma ponta solta arbitrária que poderia, dessa forma, desfazer a estrutura de todo o projeto. Para isso, precisamos executar ações em uma ordem específica. Em seguida, veremos detalhadamente alguns exemplos de como fazer esses tipos de alterações de interoperabilidade/portabilidade relacionadas à assincronia.

Aguardar um método task<void>, deixando o restante do projeto inalterado

Um método que retorna task<void> executa o trabalho de maneira assíncrona e retorna um objeto de operação assíncrona, mas, em última análise, não produz um valor. Podemos co_await um método como esse.

Portanto, um bom lugar do qual começar a portabilidade do código assíncrono de maneira gradual é encontrar locais em que você pode chamar esses métodos. Esses lugares envolverão a criação e/ou o retorno de uma tarefa. Eles também podem envolver o tipo de cadeia de tarefas em que nenhum valor é passado de cada tarefa para a continuação dela. Em locais como esse, você pode simplesmente substituir o código assíncrono por instruções co_await, como veremos.

Observação

À medida que este tópico progredir, você verá o benefício dessa estratégia. Depois que um determinado método task<void> estiver sendo chamado exclusivamente por meio de co_await, você estará livre para portar esse método para o C++/WinRT e fazer com que ele retorne um winrt::IAsyncXxx.

Vamos encontrar alguns exemplos. Abra o projeto Simple3DGameDX (confira O exemplo de jogo Direct3D).

Importante

Nos exemplos a seguir, conforme você vê as implementações dos métodos que estão sendo alterados, tenha em mente que não precisamos alterar os chamadores dos métodos que estamos alterando. Essas alterações são localizadas e não são propagadas por meio do projeto.

Aguardar task<void> dentro de um método fire-and-forget

Vamos começar aguardando task<void> dentro de métodos fire-and-forget, pois este é o caso mais simples. Esses são métodos que funcionam de maneira assíncrona, mas o chamador do método não aguarda a conclusão desse trabalho. Basta chamar o método e esquecê-lo, apesar do fato de que ele é concluído de maneira assíncrona.

Procure na raiz do grafo de dependência no seu projeto os métodos void que contêm create_task e/ou cadeias de tarefas em que somente métodos task<void> são chamados.

No Simple3DGameDX, você encontrará código como aquele na implementação do método GameMain::Update. Ela está no arquivo de código-fonte GameMain.cpp.

GameMain::Update

Esta é uma extração da versão do C++/CX do método, mostrando as duas partes do método que é concluído de maneira assíncrona.

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());
        ...
    ...
}

Você pode ver uma chamada ao método Simple3DGame::LoadLevelAsync (que retorna uma task<void> PPL). Depois disso, há uma continuação que realiza um trabalho síncrono. LoadLevelAsync é assíncrono, mas não retorna um valor. Portanto, nenhum valor está sendo passado da tarefa para a continuação.

Podemos fazer o mesmo tipo de alteração no código nesses dois lugares. O código é explicado após a listagem abaixo. Poderíamos ter uma discussão sobre a maneira segura de acessar o ponteiro this em uma corrotina de membros de classe. Mas vamos deixar isso para uma seção posterior (A discussão adiada sobre co_await e o ponteiro this) por enquanto, esse código funciona.

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;
        ...
    ...
}

Como você pode ver, como LoadLevelAsync retorna uma tarefa, é possível fazer co_await dela. E não precisamos de uma continuação explícita, o código que segue um co_await é executado somente quando LoadLevelAsync é concluído.

A introdução do co_await transforma o método em uma corrotina, portanto, não pudemos deixá-lo retornando void. É um método fire-and-forget, então nós o alteramos para retornar winrt::fire_and_forget.

Você também precisará editar o GameMain.h. Altere o tipo de retorno de GameMain::Update de void para winrt::fire_and_forget lá na declaração também.

Você pode fazer essa alteração em sua cópia do projeto, e o jogo ainda será criado e executado da mesma forma. O código-fonte ainda é fundamentalmente C++/CX, mas agora ele está usando os mesmos padrões que o C++/WinRT, de modo que ele nos aproximou da capacidade de portar o restante do código mecanicamente.

GameMain::ResetGame

GameMain::ResetGame é outro método fire-and-forget; ele chama LoadLevelAsync também. Portanto, você poderá fazer a mesma alteração no código se quiser a prática.

GameMain::OnDeviceRestored

As coisas ficam um pouco mais interessantes em GameMain::OnDeviceRestored devido ao aninhamento mais profundo do código assíncrono, incluindo uma tarefa não operacional. Veja uma descrição das partes do método assíncronas (com o código síncrono menos interessante representado por reticências).

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

Primeiro, altere o tipo de retorno de GameMain::OnDeviceRestored de void para winrt::fire_and_forget em GameMain.h e .cpp. Você também precisará abrir DeviceResources.h e fazer a mesma alteração no tipo de retorno de IDeviceNotify::OnDeviceRestored.

Para portar o código assíncrono, remova todas as chamadas create_task e then e as chaves delas e simplifique o método para uma série simples de instruções.

Altere qualquer return que retorne uma tarefa para um co_await. Restará apenas um return que não retorna nada. Basta excluí-lo. Quando terminar, a tarefa não operacional terá desaparecido, e a descrição das partes assíncronas do método terá a seguinte aparência. Novamente, o código síncrono menos interessante é omitido.

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

Como você pode ver, essa forma de estrutura assíncrona é significativamente mais simples e mais fácil de ler.

GameMain::GameMain

O construtor GameMain::GameMain executa o trabalho de maneira assíncrona, e nenhuma parte do projeto aguarda a conclusão desse trabalho. Novamente, essa listagem descreve as partes assíncronas.

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

Mas um construtor não pode retornar winrt::fire_and_forget, então moveremos o código assíncrono para um novo método fire-and-forget GameMain::ConstructInBackground, mesclaremos o código em instruções co_await e chamaremos o novo método do construtor. Consulte o resultado.

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();
        ...
    }
    ...
}

Agora, todos os métodos fire-and-forget, na verdade, todo o código assíncrono, em GameMain foi transformado em corrotinas. Se você tiver interesse, talvez possa procurar métodos fire-and-forget em outras classes e fazer alterações semelhantes.

A discussão adiada sobre o co_await e o ponteiro this

Quando estávamos fazendo alterações em GameMain::Update, adiei uma discussão sobre o ponteiro this. Vamos apresentar essa discussão aqui.

Isso se aplica a todos os métodos que alteramos até agora; e se aplica a todas as corrotinas, não apenas as do tipo fire-and-forget. A introdução de um co_await em um método introduz um ponto de suspensão. E, por causa disso, precisamos ter cuidado com o ponteiro this, que nós utilizamos após o ponto de suspensão sempre que acessamos um membro de classe.

A breve história é que a solução é chamar implementa::get_strong. Contudo, para uma discussão completa sobre o problema e a solução, confira Acessar com segurança o ponteiro this em uma corrotina de membro de classe.

Você pode chamar implements::get_strong apenas em uma classe derivada de winrt::implements.

Derivar GameMain de winrt::implements

A primeira alteração que precisamos fazer é em GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain continuará implementando DX::IDeviceNotify, mas vamos alterá-lo para fazer a derivação de winrt::implements.

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

Em seguida, em App.cpp, você encontrará esse método.

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

Mas agora que GameMain deriva de winrt::implements, precisamos construí-lo de maneira diferente. Nesse caso, usaremos o modelo de função winrt::make_self. Para obter mais informações, confira Criar instâncias e retornar tipos de implementação e interfaces.

Substitua essa linha de código por esta.

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

Para fechar o loop nessa alteração, também precisaremos alterar o tipo de m_main. Em App.h, você encontrará esse código.

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

Altere essa declaração de m_main para essa.

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

Agora podemos chamar implements::get_strong

Para GameMain::Update e para qualquer um dos outros métodos aos quais adicionamos um co_await, veja aqui como você pode chamar get_strong no início de uma corrotina para que uma referência forte sobreviva até a conclusão da corrotina.

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

Aguardar task<void> de dentro de método task<void>

O próximo caso mais simples é aguardar task<void> dentro de um método que, por sua vez, retorna task<void>. Isso porque podemos fazer co_await de um task<void> e podemos fazer co_return dele.

Você encontrará um exemplo muito simples na implementação do método Simple3DGame::LoadLevelAsync. Ela está no arquivo de código-fonte 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();
}

Há apenas um código síncrono, seguido do retorno da tarefa criada por GameRenderer::LoadLevelResourcesAsync.

Em vez de retornar essa tarefa, nós fazemos co_await dela e, em seguida, co_return do resultante void.

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

Essa não parece uma alteração profunda. Mas, agora que estamos chamando GameRenderer::LoadLevelResourcesAsync via co_await, estamos livres para portá-lo para retornar um winrt::IAsyncXxx em vez de uma tarefa. Faremos isso mais tarde na seção Portar um tipo de retorno task<void> para winrt::IAsyncXxx.

Aguardar task<void> de dentro de método task<T>

Embora não haja nenhum exemplo adequado a ser encontrado em Simple3DGameDX, podemos idealizar um exemplo hipotético apenas para mostrar o padrão.

A primeira linha no exemplo de código abaixo demonstra o co_await simples de um task<void>. Em seguida, para satisfazer o tipo de retorno task<void>, precisamos retornar de modo assíncrono um StorageFile^. Para fazer isso, nós fazemos co_await de uma API do Windows Runtime e co_return do arquivo resultante.

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

Poderíamos até portar mais do método para o C++/WinRT dessa forma.

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

O membro de dados m_renderer ainda é C++/CX nesse exemplo.

Aguardar um IAsyncXxx^ em um método task, deixando o restante do projeto inalterado

Vimos como você pode fazer co_await de task<void>. Você também pode fazer co_await de um método que retorna um IAsyncXxx, seja um método em seu projeto ou uma API assíncrona do Windows (por exemplo, StorageFolder.GetFileAsync, que aguardamos de modo cooperativo na seção anterior).

Para obter um exemplo do local em que podemos fazer esse tipo de alteração de código, vamos examinar BasicReaderWriter::ReadDataAsync (você o encontrará implementado em BasicReaderWriter.cpp).

Esta é a versão original do 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;
    });
}

A listagem de código abaixo mostra que é possível fazer co_await de APIs do Windows que retornam IAsyncXxx^. Além disso, também podemos fazer co_return do valor que BasicReaderWriter::ReadDataAsync retorna de maneira assíncrona (nesse caso, uma matriz de bytes). Essa primeira etapa mostra como fazer apenas essas alterações; na verdade, vamos portar o código do C++/CX para C++/WinRT na próxima seção.

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

Novamente, não precisamos alterar os chamadores dos métodos que estamos alterando, pois não alteramos o tipo de retorno.

Porte ReadDataAsync (principalmente) para C++/WinRT, deixando o restante do projeto inalterado

Podemos avançar um pouco e portar o método quase totalmente para C++/WinRT sem a necessidade de alterar qualquer outra parte do projeto.

A única dependência que esse método tem no restante do projeto é o membro de dados BasicReaderWriter::m_location, que é um StorageFolder^ do C++/CX. Para deixar esse membro de dados inalterado e deixar o tipo de parâmetro e o tipo de retorno inalterados, precisamos executar apenas algumas conversões, uma no início do método e outra no final. Para isso, podemos usar as funções auxiliares de interoperabilidade from_cx e to_cx.

Veja qual é a aparência de BasicReaderWriter::ReadDataAsync após a portabilidade da implementação dele predominantemente para C++/WinRT. Este é um bom exemplo de portabilidade gradual. E esse método está na fase em que podemos deixar de pensar nele como um método do C++/CX que usa algumas técnicas do C++/WinRT e vê-lo como um método do C++/WinRT que faz a interoperabilidade com C++/CX.

#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());
}

Observação

No ReadDataAsync acima, construímos e retornamos uma nova matriz do C++/CX. E, é claro, fazemos isso para satisfazer o tipo de retorno do método (para não precisar alterar o restante do projeto).

Você pode encontrar outros exemplos em seu projeto, em que, após a portabilidade, você alcança o fim do método e tudo o que você tem é um objeto do C++/WinRT. Para fazer co_return disso, basta chamar to_cx para convertê-lo. Há mais informações sobre isso e um exemplo na próxima seção.

Converter um winrt::IAsyncXxx<T> em um task<T>

Esta seção trata da situação em que você portou um método assíncrono para C++/WinRT (para que ele retornasse um winrt::IAsyncXxx<T>), mas você ainda tem código do C++/CX que chama esse método como se ele ainda estivesse retornando uma tarefa.

  • Um caso é aquele em que T é primitivo, o que não precisa de conversão.
  • O outro caso é aquele em que T é um tipo do Windows Runtime; nesse caso, você precisará convertê-lo em um T^.

Converter um winrt::IAsyncXxx<T>(T é primitivo) em um task<T>

Nesta seção, o padrão se aplica quando você está retornando de maneira assíncrona um valor primitivo (usaremos um valor booliano para ilustrar). Considere um exemplo em que um método que você já portou para o C++/WinRT tenha essa assinatura.

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

Você pode converter uma chamada para esse método em uma tarefa como esta.

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

Ou como esta.

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

Observe que o tipo de retorno task da função lambda é explícito, pois o compilador não pode deduzi-lo.

Também poderíamos chamar o método dentro de uma cadeia de tarefas arbitrária como esta. Novamente, com um tipo de retorno lambda explícito.

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

Converter um winrt::IAsyncXxx<T> (T é um tipo do Windows Runtime) em um task<T^>

O padrão nesta seção se aplica quando você está retornando de maneira assíncrona um valor do Windows Runtime (usaremos um valor StorageFile para ilustrar). Considere um exemplo em que um método que você já portou para o C++/WinRT tenha essa assinatura.

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

A próxima listagem mostra como converter uma chamada a esse método em uma tarefa. Observe que precisamos chamar a função auxiliar de interoperabilidade to_cx para converter o objeto retornado do C++/WinRT em um objeto manipulador do C++/CX (também conhecido como um hat).

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

Veja uma versão mais sucinta disso.

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

E você pode até mesmo optar por encapsular esse padrão em um modelo de função reutilizável e fazer return dele da mesma forma que normalmente retornaria uma tarefa.

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

Se você gostar dessa ideia, adicione to_task ao interop_helpers.h.

Encapsular create_async em torno de uma tarefa que usa co_return

Não é possível fazer co_return de um IAsyncXxx^ diretamente, mas você pode chegar a algo semelhante. Se você tiver uma tarefa que retorna um valor de modo cooperativo, encapsule isso dentro de uma chamada para concurrency::create_async.

Esse é exemplo hipotético, já que não há um exemplo que podemos extrair de Simple3DGameDX.

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

Como você pode ver, você pode obter o valor retornado de qualquer método do qual você possa fazer co_await.

Portar concurrency::wait para co_await winrt::resume_after

Há alguns locais em que Simple3DGameDX usa concurrency::wait para pausar o thread por um curto período. Veja um exemplo.

// 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);
    }));
    ...
}

A versão do C++/WinRT de concurrency::wait é o struct winrt::resume_after. Podemos fazer co_await desse struct dentro de uma tarefa PPL. Aqui está um exemplo de código.

// 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);
    }));
    ...
}

Observe as outras duas alterações que precisamos fazer. Alteramos o tipo de GameConstants::InitialLoadingDelay para std::chrono::duration e tornamos o tipo de retorno da função lambda explícito, porque o compilador não pode mais deduzi-lo.

Portar um tipo de retorno task<void> para winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

Nesta fase do nosso trabalho com Simple3DGameDX, todos os locais do projeto que chamam Simple3DGame::LoadLevelAsync usam co_await para chamá-lo.

Isso significa que podemos simplesmente alterar o tipo de retorno do método de task<void> para winrt::Windows::Foundation::IAsyncAction (deixando o restante inalterado).

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

Agora fica razoavelmente mecânico portar o restante desse método e as respectivas dependências (como m_level e assim por diante) para o C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Esta é a versão original doC++/CX de GameRenderer::LoadLevelResourcesAsync.

// 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 é o único local no projeto que chama GameRenderer::LoadLevelResourcesAsync e já usa co_await para chamá-lo.

Portanto, não há mais nenhuma necessidade de GameRenderer::LoadLevelResourcesAsync retornar uma tarefa, ele pode retornar um winrt::Windows::Foundation::IAsyncAction. E a implementação em si é simples o suficiente para fazer a portabilidade completa para C++/WinRT. Isso envolve fazer a mesma alteração que fizemos em Portar concurrency::wait para co_await winrt::resume_after. Não há dependências significativas no restante do projeto com as quais se preocupar.

Então, veja qual é a aparência do método após fazer a portabilidade completa para 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);
}

A meta, portar totalmente um método para C++/WinRT

Vamos concluir este passo a passo com um exemplo da meta final, portando totalmente o método BasicReaderWriter::ReadDataAsync para o C++/WinRT.

Na última vez que examinamos esse método (na seção Portar ReadDataAsync (principalmente) para C++/WinRT, deixando o restante do projeto inalterado), ele foi basicamente portado para C++/WinRT. Mas ele ainda retornou uma tarefa de 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());
}

Em vez de retornar uma tarefa, vamos alterá-la para retornar um IAsyncOperation. E, em vez de retornar uma matriz de bytes por meio dessa IAsyncOperation, retornaremos um objeto IBuffer do C++/WinRT. Isso também exigirá uma pequena alteração no código nos locais de chamada, como veremos.

Veja qual é a aparência do método após portar a implementação dele, o parâmetro dele e o membro de dados m_location para usar a sintaxe e os objetos do C++/WinRT.

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

Como você pode ver, BasicReaderWriter::ReadDataAsync em si é muito mais simples, pois fatoramos para o próprio método dele a lógica síncrona que recupera bytes do buffer.

No entanto, agora precisamos portar os sites de chamada desse tipo de estrutura no C++/CX.

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

Para esse padrão no C++/WinRT.

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

APIs importantes