Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Den winrt::implements struktmall är basen från vilken din egen C++/WinRT implementering (av körningsklasser och aktiveringsfabriker) direkt eller indirekt härleds.
I det här avsnittet beskrivs tilläggspunkterna för winrt::implementerar i C++/WinRT 2.0. Du kan välja att implementera dessa tilläggspunkter i dina implementeringar för att anpassa standardbeteendet för inspekterbara objekt (inspekterbara i den mening som IInspectable-gränssnittet).
Med dessa tilläggspunkter kan du skjuta upp nedmontering av dina implementeringstyper, säkert fråga under nedmonteringen, och ansluta till in- och utgångar för dina projekterade metoder. Det här avsnittet beskriver dessa funktioner och förklarar mer om när och hur du skulle använda dem.
Uppskjuten förstörelse
I avsnittet Diagnostisera direkta allokeringar nämnde vi att implementeringstypen inte kan ha en privat destruktör.
Fördelen med att ha en offentlig destruktor är att den möjliggör uppskjuten förstörelse, vilket är möjligheten att upptäcka det slutliga IUnknown::Release-anropet till ditt objekt och sedan överta ägarskapet av objektet för att skjuta upp dess förstörelse på obestämd tid.
Kom ihåg att klassiska COM-objekt är i sig referensberäknade; referensräkningen hanteras via funktionerna IUnknown::AddRef och IUnknown::Release. I en traditionell implementering av Releaseanropas 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;
}
Den delete this;
anropar objektets destruktor innan det frigör det minne som objektet upptas av. Detta fungerar tillräckligt bra, förutsatt att du inte behöver göra något intressant i din destructor.
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å ett tillförlitligt sätt fråga objektet om ett 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 direkt 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. Den 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 före det). 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 förstörs, 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.
På ett kanske mer praktiskt och kraftfullt sätt kan du omvandla final_release-funktionen till en coroutine och hantera dess slutliga avveckling på ett ställe, samtidigt som du kan pausa och byta 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 avbrottspunkt orsakar att den anropande tråden – som ursprungligen initierade anropet till IUnknown::Release-funktionen – returnerar, vilket därmed signalerar till anroparen att objektet som den en gång höll inte längre är tillgängligt via denna gränssnittpekare. 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 gränssnittet IUnknown funktionen QueryInterface. 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 framstå som ofarligt. Den här XAML-sidan vill rensa sin datakontext i destruktorn. Men DataContext är en egenskap för basklassen FrameworkElement, och den lever på 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 är i destruktorn är att referensräkningen har övergått 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. Vi kallar detta debouncing (lånar en term från elektronikteknik). 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 sker; eftersom det nödvändigtvis leder till att referensräkningen ökar tillfälligt – därav konceptet med 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->Dispatcher());
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-tråd. Det sista steget omfattar en fråga eftersom Dispatcher är en egenskap för DependencyObject basklass. Slutligen tas faktiskt sidan bort genom att tilldela nullptr
till std::unique_ptr . Det i sin tur kallar sidans destructor.
I destruktorn rensar vi datakontexten, som vi vet kräver en fråga efter basklassen FrameworkElement.
Allt detta är möjligt tack vare referensräkningsavstämning (eller referensräkningsstabilisering) som tillhandahålls av C++/WinRT 2.0.
Metodinmatnings- och utgångskrokar
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 funktionen vid anropet av varje projicerad gränssnittsmetod (med undantag för metoderna hos 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 fortfarande anropas om ett undantag utlöses av själva den planerade gränssnittsmetoden.
Du kan till exempel använda abi_enter för att kasta 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 Disconnect metodanrop. 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 vid inträde i varje (icke-IInspectable) av dina projekterade gränssnittsmetoder, med en referens till objektet som konstruktorparameter. abi_guard förstörs sedan när metoden avslutas. Du kan ange vilket extra tillstånd du vill i din abi_guard typ.
Om du inte definierar din egen abi_guardfinns 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.
}