Programmation de DirectX avec COM

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

Notes

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

Vous pouvez également utiliser com brut, et c’est ce qui concerne cette rubrique. Vous aurez besoin d’une compréhension de base des principes et des techniques de programmation impliqués dans la consommation des 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 s’explique par le fait que vous allez consommer les objets COM fournis par DirectX. Vous n’avez pas besoin de créer vos propres objets COM, ce qui est généralement là où la complexité se pose.

Vue d’ensemble du composant 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 binaire appelé serveur COM ; plus souvent qu’une DLL.

Une DLL traditionnelle exporte des fonctions gratuites. Un serveur COM peut faire de même. Mais les composants COM à l’intérieur du serveur COM exposent les interfaces COM et les méthodes membres appartenant à ces interfaces. Votre application crée des instances de composants COM, récupère des interfaces à partir de ceux-ci et appelle des méthodes sur ces interfaces afin de 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 une 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 de méthodes connexes 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 manière 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 comprend 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 différents langages. Vous n’avez pas besoin de savoir quoi que ce soit sur le code source de l’objet. Par exemple, les applications Visual Basic utilisent régulièrement des objets COM écrits en C++.

Composant, objet et interface

Il est important de comprendre la distinction entre les composants, les objets et les interfaces. Dans une utilisation occasionnelle, 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 un instance d’un composant. Par exemple, si tous les composants doivent implémenter l’interface IUnknown, ils implémentent normalement au moins une interface supplémentaire, et ils peuvent en implémenter plusieurs.

Pour utiliser une méthode d’interface particulière, vous devez non seulement instancier un objet, mais également obtenir l’interface appropriée à partir de celui-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 ne sont implémentées que par un seul composant ; d’autres sont utiles dans diverses circonstances et sont implémentées par de nombreux composants.

Si un composant implémente une interface, il doit prendre en charge chaque méthode dans la définition d’interface. En d’autres termes, vous devez être en mesure d’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 à l’autre. Par exemple, différents composants peuvent utiliser différents algorithmes pour parvenir au résultat final. Il n’est pas non plus garanti qu’une méthode soit prise en charge de manière non dérivée. Parfois, un composant implémente une interface couramment utilisée, mais il ne doit prendre en charge qu’un sous-ensemble des méthodes. Vous pourrez toujours appeler correctement les méthodes restantes, mais elles retourneront 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 un 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. À la place, l’auteur doit créer une interface. Bien qu’il n’y ait aucune restriction sur les méthodes qui doivent être dans cette interface, une pratique courante consiste à faire en sorte que l’interface de nouvelle génération inclue toutes les méthodes de l’ancienne interface, ainsi que toutes les nouvelles méthodes.

Il n’est pas rare qu’une interface ait plusieurs générations. En règle générale, toutes les générations effectuent essentiellement la même tâche globale, mais leurs spécificités sont différentes. Souvent, un composant COM implémente chaque génération actuelle et antérieure de la traçabilité d’une interface donnée. Cela permet aux applications plus anciennes de continuer à utiliser les anciennes interfaces 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 de descentes d’interfaces a le même nom, plus un entier qui indique la génération. Par exemple, si l’interface d’origine était nommée IMyInterface (ce qui implique la génération 1), les deux générations suivantes 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.

GUID

Les GUID sont un élément clé du modèle de programmation COM. À la base, un GUID est une structure 128 bits. Toutefois, les GUID sont créés de manière à garantir que deux GUID ne sont pas 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 un 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 IDI lorsque vous demandez une interface particulière à une instance d’un composant (un objet). L’IID d’une interface est identique, quel que soit le composant qui implémente l’interface.

Par souci de commodité, la documentation DirectX fait généralement référence aux composants et aux 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 (elle doit avoir un AUTRE ID pour être valide). Toutefois, par souci de clarté, nous vous déconseillons de le faire.

Par conséquent, la seule façon non ambiguë de faire référence à un objet ou à une interface particulière 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-xxxxxxxxxxxx}, où chaque x correspond à un chiffre hexadécimal. Par exemple, la forme de chaîne de l’IID pour l’interface ID3D12Device est {189819F1-1DB6-4B57-BE54-1821339B85F7}.

Étant donné que le GUID réel est quelque peu maladroit à utiliser et facile à mal tapez, un nom équivalent est généralement fourni ainsi. 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 de nommage habituelle consiste à ajouter respectivement IID_ ou CLSID_ au nom descriptif de l’interface ou de l’objet. Par exemple, le nom de l’IID de l’interface ID3D12Device est IID_ID3D12Device.

Notes

Les applications DirectX doivent être liées à 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 la 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 informations principales.

  • 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 libre de 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 dans 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 diverses 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 soit stocké séparément des bits qui contiennent les informations détaillées permet aux valeurs HRESULT d’avoir n’importe quel nombre de codes de réussite et d’échec. Par convention, les noms des codes de réussite sont précédés par des codes de S_ et des codes d’échec par E_. Par exemple, les deux codes les plus couramment utilisés sont S_OK et E_FAIL, qui indiquent respectivement une réussite ou un échec simple.

Le fait que les méthodes COM peuvent retourner divers codes de réussite ou d’échec signifie que vous devez être prudent dans la façon dont vous testez la valeur HRESULT . Par exemple, envisagez 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 de l’utilisation d’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 que 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 mal interprétées comme un succès.

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 pouvez ê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 une réussite ou un é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.

Vous pouvez donc 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 E_NOTIMPL et 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 renvoyer 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. La méthode IUnknown::Release est un exemple de méthode communément appelée qui ne retourne pas de HRESULT, qui retourne un ULONG. Cette méthode décrémente un nombre de références d’un objet et retourne le nombre de références actuel. Pour plus d’informations sur le comptage des références, consultez Gestion de la durée de vie d’un objet COM.

Adresse d’un pointeur

Si vous affichez quelques pages de référence de méthode COM, vous allez probablement exécuter quelque chose comme suit.

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

Bien qu’un pointeur normal soit assez familier pour tout développeur C/C++, COM utilise souvent un niveau d’indirection supplémentaire. Ce deuxième niveau d’indirection est indiqué par deux astérisque, **, après la déclaration de type, et le nom de la variable a généralement un préfixe de pp. Pour la fonction ci-dessus, le ppDevice paramètre est généralement appelé adresse d’un pointeur vers un void. Dans la 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 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 vient 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 illustrée ci-dessus. Pour utiliser une telle méthode pour obtenir un pointeur d’interface, vous déclarez une variable en tant que pointeur vers l’interface souhaitée, puis vous passez 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 retourne, 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és 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 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 des objets indirectement, la méthode de création d’objets gère cette tâche. Toutefois, si vous devez créer un objet avec CoCreateInstance, vous devez appeler Explicitement CoInitializeEx . Lorsque vous avez terminé, COM doit être non initialisé 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 ont besoin d’initialiser COM explicitement le font dans leur routine de démarrage, et elles ne initialisent pas 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 allez utiliser avec DirectX, vous pouvez normalement définir les paramètres comme suit.

rclsid Définissez ce paramètre sur le CLSID de l’objet que vous souhaitez créer.

pUnkOuter Définissez sur nullptr. Ce paramètre n’est utilisé que si vous agrégez des objets. Une discussion sur l’agrégation COM est en dehors de la portée de cette rubrique.

dwClsContext Définissez 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 sur 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 défini sur l’interface spécifiée par riid lorsque la fonction retourne. Cette variable doit être déclarée en tant que pointeur vers l’interface demandée, et la référence au pointeur dans la liste des paramètres doit être castée en tant que (LPVOID *).

La création indirecte d’un objet est généralement beaucoup plus simple, comme nous l’avons vu dans l’exemple de code ci-dessus. Vous transmettez à 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 que la méthode retourne, vous pouvez toujours spécifier divers éléments sur la façon dont l’objet doit être créé.

Par exemple, vous pouvez passer à D3D12CreateDevice une valeur spécifiant le niveau minimal de fonctionnalité D3D que l’appareil retourné doit prendre en charge, comme indiqué 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 aurez peut-être besoin de pointeurs vers plusieurs d’entre elles. 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 n’est pas 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 le pointeur d’interface à retourner. 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 » utilisé au lieu de « étend ». Ce que vous devez retenir, c’est que l’héritage est interne à l’objet. Votre application ne peut pas hériter de 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 que vous avez déjà pour l’objet. Dans ce cas, 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 afin de 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;

Notes

En C++, vous pouvez utiliser la macro plutôt que l’IID IID_PPV_ARGS explicite et le pointeur de cast : pDXGISwapChain1->QueryInterface(IID_PPV_ARGS(&pDXGISwapChain3));. Il est souvent utilisé pour les méthodes de création ainsi que Pour 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 et delete dans les new cas où vous travaillez à ce niveau, ou simplement à l’aide de la durée de vie de la pile et de l’étendue. 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 devenaient 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 correspond au 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 lorsque 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 lui-même. Vous n’avez pas besoin de connaître le nombre de références d’un objet. Tant que vous obtenez et libérez correctement les interfaces d’un objet, l’objet aura la durée de vie appropriée.

Gérer correctement le comptage des références est un élément essentiel de la programmation COM. Si vous ne le faites pas, vous pouvez facilement créer une fuite de mémoire ou un incident. L’une des erreurs les plus courantes commises par les programmeurs COM est de ne pas libérer une interface. Dans ce cas, le nombre de références n’atteint jamais zéro et l’objet reste indéfiniment en mémoire.

Notes

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 d’ID3DxxDeviceChild ne survivent jamais à leur appareil parent (autrement dit, si l’ID3DxxDevice propriétaire atteint un refcount 0, alors tous les objets enfants ne sont pas immédiatement non valides). 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, il est préférable de gérer cette opération en veillant à libérer 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 devez toujours utiliser la copie du pointeur.

Vous devez libérer tous les pointeurs d’interface, que vous ou l’objet avez incrémenté le nombre de références. Lorsque 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 vers nullptr, puis à les revenir sur nullptr lorsqu’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 encore 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 gérer les nombres de références à l’aide des méthodes IUnknown . Ce modèle nécessite que le programmeur soit diligent dans la mémorisation afin de gérer correctement le nombre dans tous les chemins de code possibles. Cela peut entraîner une gestion des erreurs compliquée et, avec la gestion des exceptions C++ activée, il 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 en C++ « pur » et peut donc ê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 utiliser #include <wrl/client.h> pour inclure uniquement cette classe et définir éventuellement le symbole __WRL_CLASSIC_COM_STRICT__ de préprocesseur. Pour plus d’informations, consultez Pointeurs intelligents COM revisités.

  • CComPtr est un pointeur intelligent fourni par la bibliothèque de modèles actifs (ATL). Microsoft::WRL::ComPtr étant 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 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 saurez si vous ne redéfinissez pas les interfaces pour ATL, car le message d’erreur suivant s’affiche.

[...]\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 intelligent.

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

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 manière.

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