Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Роджерсон Д. - Основы COM - 2000.pdf
Скачиваний:
412
Добавлен:
13.08.2013
Размер:
2.4 Mб
Скачать

36

Запрос интерфейса

Поскольку в СОМ все начинается и заканчивается интерфейсом, давайте и мы начнем с интерфейса, через который запрашиваются другие интерфейсы.

Клиент всегда взаимодействует с компонентом через некоторый интерфейс. Даже для запроса у компонента интерфейса используется специальный интерфейс IUnknown. Определение IUnknown, содержащееся в заголовочном файле UNKNWN.H, входящим в состав Win32 SDK, выглядит так:

interface IUnknown

{

virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv) = 0; virtual ULONG __stdcall AddRef() = 0;

virtual ULONG __stdcall Release() = 0;

}

В IUnknown имеется функция с именем QueryInterface. Клиент вызывает ее, чтобы определить, поддерживает ли компонент некоторый интерфейс. В этой главе я собираюсь поговорить о QueryInterface. В гл. 4 мы рассмотрим AddRef и Release, которые предоставляют способ управления временем жизни интерфейса.

IUnknown

Мне всегда казалось забавным название IUnknown. Это единственный интерфейс, о котором знают все клиенты и компоненты, и тем не менее это «неизвестный интерфейс»*. Происхождение названия просто. Все интерфейсы СОМ должны наследовать IUnknown. Таким образом, если у клиента имеется указатель на IUnknown, то клиент, не зная, указателем на какой именно интерфейс обладает, знает, что может запросить через него другие интерфейсы.

Клиент

 

IX

CA

 

 

 

 

 

Таблица виртуальных функций

 

 

 

 

 

 

 

 

 

 

 

 

 

pA

 

 

Указатель vtbl

 

 

QueryInterface

 

 

QueryInterface

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

AddRef

 

 

 

 

 

 

 

AddRef

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Release

 

 

 

 

 

 

 

Release

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Fx

 

 

 

 

 

 

 

Fx

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 3-1 Все интерфейсы СОМ наследуют IUnknown и содержат указатели на QueryInterface, AddRef и Release в первых трех элементах своих vtbl

Поскольку все интерфейсы СОМ наследуют IUnknown, в каждом интерфейсе есть функции QueryInterface, AddRef и Release — три первые функции в vtbl (см. рис. 3-1). Благодаря этому все интерфейсы СОМ можно полиморфно трактовать как интерфейсы IUnknown. Если в первых трех элементах vtbl интерфейса не содержатся указатели на три перечисленные функции, то это не интерфейс СОМ. Поскольку все интерфейсы наследуют IUnknown, постольку все они поддерживают QueryInterface. Таким образом, любой интерфейс можно использовать для получения всех остальных интерфейсов, поддерживаемых компонентом.

Поскольку все указатели интерфейсов являются также и указателями на IUnknown, клиенту не требуется хранить отдельный указатель на собственно компонент. Клиент работает только с указателями интерфейсов.

Получение указателя на IUnknown

Каким образом клиент может получить указатель на IUnknown? Мы используем функцию с именем CreateInstance, которая создает компонент и возвращает указатель на IUnknown:

IUnknown* CreateInstance();

Клиент использует CreateInstance вместо оператора new. В этой главе мы создадим простую версию данной функции, которую будем изменять на протяжении нескольких последующих глав в соответствии с нашими потребностями. В гл. 6 и 7 будет представлен «официальный» способ создания компонентов СОМ.

Теперь, когда мы знаем, как клиент получает указатель на IUnknown, давайте посмотрим, как он может использовать QueryInterface для получения других интерфейсов, а затем займемся реализацией QueryInterface.

* Unknown (англ.) — неизвестный. — Прим. перев.

37

Знакомство с QueryInterface

IUnknown содержит функцию-член QueryInterface, при помощи которой клиент определяет, поддерживается ли тот или иной интерфейс. QueryInterface возвращает указатель на интерфейс, если компонент его поддерживает; в противном случае возвращается код ошибки (тогда клиент может запросить указатель на другой интерфейс или аккуратно выгрузить компонент).

У QueryInterface два параметра:

Virtual HRESULT __stdcall QueryInterface(const IID& iid, void** ppv);

Первый параметр — идентификатор интерфейса, так называемая IID-структура. Более подробно IID будут рассматриваться в гл. 6. Пока же мы будем рассматривать их как константы, задающие интерфейс. Второй параметр — адрес, по которому QueryInterface помещает указатель на искомый интерфейс.

QueryInterface возвращает HRESULT; это не описатель (handle), как может показаться по названию. HRESULT — просто 32-разрядный код результата, записанный в определенном формате. QueryInterface может возвратить либо S_OK, либо E_NOINTERFACE. Клиент не должен прямо сравнивать возвращаемое QueryInterface значение с этими константами; для проверки надо использовать макросы SUCCEEDED или FAILED. Исчерпывающее обсуждение HRESULT содержится в гл. 6.

Теперь посмотрим, как используется, а затем — как реализуется QueryInterface.

Использование QueryInterface

Предположим, что у нас есть указатель на IUnknown, pI. Чтобы определить, можно ли использовать некоторый другой интерфейс, мы вызываем QueryInterface, передавая ей идентификатор нужного нам интерфейса. Если QueryInterface отработала успешно, мы можем пользоваться указателем:

void foo(IUnknown* pI)

{

//Определить указатель на интерфейс

IX* pIX = NULL;

//Запросить интерфейс IX

HRESULT hr = pI->QueryInterface(IID_IX, (void**)&pIX);

// Проверить значение результата if (SUCCEEDED(hr))

{

// Использовать интерфейс pIX->Fx();

}

}

В этом фрагменте кода мы запрашиваем у pI интерфейс, идентифицируемый с помощью IID_IX. Определение IID_IX содержится в заголовочном файле, предоставляемом компонентом (или, что более вероятно, оно извлекается из библиотеки типа, как будет показано в гл. 13).

Обратите внимание, что pIX устанавливается в NULL перед вызовом QueryInterface. Это пример хорошего программирования с защитой от ошибок. Как мы вскоре увидим, предполагается, что для неудачного запроса QueryInterface должна устанавливать возвращаемый указатель в NULL. Однако, поскольку QueryInterface реализуется программистом компонента, в некоторых реализациях это наверняка не будет сделано. Для безопасности следует установить указатель в NULL самостоятельно.

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

Реализация QueryInterface

Реализовать QueryInterface легко. Все, что нужно сделать, — это вернуть указатель интерфейса, соответствующего данному IID. Если интерфейс поддерживается, то функция возвращает S_OK и указатель. В противном случае возвращаются E_NOINTERFACE и NULL. Теперь давайте запишем QueryInterface для следующего компонента, реализуемого классом CA:

interface IX : IUnknown { /*...*/ }; interface IY : IUnknown { /*...*/ };

class CA : public IX, public IY { /*...*/ };

Иерархия наследования для этого класса и его интерфейсов показана на рис. 3-2.

38

 

IUnknown

 

 

 

IUnknown

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IX

 

 

 

IY

 

 

 

 

 

 

 

 

CA

Рис. 3-2 Иерархия наследования для приведенного выше фрагмента кода

Невиртуальное наследование

Обратите внимание, что IUnknown — не виртуальный базовый класс. IX и IY не могут наследовать IUnknown виртуально, так как виртуальное наследование приводит к vtbl, несовместимой с форматом СОМ. Ели бы IX и IY наследовали IUnknown виртуально, то первые три элемента их vtbl не были бы указателями на три функции-члена IUnknown.

Следующий фрагмент кода реализует QueryInterface для класса из приведенного выше фрагмента кода. Эта версия функции возвращает указатели на три разных интерфейса — IUnknown, IX и IY. Обратите внимание, что возвращаемый указатель на IUnknown всегда один и тот же несмотря на то, что класс CA наследует два таких интерфейса (от IX и от IY).

HRESULT __stdcall CA::QueryInterface(const IID& iid, void** ppv)

{

if (iid == IID_IUnknown)

{

*ppv = static_cast<IX*>(this);

}

else if (iid == IID_IX)

{

// Клиент запрашивает интерфейс IX *ppv = static_cast<IX*>(this);

}

else if (iid = IID_IY)

{

// Клиент запрашивает интерфейс IY *ppv = static_cast<IY*>(this);

}

else

{

//Мы не поддерживаем запрашиваемый клиентом интерфейс.

//Установить возвращаемый указатель в NULL.

*ppv = NULL;

return E_NOINTERFACE;

}

static_cast<IUnknown*>(*ppv)->AddRef(); // См. гл. 4 return S_OK;

}

Здесь для реализации QueryInterface использован простой оператор if-then-else. Вы можете использовать любой другой способ, обеспечивающий проверку и ветвление. Мне случалось встречать реализации на основе массивов, хэш-таблиц и деревьев; они полезны, когда компонент поддерживает много интерфейсов. Нельзя, однако, использовать оператор case*, поскольку идентификатор интерфейса — структура, а не константа.

Обратите внимание, что QueryInterface устанавливает указатель интерфейса в NULL, если интерфейс не поддерживается. Это не только требование СОМ, это вообще полезно; NULL вызовет фатальную ошибку в клиентах, которые не проверяют возвращаемые значения. Это менее опасно, чем позволить клиенту выполнять произвольный код, содержащийся по неинициализированному указателю. Кстати, вызов AddRef в конце QueryInterface в настоящий момент ничего не делает. Реализацией AddRef мы займемся в гл. 4.

Основы приведения типов

Вы, вероятно, заметили, что QueryInterface выполняет приведение указателя this, прежде чем сохранить его в ppv. Это очень важно. В зависимости от приведения, значение, сохраняемое в ppv, может изменяться. Да-да, приведение this к указателю на IX дает не тот же адрес, что приведение к указателю на IY. Например:

* Словом case этот оператор чаще называется в языках семейства Pascal. В С и С++ соответствующий оператор называется switch, хотя ключевое слово case также используется в синтаксисе данного оператора. — Прим. перев.

39

static_cast<IX*>(this) != static_cast<IY*>(this) static_cast<void*>(this) != static_cast<IY*>(this)

или, для тех, кому привычнее старый стиль,

(IX*)this != (IY*)this (void*)this != (IY*)this

Изменение указателя this при приведении типа обусловлено тем, как в С++ реализовано множественное наследование. Более подробно об этом рассказывает врезка «Множественное наследование и приведение типов».

Перед присваиванием указателю, описанному как void, надо всегда явно приводить this к нужному типу. Интересная проблема связана с возвратом указателя на IUnknown. Можно было бы написать:

*ppv = static_cast<IUnknown*>(this); // неоднозначность

Однако такое приведение неоднозначно, поскольку IUnknown наследуют оба интерфейса, IX и IY. Таким образом,

следует выбрать, какой из указателей — static_cast<IUnknown*>(static_cast<IX*>(this)) или static_cast<IUnknown*>(static_cast<IY*>(this)) — возвращать. В данном случае выбор не существенен, поскольку реализации указателей идентичны. Однако Вы должны действовать по всей программе единообразно, поскольку указатели не идентичны — а СОМ требует, чтобы для IUnknown всегда возвращался один и тот же указатель. Это требование будет обсуждаться далее в этой главе.

Множественное наследование и приведение типов

Обычно приведение указателя к другому типу не изменяет значения. Однако для поддержки множественного наследования С++ в некоторых случаях изменяет указатель на экземпляр класса. Большинство программистов на С++ не знают об этом побочном эффекте множественного наследования. Предположим, что у нас есть С++- класс CA:

class CA : public IX, public IY { ... }

Так как CA наследует и IX, и IY, то мы можем использовать указатель на CA везде, где можно использовать указатель на IX или IY. Указатель на CA можно передать функции, принимающей указатель на IX или IY, и функция будет работать правильно. Например:

void foo(IX* pIX); void bar(IY* pIY);

int main()

{

CA* pA = new CA; foo(pA); bar(pA);

delete pA; return 0;

}

foo требуется указатель на указатель таблицы виртуальных функций IX, тогда как bar — указатель на указатель таблицы виртуальный функций IY. Содержимое таблиц виртуальных функций IX и IY, конечно же, разное. Мы не можем передать bar указатель vtbl IX и ожидать, что функция будет работать. Таким образом, компилятор не может передавать один и тот же указатель и foo, и bar, он должен модифицировать указатель на CA так, чтобы тот указывал на подходящий указатель виртуальной таблицы. На рис. 3-3 показан формат размещения объекта CA в памяти.

CA::this

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IX Указатель vtbl

 

IY

 

Таблица виртуальных

 

 

 

 

 

(IX*)CA::this

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IY Указатель vtbl

 

 

 

 

 

 

функций

 

(IY*)CA::this

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

QueryInterface

 

 

 

 

 

 

 

 

 

 

 

 

Данные

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

AddRef

 

 

 

 

 

 

экземпляра для CA

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

IX

 

 

 

 

 

 

 

 

 

 

 

 

 

Release

Fx

CA

QueryInterface

AddRef

IY

Release

Fx

Рис. 3-3 Формат памяти для класса CA, который множественно наследует IX и IY

Соседние файлы в предмете Программирование на C++