Compartilhar 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

Ao criar seu programa, você poderá obter erros de vinculador semelhantes aos seguintes:

unresolved external symbol "struct _GUID const IID_IDrawable"

Esse erro significa que uma constante GUID foi declarada com vinculação externa (extern) 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ático. Se você estiver usando o Microsoft Visual C++, poderá evitar a necessidade de vincular uma biblioteca estática usando o operador __uuidof. Esse 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.

Nota

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 CoCreateInstance e QueryInterface exigir a coerção do parâmetro final para 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 a interface IFileDialogCustomize, mas passa um ponteiro IFileOpenDialog. A expressão reinterpret_cast contorna o sistema de tipos C++, portanto, o compilador não detectará 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 à vtable real na memória. Como você pode imaginar, nada de bom pode acontecer naquele momento.

Nota

Um vtable (tabela de método virtual) é uma tabela de ponteiros de função. A vtable é como COM associa uma chamada de método à sua implementação em tempo de execução. Não coincidentemente, vtables são como a maioria dos compiladores C++ implementa 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, portanto, é 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 é entediante, o que facilita o erro. Os erros típicos incluem:

  • Falha ao liberar um ponteiro de interface quando terminar de usá-lo. Essa classe de bug fará com que seu programa vaze memória e outros recursos, pois os objetos não são destruídos.
  • Chamando versão com um ponteiro inválido. Por exemplo, esse erro poderá ocorrer se o objeto nunca tiver sido criado. Essa categoria de bug provavelmente fará com que o programa falhe.
  • Desreferenciar um ponteiro de interface após de versão é chamado. Esse bug pode causar uma falha no programa. Pior, isso pode fazer com que seu programa falhe aleatoriamente mais tarde, dificultando o rastreamento do erro original.

Uma maneira de evitar esses bugs é chamar versão por meio de uma função que libera com segurança o ponteiro. 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;
    }
}

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

  1. Verifica se o ponteiro está NULL.
  2. Chama versão 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 versão.

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

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

Ponteiros Inteligentes COM

A função SafeRelease é útil, mas exige 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 encapsulasse o ponteiro da interface subjacente e inicializasse e liberasse automaticamente o ponteiro. Em 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, conforme 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 nenhum deste trabalho, pois o Microsoft Visual Studio já fornece uma classe de ponteiro inteligente como parte da ATL (Biblioteca de Modelos Ativos).

A classe de ponteiro inteligente atl é nomeada CComPtr. (Há também uma classe CComQIPtr, que não é discutida aqui.) Aqui está o exemplo Abrir Caixa de Diálogo reescrita 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 esse código e o exemplo original é que essa versão não chama explicitamente Release. Quando a instância do CComPtr fica fora do escopo, o destruidor chama de Versão no ponteiro subjacente.

CComPtr é um modelo de classe. O argumento de modelo é 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 método IFileOpenDialog::Show diretamente:

hr = pFileOpen->Show(NULL);

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

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

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

Próximo

tratamento de erros no COM