Назначение и принципы 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 производит следующие операции:

  • В стек помещается адрес, где находится переменная HResult, адрес переменной P и GUID.
  • Вычисляется адрес таблицы виртуальных методов из полученной ссылки на интерфейс.
  • Находится третий столбец данной таблицы (метод QueryInterface реализован третьим).
  • Оттуда извлекается адрес метода, и ему передается управление.
  • Процесс вычислений продолжается дальше - стек был очищен на сервере.
  •    
    Из этой схемы вызова методов интерфейса можно сделать вывод, что все языки, поддерживающие 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

    ТипОписание
    byte1 байт, целое без знака, диапазон 0..255.
    comp8 байт, целое со знаком, диапазон -2^63+1..2^63-1, сопроцессорный тип
    currency8 байт, с плавающей запятой и четырьмя знаками после запятой, диапазон -922337203685477.5808.. 922337203685477.5807, сопроцессорный тип
    DISPPARAMSСтруктура, содержит параметры вызова методов через метод Invoke интерфейса IDispatch
    double8 байт, с плавающей запятой, диапазон 5.0*10^-324.. 1.7*10^308, 15-16 знаков
    EXCEPINFOСтруктура, содержащая информацию об исключении
    GUIDГлобальный идентификатор (класса, интерфейса). Структура размером 16 байт
    HResult4 байта, целое число без знака, диапазон 0..4294967295
    integer4 байта, целое число со знаком, диапазон 2147483648..2147483647
    Largeuint8 байт, целое число со знаком, диапазон -2^63..2^63-1
    OleVariantСодержит любые данные, тип может меняться динамически. Минимальный размер - 16 байт
    PCharУказатель на строку, 4 байта
    PWideCharУказатель на строку, в который для хранения каждого символа используют 2 байта. Размер - 4 байта
    PSafeArrayУказатель на массив целых чисел, 4 байта
    ShortInt1 байт, целое со знаком, -128..127
    Single4 байта, с плавающей запятой, диапазон 1.5*10^-45..3.4* 10^38, 7-8 знаков
    SmollInt2 байта, целое со знаком, диапазон -32768..32767
    SYSINTСистемная целая переменная со знаком, в 32-разрядных операционных системах совпадает с типом integer
    SYSUINTСистемная целая переменная без знака, в 32-разрядных операционных системах совпадает с типом LongWord. Другое название переменной этого типа - Cardinal
    TDateTime8 байт, с плавающей запятой, целая часть - число дней с 30 декабря 1899 года, дробная часть - доля от 24 часов
    TDecimalСтруктура, содержит число с плавающей запятой и точность его представления (сколько имеется значимых десятичных знаков). Расшифровка - ActiveX.pas
    TLCID4 байта, целое без знака, диапазон 0..4294967295. Внутренний идентификатор
    UINT4 байта, целое без знака, диапазон 0.. 4294967295. Ассемблерный тип
    WideStringСтрока переменной длины, для хранения каждого символа используется 2 байта
    Word2 байта, целое без знака, диапазон 0..65535
    WordBool2 байта, логическая переменная (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.



    Вы можете оставить комментарий, или Трекбэк с вашего сайта.

    Оставить комментарий