Иллюстрированный самоучитель по Visual Studio .NET

Интерфейсы – основа СОМ-технологии

Разработчики СОМ не интересуются тем, как устроены компоненты внутри, но озабочены тем, как они представлены снаружи. Каждый компонент или объект СОМ рассматривается как набор свойств (данных) и методов (функций). Важно то, как пользователи СОМ-объектов смогут использовать заложенную в них функциональность. Эта функциональность разбивается на группы семантически связанных виртуальных функций, и каждая такая группа называется интерфейсом. Доступ к каждой функции осуществляется с помощью указателя на нее. В сущности, вся СОМ-технология базируется на использовании таблицы указателей на виртуальные функции (vtable).

Примечание
Слово interface (также как и слова object, element) становится перегруженным слишком большим количеством смыслов, поэтому будьте внимательны. Интерфейсы СОМ – это довольно строго определенное понятие, идентичное понятию структуры (частного случая класса) в ООП, но ограниченное соглашениями о принципах его использования
.

Каждый СОМ-компонент может предоставлять клиенту несколько интерфейсов, то есть наборов функций. Стандартное определение интерфейса описывает его как объект, имеющий таблицу указателей на виртуальные функции (vtable). В файле заголовков BaseTyps.h, однако, вы можете увидеть макроподстановку #def ine interface struct, которая показывает, как воспринимает это ключевое слово компилятор языка C++. Для него интерфейс – это структура (частный случай класса), но для разработчиков интерфейс отличается от структуры тем, что в структуре они могут инкапсулировать как данные, так и методы, а интерфейс по договоренности (by convention) должен содержать только методы. Заметим, что компилятор C++ не будет возражать, если вы внутри интерфейса все-таки декларируете какие-то данные.

Интерфейсы придумали для предоставления (exhibition) клиентам чистой, голой (одной только) функциональности. Существует договоренность называть все интерфейсы начиная с заглавной буквы "I", например lUnknown, ZPropertyNotifySink и т. д. Каждый интерфейс должен жить вечно и поэтому он именуется уникальным 128-битным идентификатором (globally unique identifier), который в соответствии с конвенцией должен начинаться с префикса IID_. Интерфейсы никогда нельзя изменять, усовершенствовать, так как нарушается обратная совместимость. Вместо этого создают новые вечные интерфейсы.

Примечание
Это непреложное требование справедливо относят к недостаткам СОМ-техно-логии, так как непрерывное усовершенствование компонентов влечет появление слишком большого числа новых интерфейсов, зарегистрированных в вашем реестре. С проблемой предлагают бороться весьма сомнительным образом – тщательным планированием компонентов. Трудно, если вообще возможно, планировать в наше время (тем более рассчитывать на вечную жизнь СОМ-объекта), когда сами информационные технологии появляются и исчезают, как грибы в дождливый сезон
.

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

interface lUnknown
{
public: virtual HRESULT _ stdcall Querylnterface(REFIID riid,
void **ppvObject) = 0;
virtual ULONG _ stdcall AddRef(void) = 0;
virtual ULONG _ stdcall Release(void) = 0;
};

Как видите, "неизвестный" содержит три чисто виртуальные функции и ни одного элемента данных. Каждый новый интерфейс, который создает разработчик, должен иметь среди своих предков IUnknown, а следовательно, он наследует все три указанных метода. Первый метод Querylnterface представляет собой фундаментальный механизм, используемый для получения доступа к желаемой функциональности СОМ-объекта. Он позволяет получить указатель на существующий интерфейс или получить отказ, если интерфейс отсутствует. Первый – входной параметр riid – содержит уникальную ссылку на зарегистрированный идентификатор желаемого интерфейса. Это та уникальная, вечная бирка (клеймо), которую конкретный интерфейс должен носить вечно. Второй – выходной параметр – используется для записи по адресу ppvObject адреса запрошенного интерфейса или нуля в случае отказа. Дважды использованное слово адрес оправдывает количество звездочек в типе void**. Тип возвращаемого значения HRESULT, обманчиво относимый к семейству handle (дескриптор), представляет собой 32-битное поле данных, в котором кодируются признаки, рассмотренные нами в четвергом уроке.

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

//====== Указатель на незнакомый объект
lUnknown *pUnk;
// Иногда приходит как параметр IМу *рМу;
// Указатель на желаемый интерфейс
//====== Запрашиваем его у объекта
HRESULT hr=pUnk › Queryinterfасе(IID_IMY, (void **)&pMy);
if (FAILED(hr)) // Макрос, расшифровывающий HRESULT
{
//В случае неудачи
delete pMy; // Освобождаем память
//====== Возвращаем результат с причиной отказа
return hr;
else //В случае успеха
//====== Используем указатель для вызова методов:
pMy › SomeMethod();
pMy › Release(); // Освобождаем интерфейс
Возможна и другая тактика:
//====== В случае успеха (определяется макросом)
if (SUCCEEDED(hr))
{
//====== Используем указатель
}
else
{
//====== Сообщаем о неудаче
}
Если Вы заметили ошибку, выделите, пожалуйста, необходимый текст и нажмите CTRL + Enter, чтобы сообщить об этом редактору.