Королевство Дельфи"Knowledge itself is power"
F.Bacon
 Лицей
  
Главная
О лицее

Список семинаров

 
 К н и г и
 
Книжная полка
 
 
Библиотека
 
  
  
 


Поиск
 
Поиск по КС
Поиск в статьях
Яndex© + Google©
Поиск книг

 
  
Тематический каталог
Все манускрипты

 
  
Карта VCL
ОШИБКИ
Сообщения системы

 
Форумы
 
Круглый стол
Новые вопросы

 
  
Базарная площадь
Городская площадь

 
   
С Л С

 
Летопись
 
Королевские Хроники
Рыцарский Зал
Глас народа!

 
  
ТТХ
Конкурсы
Королевская клюква

 
Разделы
 
Hello, World!
Лицей

Квинтана

 
  
Сокровищница
Подземелье Магов
Подводные камни
Свитки

 
  
Школа ОБЕРОНА

 
  
Арсенальная башня
Фолианты
Полигон

 
  
Книга Песка
Дальние земли

 
  
АРХИВЫ

 
 

Сейчас на сайте присутствуют:
 
  
 
Во Флориде и в Королевстве сейчас  09:08[Войти] | [Зарегистрироваться]

Урок 15. IDL - язык определения интерфейсов

Антон Григорьев
дата публикации 19-06-2008 12:16

урок из цикла: Использование COM/DCOM в Delphi


предыдущий урок содержание семинара следующий урок

Урок 15. IDL - язык определения интерфейсов

Мы уже говорили о том, что COM/DCOM является технологией, не привязанной к конкретному языку, поэтому требуется средство описания интерфейсов (а также иных типов), которое подходило бы для разных языков. С одним из таких средств мы уже познакомились — это библиотека типов. В этом уроке мы познакомимся с альтернативным средством — описанием интерфейсов на языке IDL.

То, что в COM/DCOM предусмотрено два средства для решения одной задачи, объясняется историей развития этой технологии. Библиотека типов изначально возникла как часть технологии OLE и была непригодна для описания общих COM-интерфейсов. С другой стороны, стандарт RPC, на основе которого разрабатывался COM/DCOM, содержал специальный язык определения интерфейсов — IDL (Interface Definition Language; полное название языка в этом стандарте — OSF DCE RPC IDL), который решал аналогичную задачу. Но в чистом виде этот язык не подходил для описания COM-интерфейсов, поэтому для COM был создан его диалект, получивший название MIDL — Microsoft IDL. Средств описания OLE-интерфейсов в его первых версиях не было. Возможность описания интерфейсов для RPC (использующуюся, например, в технологии CORBA), MIDL при этом не потерял. Но здесь мы будем рассматривать только то подмножество MIDL, которое используется в COM/DCOM.

Примечание: Использование одного и того же языка MIDL для описания как COM-интерфейсов, так и RPC-интерфейсов сильно затрудняет чтение справки по нему, так как в MSDN далеко не всегда чётко разделено, что относится к COM, а что — к RPC.

Постепенно разница между OLE и "чистым" COM становилась всё меньше и меньше, расширялись возможности как библиотеки типов, так и MIDL. В настоящее время, если говорить только об описании интерфейса для клиента и сервера, возможности этих двух средств практически эквивалентны. Но, как мы уже знаем, библиотека типов решает другую очень важную задачу — предоставление информации о типах библиотеке oleaut32.dll при универсальном маршалинге. Описание типов на MIDL для этих целей само по себе непригодно, зато на его основе можно получить код библиотеки proxy/stub dll, которая используется при стандартном маршалинге. Из этих особенностей легко сделать вывод об области применения каждого из этих средств: библиотеку типов обычно используют в случае универсального маршалинга, MIDL — в случае стандартного.

Примечание: Первоначально для описания библиотек типов существовал отдельный язык — ODL, и была утилита MkTypeLib, которая на основе ODL-описания генерировала tlb-файл библиотеки типов. Но со временем ODL стал настолько похож на подмножество MIDL, что оказалось проще и логичнее добавить несколько недостающих элементов в MIDL и два языка объединить в один, чем развивать ODL отдельно.

А теперь — самый неприятный факт, связанный с MIDL. Delphi ни в каком виде не поддерживает этот язык. Точнее, редактор библиотеки TLB, как мы видели, позволяет получить описание интерфейсов на языке IDL, но не более того. Взять существующий IDL-файл и что-то из него вытащить среда Delphi не способна. Так что если вам требуется написать клиент для сервера, интерфейсы которого описаны в виде IDL, следует либо транслировать их вручную, либо искать сторонние средства, с помощью которых можно получить из IDL-файла библиотеку типов.

Примечание: В комплекте поставки Delphi можно найти утилиту idl2pas.jar, которая, как можно заключить из названия, является транслятором с IDL на Паскаль. К сожалению, эта утилита имеет отношение только к трансляции CORBA-описаний, а для COM-интерфейсов она бесполезна.

Так как изначально MIDL никак не был связан с библиотекой типов, возможность описывать библиотеки типов на этом языке появилась не сразу. Текст на MIDL имеет выделенный раздел, описывающий библиотеку типов, которая строится на основе этого текста. Всё, что вне этого раздела, используется для построения proxy/stub dll, всё, что внутри — для построения библиотеки типов. И, в принципе, содержимое "библиотечного" раздела может быть никак не связано с тем, что вне его. Но на практике такие несвязанные описания обычно бесполезны, поэтому в библиотеку типов попадает то, что объявлено для proxy/stub dll. Тем не менее, при работе с чужими IDL-файлами эту возможность следует иметь ввиду и проверять, всё ли, что нужно, включено в библиотеку типов. В некоторых случаях IDL-файл может вообще не содержать описания библиотеки типов, и чтобы получить её, придётся самостоятельно добавить этот раздел.

Примечание: Для серверов, использующих стандартный маршалинг, использование IDL является более естественным, чем библиотеки типов, так как IDL позволяет не только описать интерфейсы, но и сразу построить proxy/stub dll на основе этого описания. Поэтому вероятность столкнуться с сервером, описание интерфейсов которого будет дано только на языке IDL, достаточно велика.

Раз Delphi не может помочь нам в работе с IDL-описанием, приходится использовать сторонние средства, а именно — утилиту midl.exe, входящий в состав Visual Studio. О том, что именно получается на выходе этой утилиты, мы поговорим ближе к концу урока, а пока отметим, что с её помощью на основе IDL-файла можно получить библиотеку типов, заголовочные файлы для клиента и сервера и исходный код для proxy/stub dll.

Операторы языка MIDL

Так как язык IDL предназначен только для объявления различных сущностей, но не для реализации каких-либо алгоритмов, он содержит лишь небольшое количество операторов. Синтаксис этих операторов напоминает синтаксис аналогичных операторов C++. Названия встроенных типов также в основном совпадают с типами C++. Ниже приведены основные операторы языка MIDL.

typedef. Аналогичен оператору typedef языка С++. Позволяет определить новый тип.

enum. Позволяет определить перечислимый тип. Аналогичен оператору enum языка C++, так же позволяет задавать конкретные значения констант, входящих в тип.

struct. Позволяет определить структуру (запись). Аналогичен оператору struct языка C++.

union. Позволяет определить объединение, т.е. структуру, все поля которой находятся по одному адресу в памяти (аналог вариантных записей в Паскале). Данный оператор существенно отличается от оператора union с C++, так как требует наличия селектора — специального выражения, которое показывает, какое из альтернативных полей в данный момент активно. Это нужно для того, чтобы proxy/stub dll знала, как в данный момент интерпретировать область памяти, в которой располагается объединение, и правильно сериализовала её.

В IDL существуют два типа объединений: с внутренним селектором (encapsulated union) и с внешним селектором (non-encapsulated union). В первом случае объединение содержит отдельно хранящееся поле, которое и выступает в роли селектора. Во втором случае объединение не имеет собственного селектора, но каждый раз при использовании такого типа нужно указывать, что будет селектором в данном конкретном случае. Чуть ниже мы рассмотрим примеры объединений обоих видов.

const. Используется для объявления констант. Аналогичен оператору const языка C++.

interface. Используется для определения интерфейса. Похож на оператор interface языка C++. Общий вид таков:

interface <Interface_Name> : <Base_Interface>
{
  <List_of_definitinons>
}

<Interface_Name> — это имя интерфейса, <Base_Interface> — имя интерфейса, от которого наследуется данный интерфейс. Для COM-интерфейсов, объявленных вне библиотеки типов, указание предка обязательно, даже если интерфейс наследуется напрямую от IUnknown, не указывать предка можно только для RPC-интерфейсов. Для интерфейсов, объявленных внутри библиотеки типов, интерфейс-предок можно не указывать, транслятор это пропускает, но лучше так не делать, чтобы не иметь проблем с другими программами, которые будут работать с библиотекой типов.

<List_of_definitions> включает в себя список определений. В большинстве случаев это будет просто список методов, но внутри интерфейса можно также определять константы и типы — по смыслу это будет то же самое, что и определение вне интерфейса, но область видимости идентификатора будет ограничена данным интерфейсом.

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

library. Этот оператор используется для описания библиотеки типов. Синтаксис оператора таков:

library <Library_Name>
{
  <List_of_definitions>
}

<List_of_definitions> — это список всего, что входит в библиотеку типов. Здесь могут быть объявления типов, коклассов, интерфейсов, модулей. Типы и интерфейсы, объявленные внутри оператора library, не учитываются при создании proxy/stub dll. Если какой-либо из этих объектов должен попасть и в proxy/stub dll, и в библиотеку типов, его объявляют вне оператора library, а внутри него упоминают без повторного объявления, например:

interface ISomeInterface : IUnknown
{
  HRESULT SomeMethod();
}

library MyTypeLib
{
  ...
  
  // Чтобы объявленный ранее интерфейс
  // попал в библиотеку типов,
  // упоминаем его в операторе library
  // без указания предка, содержимого и т.п.
  interface ISomeInterface;
  // А этот интерфейс попадёт только
  // в библиотеку типов
  interface IOtherInterface : IUnknown
  {
    HRESULT OtherMethod();
  }
  ...
}

В результате трансляции такого кода интерфейс ISomeInterface будет включен и в библиотеку типов, и в proxy/stub dll, а интерфейс IOtherInterface — только в библиотеку типов, он не сможет маршалироваться с помощью proxy/stub dll, сгенерированной транслятором MIDL из этого файла. Если убрать упоминание ISomeInterface из оператора library, он попадёт в proxy/stub dll, но не попадёт в библиотеку типов. Именно поэтому выше мы говорили, что содержимое "библиотечного" раздела IDL-файла может быть никак не связано с тем, что находится вне его.

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

Если оператор library в IDL-файле отсутствует, транслятор midl.exe при обработке такого файла не создаёт библиотеку типов. Оператор library может встречаться в IDL-файле только один раз, объявление второй библиотеки типов приводит к ошибке трансляции.

coclass. Этот оператор используется для описания кокласса. Его синтаксис близок к синтаксису оператора library. Внутри оператора coclass могут быть упомянуты ранее объявленные интерфейсы, которые реализуются данным коклассом.

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

Наличие объявлений коклассов в IDL-файле вне библиотеки типов никак не влияет на код proxy/stub dll, который генерирует midl.exe. По сути дела, единственным результатом такого объявления становится то, что в заголовочных файлах появляются константы CLSID_XXXX, содержащие CLSID объявленных коклассов. Коклассы, объявленные внутри оператора library, также оказывают влияние только на библиотеку типов, но не на код proxy/stub dll (как и всё содержимое этого оператора). Но Delphi при загрузке такой библиотеки сразу строит класс для каждого кокласса, что не всегда удобно, так как этот класс нужен только при разработке сервера, но не клиента.

Если кокласс объявить вне оператора library, а в операторе library только упомянуть, такой IDL-файл будет оттранслирован без ошибок, но кокласс в библиотеке типов не появится.

module. Этот оператор используется для описания модуля, т.е. обычной dll-библиотеки, которая экспортирует функции и константы. При трансляции IDL-файла прототипы функций, содержащихся в модуле, будут добавлены в заголовочный файл. Это позволяет создавать самодокументирующиеся библиотеки dll (нужно просто добавить библиотеку типов с описанием модуля в ресурсы библиотеки), но к COM отношения не имеет, поэтому мы здесь рассматривать данный оператор не будем.

import. Этот оператор импортирует описания из другого idl-файла. Благодаря ему можно наследовать свои интерфейсы от интерфейсов, объявленных в другом файле, использовать объявленные там типы данных и т.п. Имя импортируемого файла записывается в двойных кавычках. Каждый IDL-файл для COM должен импортировать oaidl.idl, содержащий описания стандартных типов и интерфейсов.

importlib. Оператор importlib позволяет библиотеке типов импортировать содержимое другой библиотеки типов. Использовать данный оператор можно только внутри оператора library. Имя импортируемой библиотеки заключается в круглые скобки и двойные кавычки (например, importlib("SomeLib.tlb")) Каждая библиотека типов должна импортировать библиотеку stdole32.tlb или stdole2.tlb, содержащую описание стандартных типов.

cpp_quote. Данный оператор позволяет вставить в генерируемый программой midl.exe заголовочный файл любой текст. Обычно cpp_quote используют для добавления в этот файл дополнительных макроопределений или директив #pragma. Что должен делать с таким оператором транслятор с IDL на язык, отличный от C или C++, спецификация не оговаривает. Если вы встретите такую директиву в IDL-файле, вам придётся самостоятельно разбираться, что и зачем она вставляет, и вручную добавлять аналогичный код к своей программе.

Атрибуты языка MIDL

Язык MIDL содержит атрибуты — сущности, не имеющие аналогов в Delphi и C/C++. Перед каждым объектом (объектом в самом широком смысле этого слова, т.е. под объектом может пониматься метод, параметр метода и т.п.) может быть перечислен набор его атрибутов, заключённых в квадратные скобки и разделённых запятыми (порядок следования атрибутов не важен). Атрибуты могут иметь параметры, которые в этом случае заключаются в круглые скобки (чтобы не путать параметры методов с параметрами атрибутов мы будем в дальнейшем параметры атрибутов называть аргументами). Некоторые типы объектов должны иметь атрибуты, другие могут их не иметь. Вот пример описания интерфейса с атрибутами:

[
  object,
  uuid(E50968D5-A303-41F9-AEE4-663939FEEE6C),
  pointer_default(unique)
]
interface ISomeInterface : IUnknown
{
  [helpstring("Adds two numbers")] HRESULT Add(
    [in] int a,
    [in] int b,
    [out, retval] int* result);
  [helpstring("Returns Count of prime numbers")] HRESULT PrimeNumbers(
    [in] int Count,
    [out, size_is(,Count)] int** PrimeNumbers);
}

Здесь атрибутами интерфейса ISomeInterface являются object, uuid и pointer_default, причём атрибуты uuid и pointer_default имеют по одному аргументу. Методы Add и PrimeNumbers имеют каждый по атрибуту helpstring с аргументом, а in, out и size_is — атрибуты тех параметров методов, перед которыми они стоят.

В этом уроке мы рассмотрим только некоторые базовые атрибуты языка MIDL. По мере изучения новых возможностей COM мы будем знакомиться с остальными атрибутами. Атрибуты бывают универсальными (т.е. применимыми к объектам разных видов) и специализированными (применимыми к объектам одного вида). В таблице 1 показаны некоторые универсальные атрибуты.

Таблица 1. Универсальные атрибуты

АтрибутОписание
helpstringАтрибут, который может применяться к объектам любого вида. Задаёт комментарий, связанный с данным объектом. Аргументом является произвольная строка. IDL поддерживает также комментарии в стиле C++, но атрибут helpstring позволяет, во-первых, привязать комментарий к объекту, а во-вторых, включить его в библиотеку типов.
uuidАтрибут, задающий GUID объекта. Применяется к интерфейсам, коклассам, библиотекам типов, модулям. Имеет один аргумент — GUID, записанный в виде XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX (без обрамляющих фигурных скобок, кавычек и т.п.).
customДанный атрибут позволяет порождать дополнительные нестандартные атрибуты. Применяется для любых объектов, кроме упоминаний интерфейсов внутри кокласса. Принимает два аргумента: GUID атрибута и значение. GUID записывается в том же виде, что и у атрибута uuid, значение может принадлежать любому VARINAT-совместимому типу. Появление такого атрибута никак не влияет на код proxy/stub dll, но информация о нём заносится в библиотеку типов. При программной обработке библиотеки типов с помощью интерфейса ITypeLib2 можно извлечь эти атрибуты и использовать в своих целях.

Атрибуты библиотеки типов

Библиотека типов обязана иметь GUID (который в данном случае называется LibID), поэтому атрибут uuid для неё является обязательным. Из прочих атрибутов мы рассмотрим здесь только атрибут version, который задаёт версию библиотеки типов. Данный атрибут имеет один аргумент, который может являться целым числом или состоять из двух целых чисел, разделённых точкой (версия и подверсия), например: version(1.3).

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

Атрибуты интерфейсов

Для COM-интерфейсов обязательным атрибутом является uuid, так как интерфейсов без IID не бывает. Прочие атрибуты интерфейса перечислены в таблице 2.

Таблица 2. Атрибуты интерфейсов

АтрибутОписание
objectДанный атрибут является обязательным для всех COM-интерфейсов, объявленных вне оператора library. Собственно говоря, наличие этого атрибута и является критерием, по которому COM-интерфейсы отличаются от RPC-интерфейсов. При объявлении интерфейса внутри оператора library атрибут object можно не указывать, так как библиотека типов по определению не может содержать RPC-интерфейсов.
oleautomationДанный атрибут указывает, что интерфейс выполняет все требования, необходимые для универсального маршалинга, и может маршалироваться с помощью oleaut32.dll. К этим требованиям относится то, что интерфейс наследуется от IUnknown, IDispatch или иного совместимого с oleaut32.dll интерфейса, все методы возвращают результат типа HRESULT, а параметры всех методов имеют VARIANT-совместимые типы. Если вы планируете использовать универсальный маршалинг для интерфейса, вы должны добавить ему этот атрибут.
dualУказывает, что интерфейс является дуальным. О дуальных интерфейсах мы будем говорить в одном из следующих уроков.
pointer_defaultДанный атрибут имеет один аргумент, который может принимать значения ptr, unique или ref. Эти значения определяют вид всех вложенных указателей, встречающихся в параметрах методов данного интерфейса, для которых этот вид не указан явно. Указателями верхнего уровня называются параметры, непосредственно являющиеся указателями. Если же указатель получается при разыменовании указателя верхнего уровня (например, если указатель верхнего уровня является указателем на указатель или указателем на структуру, содержащую указатель), такой указатель называется вложенным. Указатели верхнего уровня независимо от наличия атрибута pointer_default имеют вид ref. (О видах указателей мы поговорим чуть ниже при обсуждении атрибутов параметров.)

Когда интерфейс упоминается внутри оператора coclass, у этого упоминания тоже могут быть атрибуты. Возможные атрибуты показаны в таблице 3.

Таблица 3. Атрибуты упоминаний интерфейсов в коклассах

АтрибутОписание
defaultУказывает, что данный интерфейс является для сервера интерфейсом по умолчанию. Если COM-объект создаётся низкоуровневыми средствами (например, через фабрику класса или функцию CoCreateInstance), понятия интерфейса по умолчанию не существует, требуемый интерфейс всегда указывается явно. Но языки типа Visual Basic предоставляют разработчику только высокоуровневую обёртку над средствами создания COM-объекта. Эта обёртка на основе библиотеки типов может узнать, какой интерфейс является интерфейсом по умолчанию и при создании COM-объекта возвращать именно этот интерфейс. Что касается Delphi, то, как мы помним из предыдущих уроков, эта среда при импорте библиотеки типов для каждого кокласса создаёт класс, имя которого образовано именем кокласса и префиксом Co. Этот класс содержит две классовые функции: Create и CreateRemote. Если у кокласса назначен интерфейс по умолчанию, эти функции будут возвращать именно этот интерфейс.
sourceДанный атрибут показывает, что указанный интерфейс является для кокласса интерфейсом обратного вызова, т.е. кокласс реализует не сам интерфейс, а точку подключения к нему. Об интерфейсах обратного вызова и точках подключения мы будем говорить в одном из следующих уроков.

Атрибуты передачи параметров

Параметры методов, как мы видели выше, могут иметь свои атрибуты. Эти атрибуты перечислены в таблице 4.

Таблица 4. Атрибуты передачи параметров методов

АтрибутОписание
inДанный атрибут определяет направление передачи параметра — от клиента к серверу. Сервер через такой параметр ничего не передаёт клиенту. Если параметр является указателем на какую-то внешнюю область памяти, клиент отвечает за выделение этой памяти и за её освобождение после вызова. Если атрибуты направления для параметра не указаны, по умолчанию он считается in-параметром.
outДанный атрибут определяет направление передачи параметра — от сервера клиенту. Клиент ничего не передаёт через данный параметр серверу, он используется только для возврата значения сервером. Параметр с атрибутом out всегда должен быть указателем (из-за отсутствия в IDL понятия параметра-переменной вместо передачи по ссылке используется явная передача указателя, как и в С). Если тип параметра сам по себе является указателем (т.е. в итоге получается двойной указатель), за выделение памяти для такого параметра отвечает сервер, за освобождение — клиент.

IDL поддерживает также двунаправленные параметры, которые имеют одновременно два атрибута in и out. Как и в случае простых out-параметров, двунаправленные параметры должны передаваться через указатель. Если параметр сам по себе является указателем, то клиент при необходимости перед вызовом выделяет нужное количество динамической памяти. Сервер может при необходимости освободить эту память и выделить другой блок. Освобождение памяти после завершения работы метода лежит на клиенте.

Если вызванная функция завершается с ошибкой, сервер обязан освободить всю память, выделенную им для выходных параметров, а самим этим параметрам присвоить значение nil. Клиент в этом случае за освобождение памяти не отвечает. Для двунаправленных (in. out) параметров сервер не обязан освобождать память в случае ошибки, за освобождение памяти для них всё равно отвечает клиент. Также клиент в любом случае отвечает за освобождение памяти для входных параметров, сервер не должен освобождать их даже в случае ошибки.
defaultvalueДанный атрибут показывает, что у параметра есть значение по умолчанию. Атрибут имеет аргумент — значение по умолчанию, которое может быть любым константным выражением. Данное выражение обязано иметь VARIANT-совместимый тип, поэтому параметры прочих типов не могут иметь атрибут defaultvalue.

Несмотря на то, что в Delphi есть возможность назначать параметрам значение по умолчанию, при трансляции библиотеки типов эта возможность не используется, и defaultvalue просто игнорируется (видимо, это просто "тяжёлое наследие" старых версий Delphi, в которых у параметров не было значений по умолчанию).
optionalДанный атрибут указывает, что параметр не является обязательным. MSND утверждает, что атрибут optional применим только к параметрам, имеющим тип VARIANT или VARIANT* (указатель на VARIANT). Однако эксперименты показывают, что транслятор допускает любой тип у параметров с таким атрибутом, даже не VARINAT-совместимый (хотя лучше, наверное, ограничиться VARIANT-совместимыми типами, иначе можно получить проблемы с автоматическим построением заглушки). Кроме того, атрибут optional автоматически добавляется ко всем параметрам, у которых есть атрибут defaultvalue.
lcidДанный атрибут указывает, что параметр является идентификатором языка. Параметр должен иметь тип long и должен быть входным. В одном методе допускается не более одного параметра с атрибутом lcid. Некоторые среды позволяют клиенту при вызове метода опускать параметр с атрибутом lcid, неявно подставляя нужное значение.
retvalДанный атрибут показывает, что параметр является результатом функции. Все функции обязаны возвращать значение типа HRESULT, а реальный результат работы функции возвращать через out-параметр. Атрибут retval показывает, что именно этот параметр содержит возвращаемое значение. Некоторые среды позволяют упростить вызов методов с retval-параметром так, как будто функция возвращает не HRESULT, а именно это значение, но Delphi к таким средам не относится и, как и все низкоуровневые средства, игнорирует этот атрибут (хотя ставить его всё равно полезно на тот случай, если разработчик клиента будет использовать, например, Visual Basic). Параметр с атрибутом retval должен иметь атрибут out и быть последним в списке параметров метода.

MSDN утверждает, что последовательность параметров в методе должна быть следующая:

  1. Обязательные параметры (без атрибутов defaultvalue и optional)
  2. Необязательные параметры (как с атрибутом defaultvalue, так и без него)
  3. Параметры с атрибутом optional, но без значения по умолчанию
  4. Параметр с атрибутом lcid
  5. Параметр с атрибутом retval

Какие-то из перечисленных выше пунктов могут отсутствовать, но менять их местами нельзя.

Следует признать, что в такой формулировке порядок следования параметров не совсем понятен, так как пункт 2 включает в себя пункт 3. Более того, эксперименты показывают, что параметры со значением по умолчанию могут идти после необязательных параметров без значения по умолчанию. Похоже, что пункт 3 здесь просто лишний. Вообще, с этими необязательными параметрами не всё гладко — выше мы уже отмечали несоответствие описания параметра optional и того, как на самом деле работает транслятор. В той версии midl.exe, которая поставлялась в составе Visual Studio 6, была ещё одна ошибка — в ней допускалось описывать обязательные параметры после необязательных. Видимо, вопрос трансляции необязательных параметров не до конца проработан, поэтому лучше их если и использовать, то только в простых случаях, когда подобных вопросов не возникает, чтобы не столкнуться с очередной ошибкой то ли описания, то ли транслятора.

Ещё пара слов об ответственности за выделение и освобождение памяти для out-параметров. Общее правило здесь такое: указатели верхнего уровня указывают на память, которую выделяет клиент, вложенные указатели — на память, которую выделяет сервер. Рассмотрим это правило на простом примере. Пусть сервер должен вернуть клиенту некоторый целочисленный массив (в этом случае нужно позаботиться и о том, как proxy/stub dll узнает длину этого массива, но об этом чуть позже). Здесь допустимы такие варианты.

HRESULT SomeMethod(
  [out] int* Arr);

Здесь мы имеем одинарный указатель, Arr указывает на начало массива. При вызове такого метода клиент сам выделяет память под массив и передаёт указатель на него в качестве параметра Arr. Такой случай неудобен тем, что клиент не всегда знает размер массива, который должен вернуть сервер. В этом случае метод должен выглядеть так:

HRESULT SomrMethod(
  [out] int** Arr);

Здесь *Arr — указатель на массив, а Arr — указатель на этот указатель (при этом Arr — указатель верхнего уровня, *Arr — вложенный указатель). При вызове такого метода клиент отвечает только за выделение памяти для указателя на массив, и указатель на этот указатель передаёт в качестве параметра Arr. А сервер выделяет память под сам массив, и возвращает клиенту этот указатель через *Arr. Аналогичные правила действуют и для прочих случаев выходных указателей. Например, если у нас есть структура, содержащая указатели, и выходной указатель верхнего уровня указывает на эту структуру, то память для неё выделяет клиент, а память для того, на что указывают указатели внутри структуры — сервер. Если же нужно, чтобы и память на саму структуру тоже выделял сервер, выходной параметр должен быть указателем на указатель на эту структуру.

Атрибуты содержимого параметров и полей структур

Для того, чтобы правильно передать параметры, иногда требуется знать и них больше, чем это сообщает их тип. В первую очередь это касается указателей — иногда бывает нужно более детальное описание того, на что он указывает, чем это можно извлечь из типа. Поэтому язык MIDL содержит целый ряд атрибутов, которые позволяют уточнить размер и содержимое той области памяти, на которую ссылается параметр-указатель и построить для её передачи более эффективный код proxy/stub dll. Аналогичные проблемы возникают при передаче полей структур, поэтому те же самые атрибуты применимы и к ним.

Примечание: MIDL допускает применение большинства из этих атрибутов к методам интерфейсов — в этом случае атрибут характеризует значение, возвращаемое методом. Но для COM-интерфейсов это не актуально, так как они все должны возвращать значение типа HRESULT.

Прежде всего, в MIDL имеется три вила указателей (мы уже говорили о видах указателей при обсуждении атрибута pointer_default — см. таблицу 2). Атрибуты, устанавливающие вид указателя, показаны в таблице 5.

Таблица 5. Атрибуты вида указателя

АтрибутКомментарий
ptrНа указатель с атрибутом ptr не накладывается никаких ограничений.
uniqueГлавное ограничение, накладываемое на указатель с таким атрибутом — это то, что на область памяти, на которую он указывает, нет никаких других ссылок из параметров данного метода. Запрещены не только прямые ссылки, но и косвенные, т.е., например, когда другой параметр ссылается на структуру, которая содержит указатель, ссылающийся на ту же область памяти, что и параметр-указатель с атрибутом unique.
refНа этот указатель накладываются, во-первых, все те же ограничения, что и на указатель типа unique, во-вторых, он не может иметь значение NULL, в-третьих, сервер не может изменять такой указатель (но при необходимости может изменять содержимое той области памяти, на которую он указывает). По своему смыслу указатель вида ref очень близок к ссылкам (reference) языка C++.

Если вид указателя не задан для параметра явно, и не задан вид указателя по умолчанию для всего интерфейса с помощью атрибута pointer_default, считается, что указатель имеет вид ref.

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

Параметры-массивы в C/C++ традиционно передаются указателем на первый элемент и длиной (последнее, впрочем, бывает не всегда). MIDL позволяет использовать такой способ передачи массива. Естественно, для правильного маршалинга заместитель и заглушка должны знать размер массива, иначе массив не будет правильно сериализован. Язык MIDL поддерживает четыре различных вида массивов, которые отличаются друг от друга по тому, как определяется объём выделенной для них памяти:

  • массивы фиксированной длины (fixed arrays);
  • совместимые массивы (conformant arrays);
  • переменные массивы (varying arrays);
  • открытые массивы (open arrays).

Массивы фиксированной длины объявляются явным указанием дины в квадратных скобках, например:

HRESULT SomeFunction(
  [in] int OneDimArray[20],
  [in] double TwoDimArray[40][100]);

Это самый простой вид массивов. Допускаются как одномерные, так и многомерные массивы фиксированной длины.

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

[size_is(<Expr>)] int* Array
[size_is(<Expr>)] int Array[]
[size_is(<Expr>)] int Array[*]

Эти три записи полностью эквивалентны, выбор той или иной определяется только вкусом разработчика. Выражение <Expr> — любое допустимое в языке C++ выражение без побочных эффектов (т.е. без вызова функций и операций ++ и --), его результат задаёт количество элементов в массиве (константные выражения тоже допускаются, но в этом случае предпочтительнее объявить массив фиксированной длины — маршалинг будет эффективнее). Самый распространённый вариант использования size_is — создание отдельного параметра или элемента структуры, в котором хранится размер массива, например:

HRESULT SomeFunc(
  [in] int Count,
  [in, size_is(Count)] int* Array);

struct TSomeStruct
{
  ...
  int ArrCount;
  [size_is(ArrCount)] int* Array;
};

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

Двумерные совместимые массивы запрещены, разрешены только совместимые массивы фиксированных массивов. Но можно сделать совместимый массив указателей на совместимые массивы (это получится примерно то же самое, что и двумерный динамический массив в Delphi). Весь массив не будет занимать непрерывную область памяти, но в тех задачах, где это не важно, такой массив ведёт себя так же, как и обычный двумерный.

Таким образом, если у нас есть двойной указатель, его можно трактовать тремя различными способами: массив массивов (двумерный совместимый массив), массив указателей и указатель на указатель на массив (т.е. указатель на указатель на первый элемент массива). Тот или иной вариант определяется тем, какие аргументы заданы у атрибута size_is. В случае необходимости некоторые из аргументов можно опускать.

// Двумерный массив
HRESULT SomeFunc(
  [in] int Count1,
  [in] int Count2,
  [in, size_is(Count1, Count2)] int** Array);

// Массив указателей
HRESULT SomeFunc(
  [in] int Count,
  [in, size_is(Count,)] int** Array);

// Указатель на указатель на массив
HRESULT SomeFunc(
  [in] int Count,
  [in, size_is(, Count)] int** Array);

Аналогично описывается содержимое тройных, четверных и т.д. указателей.

Атрибуту size_is существует альтернатива — атрибут max_is. Он позволяет задать не количество элементов массива, а индекс верхнего элемента (т.е. число, на единицу меньшее размера массива, так как индексация массивов всегда начинается с нуля).

Рассмотрим случай, когда массив является входным/выходным, причём клиент передаёт меньшее количество элементов, чем сервер, но при этом сервер не должен перераспределять память, выделенную клиентом. Понятно, что в этом случае клиент должен заранее выделить память под большой массив, который при передаче входных параметров будет использоваться лишь частично. Естественно, возникает желание, чтобы proxy/stub dll не тратила сетевые ресурсы на передачу неиспользуемой части массива. Для решения этой задачи используются переменные массивы, которые определяются с помощью атрибута length_is, например:

HRESULT SomeFunc(
  [in, out] int* Count,
  [in, out, length_is(*Count)] int* Array[1024]);

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

Примечание: Обратите внимание, что в приведённом примере в аргументе атрибута length_is перед именем параметра Count стоит звёздочка. В данном случае параметр Count является не целым числом, а указателем на целое, поэтому его следует разыменовать (т.е. получить то значение, на которое указывает указатель).

Переменные массивы могут не иметь заданной максимальной длины, например:

HRESULT SomeFunc(
  [in, out] int* Count,
  [in, out, length_is(*Count)] int** Array);

Однако в таком виде переменный массив оказывается полезен только в том случае, если сервер возвращает не большее число элементов, чем передаёт клиент. Действительно, рассмотрим ситуацию, когда клиент передаёт, например, 16 элементов, а сервер должен вернуть ему 1024 элемента. Клиент, зная это, выделяет на своей стороне память под 1024 элемента, а через параметр Count передаёт информацию о том, что используется только 16 элементов. Однако теперь полный размер массива не фигурирует в объявлении метода, поэтому ни заглушка, ни заместитель не имеют информации о том, сколько памяти выделил клиент. Поэтому, когда заглушка на стороне сервера воссоздаёт массив, она выделяет память под то количество элементов, которое передано, т.е. в данном случае под 16. И попытка обратиться к элементу с большим индексом на стороне сервера либо вызовет ошибку Access violation, либо приведёт к порче "чужой" памяти. Поэтому, если сервер захочет в такой ситуации вернуть большее число элементов, чем передал клиент, ему придётся освобождать выделенную заглушкой память и выделять новый блок большего размера. Соответственно, весь смысл переменного массива при этом теряется, проще и эффективнее будет использовать совместимый массив.

Чисто входные и чисто выходные параметры-массивы тоже, в принципе, могут быть сделаны переменными массивами. Но в этих случаях никакого смысла выделять память под большее число элементов нет, поэтому здесь переменные массивы не используются.

И, наконец, IDL позволяет задавать для массива и общий размер, и количество используемых элементов, т.е. одновременно использовать атрибуты size_is и length_is. Такие массивы называются открытыми (иногда их также называют переменными совместимыми массивами). Как и в случае переменных массивов, использование открытых массивов имеет смысл только для входных/выходных параметров. Но в данном случае уже нет нужды задавать размер массива при объявлении метода, т.к. размер выделенной клиентом памяти будет известен и заглушке, и заместителю благодаря атрибуту size_is.

При использовании переменных и открытых массивов может возникнуть ситуация, когда используемые элементы располагаются не с начала массива, а начиная с какого-то элемента. В этом случае номер данного элемента можно указать через атрибут first_is, аргумент которого задаёт номер первого используемого элемента. Кроме того, можно задавать не длину используемой части массива, а индекс последнего используемого элемента с помощью атрибута last_is (атрибуты length_is и last_is, разумеется, являются взаимоисключающими).

Существует ещё один особый случай, когда следует передавать не весь массив, а только его часть — это строки, завершающиеся нулём. В этом случае нужно передавать все элементы до первого нулевого включительно, оставшаяся часть массива не интересна. Такие массивы помечаются атрибутом string, не имеющим аргументов (тип элементов массива при этом должен быть char, wchar, byte или эквивалентный одному из этих типов). При использовании атрибута string указывать явно используемое число элементов не следует, т.е. string нельзя использовать одновременно с атрибутами length_is или last_is. Атрибут string является инструкцией для заместителя, что нужно передавать столько элементов, сколько их встретится до первого нулевого элемента.

Примечание: В одном из предыдущих уроков мы рассматривали тип BSTR, используемый для передачи строк в COM/DCOM. Атрибут string не имеет к нему никакого отношения, более того, данный атрибут нельзя использовать с этим типом. Тип BSTR представляет собой высокоуровневую "безопасную" строку, которую можно использовать в любом языке программирования. Атрибут string предназначен для низкоуровневой передачи строк в виде массивов. Клиент или сервер, использующие такие строки, могут быть написаны только на языках, поддерживающих свободную работу с указателями.

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

Кроме массивов, существуют ещё и другие параметры, передающиеся как указатели, для которых нужно уточнить содержимое данных. Это указатели на интерфейсы, которые мы видели, например, в методе QueryInterface. Чтобы правильно передать такой параметр, proxy/stub dll должна обладать информацией о типе интерфейса. Допускается статическая и динамическая типизация. В случае статической типизации явно указывается тип интерфейса, и для proxy/stub dll этого достаточно, никаких дополнительных атрибутов не требуется, например:

HRESULT SomeMethod(
  [out] ISomeInterface** ppItf);

Если тип интерфейса неизвестен на момент объявления метода, соответствующий параметр должен быть указателем на IUnknown или нетипизированным указателем. Для динамической типизации используется атрибут iid_is, чьим аргументом является выражение, результат которого — ссылка на IID передаваемого интерфейса. Например, метод CreateInstance уе известного нам интерфейса IClassFactory на MIDL может быть описан так:

HRESULT SomeMethod(
  [in, unique] IUnknown* pUnkOuter,
  [in] REFIID riid,
  [out, iid_is(riid)] void** ppvObject);

Примечание: Отметим, что атрибут unique у параметра pUnkOuter появился потому, что по умолчанию все указатели верхнего уровня имеют вид ref, не допускающий значение nil. Но согласно спецификации метода pUnkOuter, данный указатель может быть нулевым, поэтому приходится явно объявлять вид указателя.

Передача интерфейсов в качестве параметров очень характерна для несоздаваемых объектов (т.е. которые создаются не через фабрику класса, а через другие COM-объекты) и для интерфейсов обратного вызова, о которых мы будем говорить в одном из следующих уроков.

Объединения и атрибут switch_is

Выше мы уже говорили об объединениях (union) и о том, что MIDL допускает объявление объединений двух видов: с внутренним и с внешним селектором. Объявление объединения с внутренним селектором выглядит так:

union <StructName> switch (<SwitchType> <SwitchName>) <UnionName>
{
  ...
  case <SwitchValue>:
    <FieldType> <FieldName>;
  ...
  default:
    <FieldType> <FieldName>;
}

Здесь <StructName> — имя структуры, которое даётся этому типу, <UnionName> — имя объединения внутри этого типа (может быть опущено), <SwitchType> — тип поля-селектора, <SwitchName> — имя поля-селектора, <SwitchValue> — значение селектора, <FieldType> и <FieldName> — тип и имя поля, соответствующего данному значению селектора (одному значению селектора может быть сопоставлено только одно поле). Поле раздела default соответствует всем значениям селектора, не указанным явно (данный раздел может быть опущен). Полученная запись эквивалентна следующему типу Delphi:

<StructName> = record
  case <SwitchName>: <SwitchType> of
    ...
    <SwitchValue>: (<FieldName>: <FieldType>);
    ...
end;

Утилита midl.exe при трансляции на C/C++ объявляет данный тип так:

struct <StructName>
{
  <SwitchType> <SwitchName>;
  union
  {
    ...
    <FieldType> <FieldName>;
    ...
  } <UnionName>;
};

Получающийся в результате подобного объявления тип является полноценным и может быть использован наравне с любым другим типом. По значению селектора proxy/stub dll определяет, какое из альтернативных полей сейчас выбрано, и правильно маршалирует его. Соответственно, разработчики клиента и сервера отвечают за то, чтобы присваивать селектору правильные значения.

Объединения с внешним селектором записываются иначе:

[switch_type(<SwitchType>)] union <UnionName>
{
  ...
  [case(<SwitchValue>)] <FieldType> <FieldName>;
  ...
  [default] <FieldType> <FieldName>;
}

Отличительной особенностью такого объединения является наличие у него атрибута switch_type, аргумент которого задаёт тип селектора. Выбор различных альтернатив теперь уже не напоминает оператор switch языка C++ — case и default (который, как и в предыдущем случае, может отсутствовать) заключаются, подобно атрибутам, в квадратные скобки, значения селекторов являются аргументами этих "атрибутов", двоеточие после не ставится. Для сравнения приведём определение двух объединений: с внутренним и с внешним селектором.

// Объединение с внутренним селектором
union Encapsulated switch (int Selector)
{
  case 0:
    int IntField;
  case 1:
    float FloatField;
  case 2:
    double DoubleField;
  default:
    hyper Int64Field;
}

// Объединение с внешним селектором
[switch_type(int)] union NonEncapsulated
{
  [case(0)]
    int IntField;
  [case(1)]
    float FloatField;
  [case(2)]
    double DoubleField;
  [default]
    hyper Int64Field;
}

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

HRESULT SomeMethod(
  [in] int Selector,
  [in, switch_is(Selector)] NonEncapsulated neParam);

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

Примечание: Хотя в документации никаких упоминаний об этом найти не удалось, практика показывает, что транслятор midl.exe понимает и объединения, записанные в стиле C/C++, т.е. вообще без селектора. Как при этом будут маршалироваться такие типы, если там есть поля-указатели, непонятно. Рекомендуем избегать объединений без селекторов.

Утилита midl.exe

Вернёмся к вопросу о том, какие файлы создаёт на выходе транслятор midl.exe. Этот транслятор является утилитой командной строки, имя исходного файла задаётся параметром при вызове этой утилиты. Пусть исходным файлом для него служит AnyName.idl. На выходе транслятор создаёт до пяти файлов:

  • AnyName.tlb — библиотека типов. Создаётся, если в IDL-файле присутствует "библиотечный" раздел.
  • AnyName.h — заголовочный файл, содержащий определения типов и интерфейсов на C и C++.
  • AnyName_i.c — файл, содержащий константы, определяющие идентификаторы всех объявленных в исходном тексте интерфейсов, коклассов и идентификатор библиотеки типов. Имена констант имеют вид IID_<имя_интерфейса>, CLSID_<имя_кокласса>, LIBID_<имя_библиотеки>.
  • Файлы AnyName_p.c и dlldata.c — исходные файлы для proxy/stub dll.

Файл AnyName.h использует макрос __cplusplus и поэтому пригоден для использования как в С, так и в С++. В нём содержатся объявления всех типов, интерфейсов и т.п., которые описаны в исходном IDL-файле (интерфейсы для С описываются в виде структур, содержащих таблицу виртуальных методов). Данный файл используется клиентом и сервером. Создаётся только если есть хоть что-то, объявленное за пределами оператора library.

Определение констант приходится выносить из заголовочного файла AnyName.h потому, что в C/C++ отсутствует раздельная компиляция, и если определить константы прямо в заголовочном файле, а потом включить этот файл в несколько файлов одного проекта, компилятор воспримет это как повтор определения уже существующей константы и выдаст ошибку. Поэтому в файле AnyName.h константы только объявляются (т.е. им не присваивается значение), а определение констант выносится в отдельный файл AnyName_i.c, который должен включаться в проект только один раз. Как и файл AnyName.h, данный файл не создаётся, если в исходном IDL-файле нет ничего, кроме раздела library.

Файлы AnyName_p.c и dlldata.c (имя последнего не зависит от имени исходного IDL-файла) являются основой для proxy/stub dll. В одном из следующих уроков мы научимся компилировать эту dll на их основе (не с помощью Delphi, к сожалению). Эти файлы создаются только если за пределами оператора library объявлен хотя бы один интерфейс. Если в IDL-файле также есть интерфейсы, объявленные внутри оператора library, эти интерфейсы не попадают в данные файлы, поэтому, если вы хотите, чтобы интерфейс попал и в proxy/stub dll, и в библиотеку типов, его надо объявить вне оператора library, а внутри этого оператора — упомянуть.

Примечание: Транслятор midl.exe имеет параметры командной строки, которые позволяют отключать создание тех или иных выходных файлов. Если указать эти параметры, выходных файлов может быть меньше пяти, даже если исходный файл содержит всё необходимое для их генерации.


предыдущий урок содержание семинара следующий урок




Смотрите также материалы по темам:
[Технологии ActiveX, COM, DCOM] [Интерфейсы COM-объектов]

 Обсуждение материала нет сообщений
  
Время на сайте: GMT минус 5 часов

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

Web hosting for this web site provided by DotNetPark (ASP.NET, SharePoint, MS SQL hosting)  
Software for IIS, Hyper-V, MS SQL. Tools for Windows server administrators. Server migration utilities  

 
© При использовании любых материалов «Королевства Delphi» необходимо указывать источник информации. Перепечатка авторских статей возможна только при согласии всех авторов и администрации сайта.
Все используемые на сайте торговые марки являются собственностью их производителей.

Яндекс цитирования