Partilhar via


Práticas de codificação COM

Este tópico descreve maneiras de tornar seu código COM mais eficaz e robusto.

O operador __uuidof

Quando você cria seu programa, você pode obter erros de vinculador semelhantes aos seguintes:

unresolved external symbol "struct _GUID const IID_IDrawable"

Este erro significa que uma constante GUID foi declarada com ligação externa (externo), e o vinculador não pôde encontrar a definição da constante. O valor de uma constante GUID geralmente é exportado de um arquivo de biblioteca estática. Se você estiver usando o Microsoft Visual C++, você pode evitar a necessidade de vincular uma biblioteca estática usando o operador __uuidof. Este operador é uma extensão de idioma da Microsoft. Ele retorna um valor GUID de uma expressão. A expressão pode ser um nome de tipo de interface, um nome de classe ou um ponteiro de interface. Usando __uuidof, você pode criar o objeto Common Item Dialog da seguinte maneira:

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

O compilador extrai o valor GUID do cabeçalho, portanto, nenhuma exportação de biblioteca é necessária.

Observação

O valor GUID é associado ao nome do tipo declarando __declspec(uuid( ... )) no cabeçalho. Para obter mais informações, consulte a documentação para __declspec na documentação do Visual C++.

 

A macro IID_PPV_ARGS

Vimos que tanto CoCreateInstance quanto QueryInterface exigem coagir o parâmetro final a um tipo de void**. Isso cria o potencial para uma incompatibilidade de tipo. Considere o seguinte fragmento de código:

// 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**.
    );

Esse código solicita o interface IFileDialogCustomize, mas passa um ponteiro IFileOpenDialog. A expressão reinterpret_cast contorna o sistema de tipo C++, portanto, o compilador não detetará esse erro. Na melhor das hipóteses, se o objeto não implementar a interface solicitada, a chamada simplesmente falhará. Na pior das hipóteses, a função é bem-sucedida e você tem um ponteiro incompatível. Em outras palavras, o tipo de ponteiro não corresponde ao vtable real na memória. Como você pode imaginar, nada de bom pode acontecer nesse momento.

Observação

Um vtable (tabela de método virtual) é uma tabela de ponteiros de função. O vtable é como COM vincula uma chamada de método à sua implementação em tempo de execução. Não por acaso, vtables são como a maioria dos compiladores C++ implementam métodos virtuais.

 

A macro IID_PPV_ARGS ajuda a evitar essa classe de erro. Para usar essa macro, substitua o seguinte código:

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

com isso:

IID_PPV_ARGS(&pFileOpen)

A macro insere automaticamente __uuidof(IFileOpenDialog) para o identificador de interface, por isso é garantido que corresponda ao tipo de ponteiro. Aqui está o código modificado (e correto):

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

Você pode usar a mesma macro com QueryInterface:

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

O padrão SafeRelease

A contagem de referências é uma daquelas coisas na programação que é basicamente fácil, mas também é tediosa, o que torna fácil errar. Os erros típicos incluem:

  • Falha ao liberar um ponteiro de interface quando você terminar de usá-lo. Essa classe de bug fará com que seu programa vaze memória e outros recursos, porque os objetos não são destruídos.
  • Chamar versão com um ponteiro inválido. Por exemplo, esse erro pode acontecer se o objeto nunca foi criado. Esta categoria de bug provavelmente fará com que o seu programa falhe.
  • A desreferenciação de um ponteiro de interface após de lançamento é chamada. Este bug pode fazer com que o seu programa falhe. Pior, isso pode fazer com que seu programa falhe aleatoriamente mais tarde, tornando difícil rastrear o erro original.

Uma maneira de evitar esses bugs é chamar Release através de uma função que libera o ponteiro com segurança. O código a seguir mostra uma função que faz isso:

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

Esta função usa um ponteiro de interface COM como parâmetro e faz o seguinte:

  1. Verifica se o ponteiro está NULL.
  2. Chama Release se o ponteiro não estiver NULL.
  3. Define o ponteiro como NULL.

Aqui está um exemplo de como usar SafeRelease:

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);
}

Se CoCreateInstance for bem-sucedida, a chamada para SafeRelease liberará o ponteiro. Se CoCreateInstance falhar, pFileOpen permanecerá NULL. A função SafeRelease verifica isso e ignora a chamada para Release.

Também é seguro chamar SafeRelease mais de uma vez no mesmo ponteiro, como mostrado aqui:

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

Ponteiros inteligentes COM

A função SafeRelease é útil, mas requer que você se lembre de duas coisas:

  • Inicialize cada ponteiro de interface para NULL .
  • Chame SafeRelease antes que cada ponteiro saia do escopo.

Como programador C++, você provavelmente está pensando que não deveria ter que se lembrar de nenhuma dessas coisas. Afinal, é por isso que o C++ tem construtores e destruidores. Seria bom ter uma classe que envolvesse o ponteiro da interface subjacente e inicializasse e liberasse automaticamente o ponteiro. Por outras palavras, queremos algo assim:

// Warning: This example is not complete.

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

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

A definição de classe mostrada aqui está incompleta e não é utilizável como mostrado. No mínimo, você precisaria definir um construtor de cópia, um operador de atribuição e uma maneira de acessar o ponteiro COM subjacente. Felizmente, você não precisa fazer nada desse trabalho, porque o Microsoft Visual Studio já fornece uma classe de ponteiro inteligente como parte da ATL (Ative Template Library).

A classe de ponteiro inteligente ATL é denominada CComPtr. (Há também uma classe CComQIPtr, que não é discutida aqui.) Aqui está o exemplo de Caixa de Diálogo Abrir reescrito para usar 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;
}

A principal diferença entre este código e o exemplo original é que esta versão não chama explicitamente Release. Quando a instância CComPtr sai do escopo, o destruidor chama Release no ponteiro subjacente.

CComPtr é um modelo de classe. O argumento template é o tipo de interface COM. Internamente, CComPtr contém um ponteiro desse tipo. CComPtr substitui operador>() e operador&() para que a classe aja como o ponteiro subjacente. Por exemplo, o código a seguir é equivalente a chamar o IFileOpenDialog::Show método diretamente:

hr = pFileOpen->Show(NULL);

CComPtr também define um método CComPtr::CoCreateInstance, que chama a função deCOMCoCreateInstance com alguns valores de parâmetro padrão. O único parâmetro necessário é o identificador de classe, como mostra o exemplo a seguir:

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

O método CComPtr::CoCreateInstance é fornecido puramente como uma conveniência; você ainda pode chamar a função COM CoCreateInstance, se preferir.

Seguinte

Tratamento de erros no COM