Глава 3
QueryInterface

Вы - млекопитающее?
> да
У Вас две ноги?
> нет
У Вас длинные уши?
> да
Вы крупное животное?
> нет
Вы кролик?
> да
Тот, кто достаточно долго работает с компьютерами, вспомнит одну из первых компьютерных игр — «Угадай животное» (Animal). Эта проста: маленькая программа демонстрировала, что компьютер может обучаться она была чем-то вроде примитивной экспертной системы. Человек должен был задумать животное, компьютер задавал ему вопросы и пытался отгадать задуманное.

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

В этой главе мы увидим, что клиент СОМ во многом напоминает программу Animal. Animal не знала, какое животное Вы задумали; клиент СОМ не знает, какие интерфейсы поддерживает компонент. Чтобы определить, поддерживается ли некоторый интерфейс, клиент запрашивает у компонента этот интерфейс во время выполнения. Это похоже на то, как программа Animal расспрашивала Вас о характерных чертах задуманного животного. Более того, как программа Animal на самом деле мало что понимала в животных, так и клиент СОМ не имеет полного представление и возможностях компонента, который использует.

Однажды летом я учил первоклассников программировать на Logo. На занятиях мы писали на Logo версию Animal. Поиграв день с программой, мы распечатали созданное ею двоичное дерево и узнали кое-что интересное. С точки зрения Animal, кролик — это четвероногое млекопитающее с большими ушами, тогда как слон — большое четвероногое млекопитающее с большими ушами. Что и говорить, представление программы о слоне или кролике весьма ограниченно.

Знания клиента о компоненте также весьма ограниченны. Клиент может запросить компонент, поддерживает ли тот некоторый интерфейс. Задав достаточно вопросов, клиент получит сравнительно определенное представление о компоненте, но оно никогда не бывает полным. Дети, которых я учил, прекрасно представляли, как выглядит кролик, но программа Animal этого не знала.

Если у Animal недостаточное понимание предмета было побочным эффектом реализации, то для клиента недостаточное понимание существа компонента — не случайность. Мы хотим, чтобы клиент знал о компоненте настолько мало, насколько возможно. Чем меньше его знания, тем не менее можно изменить компонент, не нарушив работы клиента. Если клиент должен явно запрашивать у компонента каждый интерфейс, который собирается использовать, то на клиент и влияют изменения только интерфейсов. Компонент можно заменить другим, предоставляющим дополнительные интерфейсы, не, влияя на клиент. Если клиент не использует некоторый интерфейс, то новому компоненту нет необходимости его поддерживать.

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

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

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

Клиент всегда взаимодействует с компонентом через некоторый интерфейс. Даже для запроса у компонента интерфейса используется специальный интерфейс 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, то клиент, не зная, указателем на какой именно интерфейс обладает, знает, что может запросить через него другие интерфейсы. РИС. 3-1. Все интерфейсы СОМ наследуют IUnknown и содержат указатели в QueryInterface, AddRef и Release в первых трех элементах своих vtbl.

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

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

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

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

IUnknown* CreateInstance();

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

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

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

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

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

virtual HRESULT __stdcall 
	QueryInterface(const IID& ild, void**);

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

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

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

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

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

void foo(IUnknown* pi)
{
// Определить указатель на интерфейс
IX* рIХ = NULL;
// Запросить интерфейс IX
HRESULT hr = pI->QueryInterface(IID_IX, (void**)&pIX);
// Проверить значение результата
if (SUCCEEDED(hr))
{
// Использовать интерфейс
pIXFx.O ;
}
}

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

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

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

Реализация QueryInterface

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

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

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

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

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

HRESULT __stdcall 
	CA::QueryInterface(const IID& iid, void** ppv)
{
if (iid == IID_IUnknown)
{
// Клиент запрашивает интерфейс IUnknown
*ppv = static_cast(this);
}
else if (iid == IID_IX)
{
// Клиент запрашивает интерфейс IX
*ppv = static_cast(this);
}
else if (iid == IID_IY)
{
// Клиент запрашивает интерфейс IY
*ppv = static_cast(this);
}
else
{
// Мы не поддерживаем запрашиваемый 
// клиентом интерфейс.
// Установить возвращаемый указатель в NULL.
*ppv = NULL;
return E_NOINTERFACE ;
}
static_cast(*ppv)->AddRef(); // См. гл. 4.
return S_OK;
}

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

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

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

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

static_cast(this) != static_cast(this)
static_cast(this) != static_cast(this)
или, для тех, кому привычнее старый стиль,
(IX*)this != (IY*)this
(void*)this != (IY*)this

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

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

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

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

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

class СА : public IX, public IY {...}

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

void foo(IX* pIX);
void bar(IY* plY);
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, он должен модифицировать указатель на СА так, чтобы тот указывал на подходящий указатель виртуальной таблицы. На рис. 3-3 показан формат размещения объекта СА в памяти.
РИС. З-З. Формат памяти для класса СА, который множественно

Из рис. 3-3 видно, что указатель this для СА указывает на указатель таблицы виртуальных функций IX. Таким образом, мы можем без изменения использовать для СА указатель this вместо указателя на IX. Однако очевидно, что указатель this для СА не указывает на указатель vtbl IY. Следовательно, указатель this СА надо модифицировать, прежде чем передавать функции, ожидающей указатель на IY. Для этого компилятор добавляет к указателю this СА смещение указателя vtbl IY. Приведенный ниже код:

IY* рС = рА;
компилятор транслирует во что-то вроде
IY* рС = (char*)pA + AIY;
Более подробную информацию Вы можете найти в разделе «Multiple Inheritance and Casting» книги Маргарет А. Эллис (Margaret A. Ellis) и Бьярна Страустрапа (Bjarne Strourstrup) The Annotated C++ Reference Manual. Компиляторы C++ не обязаны реализовывать vtbl при множественном наследовании именно так, как это показано на рис. 3-3.

А теперь все вместе

Давайте соберем вместе все элементы и рассмотрим полный пример реализации и использования QueryInterface. В листинге 3-1 представлен полный текст этой простой программы. Копию программы можно найти на прилагаемом к книге компакт-диске. Программа состоит из трех частей.

В первой части объявляются интерфейсы IX, IY и IZ. Интерфейс IUnknown объявлен в заголовочном файле UNKNOWN.H Win32 SDK.

Вторая часть — это реализация компонента. Класс СА реализует компонент, поддерживающий интерфейсы IX и ГУ. Реализация QueryInterface совпадает с той, что была представлена в предыдущем разделе книги. Функция CreateInstance определена после класса СА. Клиент использует ее, что бы создать компонент, представляемый при помощи СА, и получить указатель на IUnknown этого компонента.

После CreateInstance следуют определения IID для интерфейсов. Как видно из этих определений, IID — весьма громоздкая структура (более подробно мы рассмотрим ее в гл. 7). Наш пример программы компонуется с UUID.LIB чтобы получить определения для IID_IUnknown (т. е. IID для IVnknown).

Третья и последняя часть — функция main, которая выступает в качестве клиента.

//IUNKNOWN.CPP
//
// IUnknown.cpp
// Чтобы скомпилировать: cl IUnknown.cpp UUID.lib
//

#include 
#include 
void trace(const char* msg) { cout << msg << endl;}
// Интерфейсы
interface IX : IUnknown
{
	virtual void __stdcall Fx() = 0;
};
interface IY : IUnknown
{
	virtual void __stdcall Fy() = 0;
};

interface IZ : IUnknown
{
	virtual void __stdcall Fz() = 0;
};

// Предварительные объявления GUID
extern const IID IID_IX;
extern const IID IID_IY;
extern const IID IID_IZ;
//
// Компонент
//
class CA :	public IX,
			public IY
{
// Реализация IUnknown
virtual HRESULT __stdcall 
	QueryInterface(const IID& iid, void** ppv);
virtual ULONG __stdcall AddRef() { return 0;}
virtual ULONG __stdcall Release() { return 0;}
//Реализация интерфейса IX
virtual void __stdcall Fx() { cout << "Fx" << endl;}
//Реализация интерфейса IY
virtual void __stdcall Fy() { cout << "Fy" << endl;}
};
HRESULT __stdcall 
	CA::QueryInterface(const IID& iid, void** ppv)
{
	if (iid == IID_IUnknown)
	{
	 trace
	  ("QueryInterface: Вернуть указатель на IUnknown.");
		*ppv = static_cast(this);
	}
	else if (iid == IID_IX)
	{
	 trace
	  ("QueryInterface: Вернуть указатель на IX.");
		*ppv = static_cast(this);
	}
	else if (iid == IID_IY)
	{
	 trace
	  ("QueryInterface: Вернуть указатель на IY.");
		*ppv = static_cast(this);
	}
	else 
	{
	 trace
	  ("QueryInterface: Интерфейс не поддерживается.");
		*ppv = NULL;
		return E_NOINTERFACE;
	}
	reinterpret_cast(*ppv)->AddRef(); // См. гл.4.
	return S_OK;
}
// 
// Функция создания
//
IUnknown* CreateInstance()
{
IUnknown* pI = static_cast(new CA);
pI->AddRef();
return pI;
}

//
// IID
//
// {32bb8320-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IX =
{0x32bb8320, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

// {32bb8321-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IY =
{0x32bb8321, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

// {32bb8322-b41b-11cf-a6bb-0080c7b2d682}
static const IID IID_IZ =
{0x32bb8322, 0xb41b, 0x11cf,
{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}};

//
// Клиент
//

int main()
{ 
	HRESULT hr;
	trace
	("Клиент:     Получить указатель на IUnknown.");
	IUnknown* pIUnknown = CreateInstance();
	
	trace("Клиент: Получить интерфейс IX.");
	IX* pIX = NULL;
	
	hr = 
	 pIUnknown->QueryInterface(IID_IX, (void**)&pIX);
	if (SUCCEEDED(hr))
	{
		trace("Клиент: IX получен успешно.");
		pIX->Fx();     // Использовать интерфейс IX.
	}
	
	trace ("Клиент: Получить интерфейс IY.");
	IY* pIY = NULL;

	hr = 
	 pIUnknown->QueryInterface(IID_IY, (void**)&pIY);

	if (SUCCEEDED(hr))
	{
		trace("Клиент: IY получен успешно.");
		pIY->Fy();     // Использовать интерфейс IY.
	}

	trace("Клиент: Запросить неподдерживаемый интерфейс.");

	IZ* pIZ = NULL;
	hr = pIUnknown->QueryInterface(IID_IZ, (void**)&pIZ);

	if (SUCCEEDED(hr))
	{
		trace("Клиент: Интерфейс IZ получен успешно.");
		pIZ->Fz();
	}
	else 
	{
		trace("Клиент: Не могу получить интерфейс IZ.");
	}
	trace("Клиент: Получить интерфейс IY через интерфейс IX.");

	IY* pIYfromIX = NULL;

	hr = pIX->QueryInterface(IID_IY, (void**)&pIYfromIX);

	if (SUCCEEDED(hr))

	{

		trace("Клиент: IY получен успешно.");

		pIYfromIX->Fy();
	}

	trace("Клиент:   Получить интерфейс IUnknown через IY.");
	IUnknown* pIUnknownFromIY = NULL;
	hr = pIY->QueryInterface(IID_IUnknown, (void**)&pIUnknownFromIY);

	if (SUCCEEDED(hr))
	{
		cout << "Совпадают ли указатели на IUnknown? ";
		if (pIUnknownFromIY == pIUnknown)
		{
		 cout << "Да, pIUnknownFromIY == pIUnknown." << endl;
		}
		else
		{
		 cout << "Нет, pIUnknownFromIY != pIUnknown." << endl;
		}
	}
// Удалить компонент.
delete pIUnknown;

return 0;
}

Эта программа выдает на экран следующее:

Клиент:                  Получить указательна IUnknown.
Клиент:                  Получить интерфейс IX.
QueryInterface:          Вернуть указатель на IX.
Клиент:                  IX получен успешно.
Клиент:                  Получить интерфейс IY.
QueryInterface:          Вернуть указатель на IY.
Клиент:                  IY получен успешно.
Клиент:                  Запросить неподдерживаемый интерфейс.
QueryInterface:          Интерфейс не поддерживается.
Клиент:                  Не могу получить интерфейс IZ.
Клиент:                  Получить интерфейс IY через интерфейс IX.
QueryInterface:          Вернуть указатель на IY.
Клиент:                  IY получен успешно.
Клиент:                  Получить интерфейс IUnknown через IY.
QueryInterface:          Вернуть указатель на IUnknown.
Совпадают ли указатели   Да, pIUnknownFromlY == pIUnknown.
IUnknown?

Клиент начинает с создания компонента при помощи CreateInstance. CreateInstance возвращает указатель на интерфейс IUnknown компонента. Клиент при помощи QueryInterface запрашивает через интерфейс IUnknown указатель на интерфейс IX компонента. Для проверки успешного окончания используется макрос SUCCEEDED. Если указатель на /Хполучен успешно, то клиент с его помощью вызывает функцию этого интерфейса Fx. Затем клиент использует указатель на IUnknown, чтобы получить указатель на интерфейс IY. В случае успеха клиент пользуется этим указателем. Поскольку класс СА реализует как IX, так и IY, QueryInterface успешно обрабатывает запросы на эти интерфейсы. Однако СА не реализует интерфейс IZ. Поэтому — когда клиент запрашивает этот интерфейс, QueryInterface возвращает код ошибки E_NOINTERFACE. Макрос SUCCEEDED возвращает FALSE, и pIZ не используется (для доступа к функциям-членам IZ).

Теперь мы дошли до по-настоящему интересных вещей. Клиент запрашивает указатель на интерфейс IY через указатель на интерфейс IX, рIХ. Поскольку компонент поддерживает IY, этот запрос будет успешным, и клиент сможет использовать возвращенный указатель на интерфейс IY так же, как он использовал первый указатель.

Наконец, клиент запрашивает интерфейс IUnknown через указатель на IY. Поскольку все интерфейсы СОМ наследуют IUnknown, этот запрос должен быть успешным. Однако самое интересное, что возвращенный указатель на IUnknown, pIUnknownFromlY, совпадает с первым указателем на IUnknown, pIUnknown. Как мы увидим далее, это одно из требований СОМ: Querylnteface должна, возвращать один и тот же указатель на все запросы к IUnknown.

Пример показывает, что при помощи QueryInterface можно получить любой из интерфейсов СА через любой другой. Это одно из важных правил реализации QueryInterface. Давайте более подробно рассмотрим его и другие правила.

Правила и соглашения QueryInterface

В этом разделе представлены некоторые правила, которым должны следовать все реализации QueryInterface. Если их выполнять, клиент сможет узнать о компоненте достаточно, чтобы (надеяться) управлять им и использовать его в своих целях. Без этих правил поведение Querylnterface было бы неопределенным, и писать программы было бы невозможно.

Теперь рассмотрим эти правила подробно.

Вы всегда получаете один и тот же IUnknown

У данного экземпляра компонента есть только один интерфейс IUnknown Всегда, когда Вы запрашиваете у компонента IUnknown (не важно, черезкакой интерфейс), в ответ вы получите одно и то же значение указателя Вы можете определить, указывают ли два интерфейса на один компонент запросив у каждого из них IUnknown и сравнив результаты. Приведенная ниже функция SameComponents определяет, указывают ли р1Х и рГУ на ин терфейсы одного компонента:

BOOL SameComponents(IX* pIX, IY* plY)
{

IUnknown* pl1 = NULL;

IUnknown* pl2 = NULL;

// Получить указатель на IUnknown через pIX.
pIX->QueryInterface(IID_IUnknown, (void**)&pl1);

// Получить указатель на IUnknown через plY.
pIY->QueryInterface(IID_IUnknown, (void**)&pl2);

// Сравнить полученные указатели.
return pl1 == pl2;

}

Это важное правило. Без него нельзя было бы определить, указывают ли два интерфейса на один и тот же компонент.

Вы можете получить интерфейс снова, если смогли получить его раньше

Если QueryInterface однажды успешно обработала запрос на некоторый ин- 1 терфейс, то все последующие запросы для того же компонента также бу- ] дут успешными. Если же запрос был неудачным, то для этого интерфейса QueryInterface всегда будет возвращать ошибку. Это правило относится толь- ко к конкретному экземпляру компонента. Ко вновь созданному экземпля- ру оно неприменимо.

Представьте себе, что произошло бы, если бы набор поддерживаемых интерфейсов мог изменяться со временем. Писать код клиента было бы крайне сложно. Когда клиент должен запрашивать интерфейсы у компо- нента? Как часто это делать? Что произойдет, если клиент не сможет по- лучить интерфейс, который только что использовал? Без фиксированно- го набора интерфейсов клиент не мог бы сколько-нибудь уверенно опре- делить возможности компонента.

Вы можете снова получить интерфейс, который у Вас уже есть

Если у Вас есть интерфейс /X, то Вы можете запросить через него интер- фейс IX и получите в ответ указатель на IX. Код выглядит так:

void f(IX* pIX)
{
	IX* рIХ2 = NULL ;
	// Запросить IX через IX.
	HRESULT hr = pIX->QueryInterface(IID_IX, (void**)&pIX2);
	assert(SUCCEEDED(hr));// Запрос должен быть успешным.
}

Это правило звучит несколько странно. Зачем Вам интерфейс, который у Вас уже есть? Вспомните, однако, что все интерфейсы полиморфны относительно IUnknown и многим функциям передается указатель на IUnknown. У этих функций должна быть возможность использовать любой указатель на IUnknown и получить по нему любой другой интерфейс. Это иллюстрирует приведенный ниже пример:

void f(IUnknown* pI)
{
	HRESULT hr;
	IX* pIX = NULL;

	// Запросить IX через pi.
	hr = pI->QueryInterface(IID_IX, (void**)&pIX);

	// Что-нибудь содержательное.
}
void main()
{
	// Получаем откуда-то указатель на IX.
	IX* pIX = GetIX();
	// Передаем его в функцию.
	f(pIX);
}

Функция f сможет получить указатель на IX по переданному ей указателю, хотя последний и так указывает на IX.

Вы всегда можете вернуться туда, откуда начали

Если у Вас есть указатель на интерфейс IX и с его помощью Вы успешно получаете интерфейс IY, то можно получить «обратно» интерфейс IX через указатель на IY. Иными словами, независимо от того, какой интерфейс у Вас есть сейчас, можно снова получить интерфейс, с которого Вы начала. Это иллюстрирует следующий код:
void f(IUnknown* pI)
{
	HRESULT hr;
	IX* pIX2 = NULL;
	IY* pIY = NULL;

	// Получить IY через IX.
	hr = pIX->QueryInterface(IID_IY, (void**)&pIY);
	if(SUCCEEDED(hr))
	{
		// Получить IX через IY.
		hr = pIY->QueryInterface(IID_IX, (void**)&pIX2);
		// QueryInterface должна отработать успешно.
		assert(SUCCEEDED(hr));
	}
}

Если Вы смогли попасть куда-то хоть откуда-нибудь, Вы можете попасть туда откуда угодно

Если Вы можете получить у компонента некоторый интерфейс, то его можно получить с помощью любого из интерфейсов, поддерживаемых компонентом. Если можно получить интерфейс IY через IX, a IZ— через IY, то IZ можно получить и через IX. В программе это выглядит так:

void f(IX* pIX)
{
HRESULT hr;
IY* plY = NULL;
// Запросить IY у IX.
hr = pIX->QueryInterface.(IID_IY, (void**)&pIY) ;
if (SUCCEDED(hr))
{
IZ* pIZ = NULL ;
// Запросить IZ у IY.
hr = pIY->QueryInterface(IID_IZ, (void**)&pIZ) ;
if (SUCCEEDED^hr))
{
// Запросить IZ у IX.
hr = pIX->QueryInterface(IID_IZ, (void**)&pIZ) ;
// Это должно работать.
assert(SUCCEEDED(hr)) ;
}
}
}

Это правило делает QueryInterface пригодной для использования. Вообразите что сталось бы, если бы получение указателя на интерфейс зависело от того через какой интерфейс Вы делаете запрос. Вы редактируете код своего клиента, переставляя две функции местами, — и все перестает работать. Написать клиент для такого компонента было бы практически невозможно.

Общая задача всех приведенных правил — сделать использование QueryInterface простым, логичным, последовательным и определенным. По счастью, при реализации QueryInterface для компонента следовать правилам нетрудно. Если компоненты реализуют QueryInterface корректно, то клиенту нет нужды беспокоиться об этих правилах. Пожалуйста, учтите, что простота реализации и использования QueryInterface не снижают значения этой функции. В СОМ нет ничего важнее, чем QueryInterface.

QueryInterface определяет компонент

QueryInterface — это самая важная часть СОМ, поскольку она определяет компонент. Интерфейсы, поддерживаемые компонентом, — это те интерфейсы указатели на которые возвращает QueryInterface. Их определяет реализация QueryInterface, а не заголовочный файл для класса C++, реализующего компонент. Компонент не определяется и иерархией наследования этого класса C++. Его определяет исключительно реализация QueryInterface.

Поскольку реализация QueryInterface для клиента невидима, то он не знает, какие интерфейсы поддерживаются. Единственный способ, которым клиент может об этом узнать, — запросить компонент. Такой порядок совершенно отличен от обычного в C++, где клиент имеет заголовочный файл класса и знает обо всех членах последнего. В некотором смысле концепция СОМ больше напоминает знакомство с человеком на вечеринке, чем при приеме на работу. Придя наниматься на работу, человек представляет Вам резюме, его характеризующее. Такое резюме похоже на объявление класса C++. На вечеринке же новый знакомый не вручает Вам никакой «сводки данных» о себе; чтобы узнать о нем, Вы задаете вопросы. Это больше похоже на практику СОМ (и на игру Animal).

Вы не можете воспользоваться всеми знаниями сразу

Первым вопросом, который я задал при изучении СОМ, был: «Почему я не могу сразу запросить у компонента все его интерфейсы?». Ответ в духе Дзэн гласит: «Что станешь ты делать со списком интерфейсов, поддерживаемых компонентом?» Оказывается, это очень хороший ответ (хотя он и сформулирован как вопрос).

Допустим на мгновение, что клиент может запросить у компонента все поддерживаемые им интерфейсы. Допустим, что наш компонент поддерживает интерфейсы IX и IY, но клиент был написан раньше и ничего не знает об IY. Итак, он создает компонент и запрашивает его интерфейсы. Компонент должен был бы вернуть IХ и IY. Клиенту неизвестен интерфейс IY, поэтому он никак не может его использовать. Чтобы клиент мог сделать что-либо осмысленное с непонятным интерфейсом, он должен был бы прочитать документацию этого интерфейса и написать соответствующий код. При сегодняшнем уровне развития технологии это невозможно.

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

СОМ все же предоставляет средство, библиотеки типа (type libraries), для определения интерфейсов, которые предоставляет компонент, во время выполнения. Хотя клиент может использовать библиотеку типа для определения параметров функций некоторого интерфейса, он по-прежнему не знает, как писать программы, использующие эти функции. Эта работа остается программисту. Библиотеки типа будут рассматриваться в гл. 11.

Во многих случаях клиенты могут использовать только компоненты, реализующие определенный набор интерфейсов. Создавать компонент, запрашивать у него интерфейсы по одному и в конце концов выяснить, что он не поддерживает один из нужных, — это пустая трата времени. Чтобы ее избежать, можно определить объединенный набор интерфейсов как категорию компонентов (component category). Затем компоненты могут опубликовать сведения о том, принадлежат ли они к некоторой категории. Эту информацию клиенты могут получить, не создавая компонентов; подробнее об этом будет рассказано в гл. 6.

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

Работа с новыми версиями компонентов

Как Вы уже знаете, интерфейсы СОМ неизменны. После того, как интерфейс опубликован и используется каким-либо клиентом, он никогда не меняется. Но что именно я имею в виду, когда говорю, что интерфейсы остаются теми же? У каждого интерфейса имеется уникальный идентификатор интерфейса (IID). Вместо того, чтобы изменять интерфейс, фактически нужно создать новый, с новым IID. Если QueryInterface получает запрос со старым IID, она возвращает старый интерфейс. Если же QueryInterface получает запрос с новым IID, то возвращается новый интерфейс. С точки зрения QueryInterface IID м есть интерфейс.
QueryMultipleInterfaces
В распределенной СОМ (DCOM) определен новый интерфейс IMultiQI. Он имеет единственную функцию-член — QueryMultipleInterfaces. Эта функция позволяет клиенту запросить у компонента несколько интерфейсов за один вызов. Один вызов QueryMuttipleInterfaces заменяет несколько циклов «запрос-ответ» по сети, что повышает производительность.

Итак, интерфейс, соответствующий данному IID, неизменен. Новый интерфейс может наследовать старому или быть совершенно другим. На существующие клиенты это не влияет, так как старый интерфейс не меняется. Новые же клиенты могут использовать как старые, так и новые компоненты, поскольку могут запрашивать как старый, так и новый интерфейс.

Основная сила этого метода работы с версиями в его прозрачности. Клиенту не требуется выполнять никаких действий, чтобы проверить, имеет ли он дело с корректной версией интерфейса. Если клиент может найти интерфейс, то этот интерфейс корректен. Существо интерфейса полностью связано с его версией. Если изменяется версия, то изменяется и интерфейс. Здесь не может возникнуть никакой путаницы.

В качестве примера предположим, что у нас есть программа моделирования полета, названная Pilot, которая использует средства разных поставщиков для моделирования летательных аппаратов. Для того, чтобы работать с Pilot, компонент-«летательный аппарат» должен реализовывать интерфейс IFly. Пусть у одного из поставщиков имеется такой компонент, называемый Bronco и поддерживающий интерфейс IFly. Мы решаем модернизировать Pilot и выпускаем новую версию, FastPilot. FastPilot расширяет набор «поведений» самолета при помощи интерфейса IFlyFast, в дополнение к IFly. Компания, продающая Bronco, добавляет интерфейс FlyFast и создает FastBronco.

FastPilot по-прежнему поддерживает IFly, поэтому, если у пользователя есть копия Bronco, то FastPilot по-прежнему может ее использовать. FastPilot будет сначала запрашивать у компонента IFlyFast, а. если компонент его не поддерживает, - IFly. FastBronco no-прежнему поддерживает IFly, так что если у кого-то есть старый Pilot, то FastBronco будет работать и с ним. На рис. 3-4 возможные взаимосвязи представлены графически.

РИС. 3-4. Различные комбинации старых и новых версий клиентов и компонентов.

В результате клиент и компонент смогут работать в любом сочетании.

В некоторых случаях новый компонент или клиент не могут поддерживать обратную совместимость, поскольку та сложна или слишком медленно работает. Однако и тогда обработка версий в СОМ не теряет своей силы. Как и раньше, IID интерфейса определяет его версию. Всякий раз, когда клиент получает интерфейс, он получает корректную версию, поскольку другая версия — это другой интерфейс с другим IID.

Когда нужно создавать новую версию

Для того, чтобы механизм версий СОМ работал, программисты должны строго следить за присвоением новым версиям существующих интерфейсов новых IID. Вы обязаны создать новый интерфейс с новым IID, если изменили хотя бы одну из приведенных ниже характеристик:

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

Имена версий интерфейсов

Если Вы создаете новую версию интерфейса, следует изменить и его имя. Стандартное соглашение СОМ на этот счет заключается в добавлении номера к концу имени. Согласно ему, IFly становится IFly2, а не IFastFly. Конечно, свои собственные интерфейсы Вы можете называть как угодно. Если же интерфейсы принадлежат кому-либо другому, то следует запросить у него разрешение, прежде чем создавать новую версию или присваивать новое имя.

Неявные соглашения

Неизменность имен и параметров функций еще не гарантирует, что модификация компонента не повредит клиенту. Клиент использует функции интерфейса определенным образом или в определенной последовательности. Если реализация компонента изменяется так, что этот образ или последовательность действий не срабатывают, то работа клиента нарушится.

Это можно представить себе при -помощи аналогии. Юридические договоры должны ясно и четко указывать обязанности сторон. Однако, похоже, сколь бы коротким и простым ни был договор, в нем всегда найдется что-нибудь набранное мелким шрифтом. И это обязательно будет что-то, что Вы не считали важным, когда подписывали бумаги, но что теперь может обойтись в тысячи долларов. Размер шрифта не имеет значения, юридическая сила зависит не от него.

Интерфейс — своего рода форма договора между клиентом и компонентом. Как и во всех договорах, здесь есть кое-что «мелким шрифтом». В случае интерфейсов это способ их использования. Способ, которым клиент использует функции интерфейса, составляет предмет договора с компонентом, реализующим интерфейс. Если компонент изменяет реализацию интерфейса, он должен гарантировать, что клиент сможет пользоваться функциями прежним способом. В противном случае клиент не будет работать, и его придется перекомпилировать.. Пусть, например, клиент вызывает функции Fool, Foo2 и Foo3 в этом порядке. Если компонент изменится так, что первой нужно будет вызывать Foo3, он нарушит неявное-соглашение, определяющее способ и последовательность использования функций интерфейса.

Все интерфейсы «заключают» неявные соглашения. Это становится проблемой, только если мы хотим реализовать интерфейс способом, не совместимым с принятым порядком. Чтобы избежать нарушения неявного соглашения, у Вас есть два варианта. Первый заключается в том, чтобы сделать интерфейс работоспособным независимо от последовательности и способа вызова его функций-членов. Второй вариант — заставить всех клиентов использовать интерфейс одинаково и документировать этот порядок. Теперь, если компонент изменяется и нарушает работу клиента, он разрывает явный договор, а не неявный. Оба решения требуют огромной предусмотрительности и тщательного планирования.

«У Вас две ноги?»

Теперь Вы знаете, «что за животное» СОМ. QueryInterface — это единственная особенность, которая в действительности отличает создание компонентов СОМ от написания классов C++. QueryInterface дает СОМ большую часть ее гибкости и способности к инкапсуляции. QueryInterface определяет «поведения», поддерживаемые компонентом во время выполнения, и максимально использует силу динамической компоновки. Полностью скрывая детали компонента от клиента, QueryInterface максимально защищает последний от влияния возможных изменений компонента. QueryInterface также является «становым хребтом» элегантного и прозрачного механизма работы с версиями. Этот механизм позволяет старым и новым компонентам взаимодействовать и работать вместе.

В этой главе Вы также познакомились с IUnknown — корневым интерфейсом, поддерживаемым всеми другими. QueryInterface — это лишь одна из трех функций, составляющих IUnknown. В следующей главе мы увидим, как совместное использование двух его других функций-членов, AddRef и Release, заменяет оператор delete (который мы использовали в предыдущих примерах). Но, может, перед этим немного поиграем в Animal?

Имеете ли Вы отношение к удалению компонентов из памяти?
> да
Имеете ли Вы отношение к подсчету ссылок?
> да
Вы AddRef?
> нет
Кто же Вы?
> Release
Чем Вы отличаетесь от AddRef?
> уменьшаю счетчик ссылок
Спасибо

[Назад] [Содержание] [Вперед]


Смотрите также:



Hosted by uCoz