Поделиться через


Рекомендации по программированию COM

В этом разделе описываются способы повышения эффективности и надежности кода COM.

Оператор __uuidof

При сборке программы могут возникнуть ошибки компоновщика, аналогичные следующим:

unresolved external symbol "struct _GUID const IID_IDrawable"

Эта ошибка означает, что константу GUID объявляли с внешней компоновкой (экстерном), а компоновщик не мог найти определение константы. Значение константы GUID обычно экспортируется из статического файла библиотеки. Если вы используете Microsoft Visual C++, вы можете избежать необходимости связать статическую библиотеку с помощью оператора __uuidof . Этот оператор является расширением языка Майкрософт. Он возвращает значение GUID из выражения. Выражение может быть именем типа интерфейса, именем класса или указателем интерфейса. С помощью __uuidof можно создать объект Common Item Dialog следующим образом:

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

Компилятор извлекает значение GUID из заголовка, поэтому экспорт библиотеки не требуется.

Примечание.

Значение GUID связано с именем типа путем объявления __declspec(uuid( ... )) в заголовке. Дополнительные сведения см. в документации по __declspec в документации по Visual C++.

 

Макрос IID_PPV_ARGS

Мы видели, что как CoCreateInstance, так и QueryInterface требуют принудительного принуждения конечного параметра к типу void**. Это создает потенциал для несоответствия типа. Рассмотрим следующий фрагмент кода:

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

Этот код запрашивает интерфейс IFileDialogCustomize, но передает указатель IFileOpenDialog. Выражение reinterpret_cast обходит систему типов C++, поэтому компилятор не перехватывает эту ошибку. В лучшем случае, если объект не реализует запрошенный интерфейс, вызов просто завершается ошибкой. В худшем случае функция завершается успешно, и у вас есть несогласованный указатель. Другими словами, тип указателя не соответствует фактической vtable в памяти. Как вы можете себе представить, ничего хорошего может произойти в этот момент.

Примечание.

Vtable (таблица виртуальных методов) — это таблица указателей функций. Vtable — это то, как COM привязывает вызов метода к его реализации во время выполнения. Не случайно, vtables — это то, как большинство компиляторов C++ реализуют виртуальные методы.

 

Макрос IID_PPV_ARGS помогает избежать этого класса ошибок. Чтобы использовать этот макрос, замените следующий код:

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

следующим кодом:

IID_PPV_ARGS(&pFileOpen)

Макрос автоматически вставляется __uuidof(IFileOpenDialog) для идентификатора интерфейса, поэтому он гарантированно соответствует типу указателя. Ниже приведен измененный (и правильный) код:

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

Вы можете использовать тот же макрос с QueryInterface:

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

Шаблон Сейф Release

Подсчет ссылок является одной из тех вещей в программировании, что в основном легко, но также мучен, что делает его легко получить неправильно. Типовые ошибки включают:

  • Не удается освободить указатель интерфейса при его использовании. Этот класс ошибок приведет к утечке памяти и других ресурсов программы, так как объекты не уничтожаются.
  • Вызов выпуска с недопустимым указателем. Например, эта ошибка может произойти, если объект никогда не был создан. Эта категория ошибок, вероятно, приведет к сбою программы.
  • Отмена ссылки на указатель интерфейса после вызова выпуска. Эта ошибка может привести к сбою программы. Хуже, это может привести к сбою программы в случайное время, что затрудняет отслеживание исходной ошибки.

Одним из способов избежать этих ошибок является вызов выпуска через функцию, которая безопасно освобождает указатель. В следующем коде показана функция, которая делает это:

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

Эта функция принимает указатель com-интерфейса в качестве параметра и выполняет следующие действия.

  1. Проверяет, имеет ли указатель значение NULL.
  2. Вызывает выпуск, если указатель не имеет значения NULL.
  3. Задает указатель на NULL.

Ниже приведен пример использования 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);
}

Если CoCreateInstance выполнен успешно, вызов SafeRelease выпуска указателя. Если CoCreateInstance завершается ошибкой, pFileOpen остается NULL. Функция SafeRelease проверка для этого и пропускает вызов выпуска.

Также можно вызывать SafeRelease несколько раз в одном указателе, как показано ниже:

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

Смарт-указатели COM

Эта SafeRelease функция полезна, но она требует запоминания двух вещей:

  • Инициализировать каждый указатель интерфейса на NULL.
  • Вызов SafeRelease перед тем, как каждый указатель выходит из область.

Как программист C++ вы, вероятно, думаете, что вам не нужно помнить ни о чем из этих вещей. В конце концов, поэтому В C++ есть конструкторы и деструкторы. Было бы приятно иметь класс, который упаковывает базовый указатель интерфейса и автоматически инициализирует и освобождает указатель. Другими словами, мы хотим что-то подобное:

// Warning: This example is not complete.

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

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

Определение класса, показанное здесь, является неполным и не подходит для использования, как показано ниже. Как минимум, необходимо определить конструктор копирования, оператор назначения и способ доступа к базовому указателю COM. К счастью, вам не нужно делать никаких действий, так как Microsoft Visual Studio уже предоставляет интеллектуальный класс указателя в рамках библиотеки активных шаблонов (ATL).

Класс смарт-указателя ATL называется CComPtr. (Существует также Класс CComQIPtr, который здесь не обсуждается.) Ниже приведен пример "Открыть диалоговое окно", перезаписанный для использования 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;
}

Основное различие между этим кодом и исходным примером заключается в том, что эта версия явно не вызывает выпуск. Когда экземпляр CComPtr выходит из область, деструктор вызывает Release на базовом указателе.

CComPtr — это шаблон класса. Аргумент шаблона — это тип com-интерфейса. Внутри системы CComPtr содержит указатель этого типа. CComPtr переопределяет оператор->() и operator&(), чтобы класс действовал как базовый указатель. Например, следующий код эквивалентен вызову метода IFileOpenDialog::Show напрямую:

hr = pFileOpen->Show(NULL);

CComPtr также определяет метод CComPtr::CoCreateInstance, который вызывает функцию COM CoCreateInstance с некоторыми значениями параметров по умолчанию. Единственным обязательным параметром является идентификатор класса, как показано в следующем примере:

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

Метод CComPtr::CoCreateInstance предоставляется исключительно в качестве удобства. Вы по-прежнему можете вызвать функцию COM CoCreateInstance, если вы предпочитаете.

Следующий

Обработка ошибок в COM