Интерфейсы
— основа СОМ-технологии
Разработчики
СОМ не интересуются тем, как устроены компоненты внутри, но озабочены тем, как
они представлены снаружи. Каждый компонент или объект СОМ рассматривается как
набор свойств (данных) и методов (функций). Важно то, как пользователи СОМ-объектов
смогут использовать заложенную в них функциональность. Эта функциональность
разбивается на группы семантически связанных виртуальных функций, и каждая такая
группа называется интерфейсом. Доступ к каждой функции осуществляется с помощью
указателя на нее. В сущности, вся СОМ-технология базируется на использовании
таблицы указателей на виртуальные функции (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;
};
Как видите,
«неизвестный» содержит три чисто виртуальные функции и ни одного элемента данных.
Каждый новый интерфейс, который создает разработчик, должен иметь среди своих
предков I Unknown, а следовательно, он наследует все три указанных метода. Первый
метод Querylnterface представляет собой фундаментальный механизм, используемый
для получения доступа к желаемой функциональности СОМ-объекта. Он позволяет
получить указатель на существующий интерфейс или получить отказ, если интерфейс
отсутствует. Первый — входной параметр riid — содержит уникальную ссылку на
зарегистрированный идентификатор желаемого интерфейса. Это та уникальная, вечная
бирка (клеймо), которую конкретный интерфейс должен носить вечно. Второй — выходной
параметр — используется для записи по адресу ppvOb j ect адреса запрошенного
интерфейса или нуля в случае отказа. Дважды использованное слово адрес оправдывает
количество звездочек в типе void**. Тип возвращаемого значения HRESULT, обманчиво
относимый к семейству handle (дескриптор), представляет собой 32-битное иоле
данных, в котором кодируются признаки, рассмотренные нами в четвергом уроке.
Предположим,
вы хотите получить указатель на какой-либо произвольный интерфейс 1Му, уже зарегистрированный
системой и получивший уникальный идентификатор IID_IMY, с тем чтобы пользоваться
предоставляемыми им методами. Тогда следует действовать по одной из общепринятых
схем
1:
//======
Указатель на незнакомый объект
lUnknown
*pUnk;
//
Иногда приходит как параметр IМу *рМу;
//
Указатель на желаемый интерфейс
//======
Запрашиваем его у объекта
HRESULT
hr=pUnk->Query!nterfасе(IID_IMY,
(void **)&pMy);
if
(FAILED(hr)) // Макрос, расшифровывающий HRESULT
{
//В
случае неудачи
delete
pMy; // Освобождаем память
//======
Возвращаем результат с причиной отказа
return
hr;
else
//В случае успеха
//======
Используем указатель для вызова методов:
pMy->SomeMethod();
pMy->Release();
// Освобождаем интерфейс
Возможна
и другая тактика:
//======
В случае успеха (определяется макросом)
if
(SUCCEEDED(hr))
{
//======
Используем указатель
}
else
{
//======
Сообщаем о неудаче
}
Второй параметр
функции Queryinterf асе (указатель на указатель) позволяет возвратить в вызывающую
функцию адрес запрашиваемого интерфейса. Примерная схема реализации метода Queryinterf
асе (в классе СОМ-объекта, производном от IМу) может иметь такой вид:
HRESULT
_
stdcall СМу::Queryinterfасе(REFIID id,
void **ppv)
{
//===
В *ppv надо записать адрес искомого интерфейса
//===
Пессимистический прогноз (интерфейс не найден)
*ppv
= 0;
//
Допрашиваем REFIID искомого интерфейса. Если он
//
нам не знаком, то вернем отказ E_NOINTERFACE
//
Если нас не знают, но хотят познакомиться,
//
то возвращаем свой адрес, однако приведенный
//
к типу "неизвестного" родителя
if
(id == IID_IUnknown)
*ppv
= static_cast<IUnknown*>(this);
//
Если знают, то возвращаем свой адрес приведенный
//
к типу "известного" родителя IМу
else
if (id == IID_IMy)
*ppv
= static_cast<IMy*>(this);
//=====
Иначе возвращаем отказ
else
return
E_NOINTERFACE;
//===
Если вопрос был корректным, то добавляем единицу
//===
к счетчику наших пользователей
AddRef();
return
S_OK;
}
Методы AddRef
и Release управляют временем жизни объектов посредством подсчета ссылок (references)
на пользователей интерфейса. В соответствии с общей концепцией объект (или его
интерфейс) не может быть выгружен системой из памяти, пока не равен нулю счетчик
ссылок на его пользователей. При создании интерфейса в счетчик автоматически
заносится единица. Каждое обращение к AddRef увеличивает счетчик на единицу,
а каждое обращение к Release — уменьшает. При обнулении счетчика объект уничтожает
себя сам. Например, так:
ULONG
СМу::Release()
{
//======
Если есть пользователи интерфейса
if
(—m_Ref != 0)
return
m_Ref; // Возвращаем их число
delete
this;
//
Если нет — уходим из жизни,
//
освобождая память
return
0;
}
Вы, наверное,
заметили, что появилась переменная m_Ref. Ранее было сказано об отсутствии переменных
у интерфейсов. Интерфейсы — это голая функциональность. Но обратите внимание
на тот факт, что метод Release принадлежит не интерфейсу 1Му, а классу ему,
в котором переменные естественны. Обычно в классе СОМ-объекта и реализуются
чисто виртуальные методы всех интерфейсов, в том числе и главного интерфейса
zunknown. Класс ему обычно создает разработчик СОМ-объекта и производит его
от желаемого интерфейса, например, так:
class
СМу : public IMy
{
//
Данные и методы класса,
//
в том числе и методы lUnknown
};
В свою очередь,
интерфейс IMy должен иметь какого-то родителя, может быть, только iUnknown,
а может быть одного из его потомков, например:
interface
IMy : IClassFactory
{
//
Методы интерфейса
};
СОМ-объектом
считается любой объект, поддерживающий хотя бы lUnknown. Историческое развитие
С ОМ-технологий определило многообразие терминов типа: OLE 94, OLE-2, OCX-96,
OLE Automation и т. д. Элементы ActiveX принадлежат к той же группе СОМ-объектов.
Каждый новый термин из этой серии подразумевает все более высокий уровень предоставляемых
интерфейсов. Элементы ActiveX должны как минимум обладать способностью к активизации
на месте, поддерживать OLE Automation, допуская чтение и запись своих свойств,
а также вызов своих методов.