QueryInterface : navigation dans un objet

Tip

Modèles QueryInterface modernes : utilisation IID_PPV_ARGS et pointeurs intelligents. Les appels bruts QueryInterface sont sujettes à des erreurs (type IID et pointeur incompatibles). Le code C++ moderne doit utiliser des helpers de type sécurisé :

#include <wrl/client.h>  // Microsoft::WRL::ComPtr

Microsoft::WRL::ComPtr<IUnknown> unknown = /* ... */;
Microsoft::WRL::ComPtr<IPersistFile> persistFile;

// ✅ Best — ComPtr::As() handles QI + type safety + Release automatically
HRESULT hr = unknown.As(&persistFile);

// ✅ Good — IID_PPV_ARGS macro ensures IID matches the pointer type
hr = unknown->QueryInterface(IID_PPV_ARGS(&persistFile));

// ❌ Dangerous — IID and pointer type can mismatch silently
hr = unknown->QueryInterface(IID_IPersistFile, (void**)&persistFile);

Équivalent C++/WinRT :

#include <winrt/base.h>

winrt::com_ptr<IUnknown> unknown = /* ... */;
auto persistFile = unknown.as<IPersistFile>();      // throws on failure
auto maybePF = unknown.try_as<IPersistFile>();      // returns nullptr on failure

Règles clés : ne jamais convertir les pointeurs d’interface sans QueryInterface : les règles d’identité COM nécessitent que chaque pointeur d’interface soit obtenu via QI ou CoCreateInstance. Les casts directs (static_cast, reinterpret_cast) produisent un comportement non défini.

Une fois que vous avez un pointeur initial vers une interface sur un objet, COM dispose d’un mécanisme très simple pour déterminer si l’objet prend en charge une autre interface spécifique et, le cas échéant, pour obtenir un pointeur vers celui-ci. (Pour plus d’informations sur l’obtention d’un pointeur initial vers une interface sur un objet, consultez Obtention d’un pointeur vers un objet.) Ce mécanisme est la méthode QueryInterface de l’interface IUnknown . Si l’objet prend en charge l’interface demandée, la méthode doit retourner un pointeur vers cette interface. Cela permet à un objet de naviguer librement dans les interfaces prises en charge par un objet. QueryInterface sépare la demande « Prenez-vous en charge un contrat donné ? » par rapport à l’utilisation haute performance de ce contrat une fois les négociations réussies.

Lorsqu’un client obtient initialement l’accès à un objet, ce client reçoit, au minimum, un pointeur d’interface IUnknown (l’interface la plus fondamentale) par lequel il peut contrôler la durée de vie de l’objet, en indiquant à l’objet quand il est effectué à l’aide de l’objet, et appeler QueryInterface. Le client est programmé pour demander à chaque objet qu’il gère d’effectuer certaines opérations, mais l’interface IUnknown n’a aucune fonction pour ces opérations. Au lieu de cela, ces opérations sont exprimées via d’autres interfaces. Le client est donc programmé pour négocier avec des objets pour ces interfaces. Plus précisément, le client appelle QueryInterface pour demander à un objet une interface via laquelle le client peut appeler les opérations souhaitées.

Étant donné que l’objet implémente QueryInterface, il a la possibilité d’accepter ou de rejeter la requête. Si l’objet accepte la requête du client, QueryInterface retourne un nouveau pointeur vers l’interface demandée vers le client. Par le biais de ce pointeur d’interface, le client a accès aux méthodes de cette interface. Si, d’autre part, l’objet rejette la requête du client, QueryInterface retourne un pointeur Null ( une erreur) et le client n’a pas de pointeur à travers lequel appeler les fonctions souhaitées. Dans ce cas, le client doit traiter avec grâce cette possibilité. Par exemple, supposons qu’un client a un pointeur vers l’interface A sur un objet et demande des interfaces B et C. Supposons également que l’objet prend en charge l’interface B, mais ne prend pas en charge l’interface C. Le résultat est que l’objet retourne un pointeur vers B et signale que C n’est pas pris en charge.

Un point clé est que lorsqu’un objet rejette un appel à QueryInterface, il est impossible pour le client de demander à l’objet d’effectuer les opérations exprimées via l’interface demandée. Un client doit avoir un pointeur d’interface pour appeler des méthodes dans cette interface. Si l’objet refuse de fournir le pointeur demandé, le client doit être prêt à le faire sans, soit en n’effectuant pas ce qu’il avait prévu de faire avec cet objet ou en tentant de revenir sur un autre, peut-être moins puissant, interface. Cet aspect du fonctionnement de COM présente des avantages par rapport à d’autres systèmes orientés objet, dans lesquels il est impossible de savoir si une fonction marchera avant de l’appeler, et même alors, la gestion de l’échec reste incertaine. QueryInterface fournit un moyen fiable et cohérent de savoir si un objet prend en charge une interface avant de tenter d’appeler ses méthodes.

La méthode QueryInterface fournit également un moyen robuste et fiable pour un objet d’indiquer qu’il ne prend pas en charge un contrat donné. Autrement dit, si, dans un appel à QueryInterface , on demande à un objet « ancien » s’il prend en charge une interface « nouvelle » (un, par exemple, qui a été inventé après que l’ancien objet a été expédié), l’ancien objet sera fiable, sans provoquer de blocage, répondre « non ». La technologie qui le prend en charge est l’algorithme par lequel les ID IID sont alloués. Bien que cela puisse sembler un petit point, il est extrêmement important pour l’architecture globale du système, et la possibilité d’interroger les éléments hérités sur les nouvelles fonctionnalités est, étonnamment, une fonctionnalité qui n’est pas présente dans la plupart des autres architectures d’objets.

Utilisation et implémentation d’IUnknown