Asincronia e interoperabilità tra C++/WinRT e C++/CX
Suggerimento
Anche se è consigliabile leggere questo argomento dall'inizio, è possibile passare direttamente a un riepilogo delle tecniche di interoperabilità nella sezione Panoramica della conversione dal codice C++/CX asincrono a C++/WinRT.
Si tratta di un argomento avanzato relativo alla conversione graduale da C++/CX a C++/WinRT. Questo argomento prosegue la descrizione iniziata nell'argomento Interoperabilità tra C++/WinRT e C++/CX.
Se la dimensione o la complessità della base di codici rende necessario convertire gradualmente il progetto, è necessario un processo di conversione in cui per un certo periodo i codici C++/CX e C++/WinRT coesistono nello stesso progetto. Se si dispone di codice asincrono, potrebbe essere necessario che le catene di attività della libreria PPL (Parallel Patterns Library) e le coroutine coesistano nel progetto man mano che si converte gradualmente il codice sorgente. Questo argomento è incentrato sulle tecniche per l'interoperabilità tra i codici asincroni C++/CX e C++/WinRT. Queste tecniche possono essere usate singolarmente o insieme. Le tecniche consentono di apportare modifiche locali graduali e controllate lungo il percorso di conversione del progetto, senza che ogni modifica si propaghi in modo incontrollato in tutto il progetto.
Prima di leggere questo argomento, è consigliabile vedere Interoperabilità tra C++/WinRT e C++/CX. Questo argomento illustra infatti come preparare il progetto per una conversione graduale. Introduce inoltre due funzioni helper che è possibile usare per convertire un oggetto C++/CX in un oggetto C++/WinRT (e viceversa). Questo argomento sulla modalità asincrona si basa su tali informazioni e usa le funzioni helper.
Nota
Esistono alcune limitazioni alla conversione graduale da C++/CX a C++/WinRT. Nel caso di un progetto di componente Windows Runtime, la conversione graduale non è possibile ed è necessario convertire il progetto in un unico passaggio. Nel caso di un progetto XAML, in un determinato momento, i tipi di pagina XAML devono essere o tutti C++/WinRT o tutti C++/CX. Per altre informazioni, vedere l'argomento Passare a C++/WinRT da C++/CX.
Motivo per cui un intero argomento è dedicato all'interoperabilità del codice asincrono
La conversione da C++/CX a C++/WinRT è in genere semplice, con la sola eccezione del passaggio dalle attività della libreria PPL (Parallel Patterns Library) alle coroutine. I modelli sono diversi. Non esiste un mapping uno-a-uno naturale dalle attività della libreria PPL alle coroutine e non esiste un modo semplice, adatto a tutti i casi, per convertire meccanicamente il codice.
Il vantaggio è che la conversione dalle attività alle coroutine comporta semplificazioni significative. Inoltre, i team di sviluppo periodicamente segnalano che, una volta superati gli ostacoli tipici della conversione del codice asincrono, il resto dell'operazione di conversione è sostanzialmente meccanico.
Spesso, un algoritmo viene scritto originariamente per adattarsi alle API sincrone. Successivamente viene tradotto in attività e continuazioni esplicite. Il risultato è spesso un offuscamento involontario della logica sottostante. Ad esempio, i cicli diventano ricorsioni; i rami if-else diventano un albero annidato, ovvero una catena, di attività e le variabili condivise diventano shared_ptr. Per decostruire la struttura spesso non naturale del codice sorgente della libreria PPL, è consigliabile prima di tutto fare un passo indietro e comprendere lo scopo del codice originale, ovvero individuare la versione sincrona originale. A questo punto, inserire co_await
(attendere in modo cooperativo) nelle posizioni appropriate.
Per questo motivo, se si dispone di una versione C# (piuttosto che C++/CX) del codice asincrono da cui iniziare la conversione, si può ottenere una procedura più snella e una conversione più pulita. Il codice C# usa await
. Quindi il codice C# prevede l'inizio con una versione sincrona e quindi l'inserimento di await
nelle posizioni appropriate.
Se non è disponibile una versione C# del progetto, è possibile usare le tecniche descritte in questo argomento. Dopo aver eseguito la conversione in C++/WinRT, la struttura del codice asincrono sarà più semplice da convertire in C#, se lo si desidera.
Alcune informazioni di base sulla programmazione asincrona
Per avere un quadro di riferimento comune sui concetti e sulla terminologia della programmazione asincrona, tenere presenti alcuni aspetti relativi alla programmazione asincrona di Windows Runtime in generale, nonché la modalità con cui le due proiezioni del linguaggio C++, ciascuna in modi diversi, si sovrappongono a tale tipo di programmazione.
Il progetto è costituito da metodi che funzionano in modo asincrono. Esistono due tipi principali.
- In genere è opportuno attendere il completamento di un'operazione asincrona prima di eseguire altre operazioni. Un metodo che restituisce un oggetto operazione asincrona richiede un intervallo di tempo di attesa.
- A volte, tuttavia, non si vuole o non è necessario attendere il completamento di un'operazione eseguita in modo asincrono. In questo caso è più efficiente per il metodo asincrono non restituire un oggetto operazione asincrona. Un metodo asincrono come questo, ovvero un metodo che non include un intervallo di tempo di attesa, è definito come metodo fire-and-forget.
Oggetti asincroni di Windows Runtime (IAsyncXxx)
Lo spazio dei nomi Windows Runtime Windows::Foundation contiene quattro tipi di oggetti operazione asincrona.
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult> e
- IAsyncOperationWithProgress<TResult, TProgress>.
In questo argomento, quando si usa la forma abbreviata IAsyncXxx, si fa riferimento a questi tipi collettivamente o a uno dei quattro tipi senza specificare quale.
Codice C++/CX asincrono
Il codice C++/CX asincrono usa le attività della libreria PPL (Parallel Patterns Library). Un'attività PPL è rappresentata dalla classe concurrency::task.
In genere, un metodo C++/CX asincrono concatena le attività della libreria PPL insieme usando funzioni lambda con concurrency::create_task e concurrency::task::then. Ognuna funzione lambda restituisce un'attività che, quando viene completata, genera un valore che viene passato all'espressione lambda di continuazione dell'attività.
In alternativa, invece di richiamare create_task per creare un'attività, un metodo C++/CX asincrono può chiamare concurrency::create_async per creare un oggetto IAsyncXxx.
Il tipo restituito di un metodo C++/CX asincrono può quindi essere un'attività della libreria PPL oppure un oggetto IAsyncXxx.
In entrambi i casi, il metodo stesso usa la parola chiave return
per restituire un oggetto asincrono che, al termine, produce il valore effettivamente desiderato dal chiamante (ad esempio un file, una matrice di byte o un valore booleano).
Nota
Se un metodo C++/CX asincrono restituisce un oggetto IAsyncXxx, l'oggetto TResult (se presente) è limitato a essere un tipo di Windows Runtime. Un valore booleano, ad esempio, è un tipo di Windows Runtime, mentre un tipo proiettato C++/CX (ad esempio Platform::Array<byte>^) non lo è.
Codice C++/WinRT asincrono
C++/WinRT integra le coroutine di C++ nel modello di programmazione. Le coroutine e l'istruzione co_await
forniscono un metodo naturale per attendere in modo cooperativo un risultato.
Ognuno dei tipi IAsyncXxx viene proiettato in un tipo corrispondente nello spazio dei nomi C++/WinRT winrt::Windows::Foundation. Si farà riferimento a quelli come winrt::IAsyncXxx (rispetto a IAsyncXxx^ di C++/CX).
Il tipo restituito di una coroutine C++/WinRT può essere winrt::IAsyncXxx o winrt::fire_and_forget. Invece di usare la parola chiave return
per restituire un oggetto asincrono, una coroutine usa la parola chiave co_return
per restituire in modo cooperativo il valore effettivamente desiderato dal chiamante (ad esempio, un file, una matrice di byte o un valore booleano).
Se un metodo contiene almeno un'istruzione co_await
(o almeno un'istruzione co_return
o co_yield
), il metodo può definirsi una coroutine.
Per altre informazioni ed esempi di codice, vedi Concorrenza e operazioni asincrone con C++/WinRT.
Esempio di gioco Direct3D (Simple3DGameDX)
Questo argomento contiene procedure dettagliate di diverse tecniche di programmazione specifiche che illustrano come convertire gradualmente il codice asincrono. Come case study, verrà usata la versione di C++/CX dell'esempio di gioco Direct3D (denominata Simple3DGameDX). Verranno illustrati alcuni esempi di come è possibile acquisire il codice sorgente C++/CX originale in tale progetto e convertire gradualmente il codice asincrono in C++/WinRT.
- Scaricare il file con estensione zip dal collegamento precedente e decomprimerlo.
- Aprire il progetto C++/CX (si trova nella cartella denominata
cpp
) in Visual Studio. - Sarà quindi necessario aggiungere il supporto di C++/WinRT al progetto. La procedura da seguire per questa operazione è descritta in Acquisizione di un progetto C++/CX e aggiunta del supporto di C++/WinRT. In questa sezione il passaggio relativo all'aggiunta del file di intestazione
interop_helpers.h
al progetto è particolarmente importante perché in questo argomento si farà riferimento alle funzioni helper. - Infine, aggiungere
#include <pplawait.h>
apch.h
. In questo modo, si ottiene il supporto della coroutine per la libreria PPL. Altre informazioni su tale supporto sono illustrate nella sezione seguente.
Non eseguire ancora la compilazione, altrimenti verranno generati errori relativi a byte ambigui. Ecco come risolverli.
- Aprire
BasicLoader.cpp
e aggiungere il commentousing namespace std;
. - Nello stesso file di codice sorgente sarà quindi necessario qualificare shared_ptr come std::shared_ptr. A tale scopo, eseguire un'operazione di ricerca e sostituzione nel file.
- Qualificare quindi vector come std::vector e string come std::string.
Il progetto ora viene ricompilato, dispone del supporto di C++/WinRT e contiene le funzioni helper di interoperabilità from_cx e to_cx.
Il progetto Simple3DGameDX è ora pronto per essere gestito con le procedure dettagliate relative al codice descritte in questo argomento.
Panoramica della conversione dal codice C++/CX asincrono a C++/WinRT
In breve, in fase di conversione si modificano le attività della libreria PPL in chiamate a co_await
. Il valore restituito di un metodo verrà modificato da un'attività della libreria PPL a un oggetto winrt::IAsyncXxx di C++/WinRT. Verrà inoltre modificato qualsiasi IAsyncXxx^ in un C++/WinRT winrt::IAsyncXxx.
Si noterà che una coroutine corrisponde a un metodo che chiama co_xxx
. Una coroutine di C++/WinRT usa co_return
per restituirne il valore in modo cooperativo. Grazie al supporto della coroutine per la libreria PPL (fornito da pplawait.h
), è possibile usare co_return
per restituire un'attività della libreria PPL da una coroutine. Ed è anche possibile usare co_await
per restituire sia le attività che l'oggetto IAsyncXxx. Non è tuttavia possibile usare co_return
per restituire un IAsyncXxx^. La tabella seguente descrive il supporto per l'interoperabilità tra le varie tecniche asincrone con pplawait.h
nell'immagine.
Metodo | È possibile usare co_await ? |
È possibile usare co_return ? |
---|---|---|
Il metodo restituisce task<void> | Sì | Sì |
Il metodo restituisce task<T> | No | Sì |
Il metodo restituisce IAsyncXxx^ | Sì | Nr. Si esegue, tuttavia, il wrapping di create_async in un'attività che usa co_return . |
Il metodo restituisce winrt::IAsyncXxx | Sì | Sì |
Usare la tabella seguente per passare direttamente alla sezione di questo argomento che descrive la tecnica di interoperabilità di interesse oppure continuare a leggere da qui.
Tecnica di interoperabilità asincrona | Sezione in questo argomento |
---|---|
Utilizzare co_await per attendere un metodo task<void> da un metodo fire-and-forget o da un costruttore. |
Attendere task<void> in un metodo fire-and-forget |
Usare co_await per attendere un metodo task<void> dall'interno di un metodo task<void>. |
Attendere task<void> all'interno di un metodo task<void> |
Usare co_await per attendere un metodo task<void> dall'interno di un metodo task<T>. |
Attendere task<void> all'interno di un metodo task<T> |
Usare co_await per attendere un metodo IAsyncXxx^. |
Attendere un metodo IAsyncXxx^ in un metodo task, lasciando invariato il resto del progetto |
Usare co_return all'interno di un metodo task<void>. |
Attendere task<void> all'interno di un metodo task<void> |
Usare co_return all'interno di un metodo task<T>. |
Attendere un metodo IAsyncXxx^ in un metodo task, lasciando invariato il resto del progetto |
Eseguire il wrapping di create_async in un'attività che usa co_return . |
Eseguire il wrapping di create_async in un'attività che usa co_return |
Convertire concurrency::wait. | Convertire concurrency::wait in co_await winrt::resume_after |
Restituire winrt::IAsyncXxx invece di task<void>. | Convertire un tipo restituito task<void> a winrt::IAsyncXxx |
Convertire un elemento winrt::IAsyncXxx<T> (T è primitiva) in task<T>. | Convertire un elemento winrt::IAsyncXxx<T> (T è primitiva) in task<T> |
Convertire winrt::IAsyncXxx<T> (T è un tipo Windows Runtime) in task<T^>. | Convertire winrt::IAsyncXxx<T> (T è un tipo Windows Runtime) in task<T^> |
Ecco un breve esempio di codice che illustra una parte del supporto.
#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
Anche con queste eccezionali opzioni di interoperabilità, la conversione graduale dipende dalla scelta delle modifiche specifiche che è possibile apportare in modo che non influiscano sul resto del progetto. L'obiettivo è evitare di trovarsi impreparati e, di conseguenza, compromettere la struttura dell'intero progetto. Per questo motivo, è necessario eseguire le operazioni in un ordine specifico. Di seguito sono riportati alcuni esempi di creazione di questi tipi di modifiche di conversione/interoperabilità asincrona.
Attendere un metodo task<void>, lasciando invariato il resto del progetto
Un metodo che restituisce task<void> esegue l'operazione in modo asincrono restituendo un oggetto operazione asincrona, ma non genera un valore. È possibile co_await
un metodo come questo.
Quindi, un'ottima soluzione per iniziare a convertire gradualmente il codice asincrono consiste nel trovare le posizioni in cui chiamare tali metodi. Queste posizioni comporteranno la creazione e/o la restituzione di un'attività. Possono interessare anche il tipo di catena di attività in cui nessun valore viene passato da ogni attività alla relativa continuazione. In queste posizioni è possibile sostituire semplicemente il codice asincrono con le istruzioni co_await
, come illustrato più avanti.
Nota
Andando avanti nell'argomento, si vedrà il vantaggio di questa strategia. Dopo che un determinato metodo task<void> viene chiamato esclusivamente tramite co_await
, è possibile convertire il metodo in C++/WinRT e fare in modo che restituisca un elemento winrt::IAsyncXxx.
Ecco alcuni esempi. Aprire il progetto Simple3DGameDX. Vedere l'esempio di gioco Direct3D.
Importante
Negli esempi seguenti, quando si osservano le modifiche apportate alle implementazioni dei metodi, tenere presente che non è necessario modificare i chiamanti dei metodi che si stanno modificando. Queste modifiche sono localizzate e non si propagano a catena nel progetto.
Attendere task<void> in un metodo fire-and-forget
Iniziamo con l'attesa di task<void> all'interno dei metodi fire-and-forget, perché questo è il caso più semplice. Si tratta di metodi che funzionano in modo asincrono, ma il chiamante del metodo non attende il completamento dell'operazione. È sufficiente chiamare il metodo e non occuparsene più, nonostante il fatto che si completi in modo asincrono.
Esaminare la radice del grafico delle dipendenze del progetto per void
metodi che contengono create_task e/o catene di attività in cui vengono chiamati solo i metodi task<void>.
In Simple3DGameDX è presente codice come questo nell'implementazione del metodo GameMain::Update. Si trova nel file del codice sorgente GameMain.cpp
.
GameMain::Update
Di seguito è riportato un estratto della versione C++/CX del metodo che illustra le due parti del metodo che vengono eseguite in modo asincrono.
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());
...
...
}
È possibile visualizzare una chiamata al metodo Simple3DGame::LoadLevelAsync (che restituisce task<void> della libreria PPL). Successivamente, si può osservare una continuazione che esegue alcune operazioni sincrone. LoadLevelAsync è un'operazione asincrona, ma non restituisce alcun valore. Non viene quindi passato alcun valore dall'attività alla continuazione.
È possibile apportare lo stesso tipo di modifica al codice in queste due posizioni. Il codice viene illustrato dopo il listato seguente. Si potrebbe descrivere l'accesso sicuro al puntatore this in una coroutine membro di classe, È tuttavia consigliabile rinviare questo argomento per una sezione successiva (La discussione posticipata suco_await
e sul puntatore this): per il momento, questo codice funziona.
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;
...
...
}
Come si può notare, poiché LoadLevelAsync restituisce un'attività, è possibile co_await
tale operazione. Non è necessaria una continuazione esplicita: il codice che segue un co_await
viene eseguito solo quando LoadLevelAsync viene completato.
L'introduzione di co_await
trasforma il metodo in una coroutine, pertanto non è possibile lasciare che restituisca void
. Si tratta di un metodo fire-and-forget e quindi è stato modificato in modo che restituisca winrt::fire_and_forget.
Sarà anche necessario modificare GameMain.h
. Modificare il tipo restituito di GameMain::Update da void
a winrt::fire_and_forget anche nella dichiarazione.
È possibile apportare questa modifica alla copia del progetto e il gioco viene comunque compilato ed eseguito. Il codice sorgente è fondamentalmente ancora C++/CX, ma ora usa gli stessi modelli di C++/WinRT. A questo punto, la possibilità di convertire il resto del codice in modo meccanico è più concreta.
GameMain::ResetGame
GameMain::ResetGame è un altro metodo fire-and-forget che chiama LoadLevelAsync. È quindi possibile apportare la stessa modifica del codice se si vuole fare pratica.
GameMain::OnDeviceRestored
La questione si fa più interessante in GameMain::OnDeviceRestored perché è costituito da un annidamento più profondo di codice asincrono, inclusa un'attività senza operazioni. Ecco una descrizione delle parti asincrone del metodo (con le porzioni meno interessanti del codice sincrono rappresentate da puntini di sospensione).
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());
}
Per prima cosa, modificare il tipo restituito di GameMain::OnDeviceRestored da void
a winrt::fire_and_forget in GameMain.h
e .cpp
. Sarà inoltre necessario aprire DeviceResources.h
e apportare la stessa modifica al tipo restituito di IDeviceNotify::OnDeviceRestored.
Per convertire il codice asincrono, rimuovere tutte le chiamate create_task e then e le relative parentesi graffe e semplificare il metodo in una serie di istruzioni flat.
Modificare qualsiasi operazione return
che restituisce un'attività in un'operazione co_await
. Rimarrà un'operazione return
che non restituisce alcun elemento ed è quindi sufficiente eliminarla. Al termine, l'attività senza operazioni sarà scomparsa e l'aspetto delle parti asincrone del metodo sarà simile al seguente. Anche in questo caso, le parti meno interessanti del codice sincrono sono sostituite da puntini di sospensione.
winrt::fire_and_forget GameMain::OnDeviceRestored()
{
...
co_await m_renderer->CreateGameDeviceResourcesAsync(m_game);
...
if (m_updateState == UpdateEngineState::WaitingForResources)
{
co_await m_game->LoadLevelAsync();
...
}
...
}
Come si può notare, questa forma di struttura asincrona è molto più semplice e più facile da leggere.
GameMain::GameMain
Il costruttore GameMain::GameMain esegue l'operazione in modo asincrono e nessuna parte del progetto attende il completamento di tale operazione. Anche in questo elenco vengono descritte le parti asincrone.
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());
}
Un costruttore non può tuttavia restituire winrt::fire_and_forget e quindi il codice asincrono verrà spostato in un nuovo metodo fire-and-forget GameMain::ConstructInBackground, il codice verrà appiattito in istruzioni co_await
e verrà chiamato il nuovo metodo dal costruttore. Ecco il risultato.
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();
...
}
...
}
Ora tutti i metodi,fire-and-forget, ovvero tutto il codice asincrono, in GameMain è stato trasformato in coroutine. Se si è interessati, è possibile cercare metodi fire-and-forget in altre classi e apportare modifiche simili.
Discussione rinviata su co_await
e il puntatore this
Quando sono state descritte le modifiche apportate a GameMain::Update, è stato rinviato un approfondimento sul puntatore this. In questa sezione, viene ripreso il discorso.
Si applica a tutti i metodi modificati finora e a tutte le coroutine, non solo a quelle di tipo fire-and-forget. L'introduzione di co_await
in un metodo comporta l'inserimento di un punto di sospensione. Per questo motivo, è necessario prestare attenzione al puntatore this, che naturalmente si usa dopo il punto di sospensione ogni volta che si accede a un membro di classe.
In breve, la soluzione consiste nel chiamare implements::get_strong. Per una descrizione completa del problema e della soluzione, vedere Accesso sicuro al puntatore this in una coroutine membro di classe.
È possibile chiamare implements::get_strong solo in una classe che deriva da winrt::implements.
Derivare GameMain da winrt::implements
La prima modifica che è necessario apportare è in GameMain.h
.
class GameMain :
public DX::IDeviceNotify
GameMain continuerà a implementare DX::IDeviceNotify, ma verrà modificato in modo da derivare da winrt::implements.
class GameMain :
public winrt::implements<GameMain, winrt::Windows::Foundation::IInspectable>,
DX::IDeviceNotify
Quindi, in App.cpp
, si troverà questo metodo.
void App::Load(Platform::String^)
{
if (!m_main)
{
m_main = std::unique_ptr<GameMain>(new GameMain(m_deviceResources));
}
}
Tuttavia, ora che GameMain deriva da winrt::implements, è necessario crearlo in modo diverso. In questo caso, verrà usato il modello di funzione winrt::make_self. Per altre informazioni, vedere Creazione di istanze e restituzione di interfacce e tipi di implementazione.
Sostituire la riga di codice con questo.
...
m_main = winrt::make_self<GameMain>(m_deviceResources);
...
Per chiudere il ciclo su tale modifica, sarà necessario modificare anche il tipo di m_main. In App.h
, si troverà questo codice:
ref class App sealed :
public Windows::ApplicationModel::Core::IFrameworkView
{
...
private:
...
std::unique_ptr<GameMain> m_main;
};
Modificare la dichiarazione di m_main come segue.
...
winrt::com_ptr<GameMain> m_main;
...
È ora possibile chiamare implements::get_strong
Per GameMain::Update e per uno degli altri metodi a cui è stata aggiunta una coroutine co_await
, di seguito viene illustrato come è possibile chiamare get_strong all'inizio di una coroutine per garantire che un riferimento sicuro sopravviva fino al completamento della coroutine.
winrt::fire_and_forget GameMain::Update()
{
auto strong_this{ get_strong() }; // Keep *this* alive.
...
co_await ...
...
}
Attendere task<void> all'interno di un metodo task<void>
Il caso più semplice successivo è attendere task<void> nell’ambito di un metodo che restituisce task<void>. Questo perché è possibile co_await
un task<void> e possiamo co_return
da un oggetto dello stesso tipo.
Nell'implementazione del metodo Simple3DGame::LoadLevelAsync è disponibile un esempio molto semplice. Si trova nel file del codice sorgente 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();
}
Esiste solo un codice sincrono, seguito dalla restituzione dell'attività creata da GameRenderer::LoadLevelResourcesAsync.
Anziché restituire l'attività, viene eseguita una coroutine co_await
dell'attività e quindi una coroutine co_return
dell'oggetto void
risultante.
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();
}
Non sembra una modifica profonda. Tuttavia, ora che viene chiamato GameRenderer::LoadLevelResourcesAsync tramite co_await
, è possibile convertirlo in modo che restituisca winrt::IAsyncXxx invece di un'attività. Questa operazione verrà eseguita più avanti nella sezione Convertire un tipo restituito task<void> in winrt::IAsyncXxx.
Attendere task<void> nell’ambito di un metodo task<T>
Anche se non esistono esempi appropriati in Simple3DGameDX, è possibile realizzare un esempio ipotetico solo per illustrare il modello.
La prima riga nell'esempio di codice seguente illustra la semplice co_await
di un oggetto task<void>. Quindi, per soddisfare il tipo restituito task<T>, è necessario restituire in modo asincrono un elemento StorageFile^. A tale scopo, si usa co_await
per un'API Windows Runtime e co_return
per il file risultante.
task<StorageFile^> Simple3DGame::LoadLevelAndRetrieveFileAsync(
StorageFolder^ location,
Platform::String^ filename)
{
co_await m_renderer->LoadLevelResourcesAsync();
co_return co_await location->GetFileAsync(filename);
}
È addirittura possibile convertire una parte maggiore del metodo in C++/WinRT come segue.
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);
}
Il membro dati m_renderer è ancora in C++/CX nell'esempio.
Attendere un metodo IAsyncXxx^ in un metodo task, lasciando invariato il resto del progetto
Abbiamo visto come è possibile co_await
eseguire attività<void>. È anche possibile usare co_await
per un metodo che restituisce IAsyncXxx, che si tratti di un metodo del progetto o di un'API Windows asincrona (ad esempio, StorageFolder.GetFileAsync, che è stata attesa in modo cooperativo nella sezione precedente).
Per un esempio di come è possibile apportare questo tipo di modifica al codice, vedere BasicReaderWriter::ReadDataAsync, che verrà implementato in BasicReaderWriter.cpp
.
Di seguito è illustrata la versione originale di 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;
});
}
Il listato di codice seguente mostra che è possibile usare co_await
per API Windows che restituiscono IAsyncXxx^. È possibile anche usare co_return
per il valore restituito da BasicReaderWriter::ReadDataAsync in modo asincrono (in questo caso, una matrice di byte). Il primo passaggio illustra come apportare solo queste modifiche. Il codice C++/CX verrà effettivamente convertito in C++/WinRT nella sezione successiva.
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;
}
Anche in questo caso, non è necessario modificare i chiamanti dei metodi che vengono modificati, perché il tipo restituito non è stato modificato.
Convertire ReadDataAsync (principalmente) in C++/WinRT, lasciando invariato il resto del progetto
È possibile procedere ulteriormente e convertire il metodo quasi completamente in C++/WinRT senza dover modificare altre parti del progetto.
L'unica dipendenza che questo metodo ha sul resto del progetto è il membro dati BasicReaderWriter::m_location, che è un oggetto StorageFolder^ di C++/CX. Per lasciare invariati il membro dati, il tipo di parametro e il tipo restituito, è necessario eseguire solo alcune conversioni una all'inizio del metodo e una alla fine. A tal fine, è possibile usare le funzioni helper di interoperabilità from_cx e to_cx.
Ecco l'aspetto di BasicReaderWriter::ReadDataAsync dopo la conversione dell'implementazione prevalentemente in C++/WinRT. Si tratta di un esempio efficace di conversione graduale. Questo metodo si trova nella fase in cui è possibile non interpretarlo come un metodo C++/CX che usa alcune tecniche di C++/WinRT e vederlo come un metodo C++/WinRT che interagisce 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());
}
Nota
Sopra, in ReadDataAsync, viene creata e restituita una nuova matrice C++/CX. L'operazione viene eseguita in modo da soddisfare il tipo restituito del metodo, per non dover modificare il resto del progetto.
È possibile che si trovino altri esempi nel progetto in cui, dopo la conversione, si raggiunge la fine del metodo e si dispone solo di un oggetto C++/WinRT. Per co_return
l'oggetto, è sufficiente chiamare to_cx per convertirlo. Sono disponibili altre informazioni e un esempio nella sezione successiva.
Convertire un elemento winrt::IAsyncXxx<T> in task<T>
Questa sezione si occupa della situazione in cui è stato convertito un metodo asincrono in C++/WinRT (in modo che restituisca un elemento winrt::IAsyncXxx<T>), ma è ancora presente codice C++/CX che chiama tale metodo come se continuasse a restituire un'attività.
- Un caso è quello in cui T è primitiva e non richiede alcuna conversione.
- L'altro caso è quello in cui T è un tipo di Windows Runtime ed è quindi necessario convertirlo in T^.
Convertire un elemento winrt::IAsyncXxx<T> (T è primitiva) in un elemento task<T>
Lo schema di questa sezione si applica quando viene restituito in modo asincrono un valore primitivo (verrà usato un valore booleano per la descrizione). Si consideri un esempio in cui un metodo già convertito in C++/WinRT ha questa firma.
winrt::Windows::Foundation::IAsyncOperation<bool>
MyClass::GetBoolMemberFunctionAsync()
{
bool value = ...
co_return value;
}
È possibile convertire una chiamata a tale metodo in un'attività simile alla seguente.
task<bool> MyClass::RetrieveBoolTask()
{
co_return co_await GetBoolMemberFunctionAsync();
}
In alternativa, è possibile una conversione simile alla seguente.
task<bool> MyClass::RetrieveBoolTask()
{
return concurrency::create_task(
[this]() -> concurrency::task<bool> {
auto result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Si noti che il tipo restituito task della funzione lambda è esplicito, perché il compilatore non può dedurlo.
È anche possibile chiamare il metodo da una catena di attività arbitraria simile a quella seguente. Anche in questo caso, con un tipo restituito lambda esplicito.
...
.then([this]() -> concurrency::task<bool> {
co_return co_await GetBoolMemberFunctionAsync();
}).then([this](bool result) {
...
});
...
Convertire un elemento <winrt::IAsyncXxx> (T è un tipo di Windows Runtime) in un elemento task<T>
Lo schema di questa sezione si applica quando viene restituito in modo asincrono un valore di Windows Runtime (verrà usato un valore StorageFile per la descrizione). Si consideri un esempio in cui un metodo già convertito in C++/WinRT ha questa firma.
winrt::Windows::Foundation::IAsyncOperation<winrt::Windows::Storage::StorageFile>
MyClass::GetStorageFileMemberFunctionAsync()
{
co_return co_await winrt::Windows::Storage::StorageFile::GetFileFromPathAsync
(L"MyFile.txt");
}
Il listato seguente illustra come convertire una chiamata a tale metodo in un'attività. Si noti che è necessario chiamare la funzione helper di interoperabilità to_cx per convertire l'oggetto C++/WinRT restituito in un oggetto handle C++/CX (definito anche hat).
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
winrt::Windows::Storage::StorageFile storageFile =
co_await GetStorageFileMemberFunctionAsync();
co_return to_cx<Windows::Storage::StorageFile>(storageFile);
}
Di seguito è riportata una versione più sintetica.
task<Windows::Storage::StorageFile^> RetrieveStorageFileTask()
{
co_return to_cx<Windows::Storage::StorageFile>(GetStorageFileMemberFunctionAsync());
}
È addirittura possibile scegliere di eseguire il wrapping di tale schema in un modello di funzione riutilizzabile e usare return
come se si restituisse normalmente un'attività.
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 si preferisce questa idea, è possibile aggiungere to_task a interop_helpers.h
.
Eseguire il wrapping di create_async in un'attività che usa co_return
Non è possibile co_return
un oggetto IAsyncXxx direttamente, ma è possibile ottenere un risultato simile. Nel caso di un'attività che restituisce un valore in modo cooperativo, è possibile eseguire il wrapping di tale attività all'interno di una chiamata a concurrency::create_async.
Di seguito è riportato un esempio ipotetico, dal momento che non è disponibile un esempio che si possa estrapolare da Simple3DGameDX.
Windows::Foundation::IAsyncOperation<bool>^ MyClass::RetrieveBoolAsync()
{
return concurrency::create_async(
[this]() -> concurrency::task<bool> {
bool result = co_await GetBoolMemberFunctionAsync();
co_return result;
});
}
Come si può notare, è possibile ottenere il valore restituito da qualsiasi metodo su cui è possibile eseguire una coroutine co_await
.
Convertire concurrency::wait in co_await winrt::resume_after
Esistono alcune posizioni in cui Simple3DGameDX usa concurrency::wait per sospendere il thread per un breve periodo di tempo. Ecco un esempio.
// 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 versione di C++/WinRT di concurrency::wait è lo struct winrt::resume_after. È possibile co_await
tale struct all'interno di un'attività della libreria PPL. Ecco un esempio di codice.
// 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);
}));
...
}
Si notino le altre due modifiche che è stato necessario apportare. È stato modificato il tipo di GameConstants::InitialLoadingDelay in std::chrono::duration ed è stato reso esplicito il tipo restituito della funzione lambda, perché il compilatore non è più in grado di dedurlo.
Convertire un tipo restituito task<void> in winrt::IAsyncXxx
Simple3DGame::LoadLevelAsync
In questa fase dell'operazione con Simple3DGameDX, tutte le posizioni nel progetto che chiamano Simple3DGame::LoadLevelAsync usano co_await
per chiamarlo.
Ciò significa che è possibile semplicemente modificare il tipo restituito del metodo da task<void> a winrt::Windows::Foundation::IAsyncAction (lasciando la parte restante invariata).
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();
}
I passaggi successivi per convertire la parte restante del metodo e le relative dipendenze (ad esempio, m_level e così via) in C++/WinRT dovrebbero essere abbastanza meccanici.
GameRenderer::LoadLevelResourcesAsync
Di seguito è illustrata la versione originale di C++/CX per 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 è l'unica posizione nel progetto che chiama GameRenderer::LoadLevelResourcesAsync e usa già co_await
per chiamarlo.
Non è quindi più necessario GameRenderer::LoadLevelResourcesAsync per restituire un'attività — può restituire un winrt::Windows::Foundation::IAsyncAction. L'implementazione stessa è abbastanza semplice da convertire completamente il codice in C++/WinRT. In questo modo, si esegue la stessa modifica apportata in Convertire concurrency::wait in co_await winrt::resume_after
e non esistono dipendenze significative nel resto del progetto di cui preoccuparsi.
Ecco l'aspetto del metodo dopo la conversione completa in 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);
}
Obiettivo: conversione completa di un metodo in C++/WinRT
È possibile concludere questa procedura dettagliata con un esempio dell'obiettivo finale, eseguendo la conversione completa del metodo BasicReaderWriter::ReadDataAsync in C++/WinRT.
L'ultima volta che il metodo è stato esaminato (nella sezione Convertire ReadDataAsync (principalmente) in C++/WinRT, lasciando invariato il resto del progetto), gran parte del codice è stata convertita in C++/WinRT. Restituiva tuttavia un'attività di 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());
}
Anziché restituire un'attività, il metodo verrà modificato in modo da restituire IAsyncOperation. Inoltre, anziché restituire una matrice di byte tramite IAsyncOperation, verrà restituito un oggetto IBuffer di C++/WinRT. Come si vedrà, questa operazione richiede anche una lieve modifica al codice nei siti di chiamata.
Ecco l'aspetto del metodo dopo la conversione dell'implementazione, del parametro e del membro dati m_location per l'uso della sintassi e degli oggetti di 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() };
}
Come si può notare, l'oggetto BasicReaderWriter::ReadDataAsync è molto più semplice, perché è stato incluso nel metodo della logica sincrona che recupera i byte dal buffer.
A questo punto, è necessario convertire i siti di chiamata da questo tipo di struttura in C++/CX:
task<void> BasicLoader::LoadTextureAsync(...)
{
return m_basicReaderWriter->ReadDataAsync(filename).then(
[=](const Platform::Array<byte>^ textureData)
{
CreateTexture(...);
});
}
A questo modello in C++/WinRT:
winrt::Windows::Foundation::IAsyncAction BasicLoader::LoadTextureAsync(...)
{
auto textureBuffer = co_await m_basicReaderWriter.ReadDataAsync(filename);
auto textureData = GetBufferView(textureBuffer);
CreateTexture(...);
}
API importanti
- IAsyncAction
- IAsyncActionWithProgress<TProgress>
- IAsyncOperation<TResult>
- IAsyncOperationWithProgress<TResult, TProgress>
- implements::get_strong
- concurrency::create_async
- concurrency::create_task
- concurrency::task
- concurrency::task::then
- concurrency::wait
- winrt::fire_and_forget
- winrt::make_self