Pratiques de codage COM

Cette rubrique décrit comment rendre votre code COM plus efficace et plus robuste.

Opérateur __uuidof

Lorsque vous générez votre programme, vous pouvez obtenir des erreurs d’éditeur de liens similaires à ce qui suit :

unresolved external symbol "struct _GUID const IID_IDrawable"

Cette erreur signifie qu’une constante GUID a été déclarée avec une liaison externe (extern) et que l’éditeur de liens n’a pas pu trouver la définition de la constante. La valeur d’une constante GUID est généralement exportée à partir d’un fichier de bibliothèque statique. Si vous utilisez Microsoft Visual C++, vous pouvez éviter d’avoir à lier une bibliothèque statique à l’aide de l’opérateur __uuidof. Cet opérateur est une extension de langage Microsoft. Elle retourne une valeur GUID à partir d’une expression. L’expression peut être un nom de type d’interface, un nom de classe ou un pointeur d’interface. À l’aide de __uuidof, vous pouvez créer l’objet Common Item Dialog comme suit :

IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, 
    __uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));

Le compilateur extrait la valeur GUID de l’en-tête, donc aucune exportation de bibliothèque n’est nécessaire.

Notes

La valeur GUID est associée au nom de type en déclarant __declspec(uuid( ... )) dans l’en-tête. Pour plus d’informations, consultez la documentation pour __declspec dans la documentation Visual C++.

 

The IID_PPV_ARGS Macro

Nous avons vu que CoCreateInstance et QueryInterface nécessitent de contraindre le paramètre final à un type void** . Cela crée le risque d’incompatibilité de type. Prenons le fragment de code suivant :

// Wrong!

IFileOpenDialog *pFileOpen;

hr = CoCreateInstance(
    __uuidof(FileOpenDialog), 
    NULL, 
    CLSCTX_ALL, 
    __uuidof(IFileDialogCustomize),       // The IID does not match the pointer type!
    reinterpret_cast<void**>(&pFileOpen)  // Coerce to void**.
    );

Ce code demande l’interface IFileDialogCustomize , mais passe un pointeur IFileOpenDialog . L’expression reinterpret_cast contourne le système de type C++, de sorte que le compilateur n’intercepte pas cette erreur. Dans le meilleur des cas, si l’objet n’implémente pas l’interface demandée, l’appel échoue tout simplement. Dans le pire des cas, la fonction réussit et vous avez un pointeur incompatible. En d’autres termes, le type de pointeur ne correspond pas à la vtable réelle en mémoire. Comme vous pouvez l’imaginer, rien de bon ne peut se produire à ce moment-là.

Notes

Une vtable (table de méthode virtuelle) est une table de pointeurs de fonction. La vtable est la façon dont COM lie un appel de méthode à son implémentation au moment de l’exécution. Ce n’est pas par hasard que les vtables sont la façon dont la plupart des compilateurs C++ implémentent des méthodes virtuelles.

 

La macro IID_PPV_ARGS permet d’éviter cette classe d’erreur. Pour utiliser cette macro, remplacez le code suivant :

__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)

par le code :

IID_PPV_ARGS(&pFileOpen)

La macro s’insère __uuidof(IFileOpenDialog) automatiquement pour l’identificateur d’interface, de sorte qu’elle correspond au type de pointeur. Voici le code modifié (et correct) :

// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL, 
    IID_PPV_ARGS(&pFileOpen));

Vous pouvez utiliser la même macro avec QueryInterface :

IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));

Modèle SafeRelease

Le comptage des références est l’une de ces choses dans la programmation qui est fondamentalement facile, mais aussi fastidieuse, ce qui rend facile de se tromper. Les erreurs sont généralement les suivantes :

  • Échec de la libération d’un pointeur d’interface lorsque vous avez terminé de l’utiliser. Cette classe de bogue entraîne la fuite de mémoire et d’autres ressources de votre programme, car les objets ne sont pas détruits.
  • Appel de Release avec un pointeur non valide. Par exemple, cette erreur peut se produire si l’objet n’a jamais été créé. Cette catégorie de bogue provoquera probablement le blocage de votre programme.
  • Déréférencement d’un pointeur d’interface après l’appel de Release . Ce bogue peut entraîner le blocage de votre programme. Pire encore, votre programme peut se bloquer au hasard ultérieurement, ce qui rend difficile le suivi de l’erreur d’origine.

Une façon d’éviter ces bogues consiste à appeler Release via une fonction qui libère le pointeur en toute sécurité. Le code suivant montre une fonction qui effectue cette opération :

template <class T> void SafeRelease(T **ppT)
{
    if (*ppT)
    {
        (*ppT)->Release();
        *ppT = NULL;
    }
}

Cette fonction prend un pointeur d’interface COM comme paramètre et effectue les opérations suivantes :

  1. Vérifie si le pointeur a la valeur NULL.
  2. Appelle Release si le pointeur n’est pas NULL.
  3. Définit le pointeur sur NULL.

Voici un exemple d’utilisation SafeReleasede :

void UseSafeRelease()
{
    IFileOpenDialog *pFileOpen = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        // Use the object.
    }
    SafeRelease(&pFileOpen);
}

Si CoCreateInstance réussit, l’appel à SafeRelease libère le pointeur. Si CoCreateInstance échoue, pFileOpen reste NULL. La SafeRelease fonction vérifie cela et ignore l’appel à Release.

Il est également possible d’appeler SafeRelease plusieurs fois sur le même pointeur, comme illustré ici :

// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);

Pointeurs intelligents COM

La SafeRelease fonction est utile, mais vous devez vous souvenir de deux choses :

  • Initialisez chaque pointeur d’interface vers NULL.
  • Appelez SafeRelease avant que chaque pointeur ne sorte de l’étendue.

En tant que programmeur C++, vous pensez probablement que vous ne devriez pas avoir à vous souvenir de l’une ou l’autre de ces choses. Après tout, c’est pourquoi C++ a des constructeurs et des destructeurs. Il serait intéressant d’avoir une classe qui encapsule le pointeur d’interface sous-jacent et initialise et libère automatiquement le pointeur. En d’autres termes, nous voulons quelque chose comme ceci :

// Warning: This example is not complete.

template <class T>
class SmartPointer
{
    T* ptr;

 public:
    SmartPointer(T *p) : ptr(p) { }
    ~SmartPointer()
    {
        if (ptr) { ptr->Release(); }
    }
};

La définition de classe présentée ici est incomplète et n’est pas utilisable comme indiqué. Au minimum, vous devez définir un constructeur de copie, un opérateur d’affectation et un moyen d’accéder au pointeur COM sous-jacent. Heureusement, vous n’avez pas besoin d’effectuer ce travail, car Microsoft Visual Studio fournit déjà une classe de pointeur intelligent dans le cadre de la bibliothèque de modèles actifs (ATL).

La classe de pointeur intelligent ATL est nommée CComPtr. (Il existe également une classe CComQIPtr , qui n’est pas abordée ici.) Voici l’exemple Ouvrir la boîte de dialogue réécrit pour utiliser CComPtr.

#include <windows.h>
#include <shobjidl.h> 
#include <atlbase.h> // Contains the declaration of CComPtr.

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | 
        COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        CComPtr<IFileOpenDialog> pFileOpen;

        // Create the FileOpenDialog object.
        hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
        if (SUCCEEDED(hr))
        {
            // Show the Open dialog box.
            hr = pFileOpen->Show(NULL);

            // Get the file name from the dialog box.
            if (SUCCEEDED(hr))
            {
                CComPtr<IShellItem> pItem;
                hr = pFileOpen->GetResult(&pItem);
                if (SUCCEEDED(hr))
                {
                    PWSTR pszFilePath;
                    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                    // Display the file name to the user.
                    if (SUCCEEDED(hr))
                    {
                        MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
                        CoTaskMemFree(pszFilePath);
                    }
                }

                // pItem goes out of scope.
            }

            // pFileOpen goes out of scope.
        }
        CoUninitialize();
    }
    return 0;
}

La main différence entre ce code et l’exemple d’origine est que cette version n’appelle pas explicitement Release. Lorsque le instance CComPtr sort de l’étendue, le destructeur appelle Release sur le pointeur sous-jacent.

CComPtr est un modèle de classe. L’argument de modèle est le type d’interface COM. En interne, CComPtr contient un pointeur de ce type. CComPtr remplace operator->() et operator&() afin que la classe agisse comme le pointeur sous-jacent. Par exemple, le code suivant équivaut à appeler directement la méthode IFileOpenDialog::Show :

hr = pFileOpen->Show(NULL);

CComPtr définit également une méthode CComPtr::CoCreateInstance , qui appelle la fonction COM CoCreateInstance avec des valeurs de paramètre par défaut. Le seul paramètre requis est l’identificateur de classe, comme le montre l’exemple suivant :

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

La méthode CComPtr::CoCreateInstance est fournie uniquement pour des raisons pratiques; vous pouvez toujours appeler la fonction COM CoCreateInstance , si vous préférez.

Suivant

Gestion des erreurs dans COM