Partager via


Programmation de DirectX avec COM

Microsoft Component Object Model (COM) est un modèle de programmation orienté objet utilisé par plusieurs technologies, y compris la majeure partie de la surface d’API DirectX. Pour cette raison, vous (en tant que développeur DirectX) utilisez inévitablement COM lorsque vous programmez DirectX.

Note

La rubrique Consommer des composants COM avec C++/WinRT montre comment consommer des API DirectX (et toute API COM, à ce sujet) à l’aide de C++/WinRT. C’est de loin la technologie la plus pratique et recommandée.

Vous pouvez également utiliser COM brut, et c’est ce que cette rubrique concerne. Vous aurez besoin d’une compréhension de base des principes et des techniques de programmation impliqués dans la consommation d’API COM. Bien que COM ait la réputation d’être difficile et complexe, la programmation COM requise par la plupart des applications DirectX est simple. En partie, cela est dû au fait que vous consommerez les objets COM fournis par DirectX. Il n’est pas nécessaire de créer vos propres objets COM, ce qui est généralement l’endroit où se produit la complexité.

Vue d’ensemble des composants COM

Un objet COM est essentiellement un composant encapsulé de fonctionnalités qui peut être utilisé par les applications pour effectuer une ou plusieurs tâches. Pour le déploiement, un ou plusieurs composants COM sont empaquetés dans un fichier binaire appelé serveur COM ; plus souvent qu’une DLL.

Une DLL traditionnelle exporte des fonctions gratuites. Un serveur COM peut faire de même. Toutefois, les composants COM à l’intérieur du serveur COM exposent des interfaces COM et des méthodes membres appartenant à ces interfaces. Votre application crée des instances de composants COM, récupère des interfaces à partir de celles-ci et appelle des méthodes sur ces interfaces pour tirer parti des fonctionnalités implémentées dans les composants COM.

Dans la pratique, cela ressemble à l’appel de méthodes sur un objet C++ standard. Mais il y a des différences.

  • Un objet COM applique une encapsulation plus stricte qu’un objet C++. Vous ne pouvez pas simplement créer l’objet, puis appeler n’importe quelle méthode publique. Au lieu de cela, les méthodes publiques d’un composant COM sont regroupées en une ou plusieurs interfaces COM. Pour appeler une méthode, vous créez l’objet et récupérez à partir de l’objet l’interface qui implémente la méthode. Une interface implémente généralement un ensemble associé de méthodes qui fournissent l’accès à une fonctionnalité particulière de l’objet. Par exemple, l’interface ID3D12Device représente une carte graphique virtuelle, et contient des méthodes qui vous permettent de créer des ressources, par exemple, et de nombreuses autres tâches liées à l’adaptateur.
  • Un objet COM n’est pas créé de la même façon qu’un objet C++. Il existe plusieurs façons de créer un objet COM, mais toutes impliquent des techniques spécifiques à COM. L’API DirectX inclut diverses fonctions et méthodes d’assistance qui simplifient la création de la plupart des objets COM DirectX.
  • Vous devez utiliser des techniques spécifiques à COM pour contrôler la durée de vie d’un objet COM.
  • Le serveur COM (généralement une DLL) n’a pas besoin d’être chargé explicitement. Vous ne liez pas non plus à une bibliothèque statique pour utiliser un composant COM. Chaque composant COM a un identificateur inscrit unique (identificateur global unique ou GUID), que votre application utilise pour identifier l’objet COM. Votre application identifie le composant et le runtime COM charge automatiquement la DLL de serveur COM appropriée.
  • COM est une spécification binaire. Les objets COM peuvent être écrits et accessibles à partir de diverses langues. Vous n’avez pas besoin de connaître le code source de l’objet. Par exemple, les applications Visual Basic utilisent régulièrement des objets COM qui ont été écrits en C++.

Composant, objet et interface

Il est important de comprendre la distinction entre les composants, les objets et les interfaces. En cas d’utilisation informelle, vous pouvez entendre un composant ou un objet référencé par le nom de son interface principale. Mais les termes ne sont pas interchangeables. Un composant peut implémenter n’importe quel nombre d’interfaces ; et un objet est une instance d’un composant. Par exemple, alors que tous les composants doivent implémenter l’interface IUnknown, ils implémentent normalement au moins une interface supplémentaire et peuvent implémenter plusieurs.

Pour utiliser une méthode d’interface particulière, vous ne devez pas seulement instancier un objet, vous devez également obtenir l’interface correcte à partir de celle-ci.

En outre, plusieurs composants peuvent implémenter la même interface. Une interface est un groupe de méthodes qui effectuent un ensemble logique d’opérations. La définition de l’interface spécifie uniquement la syntaxe des méthodes et leurs fonctionnalités générales. Tout composant COM qui doit prendre en charge un ensemble particulier d’opérations peut le faire en implémentant une interface appropriée. Certaines interfaces sont hautement spécialisées et sont implémentées uniquement par un seul composant ; d’autres sont utiles dans diverses circonstances et sont implémentés par de nombreux composants.

Si un composant implémente une interface, il doit prendre en charge chaque méthode dans la définition de l’interface. En d’autres termes, vous devez pouvoir appeler n’importe quelle méthode et être sûr qu’elle existe. Toutefois, les détails de l’implémentation d’une méthode particulière peuvent varier d’un composant à un autre. Par exemple, différents composants peuvent utiliser différents algorithmes pour arriver au résultat final. Il n’existe pas non plus de garantie qu’une méthode sera prise en charge de manière nontriviale. Parfois, un composant implémente une interface couramment utilisée, mais il doit prendre en charge uniquement un sous-ensemble des méthodes. Vous pourrez toujours appeler les méthodes restantes avec succès, mais elles retournent un HRESULT (qui est un type COM standard représentant un code de résultat) contenant la valeur E_NOTIMPL. Vous devez consulter sa documentation pour voir comment une interface est implémentée par n’importe quel composant particulier.

La norme COM exige qu’une définition d’interface ne change pas une fois qu’elle a été publiée. L’auteur ne peut pas, par exemple, ajouter une nouvelle méthode à une interface existante. L’auteur doit à la place créer une nouvelle interface. Bien qu’il n’existe aucune restriction sur les méthodes qui doivent être dans cette interface, une pratique courante consiste à inclure toutes les méthodes de l’ancienne interface, ainsi que toutes les nouvelles méthodes.

Il n’est pas inhabituel pour une interface d’avoir plusieurs générations. En règle générale, toutes les générations effectuent essentiellement la même tâche globale, mais elles sont différentes dans des spécificités. Souvent, un composant COM implémente toutes les générations actuelles et antérieures de la lignée d’une interface donnée. Cela permet aux applications plus anciennes de continuer à utiliser les interfaces plus anciennes de l’objet, tandis que les applications plus récentes peuvent tirer parti des fonctionnalités des interfaces plus récentes. En règle générale, un groupe descendant d’interfaces ont tous le même nom, accompagné d'un entier qui indique la génération. Par exemple, si l’interface d’origine était nommée IMyInterface (impliquant la génération 1), les deux prochaines générations seraient appelées IMyInterface2 et IMyInterface3. Dans le cas des interfaces DirectX, les générations successives sont généralement nommées pour le numéro de version de DirectX.

les GUID

Les GUID sont une partie clé du modèle de programmation COM. Au plus simple, un GUID est une structure 128 bits. Toutefois, les GUID sont créés de manière à garantir qu’il n’y a pas deux GUID identiques. COM utilise largement les GUID à deux fins principales.

  • Pour identifier de manière unique un composant COM particulier. Un GUID affecté à l’identification d’un composant COM est appelé identificateur de classe (CLSID) et vous utilisez un CLSID lorsque vous souhaitez créer une instance du composant COM associé.
  • Pour identifier de manière unique une interface COM particulière. Un GUID affecté à l’identification d’une interface COM est appelé identificateur d’interface (IID) et vous utilisez un IID lorsque vous demandez une interface particulière à partir d’une instance d’un composant (un objet). L’IID d’une interface est identique, quel que soit le composant qui implémente l’interface.

Pour plus de commodité, la documentation DirectX fait normalement référence aux composants et interfaces par leurs noms descriptifs ( par exemple, ID3D12Device) plutôt que par leurs GUID. Dans le contexte de la documentation DirectX, il n’y a aucune ambiguïté. Il est techniquement possible pour un tiers de créer une interface avec le nom descriptif ID3D12Device (il faudrait disposer d’un AUTRE IID pour être valide). Dans l’intérêt de la clarté, cependant, nous ne recommandons pas cela.

Par conséquent, la seule façon non ambiguë de faire référence à un objet ou une interface particulier est par son GUID.

Bien qu’un GUID soit une structure, un GUID est souvent exprimé sous forme de chaîne équivalente. Le format général de la forme de chaîne d’un GUID est de 32 chiffres hexadécimaux, au format 8-4-4-4-12. Autrement dit, {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}, où chaque x correspond à un chiffre hexadécimal. Par exemple, la forme de chaîne de l’IID de l’interface ID3D12Device est {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Étant donné que le GUID réel est peu pratique et facile à mal orthographier, un nom équivalent est généralement également fourni. Dans votre code, vous pouvez utiliser ce nom au lieu de la structure réelle lorsque vous appelez des fonctions, par exemple lorsque vous passez un argument pour le riid paramètre à D3D12CreateDevice. La convention d’affectation de noms habituelle consiste à ajouter IID_ ou CLSID_ au nom descriptif de l’interface ou de l’objet, respectivement. Par exemple, le nom de l’IID de l’interface ID3D12Device est IID_ID3D12Device.

Note

Les applications DirectX doivent établir un lien avec dxguid.lib et uuid.lib pour fournir des définitions pour les différents GUID d’interface et de classe. Visual C++ et d’autres compilateurs prennent en charge l’extension de langage d’opérateur __uuidof , mais une liaison explicite de style C avec ces bibliothèques de liens est également prise en charge et entièrement portable.

Valeurs HRESULT

La plupart des méthodes COM retournent un entier 32 bits appelé HRESULT. Avec la plupart des méthodes, HRESULT est essentiellement une structure qui contient deux éléments d’information principaux.

  • Indique si la méthode a réussi ou échoué.
  • Informations plus détaillées sur le résultat de l’opération effectuée par la méthode.

Certaines méthodes retournent une valeur HRESULT du jeu standard défini dans Winerror.h. Toutefois, une méthode est gratuite pour retourner une valeur HRESULT personnalisée avec des informations plus spécialisées. Ces valeurs sont normalement documentées sur la page de référence de la méthode.

La liste des valeurs HRESULT que vous trouvez sur la page de référence d’une méthode n’est souvent qu’un sous-ensemble des valeurs possibles qui peuvent être retournées. La liste couvre généralement uniquement les valeurs spécifiques à la méthode, ainsi que les valeurs standard qui ont une signification spécifique à la méthode. Vous devez supposer qu’une méthode peut retourner une variété de valeurs HRESULT standard, même si elles ne sont pas documentées explicitement.

Bien que les valeurs HRESULT soient souvent utilisées pour retourner des informations d’erreur, vous ne devez pas les considérer comme des codes d’erreur. Le fait que le bit qui indique la réussite ou l’échec est stocké séparément des bits qui contiennent les informations détaillées permet aux valeurs HRESULT d’avoir un nombre quelconque de codes de réussite et d’échec. Par convention, les noms des codes de réussite sont précédés de S_ et de codes d’échec par E_. Par exemple, les deux codes les plus couramment utilisés sont S_OK et E_FAIL, ce qui indique respectivement une réussite ou un échec simples.

Le fait que les méthodes COM peuvent retourner une variété de codes de réussite ou d’échec signifie que vous devez faire attention à la façon dont vous testez la valeur HRESULT . Par exemple, considérez une méthode hypothétique avec des valeurs de retour documentées de S_OK si elle réussit et E_FAIL si ce n’est pas le cas. Toutefois, n’oubliez pas que la méthode peut également retourner d’autres codes d’échec ou de réussite. Le fragment de code suivant illustre le danger d’utiliser un test simple, où hr contient la valeur HRESULT retournée par la méthode.

if (hr == E_FAIL)
{
    // Handle the failure case.
}
else
{
    // Handle the success case.
}  

Tant que, dans le cas d’échec, cette méthode ne retourne jamais E_FAIL (et pas un autre code d’échec), ce test fonctionne. Toutefois, il est plus réaliste qu’une méthode donnée soit implémentée pour retourner un ensemble de codes d’échec spécifiques, peut-être E_NOTIMPL ou E_INVALIDARG. Avec le code ci-dessus, ces valeurs seraient interprétées de manière incorrecte comme une réussite.

Si vous avez besoin d’informations détaillées sur le résultat de l’appel de méthode, vous devez tester chaque valeur HRESULT pertinente. Toutefois, vous serez peut-être intéressé uniquement par la réussite ou l’échec de la méthode. Un moyen robuste de tester si une valeur HRESULT indique la réussite ou l’échec consiste à passer la valeur à l’une des macros suivantes, définies dans Winerror.h.

  • La SUCCEEDED macro retourne TRUE pour un code de réussite et FALSE pour un code d’échec.
  • La FAILED macro retourne TRUE pour un code d’échec et FALSE pour un code de réussite.

Par conséquent, vous pouvez corriger le fragment de code précédent à l’aide de la FAILED macro, comme illustré dans le code suivant.

if (FAILED(hr))
{
    // Handle the failure case.
}
else
{
    // Handle the success case.
}  

Ce fragment de code corrigé traite correctement les E_NOTIMPL et les E_INVALIDARG comme des échecs.

Bien que la plupart des méthodes COM retournent des valeurs HRESULT structurées, un petit nombre utilise HRESULT pour retourner un entier simple. Implicitement, ces méthodes réussissent toujours. Si vous passez un HRESULT de ce type à la macro SUCCEEDED, la macro retourne toujours TRUE. Un exemple de méthode couramment appelée qui ne retourne pas d’HRESULT est la méthode IUnknown::Release, qui retourne un ULONG. Cette méthode décrémente le nombre de références d’un objet par un et retourne le nombre de références actuel. Consultez La gestion de la durée de vie d’un objet COM pour une discussion sur le comptage des références.

Adresse d’un pointeur

Si vous consultez quelques pages de référence de méthode COM, vous rencontrerez probablement quelque chose de similaire.

HRESULT D3D12CreateDevice(
  IUnknown          *pAdapter,
  D3D_FEATURE_LEVEL MinimumFeatureLevel,
  REFIID            riid,
  void              **ppDevice
);

Bien qu’un pointeur normal soit familier à n’importe quel développeur C/C++, COM utilise souvent un niveau supplémentaire d’indirection. Ce deuxième niveau d’indirection est indiqué par deux astérisques, **suivant la déclaration de type et le nom de la variable a généralement un préfixe .pp Pour la fonction ci-dessus, le ppDevice paramètre est généralement appelé l’adresse d’un pointeur vers un void. Dans cette pratique, dans cet exemple, ppDevice est l’adresse d’un pointeur vers une interface ID3D12Device .

Contrairement à un objet C++, vous n’accédez pas directement aux méthodes d’un objet COM. Au lieu de cela, vous devez obtenir un pointeur vers une interface qui expose la méthode. Pour appeler la méthode, vous utilisez essentiellement la même syntaxe que vous le feriez pour appeler un pointeur vers une méthode C++. Par exemple, pour appeler la méthode IMyInterface ::D oSomething , vous devez utiliser la syntaxe suivante.

IMyInterface * pMyIface = nullptr;
...
pMyIface->DoSomething(...);

La nécessité d’un deuxième niveau d’indirection provient du fait que vous ne créez pas directement de pointeurs d’interface. Vous devez appeler l’une des différentes méthodes, telles que la méthode D3D12CreateDevice indiquée ci-dessus. Pour utiliser cette méthode pour obtenir un pointeur d’interface, vous déclarez une variable comme pointeur vers l’interface souhaitée, puis vous transmettez l’adresse de cette variable à la méthode. En d’autres termes, vous passez l’adresse d’un pointeur à la méthode. Lorsque la méthode est retournée, la variable pointe vers l’interface demandée et vous pouvez utiliser ce pointeur pour appeler l’une des méthodes de l’interface.

IDXGIAdapter * pIDXGIAdapter = nullptr;
...
ID3D12Device * pD3D12Device = nullptr;
HRESULT hr = ::D3D12CreateDevice(
    pIDXGIAdapter,
    D3D_FEATURE_LEVEL_11_0,
    IID_ID3D12Device,
    &pD3D12Device);
if (FAILED(hr)) return E_FAIL;

// Now use pD3D12Device in the form pD3D12Device->MethodName(...);

Création d’un objet COM

Il existe plusieurs façons de créer un objet COM. Il s’agit des deux plus couramment utilisées dans la programmation DirectX.

  • Indirectement, en appelant une méthode ou une fonction DirectX qui crée l’objet pour vous. La méthode crée l’objet et retourne une interface sur l’objet. Lorsque vous créez un objet de cette façon, vous pouvez parfois spécifier l’interface à retourner, d’autres fois que l’interface est implicite. L’exemple de code ci-dessus montre comment créer indirectement un objet COM d’appareil Direct3D 12.
  • Directement, en passant le CLSID de l’objet à la fonction CoCreateInstance. La fonction crée une instance de l’objet et retourne un pointeur vers une interface que vous spécifiez.

Une fois, avant de créer des objets COM, vous devez initialiser COM en appelant la fonction CoInitializeEx. Si vous créez indirectement des objets, la méthode de création d’objet gère cette tâche. Toutefois, si vous devez créer un objet avec CoCreateInstance, vous devez appeler CoInitializeEx explicitement. Lorsque vous avez terminé, COM doit être désinitialisé en appelant CoUninitialize. Si vous effectuez un appel à CoInitializeEx , vous devez le faire correspondre à un appel à CoUninitialize. En règle générale, les applications qui doivent initialiser explicitement le COM le font dans leur routine de démarrage, et elles désinitialisent le COM dans leur routine de nettoyage.

Pour créer une instance d’un objet COM avec CoCreateInstance, vous devez disposer du CLSID de l’objet. Si ce CLSID est disponible publiquement, vous le trouverez dans la documentation de référence ou dans le fichier d’en-tête approprié. Si le CLSID n’est pas disponible publiquement, vous ne pouvez pas créer l’objet directement.

La fonction CoCreateInstance a cinq paramètres. Pour les objets COM que vous utiliserez avec DirectX, vous pouvez normalement définir les paramètres comme suit.

rclsid Définissez-le sur le CLSID de l’objet que vous souhaitez créer.

pUnkOuter Défini sur nullptr. Ce paramètre est utilisé uniquement si vous agrégeez des objets. Une discussion sur l’agrégation COM est en dehors de l’étendue de cette rubrique.

dwClsContext Défini sur CLSCTX_INPROC_SERVER. Ce paramètre indique que l’objet est implémenté en tant que DLL et s’exécute dans le cadre du processus de votre application.

Riid Définissez l’IID de l’interface que vous souhaitez retourner. La fonction crée l’objet et retourne le pointeur d’interface demandé dans le paramètre ppv.

Ppv Définissez cette valeur sur l'adresse d'un pointeur qui sera associé à l'interface spécifiée par riid lorsque la fonction se termine. Cette variable doit être déclarée en tant que pointeur vers l’interface demandée, et la référence au pointeur de la liste de paramètres doit être convertie en tant que (LPVOID *).

La création d’un objet indirectement est généralement beaucoup plus simple, comme nous l’avons vu dans l’exemple de code ci-dessus. Vous passez la méthode de création d’objet à l’adresse d’un pointeur d’interface, puis la méthode crée l’objet et retourne un pointeur d’interface. Lorsque vous créez un objet indirectement, même si vous ne pouvez pas choisir l’interface retournée par la méthode, vous pouvez souvent spécifier une variété d’éléments sur la façon dont l’objet doit être créé.

Par exemple, vous pouvez passer à D3D12CreateDevice une valeur spécifiant le niveau de fonctionnalité D3D minimal pris en charge par l’appareil retourné, comme illustré dans l’exemple de code ci-dessus.

Utilisation d’interfaces COM

Lorsque vous créez un objet COM, la méthode de création retourne un pointeur d’interface. Vous pouvez ensuite utiliser ce pointeur pour accéder à l’une des méthodes de l’interface. La syntaxe est identique à celle utilisée avec un pointeur vers une méthode C++.

Demande d’interfaces supplémentaires

Dans de nombreux cas, le pointeur d’interface que vous recevez de la méthode de création peut être le seul dont vous avez besoin. En fait, il est relativement courant pour un objet d’exporter une seule interface autre que IUnknown. Toutefois, de nombreux objets exportent plusieurs interfaces et vous pouvez avoir besoin de pointeurs vers plusieurs d’entre eux. Si vous avez besoin de plus d'interfaces que celle retournée par la méthode de création, il n'est pas nécessaire de créer un nouvel objet. Au lieu de cela, demandez un autre pointeur d’interface à l’aide de la méthode IUnknown ::QueryInterface de l’objet.

Si vous créez votre objet avec CoCreateInstance, vous pouvez demander un pointeur d’interface IUnknown , puis appeler IUnknown ::QueryInterface pour demander chaque interface dont vous avez besoin. Toutefois, cette approche est peu pratique si vous avez besoin d’une seule interface et qu’elle ne fonctionne pas du tout si vous utilisez une méthode de création d’objet qui ne vous permet pas de spécifier quel pointeur d’interface doit être retourné. Dans la pratique, vous n’avez généralement pas besoin d’obtenir un pointeur IUnknown explicite, car toutes les interfaces COM étendent l’interface IUnknown .

L’extension d’une interface est conceptuellement similaire à l’héritage d’une classe C++. L’interface enfant expose toutes les méthodes de l’interface parente, plus une ou plusieurs de ses propres méthodes. En fait, vous verrez souvent « hérite de » employé au lieu de « étend ». Vous devez vous rappeler que l’héritage est interne à l’objet. Votre application ne peut pas hériter ou étendre l’interface d’un objet. Toutefois, vous pouvez utiliser l’interface enfant pour appeler l’une des méthodes de l’enfant ou du parent.

Étant donné que toutes les interfaces sont des enfants d’IUnknown, vous pouvez appeler QueryInterface sur l’un des pointeurs d’interface dont vous disposez déjà pour l’objet. Lorsque vous le faites, vous devez fournir l’IID de l’interface que vous demandez et l’adresse d’un pointeur qui contiendra le pointeur d’interface lorsque la méthode retourne.

Par exemple, le fragment de code suivant appelle IDXGIFactory2 ::CreateSwapChainForHwnd pour créer un objet de chaîne d’échange principal. Cet objet expose plusieurs interfaces. La méthode CreateSwapChainForHwnd retourne une interface IDXGISwapChain1 . Le code suivant utilise ensuite l’interface IDXGISwapChain1 pour appeler QueryInterface pour demander une interface IDXGISwapChain3 .

HRESULT hr = S_OK;

IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
    pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
    hWnd,
    &swapChainDesc,
    nullptr,
    nullptr,
    &pDXGISwapChain1));
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;

Note

En C++, vous pouvez utiliser la IID_PPV_ARGS macro plutôt que le pointeur IID explicite et le pointeur de cast : pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Cela est souvent utilisé pour les méthodes de création ainsi que QueryInterface. Pour plus d’informations , consultez combaseapi.h .

Gestion de la durée de vie d’un objet COM

Lorsqu’un objet est créé, le système alloue les ressources de mémoire nécessaires. Lorsqu’un objet n’est plus nécessaire, il doit être détruit. Le système peut utiliser cette mémoire à d’autres fins. Avec les objets C++, vous pouvez contrôler la durée de vie de l’objet directement avec les opérateurs new et delete dans les cas où vous travaillez à ce niveau, ou simplement en utilisant la pile et la portée. COM ne vous permet pas de créer ou de détruire directement des objets. La raison de cette conception est que le même objet peut être utilisé par plusieurs parties de votre application ou, dans certains cas, par plusieurs applications. Si l’une de ces références devait détruire l’objet, les autres références deviennent non valides. Au lieu de cela, COM utilise un système de comptage de références pour contrôler la durée de vie d’un objet.

Le nombre de références d’un objet est le nombre de fois où l’une de ses interfaces a été demandée. Chaque fois qu’une interface est demandée, le nombre de références est incrémenté. Une application libère une interface quand cette interface n’est plus nécessaire, décrémentant le nombre de références. Tant que le nombre de références est supérieur à zéro, l’objet reste en mémoire. Lorsque le nombre de références atteint zéro, l’objet se détruit. Vous n’avez pas besoin de connaître le nombre de références d’un objet. Tant que vous obtenez et relâchez correctement les interfaces d’un objet, l’objet aura la durée de vie appropriée.

La gestion correcte du comptage des références est une partie cruciale de la programmation COM. L’échec de cette opération peut facilement créer une fuite de mémoire ou un incident. L’une des erreurs les plus courantes que font les programmeurs COM est de ne pas libérer une interface. Lorsque cela se produit, le nombre de références n’atteint jamais zéro, et l’objet reste en mémoire indéfiniment.

Note

Direct3D 10 ou version ultérieure a légèrement modifié les règles de durée de vie des objets. En particulier, les objets dérivés de ID3DxxDeviceChild ne dépassent jamais l'existence de leur appareil parent (autrement dit, si le ID3DxxDevice propriétaire atteint un compte de références de 0, alors tous les objets enfants deviennent immédiatement invalides). En outre, lorsque vous utilisez des méthodes Set pour lier des objets au pipeline de rendu, ces références n’augmentent pas le nombre de références (autrement dit, il s’agit de références faibles). Dans la pratique, cela est mieux géré en vous assurant que vous relâchez entièrement tous les objets enfants de l’appareil avant de libérer l’appareil.

Incrémentation et décrémentation du nombre de références

Chaque fois que vous obtenez un nouveau pointeur d’interface, le nombre de références doit être incrémenté par un appel à IUnknown ::AddRef. Toutefois, votre application n’a généralement pas besoin d’appeler cette méthode. Si vous obtenez un pointeur d’interface en appelant une méthode de création d’objet ou en appelant IUnknown ::QueryInterface, l’objet incrémente automatiquement le nombre de références. Toutefois, si vous créez un pointeur d’interface d’une autre manière, par exemple en copiant un pointeur existant, vous devez appeler explicitement IUnknown ::AddRef. Sinon, lorsque vous relâchez le pointeur d’interface d’origine, l’objet peut être détruit même si vous devrez peut-être utiliser la copie du pointeur.

Vous devez libérer tous les pointeurs d’interface, que vous ou l’objet incrémentiez le nombre de références. Quand vous n’avez plus besoin d’un pointeur d’interface, appelez IUnknown ::Release pour décrémenter le nombre de références. Une pratique courante consiste à initialiser tous les pointeurs d’interface à nullptr, puis à les réinitialiser à nullptr quand ils sont libérés. Cette convention vous permet de tester tous les pointeurs d’interface dans votre code de nettoyage. Ceux qui ne sont pas nullptr sont toujours actifs, et vous devez les libérer avant de mettre fin à l'application.

Le fragment de code suivant étend l’exemple présenté précédemment pour illustrer comment gérer le comptage des références.

HRESULT hr = S_OK;

IDXGISwapChain1 * pDXGISwapChain1 = nullptr;
hr = pIDXGIFactory->CreateSwapChainForHwnd(
    pCommandQueue, // For D3D12, this is a pointer to a direct command queue.
    hWnd,
    &swapChainDesc,
    nullptr,
    nullptr,
    &pDXGISwapChain1));
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3 = nullptr;
hr = pDXGISwapChain1->QueryInterface(IID_IDXGISwapChain3, (LPVOID*)&pDXGISwapChain3);
if (FAILED(hr)) return hr;

IDXGISwapChain3 * pDXGISwapChain3Copy = nullptr;

// Make a copy of the IDXGISwapChain3 interface pointer.
// Call AddRef to increment the reference count and to ensure that
// the object is not destroyed prematurely.
pDXGISwapChain3Copy = pDXGISwapChain3;
pDXGISwapChain3Copy->AddRef();
...
// Cleanup code. Check to see whether the pointers are still active.
// If they are, then call Release to release the interface.
if (pDXGISwapChain1 != nullptr)
{
    pDXGISwapChain1->Release();
    pDXGISwapChain1 = nullptr;
}
if (pDXGISwapChain3 != nullptr)
{
    pDXGISwapChain3->Release();
    pDXGISwapChain3 = nullptr;
}
if (pDXGISwapChain3Copy != nullptr)
{
    pDXGISwapChain3Copy->Release();
    pDXGISwapChain3Copy = nullptr;
}

Pointeurs intelligents COM

Jusqu’à présent, le code a explicitement appelé Release et AddRef pour maintenir le nombre de références à l’aide des méthodes IUnknown. Ce modèle nécessite que le programmeur soit attentif à se rappeler de maintenir correctement le nombre dans tous les chemins de code possibles. Cela peut entraîner une gestion complexe des erreurs, et la gestion des exceptions C++ activée peut être particulièrement difficile à implémenter. Une meilleure solution avec C++ consiste à utiliser un pointeur intelligent.

  • winrt ::com_ptr est un pointeur intelligent fourni par les projections de langage C++/WinRT. Il s’agit du pointeur intelligent COM recommandé à utiliser pour les applications UWP. Notez que C++/WinRT nécessite C++17.

  • Microsoft ::WRL ::ComPtr est un pointeur intelligent fourni par la bibliothèque de modèles C++ Windows Runtime (WRL). Cette bibliothèque est « pure » C++ afin qu’elle puisse être utilisée pour les applications Windows Runtime (via C++/CX ou C++/WinRT) ainsi que pour les applications de bureau Win32. Ce pointeur intelligent fonctionne également sur les versions antérieures de Windows qui ne prennent pas en charge les API Windows Runtime. Pour les applications de bureau Win32, vous pouvez uniquement inclure #include <wrl/client.h> cette classe et éventuellement définir le symbole __WRL_CLASSIC_COM_STRICT__ de préprocesseur. Pour plus d’informations, consultez les pointeurs intelligents COM revisités.

  • CComPtr est un pointeur intelligent fourni par la bibliothèque de modèles active (ATL). Microsoft ::WRL ::ComPtr est une version plus récente de cette implémentation qui résout un certain nombre de problèmes d’utilisation subtils. L’utilisation de ce pointeur intelligent n’est donc pas recommandée pour les nouveaux projets. Pour plus d’informations, consultez Comment créer et utiliser CComPtr et CComQIPtr.

Utilisation d’ATL avec DirectX 9

Pour utiliser la bibliothèque de modèles actifs (ATL) avec DirectX 9, vous devez redéfinir les interfaces pour la compatibilité ATL. Cela vous permet d’utiliser correctement la classe CComQIPtr pour obtenir un pointeur vers une interface.

Vous savez si vous ne redéfinissez pas les interfaces pour ATL, car vous verrez le message d’erreur suivant.

[...]\atlmfc\include\atlbase.h(4704) :   error C2787: 'IDirectXFileData' : no GUID has been associated with this object

L’exemple de code suivant montre comment définir l’interface IDirectXFileData.

// Explicit declaration
struct __declspec(uuid("{3D82AB44-62DA-11CF-AB39-0020AF71E433}")) IDirectXFileData;

// Macro method
#define RT_IID(iid_, name_) struct __declspec(uuid(iid_)) name_
RT_IID("{1DD9E8DA-1C77-4D40-B0CF-98FEFDFF9512}", IDirectXFileData);

Après avoir redéfini l’interface, vous devez utiliser la méthode Attach pour attacher l’interface au pointeur d’interface retourné par ::D irect3DCreate9. Si ce n’est pas le cas, l’interface IDirect3D9 ne sera pas correctement libérée par la classe de pointeur intelligente.

La classe CComPtr appelle en interne IUnknown ::AddRef sur le pointeur d’interface lorsque l’objet est créé et lorsqu’une interface est affectée à la classe CComPtr . Pour éviter la fuite de pointeur d’interface, n’appelez pas **IUnknown::AddRef** sur l’interface retournée par ::Direct3DCreate9.

Le code suivant libère correctement l’interface sans appeler IUnknown ::AddRef.

CComPtr<IDirect3D9> d3d;
d3d.Attach(::Direct3DCreate9(D3D_SDK_VERSION));

Utilisez le code précédent. N’utilisez pas le code suivant, qui appelle IUnknown ::AddRef suivi de IUnknown ::Release et ne libère pas la référence ajoutée par ::D irect3DCreate9.

CComPtr<IDirect3D9> d3d = ::Direct3DCreate9(D3D_SDK_VERSION);

Notez qu’il s’agit du seul endroit dans Direct3D 9 où vous devrez utiliser la méthode Attach de cette façon.

Pour plus d’informations sur les classes CComPTR et CComQIPtr , consultez leurs définitions dans le Atlbase.h fichier d’en-tête.