Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Die winrt::implements struct template ist die Basis, von der aus Ihre eigenen C++/WinRT Implementierungen (von Laufzeitklassen und Aktivierungsfabriken) direkt oder indirekt abgeleitet werden.
In diesem Thema werden die Erweiterungspunkte von winrt::implements in C++/WinRT 2.0 erläutert. Sie können diese Erweiterungspunkte in Ihren Implementierungstypen implementieren, um das Standardverhalten von prüfbaren Objekten anzupassen (prüfbaren im Sinne der IInspectable Schnittstelle).
Diese Erweiterungspunkte ermöglichen es Ihnen, die Zerstörung Ihrer Implementierungstypen hinauszuzögern, während der Zerstörung sicher Abfragen durchzuführen und den Einstieg in und Ausstieg aus Ihren projizierten Methoden einzuhängen. In diesem Thema werden diese Features beschrieben und mehr darüber erläutert, wann und wie Sie sie verwenden würden.
Verzögerte Zerstörung
In dem Thema Diagnose von direkten Zuordnungen wurde erwähnt, dass Ihr Implementierungstyp keinen privaten Destruktor haben kann.
Der Vorteil eines öffentlichen Destruktors besteht darin, dass er eine verzögerte Zerstörung ermöglicht, womit die Möglichkeit besteht, den endgültigen IUnknown::Release Aufruf Ihres Objekts zu erkennen, und dann den Besitz dieses Objekts zu übernehmen, um seine Zerstörung unbegrenzt zu verzögern.
Erinnern Sie sich daran, dass klassische COM-Objekte systemintern referenziert werden; die Referenzanzahl wird über die Funktionen "IUnknown::AddRef " und "IUnknown::Release " verwaltet. In einer herkömmlichen Implementierung von Release wird der C++-Destruktor eines klassischen COM-Objekts aufgerufen, sobald die Referenzanzahl 0 erreicht.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
Die delete this; ruft den Destruktor des Objekts auf, bevor der vom Objekt belegte Speicher freigegeben wird. Vorausgesetzt, Sie müssen nichts Besonderes in Ihrem Destruktor tun, funktioniert dies gut genug.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
Was meinen wir mit interessant? Zum einen ist ein Destruktor inhärent synchron. Sie können keine Threads wechseln – vielleicht um einige threadspezifische Ressourcen in einem anderen Kontext zu zerstören. Sie können das Objekt nicht zuverlässig nach einer anderen Schnittstelle abfragen, die Sie möglicherweise benötigen, um bestimmte Ressourcen freizugeben. Die Liste geht weiter. Für Fälle, in denen Ihre Zerstörung nicht trivial ist, benötigen Sie eine flexiblere Lösung. Hier kommt die final_release-Funktion von C++/WinRT ins Spiel.
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.
}
};
Wir haben die C++/WinRT-Implementierung von Release- aktualisiert, um Ihre final_release direkt aufzurufen, sobald die Referenzanzahl Ihres Objekts auf 0 wechselt. In diesem Zustand kann das Objekt sicher sein, dass es keine weiteren offenen Verweise gibt, und es hat jetzt ausschließliches Eigentum an sich. Aus diesem Grund kann sie den Besitz von sich selbst an die statische final_release Funktion übertragen.
Mit anderen Worten: Das Objekt hat sich von einem, das gemeinsamen Besitz unterstützt, in ein Objekt transformiert, das sich im ausschließlichen Besitz befindet. Die std::unique_ptr besitzt ausschließlichen Besitz des Objekts, sodass das Objekt natürlich als Teil der Semantik zerstört wird – daher die Notwendigkeit eines öffentlichen Destruktors – wenn der std::unique_ptr den Gültigkeitsbereich überschreitet (vorausgesetzt, dass es nicht an anderer Stelle verschoben wird). Und das ist der Schlüssel. Sie können das Objekt unbegrenzt verwenden, vorausgesetzt, dass das std::unique_ptr das Objekt lebendig hält. Hier ist eine Abbildung, wie Sie das Objekt an eine andere Stelle verschieben können.
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));
}
};
Dieser Code speichert das Objekt in einer Sammlung mit dem Namen batch_cleanup, deren Aufgabe es ist, alle Objekte zu einem späteren Zeitpunkt während der Laufzeit der App zu bereinigen.
Normalerweise wird das Objekt zerstört, wenn die std::unique_ptr zerstört wird, aber Sie können die Zerstörung beschleunigen, indem Sie std::unique_ptr::resetaufrufen, oder Sie können sie verschieben, indem Sie die std::unique_ptr irgendwo speichern.
Vielleicht praktischer und mächtiger können Sie die final_release Funktion in eine Coroutine umwandeln und ihre endgültige Zerstörung an einer Stelle verwalten, während Sie bei Bedarf Threads anhalten und wechseln können.
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.
}
};
Ein Anhaltepunkt bewirkt, dass der aufrufende Thread , der ursprünglich den Aufruf der IUnknown::Release--Funktion initiiert hat, zurückgibt und somit dem Aufrufer signalisiert, dass das objekt, das es einmal gehalten hat, nicht mehr über diesen Schnittstellenzeiger verfügbar ist. Benutzeroberflächenframeworks müssen häufig sicherstellen, dass Objekte im spezifischen UI-Thread zerstört werden, der das Objekt ursprünglich erstellt hat. Diese Funktion macht das Erfüllen einer solchen Anforderung einfach, da die Zerstörung von der Freigabe des Objekts getrennt ist.
Beachten Sie, dass das an final_release übergebene Objekt lediglich ein C++-Objekt ist; es ist kein COM-Objekt mehr. Das bedeutet beispielsweise, dass vorhandene COM-schwache Verweise auf das Objekt nicht mehr aufgelöst werden.
Sichere Abfragen während der Zerstörung
Aufbauend auf dem Konzept der verzögerten Zerstörung ist die Möglichkeit, Schnittstellen während des Zerstörungsprozesses sicher abzufragen.
Klassische COM basiert auf zwei zentralen Konzepten. Die erste Methode ist die Verweiszählung, und die zweite Methode ist das Abfragen von Schnittstellen. Zusätzlich zu AddRef und Releasebietet die IUnknown Schnittstelle QueryInterface. Diese Methode wird stark von bestimmten UI-Frameworks verwendet, wie zum Beispiel XAML, um die XAML-Hierarchie zu durchlaufen und dabei ihr zusammensetzbares Typsystem zu simulieren. Betrachten Sie ein einfaches Beispiel.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Das kann harmlos erscheinen. Diese XAML-Seite möchte in ihrem Destruktor den Datenkontext löschen. DataContext- ist jedoch eine Eigenschaft der FrameworkElement- Basisklasse und lebt auf der eindeutigen IFrameworkElement- Schnittstelle. Daher muss C++/WinRT einen Aufruf QueryInterface- einfügen, um die richtige vtable nachzuschlagen, bevor die DataContext- eigenschaft aufgerufen werden kann. Aber der Grund, warum wir überhaupt im Destruktor sind, ist, dass der Referenzzähler auf 0 gefallen ist. Das Aufrufen von QueryInterface- erhöht hier vorübergehend die Referenzanzahl; und wenn sie wieder 0 erreicht, wird das Objekt erneut zerstört.
C++/WinRT 2.0 wurde gehärtet, um dies zu unterstützen. Dies ist die C++/WinRT 2.0-Implementierung von Release in vereinfachter Form.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
Wie Sie vielleicht vorhergesagt haben, wird zuerst die Referenzanzahl verringert und dann nur gehandelt, wenn keine ausstehenden Verweise vorhanden sind. Bevor jedoch die statische final_release-Funktion aufgerufen wird, die wir weiter oben in diesem Thema beschrieben haben, wird die Referenzanzahl stabilisiert, indem sie auf 1 gesetzt wird. Wir bezeichnen dies als entprenkende (Anleihe eines Begriffs aus der Elektrotechnik). Dies ist entscheidend, um zu verhindern, dass die endgültige Referenz freigegeben wird. In diesem Fall ist die Referenzanzahl instabil und kann einen Aufruf von QueryInterface-nicht zuverlässig unterstützen.
Das Aufrufen QueryInterface ist gefährlich, nachdem die endgültige Referenz freigegeben wurde, da die Referenzanzahl dann unbegrenzt wachsen kann. Es liegt in Ihrer Verantwortung, nur bekannte Codepfade aufzurufen, die die Lebensdauer des Objekts nicht verlängern. C++/WinRT kommt Ihnen entgegen, indem es sicherstellt, dass diese QueryInterface--Aufrufe zuverlässig ausgeführt werden können.
Es macht dies, indem es die Referenzanzahl stabilisiert. Wenn der endgültige Bezug freigegeben wurde, ist die tatsächliche Bezugsanzahl entweder 0 oder ein unvorhergesehener Wert. Letzteres kann auftreten, wenn schwache Verweise beteiligt sind. So oder so ist dies nicht nachhaltig, wenn ein nachfolgender Aufruf von QueryInterface auftritt; da dies notwendigerweise dazu führt, dass die Referenzanzahl vorübergehend erhöht wird – daher der Bezug auf das Entprellen. Durch das Setzen auf 1 wird sichergestellt, dass ein letzter Aufruf von Release auf diesem Objekt nie wieder erfolgt. Das ist genau das, was wir wollen, da die std::unique_ptr jetzt das Objekt besitzt, aber gebundene Aufrufe an QueryInterface/Release Paaren sicher sind.
Betrachten Sie ein interessanteres Beispiel.
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;
}
};
Zuerst wird die Funktion final_release aufgerufen, um der Implementierung mitzuteilen, dass es Zeit für die Bereinigung ist. Es stellt sich heraus, dass final_release eine Coroutine ist. Um einen ersten Unterbrechungspunkt zu simulieren, wartet er einige Sekunden im Threadpool. Anschließend wird es auf dem Dispatcher-Thread der Seite fortgesetzt. Der letzte Schritt beinhaltet eine Abfrage, da Dispatcher eine Eigenschaft der Basisklasse DependencyObject ist. Schließlich wird die Seite tatsächlich durch das Zuweisen von nullptr zum std::unique_ptrgelöscht. Dies wiederum ruft den Destruktor der Seite auf.
Innerhalb des Destruktors leeren wir den Datenkontext, was, wie wir wissen, die Abfrage für die FrameworkElement- Basisklasse erfordert.
All dies ist möglich, weil die Entprellung der Referenzanzahl (oder Referenzzähler-Stabilisierung) von C++/WinRT 2.0 bereitgestellt wird.
Methodeneingabe- und Exit-Hooks
Ein weniger häufig verwendeter Erweiterungspunkt ist die Struktur abi_guard, sowie die Funktionen abi_enter und abi_exit.
Wenn Ihr Implementierungstyp eine Funktion abi_enterdefiniert, wird diese Funktion beim Eintritt in jede der projizierten Schnittstellenmethoden aufgerufen, ausgenommen die Methoden von IInspectable.
Wenn Sie abi_exitdefinieren, wird es beim Beenden jeder solchen Methode aufgerufen; es wird jedoch nicht aufgerufen, wenn Ihre abi_enter eine Ausnahme auslöst. Es wird weiterhin aufgerufen, wenn eine Ausnahme von der projizierten Schnittstellenmethode selbst ausgelöst wird.
Als Beispiel können Sie abi_enter verwenden, um eine hypothetische invalid_state_error Ausnahme auszulösen, wenn ein Client versucht, ein Objekt zu verwenden, nachdem das Objekt in einen nicht verwendbaren Zustand versetzt wurde , z. B. nach einem ShutDown- oder Disconnect Methodenaufrufs. Die C++/WinRT-Iteratorklassen verwenden dieses Feature, um eine ungültige Zustandsausnahme in der abi_enter-Funktion auszulösen, wenn sich die zugrunde liegende Auflistung geändert hat.
Zusätzlich zu den einfachen abi_enter- und abi_exit-Funktionen können Sie einen geschachtelten Typ namens abi_guarddefinieren. In diesem Fall wird eine Instanz von abi_guard für den Eintrag zu jedem (nichtIInspectable) Ihrer projizierten Schnittstellenmethoden erstellt, wobei ein Verweis auf das Objekt als Konstruktorparameter vorhanden ist. Der abi_guard wird dann beim Beenden der Methode destruktiert. Sie können jeden zusätzlichen Status, den Sie möchten, in Ihren abi_guard Typ einfügen.
Wenn Sie Ihre eigene abi_guardnicht definieren, gibt es eine Standardimplementierung, die abi_enter bei der Erstellung aufruft und abi_exit bei der Zerstörung.
Diese Schutzvorrichtungen werden nur verwendet, wenn eine Methode über die projizierte Schnittstelleaufgerufen wird. Wenn Sie Methoden direkt auf das Implementierungsobjekt anwenden, gehen diese Aufrufe ohne Schutzmaßnahmen direkt an die Implementierung.
Hier ist ein Codebeispiel.
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.
}