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ática. Se você estiver usando 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.

Observação

O valor guid está 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 exigem coerção do parâmetro final para um tipo void** . Isso cria o potencial para uma incompatibilidade de tipo. Considere o fragmento de código a seguir:

// 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 capturará 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.

Observação

Uma 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)

por este:

IID_PPV_ARGS(&pFileOpen)

A macro insere __uuidof(IFileOpenDialog) automaticamente 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 é tediosa, 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 Release 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 seu programa falhe.
  • Desreferenciando um ponteiro de interface após Release ser chamado. Esse bug pode causar 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 Release 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 é NULL.
  2. Chama Release se o ponteiro não for 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-sucedido, a chamada para SafeRelease liberará o ponteiro. Se CoCreateInstance falhar, pFileOpen permanecerá NULL. A SafeRelease função verifica isso e ignora a chamada para Release.

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 SafeRelease função é ú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 encapsula o ponteiro de interface subjacente e inicializa e libera 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 é chamada 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 main 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 Release 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 operator->() e operator&() para que a classe atue 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 COM CoCreateInstance 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 COCreateInstance COM, se preferir.

Avançar

Tratamento de erros no COM