Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Il modello di struct winrt::implements è la base da cui derivano le vostre implementazioni C++/WinRT (di classi di runtime e fabbriche di attivazione) direttamente o indirettamente.
Questo argomento illustra i punti di estensione di winrt::implements in C++/WinRT 2.0. È possibile scegliere di implementare questi punti di estensione nei tipi di implementazione per personalizzare il comportamento predefinito degli oggetti ispezionabili ( ispezionabile nel senso dell'interfaccia IInspectable).
Questi punti di estensione consentono di posticipare la distruzione dei tipi di implementazione, di eseguire query in modo sicuro durante il processo di distruzione e di intercettare l'ingresso e l'uscita dai metodi pianificati. In questo argomento vengono descritte queste funzionalità e vengono illustrate altre informazioni su quando e come usarle.
Distruzione posticipata
Nell'argomento Diagnosi delle allocazioni dirette, abbiamo menzionato che il tipo di implementazione non può avere un distruttore privato.
Il vantaggio di avere un distruttore pubblico è che consente la distruzione posticipata, ovvero la possibilità di rilevare il finale IUnknown::Release chiamare sull'oggetto e quindi di assumere la proprietà di tale oggetto per rinviare la distruzione indefinitamente.
Tenere presente che gli oggetti COM classici sono intrinsecamente conteggiati; il conteggio dei riferimenti viene gestito tramite le funzioni IUnknown::AddRef e IUnknown::Release . In un'implementazione tradizionale di Release, un distruttore C++ di un oggetto COM classico viene richiamato dopo che il conteggio dei riferimenti raggiunge 0.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
Il delete this; invoca il distruttore dell'oggetto prima di liberare la memoria occupata dall'oggetto. Questo funziona abbastanza bene, purché non sia necessario eseguire alcuna attività significativa nel distruttore.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
Cosa si intende per interessante? Per prima cosa, un distruttore è intrinsecamente sincrono. Non è possibile cambiare thread, ad esempio per eliminare alcune risorse specifiche del thread in un contesto diverso. Non è possibile eseguire in modo affidabile query sull'oggetto per un'altra interfaccia di cui potresti aver bisogno per liberare determinate risorse. L'elenco va avanti. Per i casi in cui la distruzione non è semplice, è necessaria una soluzione più flessibile. È qui che entra in gioco la funzione final_release di C++/WinRT.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
// This is the first stop...
}
~Sample() noexcept
{
// ...And this happens only when *unique_ptr* finally deletes the object.
}
};
L'implementazione C++/WinRT di Release è stata aggiornata per chiamare il final_release esattamente quando il numero di riferimenti dell'oggetto passa a 0. In questo stato, l'oggetto può essere sicuro che non ci siano ulteriori riferimenti in sospeso e ora ha la proprietà esclusiva di se stesso. Per questo motivo, può trasferire la proprietà di se stesso alla funzione final_release statica.
In altre parole, l'oggetto si è trasformato da un oggetto che supporta la proprietà condivisa in un oggetto di proprietà esclusiva. L'std::unique_ptr ha la proprietà esclusiva dell'oggetto e di conseguenza eliminerà automaticamente l'oggetto come parte della sua semantica; da qui la necessità di un distruttore pubblico, quando il std::unique_ptr esce dallo scopo (a condizione che non venga trasferito altrove prima di allora). E questa è la chiave. È possibile utilizzare l'oggetto a tempo indeterminato, purché std::unique_ptr mantenga attivo l'oggetto. Ecco un'illustrazione di come spostare l'oggetto altrove.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
batch_cleanup.push_back(std::move(ptr));
}
};
Questo codice salva l'oggetto in una raccolta denominata batch_cleanup uno dei quali eseguirà la pulizia di tutti gli oggetti in un determinato momento in fase di esecuzione dell'app.
In genere, l'oggetto viene distrutto quando std::unique_ptr viene distrutto, ma è possibile accelerarne la distruzione chiamando std::unique_ptr::reset; oppure puoi rimandarlo salvando std::unique_ptr da qualche parte.
Forse più praticamente e in modo più efficace, è possibile trasformare la funzione final_release in una coroutine e gestire la sua eventuale distruzione in un unico punto, pur essendo in grado di sospendere e cambiare thread secondo necessità.
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
{
co_await winrt::resume_background(); // Unwind the calling thread.
// Safely perform complex teardown here.
}
};
Una sospensione determinerà che il thread chiamante, che ha originariamente avviato la chiamata alla funzione IUnknown::Release, ritorni, segnalando così al chiamante che l'oggetto che una volta deteneva non è più disponibile tramite il puntatore dell'interfaccia. I framework dell'interfaccia utente spesso devono assicurarsi che gli oggetti vengano eliminati definitivamente nel thread dell'interfaccia utente specifico che ha originariamente creato l'oggetto. Questa funzionalità rende semplice soddisfare tale requisito, perché la distruzione è separata dal rilascio dell'oggetto.
Si noti che l'oggetto passato a final_release è semplicemente un oggetto C++; non è più un oggetto COM. Ad esempio, i riferimenti deboli COM esistenti all'oggetto non si risolvono più.
Query sicure durante la distruzione
Partendo dal concetto di distruzione posticipata, consiste nella possibilità di eseguire interrogazioni sicure sulle interfacce durante la distruzione.
Com classico si basa su due concetti centrali. Il primo è il conteggio dei riferimenti e il secondo è la ricerca delle interfacce. Oltre a AddRef e Release, l'interfaccia IUnknown fornisce QueryInterface. Questo metodo viene ampiamente usato da determinati framework dell'interfaccia utente, come XAML, per attraversare la gerarchia XAML e per simulare il suo sistema di tipi componibili. Si consideri un semplice esempio.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Questo può apparire innocuo. Questa pagina XAML vuole cancellare il contesto dei dati nel distruttore. DataContext è tuttavia una proprietà della classe di base FrameworkElement e si trova nell'interfaccia IFrameworkElement distinta. Di conseguenza, C++/WinRT deve inserire una chiamata a QueryInterface per cercare la vtable corretta prima di poter chiamare la proprietà DataContext . Ma il motivo per cui ci troviamo anche nel distruttore è che il conteggio dei riferimenti è passato a 0. Chiamando QueryInterface qui incrementa temporaneamente il conteggio dei riferimenti; e quando torna nuovamente a 0, l'oggetto viene nuovamente distrutto.
C++/WinRT 2.0 è stata rafforzata per supportarlo. Ecco l'implementazione C++/WinRT 2.0 di Release, in un formato semplificato.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
Come si potrebbe prevedere, decrementa innanzitutto il conteggio dei riferimenti e quindi agisce solo se non sono presenti riferimenti in sospeso. Tuttavia, prima di chiamare la funzione final_release statica descritta in precedenza in questo argomento, il conteggio dei riferimenti viene stabilizzato impostandolo su 1. Ci riferiamo a questo come debouncing (prendendo in prestito un termine dall'ingegneria elettrica). Questo è fondamentale per impedire che il riferimento finale venga rilasciato. In questo caso, il conteggio dei riferimenti è instabile e non è in grado di supportare in modo affidabile una chiamata a QueryInterface.
La chiamata QueryInterface è rischiosa dopo il rilascio del riferimento finale, perché il conteggio dei riferimenti può crescere potenzialmente in modo illimitato. È tua responsabilità chiamare solo percorsi di codice noti che non prolunghino la vita dell'oggetto. C++/WinRT ti soddisfa a metà assicurandoti che queste QueryInterface chiamate possano essere effettuate in modo affidabile.
Ciò avviene stabilizzando il conteggio dei riferimenti. Quando il riferimento finale è stato rilasciato, il conteggio effettivo dei riferimenti è 0 o un valore selvaggiamente imprevedibile. Quest'ultimo caso può verificarsi se sono coinvolti riferimenti deboli. In entrambi i casi, ciò non è sostenibile se si verifica una chiamata successiva a QueryInterface, perché ciò causerà necessariamente un incremento temporaneo del conteggio dei riferimenti, da cui il riferimento al debouncing. Impostando a 1 si garantisce che una chiamata finale a Release non avvenga mai più su questo oggetto. Questo è esattamente ciò che vogliamo, poiché l'std::unique_ptr ora possiede l'oggetto, ma le chiamate limitate a QueryInterface/Release nelle coppie di saranno sicure.
Si consideri un esempio più interessante.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
{
co_await 5s;
co_await winrt::resume_foreground(ptr->Dispatcher());
ptr = nullptr;
}
};
In primo luogo, la funzione final_release viene chiamata, notificando all'implementazione che è il momento di eseguire la pulizia. Qui, final_release risulta essere una coroutine. Per simulare un primo punto di sospensione, inizia attendendo il pool di thread per alcuni secondi. Riprende quindi sul thread del dispatcher della pagina. L'ultimo passaggio prevede una query, dato che Dispatcher è una proprietà della classe base DependencyObject. Infine, la pagina viene effettivamente eliminata attraverso l'assegnazione di nullptr al std::unique_ptr. Questo a sua volta chiama il distruttore della pagina.
All'interno del distruttore si cancella il contesto dei dati; Che, come sappiamo, richiede una query per la classe di base FrameworkElement.
Tutto questo è possibile grazie alla stabilizzazione del conteggio dei riferimenti (o stabilizzazione del debouncing del conteggio dei riferimenti) fornita da C++/WinRT 2.0.
Ganci di ingresso e uscita del metodo
Un punto di estensione meno comunemente usato è lo struct abi_guard e le funzioni abi_enter e abi_exit .
Se il tipo di implementazione definisce una funzione abi_enter, tale funzione viene chiamata all'ingresso di ciascun metodo della tua interfaccia proiettata (quando non si contano i metodi di IInspectable).
Analogamente, se definisci abi_exit, questo verrà chiamato all'uscita da ogni metodo di questo tipo; ma non verrà chiamato se il abi_enter genera un'eccezione. verrà comunque chiamato se viene generata un'eccezione dal metodo di interfaccia proiettato stesso.
Ad esempio, è possibile usare abi_enter per generare un'eccezione ipotetica invalid_state_error se un client tenta di usare un oggetto dopo che è stato reso inutilizzabile, ad esempio dopo una chiamata ai metodi ShutDown o Disconnect. Le classi iteratore C++/WinRT usano questa funzionalità per generare un'eccezione di stato non valida nella funzione abi_enter se la raccolta sottostante è stata modificata.
Oltre alle semplici funzioni abi_enter e abi_exit, è possibile definire un tipo annidato denominato abi_guard. In tal caso, un'istanza di abi_guard viene creata all'inizio di ciascun metodo della tua interfaccia proiettata (nonIInspectable), con un riferimento all'oggetto come parametro del costruttore. Il abi_guard viene distrutto quindi all'uscita dal metodo. È possibile inserire qualsiasi stato aggiuntivo desiderato nel tipo di abi_guard.
Se non si definisce il proprio abi_guard, verrà utilizzato un valore predefinito che chiama abi_enter in fase di costruzione e abi_exit alla distruzione.
Queste guardie vengono usate solo quando un metodo viene richiamato tramite l'interfaccia proiettata. Se si richiamano i metodi direttamente nell'oggetto di implementazione, tali chiamate passano direttamente all'implementazione, senza alcuna protezione.
Ecco un esempio di codice.
struct Sample : SampleT<Sample, IClosable>
{
void abi_enter();
void abi_exit();
void Close();
};
void example1()
{
auto sampleObj1{ winrt::make<Sample>() };
sampleObj1.Close(); // Calls abi_enter and abi_exit.
}
void example2()
{
auto sampleObj2{ winrt::make_self<Sample>() };
sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}
// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.
IAsyncAction CloseAsync()
{
// Guard is active here.
DoWork();
// Guard becomes inactive once DoOtherWorkAsync
// returns an IAsyncAction.
co_await DoOtherWorkAsync();
// Guard is not active here.
}