Работа с 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;

};

Как видите, «неизвестный» содержит три чисто виртуальные функции и ни одного элемента данных. Каждый новый интерфейс, который создает разработчик, должен иметь среди своих предков 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, допуская чтение и запись своих свойств, а также вызов своих методов.

 

Назад Начало Вперед