Asincronía e interoperabilidad entre C++/WinRT y C++/CX

Tip

Aunque se recomienda leer este tema desde el principio, puede ir directamente a un resumen de las técnicas de interoperabilidad en la sección Introducción a la migración de C++/CX asincrónico a C++/WinRT .

Este es un tema avanzado relacionado con la portabilidad gradual a C++/WinRT desde C++/CX. Este tema retoma donde lo deja el tema Interoperabilidad entre C++/WinRT y C++/CX.

Si el tamaño o la complejidad del código base hace que sea necesario portar el proyecto gradualmente, necesitará un proceso de portabilidad en el que, durante un tiempo, el código de C++/CX y C++/WinRT existe en paralelo en el mismo proyecto. Si tiene código asincrónico, puede que necesite que las cadenas de tareas de la Biblioteca de patrones paralelos (PPL) y las corrutinas coexistan en su proyecto mientras migra gradualmente su código fuente. Este tema se centra en técnicas para interoperar entre código asincrónico de C++/CX y código asincrónico de C++/WinRT. Puede usar estas técnicas individualmente o juntas. Las técnicas permiten realizar cambios graduales, controlados y locales en el proceso de portar todo el proyecto, sin que cada cambio se propague en cascada de forma incontrolada por todo el proyecto.

Antes de leer este tema, es recomendable leer interoperabilidad entre C++/WinRT y C++/CX. En este tema se muestra cómo preparar el proyecto para la portabilidad gradual. También presenta dos funciones auxiliares que puedes usar para convertir un objeto de C++/CX en un objeto C++/WinRT (y viceversa). Este tema sobre la asincronía se basa en esa información y usa esas funciones auxiliares.

Note

Hay algunas limitaciones para migrar gradualmente de C++/CX a C++/WinRT. Si tiene un proyecto de componente de Windows Runtime, no es posible migrar gradualmente y tendrá que portar el proyecto en un paso. Y, para un proyecto XAML, en todo momento los tipos de página XAML deben ser o bien todos C++/WinRT o todos C++/CX. Para obtener más información, consulta el tema Mover a C++/WinRT desde C++/CX.

La razón por la que un tema completo está dedicado a la interoperabilidad de código asincrónica

La migración de C++/CX a C++/WinRT es generalmente sencilla, con la única excepción de pasar de tareas de biblioteca de patrones paralelos (PPL) a corrutinas. Los modelos son diferentes. No existe una correspondencia natural uno a uno entre las tareas de PPL y las corrutinas, y no hay una forma sencilla (que funcione en todos los casos) de portar el código mecánicamente.

La buena noticia es que la conversión de tareas a corrutinas da lugar a simplificaciones significativas. Y los equipos de desarrollo informan rutinariamente de que una vez que superan el obstáculo de migrar su código asincrónico, el resto del trabajo de portabilidad es en gran medida mecánico.

A menudo, un algoritmo se escribió originalmente para adaptarse a las API sincrónicas. Y luego eso se traducía en tareas y continuaciones explícitas: el resultado suele ser una ofuscación involuntaria de la lógica subyacente. Por ejemplo, los bucles se convierten en recursividad; las ramas if-else se convierten en un árbol anidado (una cadena) de tareas; las variables compartidas se convierten en shared_ptr. Para deconstruir la estructura a menudo no natural del código fuente de PPL, se recomienda que primero vuelva a dar un paso atrás y comprenda la intención del código original (es decir, descubrir la versión sincrónica original). Y luego inserte co_await (espera cooperativamente) en los lugares adecuados.

Por ese motivo, si tiene una versión de C# (en lugar de C++/CX) del código asincrónico desde el que se va a comenzar el puerto, puede proporcionarle un tiempo más sencillo y un puerto más limpio. El código de C# usa await. Por lo tanto, el código C# ya sigue esencialmente la filosofía de partir de una versión sincrónica y, a continuación, insertar await en los lugares adecuados.

Si no tiene una versión de C# del proyecto, puede usar las técnicas descritas en este tema. Y una vez que hayas migrado a C++/WinRT, la estructura del código asincrónico será más fácil de migrar a C#, si quieres hacerlo.

Algunos antecedentes en la programación asincrónica

Para que tengamos un marco de referencia común sobre los conceptos y la terminología de la programación asincrónica, situémonos brevemente en el contexto de la programación asincrónica de Windows Runtime en general, y también de cómo las dos proyecciones de lenguaje de C++ se basan en ella, cada una a su manera.

El proyecto tiene métodos que funcionan de forma asincrónica y hay dos tipos principales.

  • Es habitual esperar a que finalice el trabajo asincrónico antes de hacer otra cosa. Un método que devuelve un objeto de operación asíncrona es un método sobre el que se puede esperar.
  • Pero a veces no desea o necesita esperar a que se complete el trabajo realizado de forma asincrónica. En ese caso, es más eficaz que el método asincrónico no devuelva un objeto de operación asincrónica. Un método asíncrono como ese —uno cuya finalización no se espera— se conoce como método fire-and-forget.

objetos asincrónicos de Windows Runtime (IAsyncXxx)

El espacio de nombres Windows::Foundation Windows Runtime contiene cuatro tipos de objeto de operación asincrónica.

En este tema, cuando usamos la práctica abreviatura de IAsyncXxx, nos referimos a estos tipos colectivamente; o estamos hablando de uno de los cuatro tipos sin necesidad de especificar cuál.

C++/CX async

El código asincrónico de C++/CX usa tareas de biblioteca de patrones paralelos (PPL). Una tarea PPL se representa mediante la clase concurrency::task .

Normalmente, un método asíncrono de C++/CX encadena tareas de PPL usando funciones lambda con concurrency::create_task y concurrency::task::then. Cada función lambda devuelve una tarea que, cuando se completa, genera un valor que, a continuación, se pasa a la expresión lambda de la continuación de la tarea.

Como alternativa, en lugar de llamar a create_task para crear una tarea, un método asíncrono de C++/CX puede llamar a concurrency::create_async para crear un IAsyncXxx^.

Por lo tanto, el tipo de valor devuelto de un método asincrónico de C++/CX puede ser una tarea PPL o un IAsyncXxx^.

En cualquier caso, el propio método usa la return palabra clave para devolver un objeto asincrónico que, cuando se completa, genera el valor que el autor de la llamada realmente desea (quizás un archivo, una matriz de bytes o un valor booleano).

Note

Si un método asincrónico de C++/CX devuelve un IAsyncXxx^, el TResult (si existe) se limita a ser un tipo de Windows Runtime. Un valor booleano, por ejemplo, es un tipo de Windows Runtime; pero no es un tipo proyectado de C++/CX (por ejemplo, Platform::Array<byte>^).

Asincronía de C++/WinRT

C++/WinRT integra corrutinas de C++ en el modelo de programación. Las corrutinas y la instrucción co_await proporcionan una forma natural de esperar un resultado de forma cooperativa.

Cada uno de los tipos IAsyncXxx se proyecta en un tipo correspondiente en el espacio de nombres winrt::Windows::Foundation C++/WinRT. Vamos a hacer referencia a ellos como winrt::IAsyncXxx (en comparación con IAsyncXxx^ de C++/CX).

El tipo de retorno de una corrutina de C++/WinRT es o bien un winrt::IAsyncXxx, o winrt::fire_and_forget. Y en lugar de usar la return palabra clave para devolver un objeto asincrónico, una corrutina usa la co_return palabra clave para devolver de forma cooperativa el valor que el autor de la llamada realmente desea (quizás un archivo, una matriz de bytes o un valor booleano).

Si un método contiene al menos una sentencia co_await (o al menos una co_return o co_yield), entonces el método es una corrutina por esa razón.

Para obtener más información y ejemplos de código, consulta Operaciones asincrónicas y simultaneidad con C++/WinRT.

El ejemplo de juego de Direct3D (Simple3DGameDX)

Este tema contiene tutoriales de varias técnicas de programación específicas que ilustran cómo migrar gradualmente código asincrónico. Para servir como caso práctico, usaremos la versión de C++/CX del ejemplo de juego direct3D (que se denomina Simple3DGameDX). Mostraremos algunos ejemplos de cómo puede tomar el código fuente de C++/CX original en ese proyecto y migrar gradualmente su código asincrónico a C++/WinRT.

  • Descargue el archivo ZIP del vínculo anterior y descomprímalo.
  • Abra el proyecto de C++/CX (se encuentra en la carpeta denominada cpp) en Visual Studio.
  • A continuación, deberá agregar compatibilidad con C++/WinRT al proyecto. Los pasos que sigues para hacerlo se describen en Tomar un proyecto de C++/CX y agregar compatibilidad con C++/WinRT. En esa sección, el paso sobre cómo agregar el archivo de encabezado interop_helpers.h a tu proyecto es especialmente importante porque dependeremos de esas funciones auxiliares en este tema.
  • Por último, agregue #include <pplawait.h> a pch.h. Eso le proporciona compatibilidad con corrutinas para PPL (en la sección siguiente encontrará más información al respecto).

No compiles todavía; de lo contrario, obtendrás errores que indican que byte es ambiguo. Aquí se muestra cómo resolverlo.

  • Abra BasicLoader.cpp y comente using namespace std;.
  • En ese mismo archivo de código fuente, deberá calificar shared_ptr como std::shared_ptr. Puede hacerlo con una búsqueda y reemplazo dentro de ese archivo.
  • A continuación, califica el vector como std::vector y string como std::string.

El proyecto ahora se compila de nuevo, tiene compatibilidad con C++/WinRT y contiene los from_cx y to_cx funciones auxiliares de interoperabilidad.

Ya tiene listo el proyecto Simple3DGameDX para seguir las explicaciones guiadas del código de este tema.

Información general sobre cómo migrar C++/CX asincrónico a C++/WinRT

En pocas palabras, a medida que realicemos la migración, convertiremos las cadenas de tareas de PPL en llamadas a co_await. Cambiaremos el valor devuelto de un método de una tarea PPL a un objeto winrt::IAsyncXxx de C++/WinRT. Y también cambiaremos cualquier IAsyncXxx^ por un winrt::IAsyncXxx de C++/WinRT.

Recordará que una corrutina es cualquier método que llame a co_xxx. Una corrutina de C++/WinRT usa co_return para devolver de forma cooperativa su valor. Gracias a la compatibilidad de corrutinas con PPL (gracias a pplawait.h), también puede usar co_return para devolver una tarea de PPL desde una corrutina. Además, también puede co_await tanto tareas como IAsyncXxx. Pero no se puede usar co_return para devolver un IAsyncXxx^. En la tabla siguiente se describe la compatibilidad de interoperabilidad entre las diversas técnicas asincrónicas con pplawait.h en la figura.

Method ¿Puedes hacerlo co_await ? ¿Puedes hacerlo co_return ?
El método devuelve task<void> Yes Yes
El método devuelve la tarea<T> No Yes
El método devuelve IAsyncXxx^ Yes N.º Pero envuelves create_async alrededor de una tarea que usa co_return.
El método devuelve winrt::IAsyncXxx Yes Yes

Use esta tabla siguiente para ir directamente a la sección de este tema que describe una técnica de interoperabilidad de interés, o simplemente continuar leyendo desde aquí.

Técnica de interoperabilidad asíncrona Sección de este tema
Use co_await para esperar un método task<void> desde dentro de un método fire-and-forget, o dentro de un constructor. Esperar task<void> en un método de tipo fire-and-forget
Use co_await para esperar un método task<void> dentro de un método task<void>. Await task<void> dentro de un método task<void>
Use co_await para esperar un método task<void> desde dentro de un método T< de tarea>. Esperar a task<void> dentro de un método tarea<T>
Use co_await para esperar un método IAsyncXxx^ . Esperar un IAsyncXxx^ en un método de tarea , dejando el resto del proyecto sin cambios
Use co_return dentro de un método task<void> . Await task<void> dentro de un método task<void>
Use co_return dentro de un método de tarea<T>. Esperar un IAsyncXxx^ en un método de tarea , dejando el resto del proyecto sin cambios
Envuelva create_async alrededor de una tarea que utiliza co_return. Envuelve create_async en torno a una tarea que usa co_return
Portar concurrency::wait. Portar concurrency::wait a co_await winrt::resume_after
Devuelve winrt::IAsyncXxx en lugar de task<void>. Convertir un tipo de devolución task<void> a winrt::IAsyncXxx
Convierta un winrt::IAsyncXxx<T> (T es primitivo) en una tarea<T>. Convertir un winrt::IAsyncXxx<T> (T es primitivo) en una tarea<T>
Convierta un winrt::IAsyncXxx<T> (T es un tipo de Windows Runtime) en un task<T^>. Convertir un winrt::IAsyncXxx<T> (T es un tipo de Windows Runtime) en una tarea<T^>

Y este es un breve ejemplo de código que ilustra parte de la compatibilidad.

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

Important

Incluso con estas excelentes opciones de interoperabilidad, la portabilidad gradual depende de elegir los cambios que podemos realizar quirúrgicamente que no afecten al resto del proyecto. Queremos evitar tirar de un cabo suelto cualquiera y, con ello, deshacer la estructura de todo el proyecto. Para ello, tenemos que hacer cosas en un orden determinado. A continuación, veremos detenidamente algunos ejemplos de este tipo de cambios de portabilidad e interoperabilidad relacionados con la asincronía.

Esperar un método task<void> , dejando el resto del proyecto sin cambios

Un método que devuelve task<void> realiza el trabajo de forma asincrónica y devuelve un objeto de operación asincrónica, pero en última instancia no genera un valor. Podemos co_await usar un método así.

Así que un buen lugar para empezar a migrar gradualmente código asíncrono es encontrar lugares donde se llaman esos métodos. Esos lugares implicarán la creación o devolución de una tarea. También pueden implicar el tipo de cadena de tareas donde no se pasa ningún valor de cada tarea a su continuación. En sitios así, puedes simplemente reemplazar el código asíncrono por instrucciones co_await, como veremos.

Note

A medida que avanza este tema, verá la ventaja de esta estrategia. Una vez que se llama a un método task<void> determinado exclusivamente a través de co_await, ya puedes convertir ese método a C++/WinRT y hacer que devuelva un winrt::IAsyncXxx.

Vamos a encontrar algunos ejemplos. Abra el proyecto Simple3DGameDX (consulta El ejemplo de juego direct3D).

Important

En los siguientes ejemplos, a medida que vea cómo cambian las implementaciones de los métodos, tenga en cuenta que no es necesario cambiar las llamadas a los métodos que se están cambiando. Estos cambios están localizados y no se propagan al resto del proyecto.

Await task<void> en un método fire-and-forget

Empecemos por esperar task<void> en los métodos fire-and-forget, porque es el caso más sencillo. Estos son métodos que realizan trabajo de forma asincrónica, pero quien llama al método no espera a que ese trabajo se complete. Simplemente llamas al método y te olvidas de él, a pesar de que se completa de forma asíncrona.

Busque en la raíz del gráfico de dependencias de su proyecto los métodos void que contienen create_task y/o las cadenas de tareas en las que solo se llaman a métodos task<void>.

En Simple3DGameDX, encontrará código similar al de la implementación del método GameMain::Update. Se encuentra en el archivo GameMain.cppde código fuente .

GameMain::Update

Este es un extracto de la versión de C++/CX del método, que muestra las dos partes del método que se completan de forma asincrónica.

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

Se puede ver una llamada al método Simple3DGame::LoadLevelAsync (que devuelve una PPL task<void>). Después, hay una continuación que realiza algo de trabajo sincrónico. LoadLevelAsync es asincrónico, pero no devuelve un valor. Por lo tanto, no se pasa ningún valor de la tarea a la continuación.

Podemos realizar el mismo tipo de cambio en el código en estos dos lugares. El código se explica después de la lista siguiente. Podríamos mantener aquí una discusión sobre la forma segura de acceder al puntero this en una corrutina miembro de una clase. Pero vamos a aplazarlo para una sección posterior (la discusión diferida sobre co_await y este puntero), por ahora, este 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 puede ver, porque LoadLevelAsync devuelve una tarea, podemos co_await hacerlo. Y no necesitamos una continuación explícita: el código que sigue a co_await solo se ejecuta cuando se completa LoadLevelAsync.

Al añadir co_await, el método se convierte en una corrutina, así que no podíamos hacer que siguiera devolviendo void. Es un método de tipo «fire-and-forget», por lo que lo cambiamos para que devolviera winrt::fire_and_forget.

También tendrá que editar GameMain.h. Cambie también allí, en la declaración, el tipo de retorno de GameMain::Update de void a winrt::fire_and_forget.

Puedes realizar este cambio en tu copia del proyecto, y el juego se sigue compilando y ejecutando igual. El código fuente sigue siendo fundamentalmente C++/CX, pero ahora usa los mismos patrones que C++/WinRT, por lo que nos ha movido un poco más cerca de poder portar el resto del código mecánicamente.

GameMain::ResetGame

GameMain::ResetGame es otro método fire-and-forget; también llama a LoadLevelAsync. Así que puede hacer allí el mismo cambio en el código si quiere practicar.

GameMain::OnDeviceRestored

Las cosas se vuelven un poco más interesantes en GameMain::OnDeviceRestored debido a su anidamiento más profundo del código asincrónico, incluida una tarea de no-op. Este es un esquema de las partes asincrónicas del método (con el código sincrónico menos interesante representado por puntos suspensivos).

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

En primer lugar, cambie el tipo de retorno de GameMain::OnDeviceRestored de void a winrt::fire_and_forget en GameMain.h y .cpp. También deberás abrir DeviceResources.h y hacer el mismo cambio en el tipo de retorno de IDeviceNotify::OnDeviceRestored.

Para migrar el código asincrónico, elimine todas las llamadas a create_task y then, así como sus llaves, y simplifique el método hasta convertirlo en una serie lineal de instrucciones.

Cambie cualquier return que devuelva una tarea por un co_await. Quedarás con uno return que no devuelva nada, así que simplemente elimínelo. Cuando haya terminado, la tarea no-op desaparecerá y el esquema de las partes asincrónicas del método tendrá este aspecto. De nuevo, se omite el código sincrónico menos interesante.

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

Como puede ver, esta forma de estructura asincrónica es significativamente más sencilla y más fácil de leer.

GameMain::GameMain

El constructor GameMain::GameMain realiza el trabajo de forma asincrónica y ninguna parte del proyecto espera a que se complete ese trabajo. De nuevo, en esta lista se describen las partes asincrónicas.

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

Pero un constructor no puede devolver winrt::fire_and_forget, por lo que moveremos el código asincrónico a un nuevo método GameMain::ConstructInBackground fire-and-forget, aplanar el código en co_await instrucciones y llamar al nuevo método desde el constructor. Este es el 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();
        ...
    }
    ...
}

Ahora, todos los métodos de tipo «iniciar y olvidar» —de hecho, todo el código asíncrono— de GameMain se ha convertido en corrutinas. Si le apetece, quizá podría buscar métodos de tipo fire-and-forget en otras clases y realizar cambios similares.

La discusión diferida sobre co_await y el puntero this

Cuando estábamos realizando cambios en GameMain::Update, pospuse la discusión sobre el puntero this. Vamos a tener esa discusión aquí.

Esto se aplica a todos los métodos que hemos cambiado hasta ahora; y se aplica a todas las corrutinas, no solo a las de tipo «fire-and-forget». Al introducir un co_await elemento en un método se introduce un punto de suspensión. Y por eso, tenemos que tener cuidado con el puntero this, del que, por supuesto, hacemos uso después del punto de suspensión cada vez que accedemos a un miembro de la clase.

La historia corta es que la solución es llamar a implements::get_strong. Pero para obtener una explicación completa del problema y la solución, consulte Acceso seguro al puntero en una corrutina de miembro de clase.

Puedes llamar a implements::get_strong solo en una clase que derive de winrt::implements.

Derivar GameMain de winrt::implements

El primer cambio que necesitamos realizar es en GameMain.h.

class GameMain :
    public DX::IDeviceNotify

GameMain seguirá implementando DX::IDeviceNotify, pero lo cambiaremos para derivar de winrt::implements.

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

A continuación, en App.cpp, encontrará este método.

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

Pero ahora que GameMain deriva de winrt::implements, es necesario construirlo de otra manera. En este caso, usaremos la plantilla de función winrt::make_self . Para obtener más información, consulta Creación de instancias y devolución de tipos e interfaces de implementación.

Reemplace esa línea de código por esta.

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

Para cerrar el bucle en ese cambio, también es necesario cambiar el tipo de m_main. En App.h, encontrará este código.

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

Cambie esa declaración de m_main a esta.

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

Ahora podemos llamar a implements::get_strong

Para GameMain::Update, y para cualquiera de los otros métodos a los que añadimos un co_await, a continuación se muestra cómo llamar a get_strong al principio de una corrutina para garantizar que una referencia fuerte permanezca activa hasta que la corrutina finalice.

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

Await task<void> dentro de un método task<void>

El siguiente caso más sencillo es esperar a la tarea<void> dentro de un método que devuelve task<void>. Eso es porque podemos co_awaitanular< una tarea>, y podemos co_return de una.

Encontrará un ejemplo muy sencillo en la implementación del método Simple3DGame::LoadLevelAsync. Se encuentra en el archivo Simple3DGame.cppde código fuente .

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

Solo hay código sincrónico, seguido de devolver la tarea creada por GameRenderer::LoadLevelResourcesAsync.

En lugar de devolver esa tarea, la usamos co_await y, a continuación co_return , la 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();
}

Eso no parece un cambio profundo. Pero ahora que estamos llamando a GameRenderer::LoadLevelResourcesAsync a través de co_await, podemos adaptarlo para que devuelva un winrt::IAsyncXxx en lugar de una tarea. Lo haremos más adelante en la sección Convertir un tipo de valor de retorno task<void> en winrt::IAsyncXxx.

Espere task<void> en un método task<T>

Aunque no hay ejemplos adecuados que se encuentran en Simple3DGameDX, podemos intentar un ejemplo hipotético solo para mostrar el patrón.

La primera línea del ejemplo de código siguiente muestra la declaración simple de una tarea<void>. A continuación, para cumplir con el tipo de valor devuelto task<T>, necesitamos devolver de forma asincrónica un StorageFile^. Para ello, co_await una API de Windows Runtime y co_return el archivo resultante.

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

Incluso podríamos migrar más del método a C++/WinRT como este.

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

El miembro de datos m_renderer sigue siendo C++/CX en ese ejemplo.

Esperar un IAsyncXxx^ en un método de tarea , dejando el resto del proyecto sin cambios

Hemos visto cómo puedes co_awaitanular< una tarea>. También co_await puede un método que devuelva un IAsyncXxx, ya sea un método en el proyecto o una API de Windows asincrónica (por ejemplo, StorageFolder.GetFileAsync, que esperamos de forma cooperativa en la sección anterior).

Para ver un ejemplo de dónde podemos realizar este tipo de cambio de código, echemos un vistazo a BasicReaderWriter::ReadDataAsync (lo encontrará implementado en BasicReaderWriter.cpp).

Esta es la versión original de 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;
    });
}

El listado de código siguiente muestra que podemos co_await las API de Windows que devuelven IAsyncXxx^. No solo eso, también podemos co_return obtener el valor que BasicReaderWriter::ReadDataAsync devuelve de forma asincrónica (en este caso, una matriz de bytes). En este primer paso se muestra cómo realizar solo esos cambios; En realidad, migraremos el código de C++/CX a C++/WinRT en la sección siguiente.

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

De nuevo, no necesitamos cambiar los elementos que llaman a los métodos que estamos cambiando, porque no hemos cambiado el tipo de retorno.

Migrar ReadDataAsync (principalmente) a C++/WinRT, dejando el resto del proyecto sin cambios

Podemos ir un paso más allá y migrar el método casi completamente a C++/WinRT sin necesidad de cambiar ninguna otra parte del proyecto.

La única dependencia que este método tiene con respecto al resto del proyecto es el miembro de datos BasicReaderWriter::m_location, que es un StorageFolder^ de C++/CX. Para dejar ese miembro de datos sin cambios y dejar el tipo de parámetro y el tipo de valor devuelto sin cambios, solo necesitamos realizar un par de conversiones, una al principio del método y otra al final. Para ello, podemos usar las funciones auxiliares de interoperabilidad from_cx y to_cx .

Este es el aspecto de BasicReaderWriter::ReadDataAsync después de migrar su implementación principalmente a C++/WinRT. Este es un buen ejemplo de adaptación gradual. Y este método está en la fase en la que podemos alejarnos de pensarlo como un método de C++/CX que usa algunas técnicas de C++/WinRT y verlo como un método de C++/WinRT que interopera con 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());
}

Note

En ReadDataAsync anterior, creamos y devuelvemos una nueva matriz de C++/CX. Y, por supuesto, lo hacemos para satisfacer el tipo de valor devuelto del método (para que no tengamos que cambiar el resto del proyecto).

Puede encontrarse con otros ejemplos en su propio proyecto en los que, tras la migración, llega al final del método y lo único que tiene es un objeto C++/WinRT. Para eso, solo tienes que llamar a co_returnto_cx para convertirlo. Hay más información sobre eso, y un ejemplo, la sección siguiente.

Convertir un winrt::IAsyncXxx<T> en una tarea<T>

En esta sección se trata de la situación en la que ha migrado un método asincrónico a C++/WinRT (para que devuelva un winrt::IAsyncXxx<T>), pero todavía tiene código de C++/CX que llama a ese método como si todavía devolva una tarea.

  • Un caso es donde T es primitivo, que no necesita ninguna conversión.
  • El otro caso es aquel en el que T es un tipo de Windows Runtime, en cuyo caso necesitará convertirlo a un T^.

Convertir un winrt::IAsyncXxx<T> (T es primitivo) en una tarea<T>

El patrón de esta sección se aplica cuando se devuelve de forma asincrónica un valor primitivo (usaremos un valor booleano para ilustrar). Considere un ejemplo en el que un método que ya ha migrado a C++/WinRT tiene esta firma.

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

Puede convertir una llamada a ese método en una tarea como esta.

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

O así.

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

Observe que el tipo de valor devuelto de la tarea de la función lambda es explícito, ya que el compilador no puede deducirlo.

También podríamos llamar al método desde una cadena de tareas arbitraria como esta. De nuevo, con un tipo de retorno explícito de lambda.

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

Convertir un winrt::IAsyncXxx<T> (T es un tipo de Windows Runtime) en una tarea<T^>

El patrón de esta sección se aplica cuando se devuelve de forma asincrónica un valor de Windows Runtime (usaremos un valor StorageFile para ilustrarlo). Considere un ejemplo en el que un método que ya ha migrado a C++/WinRT tiene esta firma.

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

En esta lista siguiente se muestra cómo convertir una llamada a ese método en una tarea. Tenga en cuenta que es necesario llamar a la función auxiliar de interoperabilidad to_cx para convertir el objeto de C++/WinRT devuelto en un objeto de tipo identificador de C++/CX (también conocido como hat).

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

Esta es una versión más concisa de eso.

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

Y hasta puedes optar por encapsular ese patrón en una plantilla de función reutilizable y return igual que devolverías normalmente una tarea.

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

Si le gusta esa idea, es posible que desee agregar to_task a interop_helpers.h.

Envuelve create_async en una tarea que usa co_return

No co_return puedes usar IAsyncXxx^ directamente, pero puedes lograr algo similar. Si tiene una tarea que devuelve un valor de forma cooperativa, puede encapsularla en una llamada a concurrency::create_async.

Este es un ejemplo hipotético, ya que no hay un ejemplo que podamos elevar desde Simple3DGameDX.

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

Como puede ver, puede obtener el valor de retorno de cualquier método que pueda co_await.

Portar concurrency::wait a co_await winrt::resume_after

Hay un par de lugares en los que Simple3DGameDX usa concurrency::wait para pausar el hilo durante un breve periodo de tiempo. Este es un ejemplo.

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

La versión de C++/WinRT de concurrency::wait es la estructura winrt::resume_after. Podemos co_await esa estructura dentro de una tarea de PPL. Este es un ejemplo 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 los otros dos cambios que tuvimos que realizar. Hemos cambiado el tipo de GameConstants::InitialLoadingDelay a std::chrono::d uration y hemos convertido el tipo de valor devuelto de la función lambda explícita, porque el compilador ya no puede deducirlo.

Migrar un tipo de retorno task<void> a winrt::IAsyncXxx

Simple3DGame::LoadLevelAsync

En esta fase de nuestro trabajo con Simple3DGameDX, todos los lugares del proyecto que llaman a Simple3DGame::LoadLevelAsync usan co_await para llamarlo.

Esto significa que simplemente podemos cambiar el tipo de valor devuelto de ese método de task<void> a winrt::Windows::Foundation::IAsyncAction (dejando el resto sin cambios).

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

Ahora debería ser bastante mecánico portar el resto de ese método y sus dependencias (como m_level, etc.) a C++/WinRT.

GameRenderer::LoadLevelResourcesAsync

Esta es la versión original de C++/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 es el único lugar en el proyecto que llama a GameRenderer::LoadLevelResourcesAsync y ya lo usa co_await para llamarlo.

Por lo tanto, ya no es necesario que GameRenderer::LoadLevelResourcesAsync devuelva una tarea; puede devolver un winrt::Windows::Foundation::IAsyncAction en su lugar. Y la propia implementación es lo suficientemente sencilla como para migrar completamente a C++/WinRT. Esto implica hacer el mismo cambio que hicimos en Port concurrency::wait a co_await winrt::resume_after. Y no hay dependencias significativas en el resto del proyecto para preocuparse.

Por lo tanto, este es el aspecto del método después de migrarlo completamente a 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);
}

Objetivo: portar completamente un método a C++/WinRT

Vamos a resumir este tutorial con un ejemplo del objetivo final mediante la migración completa del método BasicReaderWriter::ReadDataAsync a C++/WinRT.

La última vez que vimos este método (en la sección Migrar ReadDataAsync (principalmente) a C++/WinRT, dejando el resto del proyecto sin cambios), estaba principalmente migrado a C++/WinRT. Pero seguía devolviendo una tarea 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());
}

En lugar de devolver una tarea, la cambiaremos para devolver una IAsyncOperation. Y en lugar de devolver una matriz de bytes a través de esa IAsyncOperation, en su lugar devolveremos un objeto IBuffer de C++/WinRT. Esto también requerirá un cambio menor en el código en los sitios de llamada, como veremos.

Este es el aspecto del método después de migrar su implementación, su parámetro y el miembro de datos m_location para usar la sintaxis y los objetos de 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 puede ver, BasicReaderWriter::ReadDataAsync es mucho más sencillo, ya que hemos factorizado en su propio método la lógica sincrónica que recupera bytes del búfer.

Pero ahora es necesario migrar los sitios de llamada desde este tipo de estructura en C++/CX.

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

Para este patrón en C++/WinRT.

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

API importantes