Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Le modèle de structure winrt::implements est la base dont dérivent, directement ou indirectement, vos propres implémentations C++/WinRT (de classes runtime et de fabriques d’activation).
Cette rubrique décrit les points d’extension de winrt ::implémente en C++/WinRT 2.0. Vous pouvez choisir d’implémenter ces points d’extension sur vos types d’implémentation afin de personnaliser le comportement par défaut des objets inspectables (inspectable au sens de l’interface IInspectable ).
Ces points d’extension vous permettent de différer la destruction de vos types d’implémentation, d’interroger en toute sécurité pendant la destruction et de connecter l’entrée et la sortie de vos méthodes projetées. Cette rubrique décrit ces fonctionnalités et explique plus en détail quand et comment vous les utiliseriez.
Destruction différée
Dans la rubrique Diagnostic des allocations directes , nous avons mentionné que votre type d’implémentation ne peut pas avoir de destructeur privé.
L’avantage d’avoir un destructeur public est qu’il permet la destruction différée, qui est la possibilité de détecter l’appel IUnknown ::Release final sur votre objet, puis de prendre possession de cet objet pour différer indéfiniment sa destruction.
Rappelez-vous que les objets COM classiques sont des références intrinsèques comptées ; le nombre de références est géré via les fonctions IUnknown ::AddRef et IUnknown ::Release . Dans une implémentation traditionnelle de Release, le destructeur C++ d’un objet COM classique est appelé une fois le nombre de références atteint 0.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
Le delete this; appelle le destructeur de l’objet avant de libérer la mémoire occupée par l’objet. Cela fonctionne assez bien, à condition que vous n’ayez pas besoin de faire quelque chose d’intéressant dans votre destructeur.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
Qu’est-ce que nous entendons par intéressant ? D'une part, un destructeur est, par nature, synchrone. Vous ne pouvez pas changer de threads, peut-être pour détruire certaines ressources spécifiques aux threads dans un contexte différent. Vous ne pouvez pas interroger de manière fiable l’objet pour une autre interface dont vous aurez peut-être besoin pour libérer certaines ressources. La liste se poursuit. Dans les cas où votre destruction n’est pas triviale, vous avez besoin d’une solution plus flexible. C’est là qu’intervient la fonction final_release de 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.
}
};
Nous avons mis à jour l’implémentation C++/WinRT de Release pour appeler votre final_release juste lorsque le nombre de références de votre objet passe à 0. Dans cet état, l’objet peut être sûr qu’il n’y a pas d’autres références en suspens et qu’il a désormais la propriété exclusive de lui-même. Pour cette raison, il peut transférer sa propre propriété à la fonction statique final_release.
En d’autres termes, l’objet s’est transformé d’un objet qui prend en charge la propriété partagée en une propriété exclusive. Le std ::unique_ptr a la propriété exclusive de l’objet, et il détruit donc naturellement l’objet dans le cadre de sa sémantique, donc la nécessité d’un destructeur public , lorsque le std ::unique_ptr sort de portée (à condition qu’il ne soit pas déplacé ailleurs avant cela). Et c’est la clé. Vous pouvez utiliser l’objet indéfiniment, à condition que le std ::unique_ptr conserve l’objet actif. Voici une illustration de la façon dont vous pouvez déplacer l’objet ailleurs.
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));
}
};
Ce code stocke l’objet dans une collection nommée batch_cleanup, dont l’un des rôles sera de supprimer tous les objets à un moment ultérieur de l’exécution de l’application.
Normalement, l’objet est détruit lorsque le std::unique_ptr est détruit, mais vous pouvez hâter sa destruction en appelant std::unique_ptr::reset ; ou vous pouvez la différer en conservant le std::unique_ptr quelque part.
Peut-être plus concrètement et plus puissantement, vous pouvez transformer la fonction final_release en coroutine et gérer sa destruction éventuelle en un seul endroit tout en étant en mesure de suspendre et de changer de threads en fonction des besoins.
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.
}
};
Une suspension entraîne le retour du thread appelant, qui a initialement lancé l’appel à la fonction IUnknown ::Release , et signale donc à l’appelant que l’objet qu’il a conservé n’est plus disponible via ce pointeur d’interface. Les frameworks d’interface utilisateur doivent souvent s’assurer que les objets sont détruits sur le thread d’interface utilisateur spécifique qui a créé l’objet à l’origine. Cette fonctionnalité rend la réalisation d’une telle exigence triviale, car la destruction est séparée de la libération de l’objet.
Notez que l’objet passé à final_release est simplement un objet C++ ; il n’est plus un objet COM. Par exemple, les références faibles COM existantes à l’objet ne sont plus résolues.
Requêtes sécurisées lors de la destruction
En s’appuyant sur la notion de destruction différée, il est possible d’interroger en toute sécurité les interfaces pendant la destruction.
COM classique est basé sur deux concepts centraux. La première est le comptage de références et la seconde interroge les interfaces. Outre AddRef et Release, l’interface IUnknown fournit QueryInterface. Cette méthode est fortement utilisée par certaines infrastructures d’interface utilisateur, telles que XAML, pour parcourir la hiérarchie XAML, car elle simule son système de type composable. Prenons un exemple simple.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Cela peut sembler inoffensif. Cette page XAML souhaite effacer son contexte de données dans son destructeur. Toutefois , DataContext est une propriété de la classe de base FrameworkElement , qui se trouve sur l’interface IFrameworkElement distincte. Par conséquent, C++/WinRT doit injecter un appel à QueryInterface pour trouver la table virtuelle correcte avant de pouvoir appeler la propriété DataContext. Mais la raison pour laquelle nous sommes même dans le destructeur est que le nombre de références a passé à 0. L’appel de QueryInterface ici se heurte temporairement au nombre de références ; et lorsqu’il revient à 0, l’objet se décompose à nouveau.
C++/WinRT 2.0 a été renforcé pour prendre en charge cela. Voici l’implémentation C++/WinRT 2.0 de Release, sous forme simplifiée.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
Comme vous l’avez peut-être prédit, il décrémente d’abord le nombre de références, puis agit uniquement s’il n’y a pas de références en attente. Toutefois, avant d’appeler la fonction de final_release statique que nous avons décrite précédemment dans cette rubrique, elle stabilise le nombre de références en le définissant sur 1. Nous appelons cela l’anti-rebond (par emprunt à un terme de l’électrotechnique). Ceci est essentiel pour empêcher la diffusion de la référence finale. Une fois cela arrivé, le nombre de références est instable et n’est pas en mesure de prendre en charge de manière fiable un appel à QueryInterface.
L’appel de QueryInterface est dangereux après la publication de la référence finale, car le nombre de références peut alors croître indéfiniment. Il vous incombe d’appeler uniquement les chemins de code connus qui ne prolongeront pas la durée de vie de l’objet. C++/WinRT vous répond à mi-chemin en vous assurant que ces appels QueryInterface peuvent être effectués de manière fiable.
Elle le fait en stabilisant le nombre de références. Lorsque la référence finale a été publiée, le nombre réel de références est soit 0, soit une valeur imprévisible. Ce dernier cas peut se produire si des références faibles sont impliquées. Dans tous les cas, cette situation n’est pas tenable si un appel ultérieur à QueryInterface a lieu, car cela provoquera nécessairement une augmentation temporaire du nombre de références — d’où la référence à l’anti-rebond. La définition de la valeur 1 garantit qu’un appel final à Release ne se produira plus jamais sur cet objet. C’est précisément ce que nous voulons, puisque le std ::unique_ptr possède désormais l’objet, mais les appels limités aux paires QueryInterface/Release seront sécurisés.
Prenons un exemple plus intéressant.
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;
}
};
Tout d’abord, la fonction final_release est appelée, en informant l’implémentation qu’il est temps de nettoyer. Ici, final_release est une coroutine. Pour simuler un premier point de suspension, il commence par attendre le pool de threads pendant quelques secondes. Il reprend ensuite sur le thread de file d’attente du répartiteur de la page. Cette dernière étape implique une requête, car DispatcherQueue est accessible à partir de la classe de base DependencyObject . Enfin, la page est réellement supprimée en vertu de l’affectation nullptr au std ::unique_ptr. Cela déclenche ensuite le destructeur de la page.
Dans le destructeur, nous effaçons le contexte de données, ce qui, comme nous le savons, nécessite une requête sur la classe de base FrameworkElement.
Tout cela est possible grâce à la temporisation du comptage de références (ou à sa stabilisation) fournie par C++/WinRT 2.0.
Points d’entrée et de sortie de méthode
Un point d’extension moins couramment utilisé est le struct abi_guard , et les fonctions abi_enter et abi_exit .
Si votre type d’implémentation définit une fonction abi_enter, cette fonction est appelée à l’entrée de chacune de vos méthodes d’interface projetées (sans compter les méthodes d’IInspectable).
De même, si vous définissez abi_exit, cela sera appelé à la sortie de chaque méthode de ce type ; mais elle ne sera pas appelée si votre abi_enter lève une exception. Elle sera toujours appelée si une exception est levée par votre méthode d’interface projetée elle-même.
Par exemple, vous pouvez utiliser abi_enter pour lever une exception hypothétique invalid_state_error si un client tente d’utiliser un objet après que celui-ci a été placé dans un état inutilisable, par exemple après l’appel d’une méthode ShutDown ou Disconnect. Les classes d’itérateur C++/WinRT utilisent cette fonctionnalité pour lever une exception d’état non valide dans la fonction abi_enter si la collection sous-jacente a changé.
Au-delà des fonctions abi_enter et abi_exitsimples, vous pouvez définir un type imbriqué nommé abi_guard. Dans ce cas, une instance de abi_guard est créée lors de l’entrée dans chacune des méthodes (non-IInspectable) de votre interface projetée, en prenant comme paramètre de constructeur une référence à l’objet. La abi_guard est ensuite déstructurée à la sortie de la méthode. Vous pouvez mettre dans votre type abi_guard tout état supplémentaire que vous voulez.
Si vous ne définissez pas votre propre abi_guard, il en existe un par défaut qui appelle abi_enter lors de la construction et abi_exit lors de la destruction.
Ces gardes sont utilisés uniquement lorsqu’une méthode est appelée via l’interface projetée. Si vous appelez des méthodes directement sur l’objet d’implémentation, ces appels vont directement à l’implémentation, sans aucun garde.
Voici un exemple de code.
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.
}
Windows developer