Шаг 15.
Назначение и принципы COM-технологии. IUnknown: работа с памятью, подсчет числа ссылок и запросы на предоставление интерфейсов

    На этом шаге мы рассмотрим интерфейс IUnknown.

    На уровне IUnknown, который является предком для всех интерфейсов, уже реализовано корректное решение проблемы резервирования и освобождения ресурсов системы. Напомним эту проблему: если в каком-нибудь модуле были затребованы ресурсы системы, то их нельзя возвратить системе в другом модуле.

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

    Методы интерфейса, также реализуются в классах. Однако данную процедуру применить невозможно. Если в интерфейсе будет определён >метод Destroy, который непосредственно обращается к деструктору, то при попытке вызвать его из модуля, где была получена ссылка на интерфейс, произойдет исключение. Это связано с тем, что различные модули, даже реализованные на одном языке программирования, имеют свои собственные менеджеры памяти, и освобождение ресурсов обязано происходить в рамках того же модуля, где они были востребованы.

    И еще следует принять во внимание, что один и тот же интерфейс может быть затребован одновременно несколькими клиентами. Например, два клиентских приложения одновременно работают с Microsoft Word. Если производить разрушение интерфейса тем же способом, каким производится и разрушение класса, то первое клиентское приложение просто закроет Word. Второе, в общем случае, об этом знать не будет, и сохранит ссылку на несуществующий объект. При попытке вызовов его методов нетрудно догадаться, что произойдет.

    Для решения этих проблем в интерфейсах при их реализации осуществляется подсчет ссылок. Специально для этой цели во всех интерфейсах имеются методы AddRef и Release. Метод AddRef при реализации обязан увеличивать счетчик внутренней переменной. Термин "обязан" здесь употребляется потому, что реализация методов интерфейсов возложена на разработчика, который создает COM-сервер. Он может этого и не сделать, тогда интерфейс будет работать некорректно. Соответственно метод Release обязан уменьшать этот счетчик. Кроме того, этот метод обязан проверять, равен ли счетчик ссылок нулю и при его равенстве нулю вызывать деструктор объекта, в котором реализован интерфейс.

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

    Вызов деструктора происходит из сервера - поэтому ресурсы освобождаются корректно.

    Если COM-клиент реализуется на Delphi, то нет необходимости вызывать в явном виде метод Release. Достаточно присвоить переменной, в которой хранится указатель на интерфейс, значение nil (или unassigned, если указатель хранится в переменной типа Variant). Можно вообще ничего не присваивать таким переменным - перед их разрушением (освобождение стека, удаление экземпляра класса, закрытие приложения) Delphi генерирует код, который проверяет переменные на наличие ссылки на интерфейс, и если она есть, то метод Release вызывается автоматически.

    IUnknown содержит также метод QueryInterface, который используется для получения ссылок на интерфейс клиентом. Клиент вызывает метод QueryInterface интерфейса IUnknown сервера и указывает идентификатор IID интерфейса (IID тот же самый тип, что и GUID, - применяется для идентификации интерфейса), ссылку на который он хочет получить. Метод QueryInterface обязан проверять все IID интерфейсов, которые реализованы в данном классе многокомпонентного объекта (CoClass). Если будет найден совпадающий IID, то метод QueryInterface обязан сделать следующее:

  1. Вызвать конструктор, если не был создан экземпляр класса, в котором реализован интерфейс.
  2. Вызвать метод AddRef для затребованного интерфейса, и тем самым увеличить счетчик ссылок на единицу. Иногда для группы интерфейсов реализуется общий счётчик ссылок.
  3. Поместить указатель на созданный (или имеющийся) объект, в котором реализован интерфейс, в нетипизированную переменную P.
  4. Возвратить результат S_OK.

    Если же интерфейс с данным идентификатором IID не поддерживается, то в нетипизированную переменную P возвращается nil и результат вызова метода должен быть равен E_NOINTERFACE. Типичный пример реализации метода QueryInterface приведен ниже:

function TMyClassFactory.QueryInterface(const iid: TIID; var P): HResult;
begin
  if IsEqualIID(iid, IID_IClassFactory) or
     IsEqualIID(iid, IID_IUnknown) then 
  begin
     Pointer(P):=Self;
     AddRef;
     Result:=S_OK;
  end 
  else 
  begin
     Pointer(P):=nil;
     Result:=E_NOINTERFACE;
  end;
end;

    На следующем шаге мы рассмотрим интерфейс IClassFactory.




Предыдущий шаг Содержание Следующий шаг