Uitbreidingspunten voor uw implementatietypen

De winrt::implementeert de structsjabloon is de basis waaruit uw eigen C++/WinRT-implementaties (van runtimeklassen en activeringsfabrieken) direct of indirect worden afgeleid.

In dit onderwerp worden de extensiepunten van winrt::implementeert in C++/WinRT 2.0 besproken. U kunt ervoor kiezen om deze uitbreidingspunten op uw implementatietypen te implementeren om het standaardgedrag van inspecteerbare objecten aan te passen (inspecteerbaar in de zin van de IInspectable-interface ).

Met deze uitbreidingspunten kunt u het vernietigen van uw implementatietypen uitstellen, tijdens het vernietigen veilig opvragingen uitvoeren en hooks koppelen aan het binnengaan en verlaten van uw geprojecteerde methoden. In dit onderwerp worden deze functies beschreven en wordt meer uitgelegd over wanneer en hoe u deze zou gebruiken.

Uitgestelde vernietiging

In het onderwerp Directe toewijzingen diagnosticeren hebben we aangegeven dat uw implementatietype geen private destructor mag hebben.

Het voordeel van een openbare destructor is dat het uitstellen van vernietiging mogelijk maakt, wat de mogelijkheid is om de uiteindelijke IUnknown::Release-oproep op uw object te detecteren en vervolgens eigendom te nemen van dat object om de vernietiging voor onbepaalde tijd uit te stellen.

Zoals u weet, worden klassieke COM-objecten intrinsiek meegeteld; het aantal verwijzingen wordt beheerd via de functies IUnknown::AddRef en IUnknown::Release . In een traditionele implementatie van Release wordt de C++ destructor van een klassiek COM-object aangeroepen zodra het aantal verwijzingen 0 bereikt.

uint32_t WINRT_CALL Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        delete this;
    }
 
    return remaining;
}

delete this; roept de destructor van het object aan voordat het door het object ingenomen geheugen wordt vrijgegeven. Dit werkt goed genoeg, mits u niets interessants hoeft te doen in uw destructor.

using namespace winrt::Windows::Foundation;
... 
struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    ~Sample() noexcept
    {
        // Too late to do anything interesting.
    }
};

Wat bedoelen we met interessant? Voor één ding is een destructor inherent synchroon. U kunt geen threads overschakelen, misschien om bepaalde threadspecifieke resources in een andere context te vernietigen. U kunt het object niet betrouwbaar opvragen voor een andere interface die u mogelijk nodig hebt om bepaalde resources vrij te maken. De lijst gaat verder. Voor gevallen waarin uw vernietiging niet-triviaal is, hebt u een flexibelere oplossing nodig. Dit is waar de final_release functie van C++/WinRT binnenkomt.

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

We hebben de C++/WinRT-implementatie van Release bijgewerkt om uw final_release rechtstreeks aan te roepen wanneer het aantal referentiegegevens van uw object overgaat naar 0. In die toestand kan het object ervan uitgaan dat er geen verdere uitstaande referenties zijn en dat het nu het exclusieve eigendom over zichzelf heeft. Daarom kan het eigendom van zichzelf overdragen aan de statische final_release-functie .

Met andere woorden, het object heeft zichzelf getransformeerd van een object dat gedeeld eigendom ondersteunt in een eigendom dat exclusief eigendom is. De std::unique_ptr is de exclusieve eigenaar van het object en zal het object dan ook vanzelf vernietigen volgens zijn semantiek — vandaar de noodzaak van een publieke destructor — wanneer de std::unique_ptr buiten scope raakt (mits deze daarvoor niet naar elders wordt verplaatst). En dat is de sleutel. U kunt het object voor onbepaalde tijd gebruiken, mits de std::unique_ptr het object in leven houdt. Hier volgt een afbeelding van hoe u het object ergens anders kunt verplaatsen.

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

Met deze code wordt het object opgeslagen in een verzameling met de naam batch_cleanup een van de taken is om alle objecten op een later moment in de runtime van de app op te schonen.

Normaal gesproken wordt het object vernietigd wanneer de std::unique_ptr wordt vernietigd, maar u kunt de vernietiging ervan versnellen door std::unique_ptr::reset aan te roepen; of u kunt die uitstellen door de std::unique_ptr ergens te bewaren.

Misschien praktischer en krachtiger is dat u van de functie final_release een coroutine kunt maken en de uiteindelijke vernietiging op één plek kunt afhandelen, terwijl u de uitvoering zo nodig kunt onderbreken en van thread kunt wisselen.

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

Een opschortingspunt zorgt ervoor dat de aanroepende thread — die oorspronkelijk de aanroep naar de functie IUnknown::Release heeft gedaan — terugkeert en zo aan de aanroeper signaleert dat het object waarnaar deze eerder verwees, niet langer beschikbaar is via die interfacepointer. Ui-frameworks moeten er vaak voor zorgen dat objecten worden vernietigd op de specifieke UI-thread die oorspronkelijk het object heeft gemaakt. Deze functie maakt het voldoen aan een dergelijke vereiste triviaal, omdat vernietiging wordt gescheiden van het vrijgeven van het object.

Houd er rekening mee dat het object dat is doorgegeven aan final_release slechts een C++-object is; het is geen COM-object meer. Zo verwijzen bestaande zwakke COM-verwijzingen bijvoorbeeld niet langer naar het object.

Veilige opvragingen tijdens het vernietigen

Voortbouwend op het idee van uitgestelde vernietiging is de mogelijkheid om tijdens de vernietiging veilig query's uit te voeren op interfaces.

Klassieke COM is gebaseerd op twee centrale concepten. De eerste is het tellen van verwijzingen en de tweede is het uitvoeren van query's op interfaces. Naast AddRef en Release biedt de IUnknown-interfaceQueryInterface. Die methode wordt intensief gebruikt door bepaalde UI-frameworks, zoals XAML, om de XAML-hiërarchie te doorlopen bij het simuleren van zijn samenstelbare typesysteem. Bekijk een eenvoudig voorbeeld.

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

Dat kan ongevaarlijk lijken . Deze XAML-pagina probeert in zijn destructor de datacontext te wissen. Maar DataContext is een eigenschap van de frameworkElement-basisklasse en bevindt zich op de afzonderlijke interface van IFrameworkElement . Als gevolg hiervan moet C++/WinRT een aanroep naar QueryInterface injecteren om de juiste vtable op te zoeken voordat de eigenschap DataContext kan worden aangeroepen. Maar de reden dat we überhaupt in de destructor zitten, is dat de referentieteller op 0 is gekomen. Als u hier QueryInterface aanroept, wordt die referentietelling tijdelijk verhoogd; en wanneer die weer op 0 komt, wordt het object opnieuw vernietigd.

C++/WinRT 2.0 is robuuster gemaakt om dit te ondersteunen. Hier volgt de C++/WinRT 2.0-implementatie van Release, in een vereenvoudigde vorm.

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

Zoals u misschien al had voorspeld, verlaagt het eerst de referentietelling en onderneemt het vervolgens alleen actie als er geen uitstaande referenties meer zijn. Voordat u echter de statische final_release functie aanroept die we eerder in dit onderwerp hebben beschreven, wordt het aantal verwijzingen gestabiliseerd door deze in te stellen op 1. Dit noemen we debouncing (waarbij we een term uit de elektrotechniek lenen). Dit is essentieel om te voorkomen dat de definitieve referentie wordt vrijgegeven. Zodra dat gebeurt, is het aantal verwijzingen instabiel en kan een aanroep naar QueryInterface niet betrouwbaar worden ondersteund.

Het aanroepen van QueryInterface is gevaarlijk nadat de uiteindelijke verwijzing is vrijgegeven, omdat het aantal verwijzingen dan voor onbepaalde tijd kan groeien. Het is uw verantwoordelijkheid om alleen bekende codepaden aan te roepen die de levensduur van het object niet verlengen. C++/WinRT ontmoet u halverwege door ervoor te zorgen dat deze QueryInterface-aanroepen betrouwbaar kunnen worden uitgevoerd.

Dit doet u door het aantal verwijzingen te stabiliseren. Wanneer de laatste referentie is vrijgegeven, is het daadwerkelijke aantal referenties ofwel 0, of een volstrekt onvoorspelbare waarde. Het laatste geval kan optreden als er zwakke verwijzingen betrokken zijn. Hoe dan ook, dit is niet houdbaar als er een daaropvolgende aanroep van QueryInterface plaatsvindt, want daardoor neemt de referentietelling noodzakelijkerwijs tijdelijk toe — vandaar de verwijzing naar debouncing. Als u deze instelt op 1, zorgt u ervoor dat een definitieve aanroep naar Release nooit meer op dit object zal plaatsvinden. Dat is precies wat we willen, aangezien de std::unique_ptr nu eigenaar is van het object, maar gepaarde aanroepen van QueryInterface/Release veilig zullen zijn.

Bekijk een interessanter voorbeeld.

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->DispatcherQueue());
        ptr = nullptr;
    }
};

Eerst wordt de final_release-functie aangeroepen, waarbij de implementatie wordt geïnformeerd dat het tijd is om op te schonen. In dit geval is final_release een coroutine. Om een eerste onderbrekingspunt te simuleren, wordt eerst enkele seconden op de threadpool gewacht. Vervolgens wordt de uitvoering hervat op de dispatcherwachtrijthread van de pagina. Deze laatste stap omvat een query, omdat DispatcherQueue toegankelijk is vanuit de basisklasse DependencyObject . Ten slotte wordt de pagina daadwerkelijk verwijderd door nullptr aan de std::unique_ptr toe te wijzen. Dat roept op zijn beurt de destructor van de pagina aan.

In de destructor maken we de gegevenscontext leeg; wat, zoals we weten, een query op de FrameworkElement-basisklasse vereist.

Dit alles is mogelijk dankzij de debouncing van de referentietelling (of stabilisatie van de referentietelling) die door C++/WinRT 2.0 wordt geboden.

Methode-ingangs- en afsluithaken

Een minder veelgebruikt uitbreidingspunt is de abi_guard struct en de functies abi_enter en abi_exit .

Als uw implementatietype een functie abi_enter definieert, wordt die functie aangeroepen bij het aanroepen van al uw geprojecteerde interfacemethoden (IInspectable-methoden niet meegerekend).

Op dezelfde manier geldt: als u abi_exit definieert, dan wordt die aangeroepen bij het verlaten van elke dergelijke methode; maar die wordt niet aangeroepen als uw abi_enter een uitzondering opwerpt. Deze wordt nog steeds aangeroepen als er door de geprojecteerde interfacemethode zelf een uitzondering wordt opgeworpen.

U kunt bijvoorbeeld abi_enter gebruiken om een hypothetische invalid_state_error uitzondering te genereren als een client probeert een object te gebruiken nadat het object in een onbruikbare status is geplaatst, bijvoorbeeld na een aanroep van de methode ShutDown of Verbinding verbreken . De C++/WinRT-iteratorklassen gebruiken deze functie om een ongeldige status-uitzondering in de abi_enter-functie te genereren als de onderliggende verzameling is gewijzigd.

Boven de eenvoudige abi_enter - en abi_exit-functieskunt u een genest type met de naam abi_guard definiëren. In dat geval wordt bij het aanroepen van elk van uw geprojecteerde interfacemethoden die niet van het type IInspectable zijn, een exemplaar van abi_guard gemaakt, waarbij een verwijzing naar het object als constructorparameter wordt doorgegeven. De abi_guard wordt vervolgens vernietigd bij het verlaten van de methode. U kunt elke gewenste extra status in uw abi_guard type plaatsen.

Als u uw eigen abi_guard niet definieert, is er een standaard die abi_enter aanroept bij de bouw en abi_exit bij vernietiging.

Deze bewakers worden alleen gebruikt wanneer een methode wordt aangeroepen via de verwachte interface. Als u methoden rechtstreeks op het implementatieobject aanroept, gaan deze aanroepen rechtstreeks naar de implementatie, zonder bewakers.

Hier volgt een codevoorbeeld.

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