Punti di estensione per i tipi di implementazione

Il modello struct winrt::implements è la base da cui derivano direttamente o indirettamente le tue implementazioni di C++/WinRT (classi di runtime e factory di attivazione).

Questo argomento illustra i punti di estensione di winrt::implements in C++/WinRT 2.0. Puoi scegliere di implementare questi punti di estensione nei tipi di implementazione per personalizzare il comportamento predefinito degli oggetti inspectable (inspectable nel senso dell'interfaccia IInspectable).

Questi punti di estensione ti consentono di posticipare la distruzione dei tipi di implementazione, di eseguire query in modo sicuro durante la distruzione e di associarne l'ingresso e l'uscita dai metodi proiettati. Questo argomento descrive queste funzionalità e indica quando e come usarle.

Distruzione posticipata

Nell'argomento Diagnosi delle allocazioni dirette è stato indicato che un tipo di implementazione non può avere un distruttore privato.

Un distruttore pubblico offre come vantaggio la possibilità di usufruire della distruzione posticipata, ovvero la capacità di rilevare la chiamata IUnknown::Release finale su un oggetto e quindi di assumere la proprietà di tale oggetto per rinviarne la distruzione a tempo indeterminato.

Tieni presente che, per gli oggetti COM classici, il conteggio dei riferimenti viene eseguito intrinsecamente e gestito tramite le funzioni IUnknown::AddRef e IUnknown::Release. In un'implementazione tradizionale di Release viene richiamato un distruttore C++ di un oggetto COM classico quando 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;
}

delete this; chiama il distruttore dell'oggetto prima di liberare la memoria occupata dall'oggetto. Questa operazione funziona abbastanza bene, purché non sia necessario eseguire operazioni interessanti 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? Prima di tutto, un distruttore è intrinsecamente sincrono. Non puoi cambiare thread, magari per eliminare definitivamente alcune risorse specifiche di un thread in un contesto diverso. Non puoi eseguire query in modo affidabile sull'oggetto per un'altra interfaccia di cui potresti avere bisogno per liberare alcune risorse. L'elenco continua. Per i casi in cui la distruzione non è irrilevante, hai bisogno di una soluzione più flessibile. È in questi casi 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 in modo da chiamare la funzione final_release esattamente quando il conteggio dei riferimenti dell'oggetto passa a 0. In questo stato l'oggetto può avere la certezza che non vi siano altri riferimenti in sospeso e di disporre della 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. std::unique_ptr ha la proprietà esclusiva dell'oggetto, quindi elimina naturalmente l'oggetto come parte della propria semantica (da qui l'esigenza di un distruttore pubblico) quando std::unique_ptr esce dall'ambito (purché non venga spostato altrove prima di tale operazione). Questa è la chiave. Puoi usare l'oggetto a oltranza, purché std::unique_ptr lo mantenga attivo. Ecco un esempio di come è possibile 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 che ha il compito di eseguire la pulizia di tutti gli oggetti in un determinato momento in fase di esecuzione dell'app.

La distruzione dell'oggetto in genere avviene con la distruzione distd::unique_ptr, ma puoi velocizzarne la distruzione chiamandostd::unique_ptr oppure puoi rinviarla salvandostd::unique_ptr in un punto.

Forse in modo più pratico ed efficace, puoi trasformare la funzione final_release in una coroutine e gestirne la distruzione finale in un'unica posizione, pur essendo in grado di sospendere e cambiare thread in base alle esigenze.

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à la restituzione del thread chiamante che ha avviato in origine la chiamata alla funzione IUnknown::Release e quindi segnalerà al chiamante che l'oggetto di cui era proprietario non è più disponibile tramite il puntatore di interfaccia. I framework dell'interfaccia utente devono verificare che gli oggetti vengano eliminati definitivamente nel thread dell'interfaccia utente specifico che ha originariamente creato l'oggetto. Questa funzionalità consente di soddisfare questo requisito senza difficoltà, perché la distruzione è separata dal rilascio dell'oggetto.

Si noti che l'oggetto passato a final_release è semplicemente un oggetto C++ e non è più un oggetto COM. Ad esempio, i riferimenti deboli COM esistenti all'oggetto non si risolvono più.

Query sicure durante la distruzione

La capacità di eseguire query in modo sicuro per le interfacce durante la distruzione si basa sulla nozione di distruzione posticipata.

Il modello COM classico si basa su due concetti centrali. Il primo è il conteggio dei riferimenti e il secondo è l'esecuzione di query per le interfacce. Oltre ad AddRef e Release, l'interfaccia IUnknown fornisce QueryInterface. Questo metodo viene ampiamente usato da alcuni framework interfaccia utente, ad esempio XAML, per attraversare la gerarchia XAML durante la simulazione del sistema di tipi componibile. Prendi in considerazione un semplice esempio.

struct MainPage : PageT<MainPage>
{
    ~MainPage()
    {
        DataContext(nullptr);
    }
};

Potrebbe sembrare innocuo. Questa pagina XAML vuole cancellare il contesto dei dati nel proprio distruttore. Tuttavia DataContext è una proprietà della classe di base FrameworkElement e si trova nell'interfaccia IFrameworkElement separata. Di conseguenza, C++/WinRT deve inserire una chiamata a QueryInterface per cercare l'elemento vtable corretto prima di poter chiamare la proprietà DataContext. Il motivo per cui ti trovi nel distruttore è che il conteggio dei riferimenti è passato a 0. Con la chiamata a QueryInterface, viene avviato temporaneamente il conteggio dei riferimenti. Quando il conteggio passa di nuovo a 0, l'oggetto viene distrutto un'altra volta.

C++/WinRT 2.0 è stato potenziato per ovviare a questo problema. Di seguito è riportata l'implementazione C++/WinRT 2.0 di Release in una forma semplificata.

uint32_t Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        m_references = 1; // Debouncing!
        T::final_release(...);
    }
 
    return remaining;
}

Come è prevedibile, riduce inizialmente il conteggio dei riferimenti e quindi funziona solo se non sono presenti riferimenti in sospeso. Tuttavia, prima della chiamata alla funzione final_release statica descritta in precedenza in questo argomento, il conteggio dei riferimenti viene stabilizzato tramite l'impostazione su 1. Questa procedura viene indicata come antirimbalzo, termine preso in prestito dall'ingegneria elettrica. Si tratta di un'operazione fondamentale per evitare che il riferimento finale venga rilasciato. Una volta eseguita questa operazione, il conteggio dei riferimenti è instabile e non è in grado di supportare in modo affidabile una chiamata a QueryInterface.

È rischioso eseguire una chiamata a QueryInterface dopo il rilascio del riferimento finale, perché in teoria il conteggio dei riferimenti può aumentare a oltranza. È responsabilità dell'utente chiamare solo percorsi di codice noti che non prolungheranno il ciclo di vita dell'oggetto. C++/WinRT offre una soluzione di compromesso garantendo che le chiamate a QueryInterface vengano eseguite in modo affidabile.

Per ottenere questo risultato, stabilizza il conteggio dei riferimenti. Dopo il rilascio del riferimento finale, il conteggio dei riferimenti effettivo è pari a 0 o a un valore altamente imprevedibile. Il secondo caso può verificarsi se sono coinvolti riferimenti deboli. In entrambi i casi si tratta di un'operazione insostenibile se si verifica una chiamata successiva a QueryInterface, perché il conteggio dei riferimenti verrà necessariamente incrementato temporaneamente, da qui il riferimento all'antirimbalzo. L'impostazione di 1 consente di assicurarsi che in questo oggetto non si verifichi mai di nuovo una chiamata finale a Release. Questo è esattamente l'obiettivo da raggiungere, dal momento che std::unique_ptr ora ha la proprietà dell'oggetto, ma le chiamate con vincoli a coppie di QueryInterface/Release saranno sicure.

Prendi in considerazione 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 viene chiamata la funzione final_release, la quale notifica all'implementazione che è il momento di eseguire la pulizia. In questo caso, final_release è una coroutine. Per simulare un primo punto di sospensione, inizia rimanendo in attesa sul pool di thread per alcuni secondi. Riprende quindi il thread del dispatcher della pagina. L'ultimo passaggio prevede una query, poiché Dispatcher è una proprietà della classe di base DependencyObject. La pagina viene infine effettivamente eliminata grazie all'assegnazione di nullptr a std::unique_ptr, che a sua volta chiama il distruttore della pagina.

All'interno del distruttore viene cancellato il contesto dei dati che, come è noto, richiede una query per la classe di base FrameworkElement.

Tutto questo è possibile a causa dell'antirimbalzo del conteggio dei riferimenti (o della stabilizzazione del conteggio dei riferimenti) disponibile in C++/WinRT 2.0.

Hook di ingresso e uscita del metodo

Un punto di estensione usato meno di frequente è costituito dallo structabi_guard e dalle funzioni abi_entere abi_exit.

Se il tipo di implementazione definisce una funzione abi_enter, tale funzione viene chiamata con l'ingresso in tutti i metodi di interfaccia proiettati (senza contare i metodi di IInspectable).

Analogamente, se definisci abi_exit, tale funzione verrà chiamata all'uscita da tutti i metodi di questo tipo, ma non verrà chiamata se abi_enter genera un'eccezione. Verrà comunque chiamata se viene generata un'eccezione dal tuo metodo di interfaccia proiettato.

Potresti usare abi_enter per generare un'eccezioneinvalid_state_error ipotetica se un client tenta di usare un oggetto per il quale è stato impostato lo stato di inutilizzabile, ad esempio dopo una chiamata al metodoShut­Down oDisconnect. Le classi iteratore C++/WinRT usano questa funzionalità per generare un'eccezione di stato non valido nella funzioneabi_enter se la raccolta sottostante è cambiata.

Oltre alle semplici funzioni abi_enter e abi_exit, puoi definire un tipo nidificato denominatoabi_guard. In tal caso, viene creata un'istanza di abi_guard con l'ingresso in ciascun metodo di interfaccia proiettato (non IInspectable) con un riferimento all'oggetto come parametro costruttore. Il tipo abi_guard viene quindi distrutto all'uscita dal metodo. Puoi aggiungere qualsiasi stato supplementare al tipo abi_guard.

Se non definisci un tipo abi_guard, è presente un tipo predefinito che chiama abi_enter al momento della costruzione eabi_exit al momento della distruzione.

Queste protezioni vengono usate solo quando un metodo viene richiamato tramite l'interfaccia proiettata. Se richiami 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.
}