Compartir a través de


Procedimientos de codificación COM

En este tema se describen formas de hacer que el código COM sea más eficaz y sólido.

Operador __uuidof

Al compilar el programa, es posible que obtenga errores del vinculador similares a los siguientes:

unresolved external symbol "struct _GUID const IID_IDrawable"

Este error significa que se declaró una constante GUID con vinculación externa (extern) y el enlazador no encontró la definición de la constante. El valor de una constante GUID normalmente se exporta desde un archivo de biblioteca estática. Si usa Microsoft Visual C++, puede evitar la necesidad de vincular una biblioteca estática mediante el operador __uuidof. Este operador es una extensión de lenguaje de Microsoft. Devuelve un valor GUID de una expresión. La expresión puede ser un nombre de tipo de interfaz, un nombre de clase o un puntero de interfaz. Con __uuidof, puede crear el objeto Dialog de elemento común de la siguiente manera:

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

El compilador extrae el valor GUID del encabezado, por lo que no es necesario exportar ninguna biblioteca.

Nota

El valor GUID está asociado al nombre de tipo declarando __declspec(uuid( ... )) en el encabezado. Para obtener más información, consulte la documentación de __declspec en la documentación de Visual C++.

 

The IID_PPV_ARGS Macro

Hemos visto que Tanto CoCreateInstance como QueryInterface requieren coercción del parámetro final a un tipo void** . Esto crea la posibilidad de que un tipo no coincida. Observe el fragmento de código siguiente:

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

Este código solicita la interfaz IFileDialogCustomize , pero pasa un puntero IFileOpenDialog . La expresión reinterpret_cast evita el sistema de tipos de C++, por lo que el compilador no detectará este error. En el mejor de los casos, si el objeto no implementa la interfaz solicitada, la llamada simplemente produce un error. En el peor de los casos, la función se realiza correctamente y tiene un puntero no coincidente. En otras palabras, el tipo de puntero no coincide con la tabla virtual real en memoria. Como se puede imaginar, nada bueno puede suceder en ese momento.

Nota

Una tabla vtable (tabla de métodos virtuales) es una tabla de punteros de función. La tabla virtual es cómo COM enlaza una llamada de método a su implementación en tiempo de ejecución. No coincidentemente, las tablas virtuales son la forma en que la mayoría de los compiladores de C++ implementan métodos virtuales.

 

La macro IID_PPV_ARGS ayuda a evitar esta clase de error. Para usar esta macro, reemplace el código siguiente:

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

por este:

IID_PPV_ARGS(&pFileOpen)

La macro inserta __uuidof(IFileOpenDialog) automáticamente para el identificador de interfaz, por lo que se garantiza que coincida con el tipo de puntero. Este es el código modificado (y correcto):

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

Puede usar la misma macro con QueryInterface:

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

Patrón SafeRelease

El recuento de referencias es uno de los aspectos de la programación que es básicamente fácil, pero también tedioso, lo que facilita la equivocación. Entre los errores típicos se incluyen:

  • No se puede liberar un puntero de interfaz cuando haya terminado de usarlo. Esta clase de error hará que el programa filtre memoria y otros recursos, ya que los objetos no se destruyen.
  • Llamar a Release con un puntero no válido. Por ejemplo, este error puede producirse si el objeto nunca se creó. Esta categoría de error probablemente hará que el programa se bloquee.
  • Desreferenciar un puntero de interfaz después de llamar a Release . Este error puede provocar que el programa se bloquee. Peor, puede hacer que el programa se bloquee al azar más tarde, lo que dificulta el seguimiento del error original.

Una manera de evitar estos errores es llamar a Release a través de una función que libera el puntero de forma segura. El código siguiente muestra una función que hace esto:

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

Esta función toma un puntero de interfaz COM como parámetro y hace lo siguiente:

  1. Comprueba si el puntero es NULL.
  2. Llama a Release si el puntero no es NULL.
  3. Establece el puntero en NULL.

Este es un ejemplo de cómo 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);
}

Si CoCreateInstance se realiza correctamente, la llamada a SafeRelease libera el puntero. Si se produce un error en CoCreateInstance , pFileOpen sigue siendo NULL. La SafeRelease función comprueba esto y omite la llamada a Release.

También es seguro llamar SafeRelease a más de una vez en el mismo puntero, como se muestra aquí:

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

Punteros inteligentes COM

La SafeRelease función es útil, pero requiere que recuerde dos cosas:

  • Inicialice cada puntero de interfaz a NULL.
  • Llame SafeRelease a antes de que cada puntero salga del ámbito.

Como programador de C++, probablemente esté pensando que no debe recordar ninguna de estas cosas. Después de todo, por eso C++ tiene constructores y destructores. Sería bueno tener una clase que encapsula el puntero de interfaz subyacente e inicializa y libera automáticamente el puntero. En otras palabras, queremos algo parecido a esto:

// 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 definición de clase que se muestra aquí está incompleta y no se puede usar como se muestra. Como mínimo, tendría que definir un constructor de copia, un operador de asignación y una manera de acceder al puntero COM subyacente. Afortunadamente, no es necesario realizar ningún trabajo, ya que Microsoft Visual Studio ya proporciona una clase de puntero inteligente como parte de la Biblioteca de plantillas activas (ATL).

La clase de puntero inteligente ATL se denomina CComPtr. (También hay una clase CComQIPtr , que no se describe aquí). Este es el ejemplo Abrir cuadro de diálogo 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;
}

La diferencia principal entre este código y el ejemplo original es que esta versión no llama explícitamente a Release. Cuando la instancia de CComPtr sale del ámbito, el destructor llama a Release en el puntero subyacente.

CComPtr es una plantilla de clase. El argumento de plantilla es el tipo de interfaz COM. Internamente, CComPtr contiene un puntero de ese tipo. CComPtr invalida operator->() y operator&() para que la clase actúe como el puntero subyacente. Por ejemplo, el código siguiente equivale a llamar directamente al método IFileOpenDialog::Show :

hr = pFileOpen->Show(NULL);

CComPtr también define un método CComPtr::CoCreateInstance , que llama a la función COM CoCreateInstance con algunos valores de parámetro predeterminados. El único parámetro necesario es el identificador de clase, como se muestra en el ejemplo siguiente:

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

El método CComPtr::CoCreateInstance se proporciona exclusivamente como comodidad; Todavía puede llamar a la función COM CoCreateInstance , si lo prefiere.

Siguientes

Control de errores en COM