Tilläggspunkter för dina implementeringstyper

Strukturmallen winrt::implements är den bas som dina egna C++/WinRT-implementeringar (av körningsklasser och aktiveringsfabriker) direkt eller indirekt ärver från.

I det här avsnittet beskrivs tilläggspunkterna för winrt::implements i C++/WinRT 2.0. Du kan välja att implementera dessa tilläggspunkter på dina implementeringstyper för att anpassa standardbeteendet för inspekterade objekt (kan inspekteras i den mening som iInspectable-gränssnittet ).

Dessa utökningspunkter gör att du kan fördröja destruktionen av dina implementeringstyper, utföra frågor på ett säkert sätt under destruktionen och koppla in logik vid ingången till och utgången från dina projicerade metoder. Det här avsnittet beskriver dessa funktioner och förklarar mer om när och hur du skulle använda dem.

Fördröjd radering

I ämnet Diagnostisering av direkta allokeringar nämnde vi att din implementationstyp inte får ha en privat destruktor.

Fördelen med att ha en offentlig destruktor är att den möjliggör uppskjuten förstörelse, vilket är möjligheten att identifiera det slutliga IUnknown::Release-anropet på objektet och sedan ta ägarskapet för objektet för att skjuta upp dess förstörelse på obestämd tid.

Kom ihåg att klassiska COM-objekt i sig är referensräknade; referensantalet hanteras via funktionerna IUnknown::AddRef och IUnknown::Release. I en traditionell implementering av Release anropas ett klassiskt COM-objekts C++ destructor när referensantalet når 0.

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

delete this; anropar objektets destruktor innan minnet som upptas av objektet frigörs. Detta fungerar tillräckligt bra, förutsatt att du inte behöver göra något intressant i din destruktor.

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

Vad menar vi med intressant? För det första är en destruktor synkron. Du kan inte växla trådar – kanske för att förstöra vissa trådspecifika resurser i en annan kontext. Du kan inte pålitligt fråga objektet efter något annat gränssnitt som du kan behöva för att frigöra vissa resurser. Listan fortsätter. För de fall där din förstörelse inte är trivial behöver du en mer flexibel lösning. Där kommer C++/WinRT:s final_release funktion in.

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

Vi har uppdaterat C++/WinRT-implementeringen av Release för att anropa din final_release rätt när objektets referensantal övergår till 0. I det tillståndet kan objektet vara säkert på att det inte finns några ytterligare utestående referenser och att det nu har exklusivt ägarskap för sig självt. Därför kan den överföra ägarskapet för sig själv till den statiska final_release funktionen.

Med andra ord har objektet omvandlats från ett objekt som stöder delat ägarskap till ett som uteslutande ägs. Std::unique_ptr har exklusivt ägande av objektet, så det förstör naturligtvis objektet som en del av dess semantik – därav behovet av en offentlig destruktor – när std::unique_ptr går utanför omfånget (förutsatt att det inte flyttas någon annanstans innan dess). Och det är nyckeln. Du kan använda objektet på obestämd tid, förutsatt att std::unique_ptr håller objektet vid liv. Här är en illustration av hur du kan flytta objektet någon annanstans.

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

Den här koden sparar objektet i en samling med namnet batch_cleanup vars jobb är att rensa alla objekt någon gång i appens körningstid.

Normalt förstörs objektet när std::unique_ptr destructs, men du kan påskynda dess förstörelse genom att anropa std::unique_ptr::reset; eller så kan du skjuta upp det genom att spara std::unique_ptr någonstans.

Kanske mer praktiskt och kraftfullt kan du förvandla final_release-funktionen till en coroutine och hantera dess slutliga förstörelse på ett ställe samtidigt som du kan pausa och växla trådar efter behov.

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

En suspenderingspunkt gör att den anropande tråden, som ursprungligen initierade anropet till funktionen IUnknown::Release, återgår och därmed signalerar till anroparen att objektet som den tidigare refererade till inte längre är tillgängligt via den gränssnittspekaren. Gränssnittsramverk måste ofta se till att objekt förstörs i den specifika användargränssnittstråd som ursprungligen skapade objektet. Den här funktionen gör det enkelt att uppfylla ett sådant krav, eftersom förstörelsen är skild från att släppa objektet.

Observera att objektet som skickas till final_release bara är ett C++-objekt. det är inte längre ett COM-objekt. Till exempel löses inte längre befintliga svaga COM-referenser till objektet.

Säkra frågor under destruktion

Att bygga vidare på begreppet uppskjuten förstörelse är möjligheten att på ett säkert sätt fråga efter gränssnitt under förstörelsen.

Klassisk COM bygger på två centrala begrepp. Den första är referensräkning och den andra är att fråga efter gränssnitt. Förutom AddRef och Release tillhandahåller IUnknown-gränssnittetQueryInterface. Den metoden används i hög grad av vissa gränssnittsramverk, till exempel XAML, för att passera XAML-hierarkin när den simulerar sitt sammansättningsbara typsystem. Tänk dig ett enkelt exempel.

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

Det kan verka ofarligt. Den här XAML-sidan vill rensa sin datakontext i sin destruktor. Men DataContext är en egenskap för FrameworkElement-basklassen och den finns i det distinkta IFrameworkElement-gränssnittet . Därför måste C++/WinRT mata in ett anrop till QueryInterface för att leta upp rätt vtable innan du kan anropa egenskapen DataContext . Men anledningen till att vi ens befinner oss i destruktorn är att referensräkningen har gått över till 0. Att anropa QueryInterface här stöter tillfälligt på referensantalet. och när det igen återgår till 0 förstörs objektet igen.

C++/WinRT 2.0 har förstärkts för att stödja detta. Här är C++/WinRT 2.0-implementeringen av Release i ett förenklat format.

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

Som du kanske har förutsett minskar det först referensantalet och agerar sedan endast om det inte finns några utestående referenser. Men innan du anropar den statiska final_release funktion som vi beskrev tidigare i det här avsnittet stabiliseras referensantalet genom att den anges till 1. Detta kallas debouncing (med en term lånad från elektrotekniken). Detta är viktigt för att förhindra att den slutliga referensen släpps. När det händer är referensantalet instabilt och kan inte på ett tillförlitligt sätt stödja ett anrop till QueryInterface.

Det är farligt att anropa QueryInterface när den slutliga referensen har släppts, eftersom referensantalet kan växa på obestämd tid. Det är ditt ansvar att bara anropa kända kodsökvägar som inte förlänger objektets livslängd. C++/WinRT möter dig halvvägs genom att se till att dessa QueryInterface-anrop kan göras på ett tillförlitligt sätt.

Det gör den genom att stabilisera referensantalet. När den slutliga referensen har släppts är det faktiska referensantalet antingen 0 eller något mycket oförutsägbart värde. Det senare fallet kan inträffa om svaga referenser är inblandade. Hur som helst är detta ohållbart om ett efterföljande anrop till QueryInterface inträffar. eftersom det nödvändigtvis leder till att referensantalet ökar tillfälligt – därav hänvisningen till debouncing. Om du ställer in den på 1 ser du till att ett slutgiltigt anrop till Release aldrig mer inträffar på det här objektet. Det är precis vad vi vill, eftersom std::unique_ptr nu äger objektet, men begränsade anrop till QueryInterface/Release-par kommer att vara säkra.

Överväg ett mer intressant exempel.

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

Först anropas funktionen final_release och meddelar implementeringen att det är dags att rensa. Här råkar final_release vara en coroutine. Om du vill simulera en första upphängningspunkt börjar den med att vänta på trådpoolen i några sekunder. Den återupptas sedan på sidans dispatcher-kötråd. Det sista steget omfattar en fråga eftersom DispatcherQueue är tillgänglig från base-klassen DependencyObject . Slutligen raderas sidan faktiskt genom att tilldela nullptr till std::unique_ptr. Det anropar i sin tur sidans destruktor.

I destruktorn rensar vi datakontexten, vilket, som vi vet, kräver en fråga mot FrameworkElement-basklassen.

Allt detta är möjligt på grund av referensantalets debouncing (eller referensantalstabilisering) som tillhandahålls av C++/WinRT 2.0.

Krokar för metodinträde och metodavslut

En mindre vanlig tilläggspunkt är abi_guard struct och funktionerna abi_enter och abi_exit .

Om din implementeringstyp definierar en funktion abi_enter, anropas den när var och en av dina projicerade gränssnittsmetoder anropas (dock inte metoderna för IInspectable).

På samma sätt, om du definierar abi_exit anropas det vid avslutet från varje sådan metod. men det anropas inte om din abi_enter utlöser ett undantag. Det kommer fortfarande att anropas om ett undantag genereras av själva din projicerade gränssnittsmetod.

Du kan till exempel använda abi_enter för att utlösa ett hypotetiskt invalid_state_error undantag om en klient försöker använda ett objekt när objektet har försatts i ett oanvändbart tillstånd, till exempel efter ett shutdown - eller frånkopplingsmetodanrop . Iteratorklasserna C++/WinRT använder den här funktionen för att utlösa ett ogiltigt tillståndsfel i funktionen abi_enter om den underliggande samlingen har ändrats.

Utöver de enkla funktionerna abi_enter och abi_exitkan du definiera en kapslad typ med namnet abi_guard. I så fall skapas en instans av abi_guard när var och en av metoderna i ditt projicerade gränssnitt (som inte är IInspectable) anropas, med en referens till objektet som parameter till konstruktorn. Abi_guard förstörs sedan när metoden avslutas. Du kan lägga in vilket ytterligare tillstånd du vill i din abi_guard-typ.

Om du inte definierar dina egna abi_guard finns det en standardinställning som anropar abi_enter vid konstruktion och abi_exit vid förstörelse.

Dessa skydd används endast när en metod anropas via det projicerade gränssnittet. Om du anropar metoder direkt på implementeringsobjektet går dessa anrop direkt till implementeringen, utan några vakter.

Här är ett kodexempel.

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