PC Magazine/RE logo
©СК Пресс 97-2
e-mail: pcmagedt@aha.ru

PC Magazine, November 5, 1996, p. 285

Сдвоенные интерфейсы средствами Delphi

Джон Лэм


Сдвоенный интерфейс это гибкость и высокое быстродействие

В предыдущих выпусках мы затронули вопросы, касающиеся технологии Automation - прежде она называлась OLE Automation (OLE автоматизация) - и групп автоматизации. Для простоты изложения был выбран достаточно элементарный пример, демонстрирующий обращение к функциям через интерфейс IDispatch. В этой статье мы рассмотрим наиболее мощные из интерфейсов автоматизации: сдвоенные (dual) интерфейсы.

Сдвоенному интерфейсу свойственны одновременно гибкость IDispatch, обеспечивающего связывание объектов лишь на стадии исполнения, и высокое быстродействие COM интерфейса. Программа, обладающая средствами дл обращения к вашим функциям через сдвоенный интерфейс, получает огромный выигрыш в быстродействии: в зависимости от типов передаваемых параметров оно может быть в 500 раз выше, чем при вызове той же функции, но через IDispatch! В то же время сдвоенный интерфейс остается таким же универсальным, как интерфейс IDispatch: старые программы по-прежнему смогут работать с вашим объектом автоматизации (Automation object). В этой статье мы покажем, как, приложив немного усилий, дополнить такие объекты Delphi 2.0 возможностью подключения сдвоенного интерфейса.

Исходный текст всех приведенных в статье примеров можно получить в службе PC Magazine Online.

Краткий обзор технологии автоматизации

Причина появления этой технологии - необходимость в совместном использовании ресурсов программных библиотек. Применяя механизм библиотек динамической компоновки (DLL) Windows и поставляемые .lib и .h файлы, разработчики могли бы компоновать DLL модули со своими программами. Однако, чтобы все работало без сбоев, компилятор должен в ходе исполнения программы генерировать команды для выдачи адресов библиотечных функций. Это означает, что при необходимости подключения новой библиотеки разработчику придетс перекомпилировать свою программу. Более того, при внесении любых изменений в библиотеку вы будете вынуждены повторить компиляцию всех программ, которые к ней обращаются.

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

Данная технология разработана на основе COM (Component Object Model - модель компонентного объекта), предложенной и реализованной фирмой Microsoft в качестве базовой схемы взаимодействия объектов между собой. Она широко применяется разработчиками программ для среды Windows, прежде всего благодаря тому, что не ограничивает их выбор необходимостью использования лишь одного инструментального пакета или языка программирования. COM объекты можно создавать на базе Visual Basic, Delphi или Си++, хотя, как будет показано далее, применение Delphi и Си++ сулит определенные преимущества.

Типы COM объектов

Все COM объекты делятся на три категории: внутренние (in process), внешние и удаленные (remote). Внутренние COM объекты размещаются в адресном пространстве вызвавшей их программы; такие объекты организуются в виде DLL модулей. Работа внешних COM объектов протекает в их собственном адресном пространстве; они, как правило, реализуются в виде исполнимых файлов. Удаленные COM-объекты находятся на другом компьютере и могут быть как DLL-модулями, так и исполнимыми файлами. Средства для работы с удаленными COM объектами будут предусмотрены лишь в библиотеках Distributed COM (модель распределенного компонентного объекта), которые войдут в состав Windows NT 4.0, а затем и Windows 95. В этой статье речь пойдет в основном о внутренних COM объектах.

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

Любой COM объект должен обеспечивать интерфейс IUnknown с тремя принадлежащими функциями. Функци QueryInterface обеспечивает вызывающему объекту или программе возможность выяснить вопрос о наличии у COM-объекта других интерфейсов; AddRef и Release отслеживают число обращений к конкретному объекту и, в конечном счете, управляют "продолжительностью жизни" объекта. При каждом обращении к AddRef происходит приращение счетчика ссылок на единицу, а при каждом вызове Release - его уменьшение. Когда значение счетчика становится равным нулю, данный объект можно спокойно удалять из памяти.

Интерфейс IDispatch

Разработчики механизма автоматизации предусмотрели такую схему вызова функций, при которой на этапе компиляции их имена могут быть и неизвестны. Данна технология называется поздним связыванием (late binding). Имена функций определяются во врем исполнения, и делается это через интерфейс IDispatch.

IDispatch - это интерфейс COM объекта с семью принадлежащими функциями. Первые три совпадают с уже упомянутыми функциями интерфейса IUnknown. Нас же будут интересовать остальные четыре функции. Две из них - GetTypeInfo и GetTypeInfoCount - относятся к библиотекам типов (type library), речь о которых пойдет дальше. А основные функции интерфейса IDispatch выполняют две другие - Invoke и GetIDsOfNames.

Рассмотрим простейший случай. Контроллер автоматизации создаст экземпляр вашего COM объекта, однако у него нет информации о функциях этого объекта. Чтобы узнать о наличии у данного объекта конкретной функции, контроллер обращается к функции GetIDsOfNames, передавая ей строку с именем интересующей его функции. Если у вашего объекта есть функция с таким именем, контроллеру передается идентификатор диспетчеризации DispID, используя который он вызывает функцию Invoke данного объекта, передавая попутно все необходимые дл нее параметры.

Каким образом контроллеру автоматизации становятс известны сведения о параметрах для запрашиваемой функции? Грамотно составленная программа-контроллер черпает эти сведения из библиотеки типов (см. далее) COM объекта. Если таковая отсутствует или контроллер не обладает средствами для ее чтения, ему не удастс узнать, какие параметры нужны. В этом случае ответственность за безошибочность списка передаваемых контроллером параметров несет программист, работающий с ним, поскольку он всегда может свериться с сопроводительной документацией на данный COM объект. Используя подготовленный программистом список, контроллер формирует массив параметров и передает его COM объекту. В каждом элементе этого массива имеется и информация о типе параметра, поэтому, когда COM объект получает этот массив, в его распоряжении оказываются и фактические типы переданных параметров.

Все элементы данного массива параметров сформированы как переменные с типом Variant, что оговорено в спецификации технологии автоматизации. Этот тип вам, должно быть, знаком, поскольку является одним из основных типов данных в Visual Basic. Переменна Variant представляет собой 16-байт блок, объединяющий в себе многие распространенные типы данных. Данные большинства хранящихся в Variant типов передаются по значению; они целиком содержатся в Variant. Имеется, однако, одно существенное исключение: строковые переменные. В OLE автоматизации они называются Basic строками, или BSTR, - смесь строки в стиле Pascal и в стиле Си и с нулевым символом в конце строки. А поскольку в соответствии со стилем Pascal хранитс значение длины строки, обеспечивается высокое быстродействие при объединении и просмотре строк, а наличие завершающего нулевого символа означает, что такие строки могут быть переданы функциям API, дл которых последнее условие существенно.

Сдвоенные интерфейсы

Как уже отмечалось, у интерфейса IDispatch семь функций. В сдвоенном интерфейсе вы должны поместить в таблицу vtable, начиная с позиции 8, элементы для ваших функций автоматизации. Эта таблица COM объекта построена так же, как vtable языка Си++, которая, в свою очередь, идентична таблице виртуальных методов, используемой в Delphi. Поэтому все, что от вас требуется, - это получить новый, производный от IDispatch класс и реализовать функции объекта автоматизации как виртуальные.

С помощью средств Delphi 2.0 вы можете создавать как контроллеры, так и серверы автоматизации. Чтобы организовать управление объектом автоматизации средствами Delphi, достаточно объявить переменную с типом Variant и вызвать функцию CreateOleObject, описание которой дано в модуле oleauto.pas:

uses OleAuto; ... var WordObject: variant; begin WordObject := CreateOleObject("Word.Basic"); ... WordObject.FileNewDefault;

Процедура создания сервера тоже исключительно проста: достаточно вывести новый класс, производный от TAutoObject:

uses OleAuto; ... type TMyAutoServer=class(TAutoObject) ... automated procedure Foo; ... end;

Как вы могли убедиться, создание и контроллеров, и серверов автоматизации в среде Delphi не составляет труда. Однако, поскольку тема статьи - создание таких серверов, давайте кратко рассмотрим те функции, которые обеспечивают простоту этой процедуры в Delphi.

Создание элементарного объекта автомата

При создании сервера автоматизации первым делом следует решить, в какую форму его облечь. Наиболее простая его форма - обычная библиотека подпрограмм. С появлением механизма автоматизации, используемого при программировании для Windows, стало возможным организовывать библиотеки как в виде DLL модулей, так и в виде исполнимых файлов. В обоих случаях способ создания и вызова объекта автоматизации идентичен: нужно лишь указать, какого типа создать проект. Создадим образец сервера автоматизации, оформленного в виде DLL модуля.

Для создания нового DLL-проекта выберите из меню New Галереи функциональных объектов (Object Repository) системы Delphi пиктограмму DLL Expert (эксперт). Сначала отредактируйте файл проекта (.DPR). Добавьте в оператор uses аргумент OleAuto и для экспорта функций, реализованных в oleauto.pas, введите еще одно предложение:

exports DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer;

Все эти четыре функции реализованы в модуле oleauto.pas. Функции DllRegisterServer и DllUnregisterServer выполняют соответственно запись и удаление сведений о вашем сервере в системном реестре Windows 95. DllGetClassObject - стандартная точка входа для любых COM объектов; она используется средствами COM библиотек для генерации новых экземпляров какого-либо COM объекта, оформленного в виде DLL модуля. Функци DllCanUnloadNow вызывается из COM библиотек дл выяснения возможности выгрузки из памяти данного DLL- модуля.

Подготовив файл проекта DLL модуля, можно перейти к разработке собственно объекта автоматизации. Вновь вызовите диалоговое окно Галереи объектов Delphi и выберите из меню New элемент Automation Object Expert (эксперт объекта автоматизации). В поле Class Name введите имя класса для вашего объекта автомата например TMyAutoObject. Наверняка вы обратили внимание, что Delphi автоматически заполняет поле OLE Class Name, поместив в него название проекта и следом имя класса вашего объекта автоматизации, но без первой буквы "T". В поле Description можно ввести любой комментарий. Не беспокойтесь в данном случае о значении пол Instancing, поскольку для любых объектов автоматизации, организованных в виде DLL модулей, всегда предполагается "Multiple Instance" (несколько экземпляров).

Щелкните мышью на кнопке OK, и Delphi создаст новый модуль. Давайте взглянем на сгенерированную программу:

unit Unit1; interface uses OleAuto;

В любом модуле автоматизации будет присутствовать ссылка на модуль OleAuto. Модуль oleauto.pas будет рассмотрен более подробно, когда мы перейдем к созданию объекта автоматизации с расширенным интерфейсом.

type TMyAutoObject = class(TAutoObject)

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

private { Объявление Private компонент } automated { Объявление Automated компонент } end;

Обратите внимание на новое ключевое слово automated. Все объявленные в этом фрагменте функции, процедуры и параметры будут рассматриваться как методы и параметры автоматизации. Компилятор Delphi генерирует таблицы, в которых приводятся имена функций и типы их параметров, что обеспечивает гарантию соблюдения типов передаваемых параметров в сеансах связи вызывающих программ с этими методами.

implementation procedure RegisterMyAutoObject; const AutoClassInfo: TAutoClassInfo = ( AutoClass: TMyAutoObject; ProgID: 'Project1.MyAutoObject'; ClassID: '{76D47E00-B0AA-11CF-896C-444553540000}'; Description: 'Пример сервера автомата'; Instancing: acMultiInstance); begin Automation.RegisterClass(AutoClassInfo); end;

Эксперт Automation Expert автоматически генерирует содержимое структуры AutoClassInfo. Преобладающая часть вводимой в окне этого эксперта информации появляется в соответствующих полях AutoClassInfo. Имеется, однако, одно дополнительное поле чрезвычайной важности: поле ClassID. Для генерации этого номера Delphi обращается к функции CoCreateGuid COM библиотеки. Эта функция, используя некоторый алгоритм, формирует уникальный идентификатор GUID (Globally Unique IDentifier - глобальный уникальный идентификатор), исходя из уникального номера машины и Ethernet адреса сетевой платы.

initialization RegisterMyAutoObject; end.

При первой инициализации данного модуля Delphi обращается к функции RegisterMyAutoObject. Системе необходимо инициализировать ряд внутренних информационных структур, через которые будут отслеживаться все объекты автоматизации, экспортированные из этой DLL (или EXE файла в случае внешнего сервера).

Необходимые дополнения

Как вы, наверное, уже поняли, в Delphi предусмотрен очень удобный механизм для разработки и создани серверов автоматизации. Однако есть один недостаток, осложняющий процедуру построения серверов со сдвоенным интерфейсом: Delphi не обеспечивает автоматической генерации библиотеки типов, без которой у контроллера автоматизации нет информации о том, какие параметры требуются для прямого обращения к функциям сдвоенного интерфейса.

Это не означает, что в Delphi невозможно создать серверы автоматизации со сдвоенным интерфейсом; однако для подготовки библиотеки типов придется предпринять ряд дополнительных административных шагов. А это займет массу времени и чревато ошибками. Было бы прекрасно, если бы какая то из независимых фирм-разработчиков взялась за создание инструментов для автоматизации такой процедуры.

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

Использовать библиотеки типов можно как программным путем, так и просто просматривать на экране. Вряд ли кому либо из нас придет на ум изучать ее в двоичном формате; для этих целей имеется ряд удобных браузеров. Рассмотрим фрагмент библиотеки типов для Microsoft Excel 7.0, представленный программой просмотра Object Navigator, автор Matt Curland (из книги Object Programming with Visual Basic 4.0 издательства Microsoft Press).

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

Однако для нас сейчас гораздо важнее следующее - как связана библиотека типов со сдвоенным интерфейсом. Как уже отмечалось, сдвоенный интерфейс представляет собой комбинацию двух интерфейсов: IDispatch и специального COM-интерфейса. Первые семь его функций - принадлежащие функции интерфейсов IDispatch и IUnknown; а все остальные - принадлежащие функции сдвоенного интерфейса. Вы, наверное, спросите: как контроллеру становится известно о соответствии какой-то конкретному элементу таблицы vtable? Конечно же, через библиотеку типов.

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

Используя информацию из библиотеки типов, контроллер, обладающий средствами для работы со сдвоенными интерфейсами, может перед обращением к конкретной функции автоматизации засылать в стек параметры правильного типа. Это - очень важно: если функция автоматизации начинает извлекать из стека параметры не того типа, который требуется, нежелательные последствия неизбежны! Обратите внимание, что это в корне отличается от того, как происходит вызов функций через метод Invoke интерфейса IDispatch, где перед вызовом этого метода должен быть сгенерирован массив переменных с типом Variant.

Файлы ODL

Как создать библиотеку типов? В комплектах инструментальных пакетов Win32 SDK и OLE SDK фирмы Microsoft имеется утилита Mktyplib.exe. Она читает файлы ODL (Object Definition Language) и формирует двоичный файл библиотеки типов. Если же вы предусматриваете RPC взаимодействие (remote procedure call - вызов удаленных процедур), то используетс другая аналогичная утилита - MIDL.exe - котора считывает файлы IDL (Interface Definition Language - язык описания интерфейсов) и преобразует их в программу на языке Си, отвечающую за привязку к объектам COM и RPC. Отметим, что разница между файлами ODL и IDL очень небольшая. В комплекте SDK для версии Windows NT 4.0 Beta 1 содержится новый вариант MIDL.exe (Version 3.0), компилирующий файлы IDL уже непосредственно и в файлы библиотек типов. По нашему мнению, в будущем постепенно прекратится использование файлов ODL, поскольку их функция ограничивается практически лишь описанием библиотек типов.

Давайте рассмотрим файл ODL, с применением которого был создан наш простой сдвоенный интерфейс:

// Project1.odl: файл исходного текста для создани // библиотеки типов - Project1.dll // После его обработки утилитой Type Library (mktyplib.exe) // будет создан файл библиотеки типов Project1.tlb. [ uuid(062C7183-7DF1-11CF-896C-444553540000), version(1.0) ]

Аргумент uuid - это идентификатор класса библиотеки типов конкретного объекта автоматизации; он используется API функциями LoadRegTypeLib и QueryPathOfRegTypeLib модели компонентного объекта при работе с библиотеками, зарегистрированными в системном реестре.

library ProjX { importlib("stdole32.tlb");

Этот оператор осуществляет импорт описаний стандартных типов для компилятора Mktyplib. Дело в том, что для ODL файла используется синтаксис стиля Си++, и ему необходима информация об отдельных, заранее определенных типах. Для этого и предназначен файл stdole32.tlb.

// Пример сдвоенного интерфейса [ uuid(062C7182-7DF1-11CF-896C-444553540000), helpstring("Сдвоенный интерфейс сервера Foo."), oleautomation, dual ]

В этом предложении дается описание сдвоенного интерфейса. Параметр uuid служит его уникальным идентификатором, который необходим любому новому интерфейсу в технологии COM. Благодаря ему при работе с этим объектом автоматизации разработчики будут обращаться к его частному сдвоенному интерфейсу, а не к общему интерфейсу IDispatch. Сопроводительный комментарий к библиотеке задается через аргумент helpstring. Наконец, ключевые аргументы - параметры oleautomation и dual. Параметр dual характеризует данный интерфейс как сдвоенный.

interface IDualInterface : IDispatch { HRESULT Foo([in] long i, [out, retval] long* iRet ); HRESULT Bar([in] BSTR s ); };

В этом фрагменте дается описание структуры сдвоенного интерфейса IDualInterface. Оператор interface объявляет новый интерфейс, IDualInterface, как производный от IDispatch. Это означает, что первые семь принадлежащих функций нового интерфейса - это функции IDispatch.

Следующие предложения идентифицируют его новые функции. Все принадлежащие функции сдвоенного интерфейса должны передавать в вызывающую программу параметр HRESULT - стандартный код ошибки, описание которого содержится в файле ole2.pas. Поэтому если функция передает что-то еще, то ей придется пересылать это значение в одном из параметров в своем списке аргументов. Как это делается, показано в первой из функций, Foo: ее результат передается в аргументе iRet.

Обратите внимание на наличие ряда специальных указаний, обрамленных квадратными скобками. Все параметры каждой принадлежащей функции интерфейса должны помечаться как in (входные) или out (выходные). Директива retval указывает, что именно в этом параметре будет передаваться return-значение данной функции, например iRet функции Foo.

Функция Bar служит примером того, как передаются и принимаются строковые переменные объектом автоматизации. Учтите, что все строки должны иметь принятый в автоматизации строковый тип BSTR. В строках типа BSTR содержится значение длины строки, а последний символ в них NULL. Однако учтите, что в Win32 символы в строке типа BSTR представлены в 2 байт кодировке Unicode.

//Сведения о классе для интерфейса // TDualInterface [ uuid(062C7181-7DF1-11CF-896C-444553540000) ] coclass TDualInterface { [default] interface IDualInterface; };

Здесь наш объект автоматизации идентифицируется как COM объект. Значение uuid должно совпадать с идентификатором uuid, сгенерированным для нашего объекта экспертом автоматизации системы Delphi.

Если при создании своих ODL файлов вы используете в качестве шаблона файл Project1.odl, то формирование собственной библиотеки типов не составит для вас труда. Для запуска утилиты Mktyplib достаточно набрать:

MKTYPLIB Project1.odl

и эта утилита преобразует файл Project1.odl в Project1.tlb. Прибегнув к помощи утилиты MIDL 3.0 из комплекта Windows NT 4.0 Beta 1 SDK, вы можете внести изменения в свой ODL файл, с тем чтобы обеспечить возможность применения синтаксиса IDL, либо работать с MIDL 3.0 в режиме совместимости с Mktyplib:

MIDL /mktyplib203 Project1.odl

Реализация расширенного интерфейса

Чтобы подготовить в Delphi сдвоенные интерфейсы, нужно внести некоторые исправления в текст модул oleauto.pas библиотеки VCL (Visual Component Library). Создадим новый модуль dualauto.pas со всеми необходимыми для oleauto.pas изменениями. Их объем минимален, и мы постараемся дать к ним пояснения.

Сначала разберемся с oleauto.pas; прежде всего, с тем, как в oleauto.pas реализует интерфейс IDispatch. Пока мы не будем касаться того, как работает большинство его функций для позднего связывани (например, GetIDsOfNames).

Все новые объекты автоматизации порождаются из класса TAutoObject, который обеспечивает все принадлежащие функции IDispatch. Однако в виртуальной таблице vtable для TAutoObject не представлены прямые ссылки на принадлежащие функции интерфейса IDispatch. В самом деле, в TAutoObject ни одна из этих функций IDispatch не описана как виртуальная, что видно из определения TAutoObject, приведенного на лист. 1.


Лист. 1. Описание TAutoObject.
TAutoObject = class(TObject) private FRefCount: Integer; PAutoDispatch: TAutoDispatch; function GetIDsOfNames(Names: POleStrList; Count: Integer; DispIDs: PDispIDList): Hresult; function GetOleObject: Variant; function Invoke(DispID: TDispID; Flags: Integer; var Params: TDispParams; VarResult: PVariant; ExcepInfo: PExcepInfo; ArgErr: PInteger): Hresult; procedure InvokeMethod(AutoEntry, Args, Result: Pointer); function QueryInterface(const iid, TIID; var obj): HResult; protected procedure GetExceptionInfo(ExceptObject: TObject; var ExcepInfo: TExcepInfo); virtual; public constructor Create; virtual; destructor Destroy; override; function AddRef: Integer; function Release: Integer; property AutoDispatch: TAutoDispatch read FAutoDispatch; property OleObject: Variant read GetOleObject; property RefCount: Integer read FRefCount; end;

В таком случае, как же Delphi формирует таблицу vtable для принадлежащих функций IDispatch? Дело в том, что класс TAutoObject содержит вложенный класс, именуемый TAutoDispatch, назначение которого заключается лишь в одном: делегировать свои функциональные возможности объекту TAutoObject. Он связывается с родительским объектом TAutoObject через отдельную private переменную, память под которую отводится при создании TAutoDispatch и в которой хранится ссылка на родительский класс. Вы можете убедиться, просмотрев листинг 2, что все принадлежащие функции TAutoDispatch виртуальные, поскольку описаны ключевым словом override.


Лист. 2. Описание TAutoDispatch.

TAuto Dispatch = class(IDispatch) private FAutoObject: TAutoObject; public constructor Create(AutoObject: TAutoObject); function QueryInterface(const iid: TIID; var obj): HResult; override; function AddRef: Longint; override; function Release: Longint; override; function GetTypeInfoCount(var ctinfo: Integer): HResult; override; function GetTypeInfo(itinfo: Integer; lcid: TLCID; var tinfo: ITypeInfo): HResult; override; function GetIDsOfNames(const iid: TIID; rgszNames: POleStrList; cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult; override; function Invoke(dispIDMember: TDispID; const iid: TIID; lcid; TLCID; flags: Word; var dispParams: TDispParams; varResult: PVariant; excepInfo: PExcepInfo; argErr: PInteger): HResult; override; function GetAutoObject: TAutoObject; stdcall; end;

Такой способ реализации интерфейса IDispatch облегчит нашу задачу. Поскольку сдвоенный интерфейс наследуется из стандартного интерфейса IDispatch, основная наша задача - задать определение нового класса, производного от TAutoDispatch. Правда, придетс внести кое-какие коррективы.

Большинство вносимых в oleauto.pas изменений подразделяются на две группы: преобразование отдельных компонентов данных и методов из разряда private в protected для доступа к ним из производных классов, а также изменения для облегчения создания интерфейсов IDispatch, обеспечивающих соответствие передаваемых типов. Например, я добавил в структуру AutoClassInfo новое поле, так что мы можем присвоить нашему расширенному интерфейсу уникальный идентификатор.

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

Нам необходима также возможность доступа к компоненту FAutoObject класса TAutoDispatch из его производных классов. Учтите, что классы, объявленные в одном и том же программном модуле, имеют неограниченный доступ ко всем private переменным и методам любого другого класса в пределах данного модуля. Однако мы хотим использовать модуль dualauto.pas как обобщенную библиотечную функцию, и поэтому нам необходима возможность определить новый класс как производный от TAutoDispatch, но в другом модуле. Единственный способ добиться этого - объявить компонент FAutoObject как protected вместо private. Взгляните на новое описание TAutoDispatch на лист. 3.


Лист. 3. Новое описание TAutoDispatch.

TAutoDispatch = class(IDispatch) protected //* Преобразовано в protected! FAutoObject: TAutoObject; public constructor Create(AutoObject: TAutoObject); function QueryInterface(const iid: TIID; var obj): HResult; override; function AddRef: Longint; override; function Release: Longint; override; function GetTypeInfoCount(var ctinfo: Integer): HResult; override; function GetTypeInfo(itinfo: Integer; lcid: TLCID; var tinfo: ITypeInfo): HResult; override; function GetIDsOfNames(const iid: TIID; rgszNames: POleStrList; cNames: Integer; lcid: TLCID; rgdispid: PDispIDList): HResult; override; function Invoke(dispIDMember: TDispID; const iid: TIID; lcid; TLCID; flags: Word; var dispParams: TDispParams; varResult: PVariant; excepInfo: PExcepInfo; argErr: PInteger): HResult; override; // *** Превратить функцию GetAutoObject из виртуальной в статическую. // function GetAutoObject: TAutoObject; virtual; stdcall; function GetAutoObject: TAutoObject; stdcall; end;

Нам потребуется также доступ к принадлежащим функциям FRefCount и FAutoDispatch класса TAutoObject. Поэтому также пришлось поменять объявление их переменных с protected на private.

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

TAutoClassInfo TRegistryClass TClassFactory DllGetClassObject

Сжатые рамки статьи не позволяют во всех подробностях рассказать об исправлениях в каждом из этих классов и структур. При желании вы можете изучить исходный текст модуля dualauto.pas. Количество внесенных в него исправлений минимально - только обеспечивающие возможность использования параметра FIID в классе, производном от TAutoObject.

Единственное важное изменение в этой программе было проведено с функцией QueryInterface класса TAutoObject. Нам хотелось, чтобы контроллер автоматизации специфическим образом обращался к расширенному интерфейсу. Это связано с тем, что контроллеры автоматизации, оснащенные средствами для обращений к сдвоенным интерфейсам, всегда предварительно связываются с библиотекой типов объекта автоматизации. Таким образом, контроллеру становится известен идентификатор IID нашего сдвоенного интерфейса и он может вызвать его метод QueryInterface. Именно поэтому мы расширили процесс регистрации нашего объекта автоматизации, дополнив его программной строкой дл идентификации расширенного интерфейса. Нам пришлось исправить лишь одну строку в программе TAutoObject.QueryInterface, заменив исходную:

if IsEqualIID(iid, IID_IUnknown) or IsEqualIID(iid, IID_IDispatch) or IsEqualIID(iid, IID_IAutoDispatch) then

на:

// Исправления - чтобы с помощью нашего нового IID // обратиться к QueryInterface if IsEqualIID(iid, IID_IUnknown) or IsEqualIID(iid, IID_IDispatch) or IsEqualIID(iid, IID_IAutoDispatch) or IsEqualIID(iid, IID_FIID) then

Вот и все изменения в файле oleauto.pas. Осталось только заменить oleauto.pas на dualauto.pas во всех наших объектах автоматизации и запомнить, что dualauto.pas - это oleauto.pas, дополненный средствами работы с расширенными интерфейсами.

Создание объекта с расширенным интерфейсом

Теперь, когда oleauto.pas дополнен средствами дл работы со сдвоенными интерфейсами, пользоваться им очень просто. Создайте модуль средствами эксперта Expert Automation системы Delphi, присвоив новому классу имя TDualInterface. Затем вы объявите AutoClassInfo следующим образом:

const AutoClassInfo: TAutoClassInfo = ( AutoClass: TDualInterface; ProgID: 'Project1.DualInterface'; ClassID: '{062C7181-7DF1-11CF-896C-444553540000}'; IID: '{062C7182-7DF1-11CF-896C-444553540000}'; // <- Вставьте эту строку Description: ' '; Instancing: acMultiInstance);

Такая структура в том виде, в каком ее создает Delphi, не содержит поле IID, а значение ее параметра ClassID отличается от приведенного здесь, так как его генерирует принадлежащая функция CoCreateGuid из COM библиотек. Следует ввести поле IID и его значение GUID. Для простоты я использовал как GUID значение ClassID, увеличив последнюю цифру в первой группе на 1.

Добавим пару функций в наш класс TDualInterface. Назовем их Foo и Bar. Первая принимает некоторое целое число, добавляет к нему 1 и передает результат вызывающей программе; вторая выводит в окне сообщений строку. Перед вами их описание:

type TDualInterface = class(TAutoObject) private {Список private компонент} public // Переопределение конструктора TAutoObject constructor Create; override; automated {Список automated компонент} function Foo(i: Integer): Integer; procedure Bar(s: String); end;

Далее необходимо дать определение новому классу, TDualDispatch, как производному от TAutoDispatch. Ваши реальные методы автоматизации в TDualDispatch будут представлены функциями заглушками. Назначение подобных заглушек - делегировать реальную функциональность вашей реализации в TDualInterface. Перед вами пример определения нашего класса TDualDispatch:

// Диспетчер расширенного интерфейса ! TDualDispatch = class(TAutoDispatch) public // Определение новых виртуальных функций // ** Они должны быть stdcall !! function Foo(i: Integer; var iRet: Integer): Hresult; virtual; stdcall; function Bar(s: PWideChar): Hresult; virtual; stdcall; end;

Обратите внимание, что все функции, к которым будут обращения, необходимо объявлять в соответствии с соглашением stdcall.

Функция Bar просто выводит на экран строку, переданную контроллером автоматизации. Эта строка пересылается как указатель на строку типа WideChar (кодировка Unicode). Существует правило, в соответствии с которым все передаваемые функциям COM API строковые переменные должны быть представлены в кодировке Unicode. Рассотрим функции Foo и Bar:

function TDualDispatch.Foo(i: Integer; var iRet: Integer): Hresult; begin // Переадресовать вызов реальному варианту Foo iRet := TDualInterface(FAutoObject).Foo(i); Result := S_OK; end;

Foo переадресовывает свой вызов функции Foo интерфейса TDualInterface. Как уже отмечалось, все return-значения функций интерфейса TDualInterface следует поместить в передаваемый в вызывающую программу параметр iRet соответствующей функции интерфейса TDualDispatch. Фактически же return-значение функции Foo - это код ошибки или признак успешности завершени операции, который сообщается контроллеру.

Функция Bar несколько сложнее, поскольку существует необходимость преобразования строки с кодировкой Unicode в строку для Delphi. Такое преобразование Delphi проводит автоматически, когда контроллер запрашивает объект автоматизации через функцию Invoke интерфейса IDispatch. Однако, поскольку в данном случае контроллер автоматизации обращается непосредственно к самой функции расширенного интерфейса, мы должны выполнить преобразование в нашей функции заглушке:

function TDualDispatch.Bar(s: PWideChar): Hresult; begin // Переадресовать вызов реальной функции Bar TDualInterface(FAutoObject).Bar(WideCharToString(s)); Result := S_OK; end; {TDualDispatch.Bar}

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

Работа объекта, снабженного расширенным интерфейсом

Теперь, когда все операции по созданию и компиляции объекта с расширенным интерфейсом завершены, осталось только одно: объект автоматизации необходимо зарегистрировать. Поскольку он оформлен в виде DLL, дл его регистрации следует воспользоваться утилитой Regsvr32.exe. Единственное, что выполняет данна утилита, - она обращается к функции DLLRegisterServer, экспортируемой нашим DLL. Остальное в ведении функции RegisterServer модуля oleauto.pas.

В настоящее время единственный способ создать контроллер автоматизации, работающий со сдвоенными интерфейсами, - с помощью средств Visual Basic 4.0 (VB). Покажем два варианта создания контроллера автоматизации. В первом случае будет использована така особенность VB 4.0, как средства связывания при компиляции. В другом варианте обращение к объекту будет осуществляться с использованием позднего связывани через интерфейс IDispatch.

Версия программы с привязкой на стадии компиляции получена путем добавления библиотеки типов Project1.tlb в текущий список References (Связи) системы VB. Дл этого нужно щелкнуть мышью на кнопке Browse (Обзор) в диалоговом окне References и выбрать нужную библиотеку типов.

Благодаря списку References VB программа может обращаться к внешнему объекту так, будто это ее собственный. Фрагмент программы со средствами связывания при компиляции:

Dim s As ProjX.TDualInterface

Здесь дано определение новой переменной s дл обращений к объекту автоматизации. ProjX - это наша библиотека типов, а TDualInterface - ссылка на объект. В процессе компиляции VB 4.0 загрузит библиотеку типов и задействует ее для соблюдения правил строгого контроля типов переменных. Другими словами, все функции, на которые в программе имеются ссылки, должны быть перечислены в библиотеке типов, и VB обеспечит правильность типов передаваемых им параметров.

Dim i As Long Dim timeStart Set s = New ProjX.TDualInterface 'Вызов функций созданного в Delphi объекта автоматизации, оформленного в виде DLL s.Bar ("Hello World") timeStart = Timer i = 0 MsgBox("Начало:" & timeStart) While i < 100000 'i = i + 1 i = s.Foo(i) Wend MsgBox("Time:" & Timer - timeStart) Set s = Nothing

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

Программа, реализующая позднее связывание, идентична показанной выше, где привязка выполнена при компиляции. Единственное отличие состоит в способе объявлени переменной для обращений к объекту. Вместо ссылок на библиотеку типов в списке References языка VB объявляется некая обобщенная переменная типа Object и вызывается принадлежащая функция CreateObject языка Visual Basic:

Dim s As Object Set s = CreateObject("Project1.TDualInterface")

Быстродействие

Каково быстродействие сдвоенного интерфейса? К сожалению, поскольку мы используем VB, достаточно трудно оценить реальный прирост скорости. Причина в том, что в VB при циклическом вызове функции сдвоенного интерфейса непроизводительные затраты времени по сравнению с вызовом той же функции нашего объекта автоматизации очень велики. Поэтому можно назвать лишь минимум - приблизительно в 12 раз быстрее. Реальное значение, вероятно, около 500. Однако, поскольку в настоящее время контроллер автоматизации, работающий с расширенным интерфейсом, может быть создан лишь на базе VB, реально рассчитывать лишь на 12 кратное увеличение.

Следует также учесть, что резкий прирост быстродействия имеет место лишь при использовании внутренних (in process) объектов автоматизации (оформленных в виде DLL). При необходимости реализации межпроцессных обращений или удаленного вызова средств автоматизации, где будет подключаться механизм межпроцессных переходов (marshaller) технологии автоматизации, прирост быстродействия будет значительно ниже - примерно 20%. Сдерживать рост быстродействи будут непроизводительные затраты Windows, связанные с переходами из одной области памяти в другую в случае межпроцессного варианта или с вызовом удаленной процедуры. Но, как и от любого механизма, сулящего резкий скачок в быстродействии, вы не станете отказываться и от использования серверов с расширенным интерфейсом.

Джон Лэм - президент компании Naleco Research (www.naleco.com), занимающейся разработками служебных программ для Windows 95.