Шаг 14.
Назначение и принципы COM-технологии. Понятие интерфейса

    На этом шаге мы познакомимся с понятием интерфейса.

    Интерфейс - центральное понятие COM- и OLE-технологии. Интерфейс можно определить как полностью абстрактный класс, не содержащий данных, для информации о своем типе применяющий 16-байтовый идентификатор, при вызове методов которого используется договоренность safecall.

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

    Для интерфейсов, так же как и для классов, определено понятие иерархии. Суть этого понятия - каждый класс (или интерфейс) может иметь одного или нескольких потомков. Во всех потомках данного класса (интерфейса) сохраняются все методы (и данные для классов), которые имеются у родителей. В Delphi классы (точно так же как и интерфейсы) могут иметь только одного родителя, поэтому они образуют иерархическое дерево. Его можно увидеть в Delphi при вызове команды Browser из меню View. В вершине иерархического дерева классов находится родоначальник всех классов - TObject, а интерфейсов - IUnknown. Все остальные потомки содержат методы TObject (IUnknown).


Рис.1. Иерархия интерфейсов в браузере объектов Delphi

    Все методы интерфейсов являются виртуальными и абстрактными (такие классы в C++ называют полностью абстрактными). Иными словами, они только объявлены, но не реализованы. Возникает вполне естественный вопрос - а зачем это надо? Для понимания этого необходимо вспомнить, что интерфейс создается в одном модуле (в основном на COM-сервере), а используется в другом (COM-клиент). Для того чтобы клиент знал, какие методы имеются в данном интерфейсе и какие параметры необходимо указать при вызове данного метода, применяются абстрактные методы. Получив ссылку на созданный в сервере интерфейс, клиент знает, например, что имеется метод (функция) QueryInterface с первым параметром типа TGUID и со вторым типа нетипизированной переменной, который после выполнения возвращает переменную типа HResult. Соответственно клиент может вызвать этот метод с данным списком параметров и сервер обязан его выполнить.

    Следует обратить внимание, что главным для интерфейса является не название метода (QueryInterface), а список параметров данного метода и то, каким по очереди он был объявлен при реализации иерархии интерфейсов. Метод QueryInterface объявляется в интерфейсе IUnknown третьим по счету (первые два метода этого интерфейса - AddRef и Release). Легко себе представить язык программирования, к которому IUnknown объявлен, например, следующим образом:

IUnknown = interface
  function AddRef: integer; safecall;
  function Release: integer; safecall;
  function GetInterf(const IID: TGUID; var P): HResult; safecall;
end;

то есть вместо метода QueryInterface объявлен метод GetInterf. При написании кода в таком абстрактном языке программирования придется вводить слово "GetInterf" вместо "QueryInterface", но полученный код будет работоспособен! Наоборот, приведенное ниже объявление IUnknown будет неработоспособно ни в одном из языков программирования:

IUnknown = interface
  function AddRef: integer; safecall;
  function QueryInterface(const IID: TGUID; var P): HResult; safecall;
  function Release: integer; safecall;
end;

    Этот код отличается от объявления IUnknown в Delphi тем, что методы QueryInterface и Release поменялись местами. Порядок объявления методов определяет место методов в виртуальной таблице.

    Таким образом, вызов методов интерфейса осуществляется следующим образом. Интерфейс создается на сервере, а клиент получает на него ссылку. После этого клиент для вызова метода QueryInterface производит следующие операции:

  1. В стек помещается адрес, где находится переменная HResult, адрес переменной P и GUID.
  2. Вычисляется адрес таблицы виртуальных методов из полученной ссылки на интерфейс.
  3. Находится третий столбец данной таблицы (метод QueryInterface реализован третьим).
  4. Оттуда извлекается адрес метода, и ему передается управление.
  5. Процесс вычислений продолжается дальше - стек был очищен на сервере.

    Из этой схемы вызова методов интерфейса можно сделать вывод, что все языки, поддерживающие COM-технологию, обязаны создавать таблицу виртуальных методов для COM-объектов. Эта таблица везде имеет одинаковый размер записи и относительный адрес в COM-объекте. Такая жесткость требований необходима для того, чтобы приложения, написанные на разных языках программирования, могли взаимодействовать друг с другом. При этом каждый из языков программирования может иметь свои собственные таблицы, такие, как, например таблица динамических методов в Delphi.

    Следует обратить внимание на порядок помещения переменных во временную память - стек и на то, что сервер очищает стек перед передачей управления процессом вычислений клиенту. В различных языках программирования переменные могут перемещаться в стек как слева на право, следуя по списку формальных параметров метода, так и справа налево. Кроме того, очищать стек может либо метод, который был вызван перед окончанием своей работы, либо стек может очищаться после возврата в основной метод. Все эти способы работы со стеком реализованы в разных языках программирования. Поэтому при вызове методов из других модулей необходимо договариваться, как следует работать со стеком. Такая договоренность (Calling Convention) содержится в директиве safecall, помещаемой после названия соответствующего метода в описании интерфейса. Таким образом, на первый взгляд абсолютно бесполезный абстрактный метод интерфейса содержит в себе очень много информации: где его искать в виртуальной таблице, список переменных и их типы, как эти переменные помещаются в стек и кто его будет разрушать после окончания работы. Директива safecall содержит еще и дополнительную информацию - при возникновении исключительной ситуации объект исключения не попадет к клиенту. Это важно потому, что объекты исключения являются языково-зависимыми: они по-разному реализуются в различных языках программирования. Кроме того, разные модули имеют разные менеджеры памяти. Соответственно, если объект исключения попадает в другой модуль, он не может быть корректно разрушен и ресурсы не могут быть отданы системе. Приложения, написанные на Delphi, пытаются разрушить такой объект в любом случае, не анализируя, из какого модуля он появился, и в этом случае, как правило, генерируется новое исключение.

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

    Если бы интерфейсы различались только по именам, то при случайном совпадении имен двух интерфейсов (а это происходило бы довольно часто: имя обычно несет в себе смысловую нагрузку), реализованных в разных модулях, вместо одного модуля загружался бы другой. Поэтому для идентификации интерфейса используется структура типа GUID (Global Universal Identifier), которая имеет размер 16 байт (128 бит). Единственный тип данных, которые предопределены для интерфейса, - это GUID. Каждый COM-интерфейс содержит собственный уникальный GUID. Если разработчик реализует новый интерфейс, то этот интерфейс обязан иметь GUID, причем уникальность должна соблюдаться не только в рамках данного компьютера разработчика, но и всего мира в целом. Чтобы получить GUID для вновь созданного интерфейса в среде Delphi, достаточно нажать клавиши Ctrl+Shift+G. Вопрос – а что будет, если в двух разных местах случайно будут сгенерированы два одинаковых GUID? Ответ на этот вопрос заключается в динамическом диапазоне GUID. Он настолько огромен, что если поставить компьютер на непрерывную случайную генерацию GUID со скоростью 1000000 в секунду, то за все время существования Вселенной с вероятностью 95% не будут созданы два одинаковых GUID.

    Описанная выше структура интерфейса естественно решает первую проблему экспорта объектов между модулями - каждый интерфейс однозначно определяется своим GUID, имеет вполне определенный список методов с вполне определенными параметрами и условиями вызова. То есть имеется абсолютно однозначный протокол вызова методов, и это гарантирует корректность передаваемых данных и сохранность стека. При помощи интерфейсов также естественно решается проблема унифицированной передачи данных. В списках формальных параметров методов интерфейса используются не все типы данных, которые определены в данном языке программирования. Точнее сказать, могут использоваться любые типы данных, но если ссылка на данный интерфейс может быть передана в другой модуль, то список формальных параметров методов интерфейса обязан содержать только определенные типы данных - так называемые OLE Automation Datatypes. В таблице 1 приведены эти типы данных.

Таблица 1. Типы данных OLE Automation
Тип Описание
byte
1 байт, целое без знака, диапазон 0..255.
comp
8 байт, целое со знаком, диапазон -2^63+1..2^63-1, сопроцессорный тип
currency
8 байт, с плавающей запятой и четырьмя знаками после запятой, диапазон -922337203685477.5808.. 922337203685477.5807, сопроцессорный тип
DISPPARAMS
Структура, содержит параметры вызова методов через метод Invoke интерфейса IDispatch
double
8 байт, с плавающей запятой, диапазон 5.0*10^-324.. 1.7*10^308, 15-16 знаков
EXCEPINFO
Структура, содержащая информацию об исключении
GUID
Глобальный идентификатор (класса, интерфейса). Структура размером 16 байт
HResult
4 байта, целое число без знака, диапазон 0..4294967295
integer
4 байта, целое число со знаком, диапазон 2147483648..2147483647
Largeuint
8 байт, целое число со знаком, диапазон -2^63..2^63-1
OleVariant
Содержит любые данные, тип может меняться динамически. Минимальный размер - 16 байт
PChar
Указатель на строку, 4 байта
PWideChar
Указатель на строку, в который для хранения каждого символа используют 2 байта. Размер - 4 байта
PSafeArray
Указатель на массив целых чисел, 4 байта
ShortInt
1 байт, целое со знаком, -128..127
Single
4 байта, с плавающей запятой, диапазон 1.5*10^-45..3.4* 10^38, 7-8 знаков
SmollInt
2 байта, целое со знаком, диапазон -32768..32767
SYSINT
Системная целая переменная со знаком, в 32-разрядных операционных системах совпадает с типом integer
SYSUINT
Системная целая переменная без знака, в 32-разрядных операционных системах совпадает с типом LongWord. Другое название переменной этого типа - Cardinal
TDateTime
8 байт, с плавающей запятой, целая часть - число дней с 30 декабря 1899 года, дробная часть - доля от 24 часов
TDecimal
Структура, содержит число с плавающей запятой и точность его представления (сколько имеется значимых десятичных знаков). Расшифровка - ActiveX.pas
TLCID
4 байта, целое без знака, диапазон 0..4294967295. Внутренний идентификатор
UINT
4 байта, целое без знака, диапазон 0.. 4294967295. Ассемблерный тип
WideString
Строка переменной длины, для хранения каждого символа используется 2 байта
Word
2 байта, целое без знака, диапазон 0..65535
WordBool
2 байта, логическая переменная (True=-1, False=0)

    Помимо перечисленных в таблице типов список формальных параметров может еще содержать ссылки на интерфейсы, определённые в модуле ActiveX, а также переменные, тип которых начинается с OLE (OLE_COLOR, OLE_XPOS и др.). Нельзя при передаче данных через интерфейсы использовать параметры типа Boolean - можно только WordBool. Нельзя использовать параметры типа string - можно только WideString. Pointer - указатель на что-нибудь в памяти - отсутствует вообще. Он используется в стандартных интерфейсах, перечисленных в модуле ActiveX.pas, но его нельзя применять при создании собственных интерфейсов. При работе с COM-объектами необходимо иметь информацию не только о том, где что-то находится, но и о том, что находится в данной области памяти. Поэтому используют всегда типизированные указатели (PChar, PWideString, PSafeArray).

    Эти ограничения введены потому, что COM-объект можно реализовывать на разных языках программирования. При этом такой язык программирования обязан поддерживать вышеперечисленные типы данных. Помимо этого, любой язык может содержать свои собственные типы данных - например, Boolean, String в Delphi. Поэтому программист может с уверенностью применять вышеперечисленные типы данных, зная, что при их передаче между модулями не произойдет искажения. Язык программирования, который не поддерживает эти типы данных, не поддерживает и COM-технологию и не сможет получить ссылку на интерфейс.

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




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