Главная » Delphi » За кулисами: языковая поддержка COM

0

В разговорах, касающихся разработки приложений COM  в Delphi, часто  можно услышать  о сильной поддержке, предоставляемой языком  Object  Pascal модели  COM. С этим трудно спорить, если учесть, что в язык встроена поддержка таких  элементов, как   интерфейсы,   варианты  и   длинные  строки.  Но   что   же   реально  означает “поддержка, встроенная в язык”? Как работают эти средства и в чем природа их зави симости от функций API COM? В настоящем разделе рассмотрим, как все эти  вещи объединяются на  низком уровне  для  обеспечения  поддержки COM  в языке  Object Pascal, и разберемся в некоторых деталях реализации этих языковых средств.

Как  уже говорилось, все  средства поддержки COM  в языке  Object  Pascal  можно разделить на три основные категории.•    Типы данных Variant и OleVariant, которые инкапсулируют в модели COM ва

риантные записи, массивы SafeArray и автоматизацию с поздним связыванием.

•  Тип данных WideString, который инкапсулирует в модели COM строки BSTR.

•  Типы   Interface и  dispinterface,  которые  инкапсулируют интерфейсы COM и автоматизацию с ранним связыванием, а также  автоматизацию со связы ванием на уровне идентификаторов (ID bound Automation).

Разработчики OLE со стажем (со времен Delphi 2), вероятно, заметили, что зарезер вированное слово  automated, благодаря которому могли  создаваться серверы автома тизации позднего связывания, практически игнорируется. Это  произошло вследствие того,  что  эта  функция была  отодвинута на задний  план  средствами “настоящей” под держки автоматизации, впервые введенной в Delphi  3. Теперь она осталась только  для совместимости с прежними версиями, а потому здесь ее рассматривать не будем.

Варианты

Варианты представляют собой  старейшую форму  поддержки технологии  COM  в Delphi, впервые появившуюся в Delphi  2. По сути, тип  Variant — это  просто большая запись, которая используется для передачи некоторых данных  одного  из многочислен ных допустимых типов.  Эта запись определена в модуле System как тип TVarData:

type

PVarData = ^TVarData;

TVarData = record

VType: Word;

Reserved1, Reserved2, Reserved3: Word;

case Integer of

varSmallint: (VSmallint: Smallint);

varInteger:           (VInteger: Integer);

varSingle:            (VSingle: Single);

varDouble:            (VDouble: Double);

varCurrency: (VCurrency: Currency);

varDate:              (VDate: Double);

varOleStr:            (VOleStr: PWideChar);

varDispatch: (VDispatch: Pointer);

varError:             (VError: LongWord);

varBoolean:           (VBoolean: WordBool);

varUnknown:           (VUnknown: Pointer);

varByte:              (VByte: Byte);

varString:            (VString: Pointer);

varAny:               (VAny: Pointer);

varArray:             (VArray: PVarArray);

varByRef:             (VPointer: Pointer);

end;

Значение поля  VType этой  записи означает тип  данных, содержащийся в типе Variant, и это может  быть любое  из обозначений типа  варианта, приведенных в на чале модуля System и перечисленных в разделе variant этой  записи (внутри опера тора case). Единственным различием между типами Variant и OleVariant является то, что тип Variant поддерживает все стандартные типы, а тип OleVariant — толь ко те из них,  которые совместимы с автоматизацией. Например, вполне приемлемоприсвоить стандартный для языка  Pascal тип String (varString) типу Variant, но для присвоения той же строки типу OleVariant придется преобразовать ее в совмес тимый с автоматизацией тип WideString (varOleStr).

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

В суровом мире  программирования COM на языках  C и C++ (за рамками классов) ва рианты представлены структурой VARIANT, определенной в файле заголовка oaidl.h. При  работе с вариантами в этой  среде приходится вручную инициализировать и управ лять ими с помощью функций API VariantXXX() из библиотеки oleaut32.dll. (Речь идет о функциях VariantInit(), VariantCopy(), VariantClear() и т.д.) Это делает работу с вариантами в языках C и C++ достаточно сложной задачей.

Поскольку поддержка вариантов встроена непосредственно в язык  Object  Pascal, компилятор организует необходимые обращения к процедурам поддержки вариантов автоматически, по мере  использования экземпляров данных  типа  Variant или Ole- Variant. Но  за комфорт в языке  приходится расплачиваться необходимостью при обретения новых знаний. Если рассмотреть таблицу импорта (import table)  “ничего не делающего”  исполняемого файла  EXE Delphi  с помощью такого  инструмента, как ути лита  Borland TDUMP.EXE или  утилита  Microsoft DUMPBIN.EXE, то  можно  заметить не сколько  подозрительных операций импорта из библиотеки oleaut32.dll: Varian- tChangeTypeEx(), VariantCopyInd() и VariantClear(). Это означает, что даже в приложении, в котором типы  Variant или  OleVariant не  используются явно,  ис полняемый файл  Delphi  все равно  зависит от функций API COM из библиотеки ole- aut32.dll.

Массивы вариантов

Массивы  вариантов в Delphi  разработаны для инкапсуляции массивов COM типа SafeArray, которые представляют собой  тип записи, используемой для инкапсуляции массива  данных  в автоматизации. Они  названы безопасными (safe),  поскольку  способны описать сами себя. Помимо данных  массива,  эта запись  содержит информацию о коли честве  измерений массива,  размере и количестве его элементов. Массивы  Variant соз даются  и управляются в Delphi  с помощью функций и процедур VarArrayXXX(), опре деленных в модуле  System и  описанных в интерактивной  справочной системе. Эти функции и процедуры являются, по сути, оболочками для функций API SafeAr- rayXXX(). Если переменная типа  Variant содержит массив Variant, то для доступа к элементам такого  массива  используется стандартный синтаксис индексации массива. Сравнивая эти средства с возможностями языков C и C++ для массивов safearray, где все приходится делать  вручную,  можно  заметить, что  использованная в языке  Object Pascal инкапсуляция отличается ясностью, компактностью и надежностью.

Автоматизация с поздним связыванием

Как уже упоминалось в настоящей главе,  типы  Variant и OleVariant позволяют писать   приложения клиенты, использующие  автоматизацию  с  поздним связыванием (позднее связывание означает, что функции вызываются во время  выполнения с помо щью метода  Invoke интерфейса IDispatch). Все это легко  можно  принять за чистуюмонету, но вот вопрос: “Где же та магическая связь между вызовом метода сервера авто матизации  из   переменной  типа   Variant в  программе  и  самим   методом  IDis- patch.Invoke(), каким то образом вызванным с правильными параметрами?”. Ответ находится на уровне более низком, чем можно было бы ожидать.

После  вызова  метода  объектами типа  Variant или  OleVariant, содержащими интерфейс IDispatch, компилятор просто генерирует обращение к вспомогатель ной функции _DispInvoke (объявленной в модуле System), которая передает управ ление  по  значению указателя  на функцию, имя  которого VarDispProc. По  умолча нию указатель  VarDispProc указывает  на метод,  который при  вызове просто возвра щает  ошибку.  Но,  если  в  директиву  uses включить модуль  ComObj,  то  в  разделе initialization модуля  ComObj указатель   VarDispProc будет  перенаправлен на другой метод с помощью  следующего оператора:

VarDispProc := @VarDispInvoke;

VarDispInvoke — это процедура в модуле ComObj, имеющая следующее объявление:

procedure VarDispInvoke(Result: PVariant; const Instance: Variant; CallDesc: PCallDesc; Params: Pointer); cdecl;

Реализация этой  процедуры обеспечивает вызов  метода  IDispatch.GetIDsOf- Names() для получения диспетчерского идентификатора (DispID) на основе имени метода,   корректно  устанавливает требуемые параметры  и  выполняет  обращение  к методу IDispatch.Invoke(). Самое интересное заключается в том, что компилятор в данном  случае не обладает никакими внутренними “знаниями” об интерфейсе IDispatch или о том,  как осуществляется вызов  метода  Invoke(). Он  просто пере дает управление в соответствии со значением указателя  на функцию. Также  интерес но то, что благодаря подобной архитектуре вполне возможно перенаправить этот указатель  функции на свою собственную процедуру, если необходимо самостоятельно обрабатывать все  вызовы автоматизации через типы  Variant и  OleVariant. При этом следует позаботиться лишь о том, чтобы  объявление пользовательской функции совпадало с объявлением процедуры VarDispInvoke. Безусловно, это — задача не для новичков, но полезно знать,  что при необходимости вполне можно  воспользоваться и таким гибким подходом.

Тип данных WideString

Тип  данных  WideString был введен  в Delphi 3 с двойной целью:  для поддержки двухбайтовых символов Unicode и для поддержки символьных строк, совместимых со строками BSTR COM. Тип  WideString отличается от близкого типа  AnsiString по нескольким основным параметрам.

•  Все символы, входящие в строку  типа  WideString, имеют  размер, равный двум байтам.

•  Для типов  WideString память  всегда выделяется с помощью функции SysAlloc- StringLen(), в следствии чего они полностью совместимы со строками BSTR.

•    Для типов WideString никогда не ведется подсчет ссылок, поэтому при при

своении значения переменных этого типа всегда копируются.

Подобно вариантам, работа со  строками BSTR при  использовании стандартных функций API  отличается громоздкостью, поэтому  встроенная в  язык  Object  Pascalподдержка типа  WideString вносит  заметное  упрощение. Однако,  поскольку   эти строки требуют  двойного объема  памяти и не поддерживают ссылок, они  менее  эф фективны по сравнению со строками типа  AnsiString. С учетом  вышесказанного, прежде чем их использовать, хорошо взвесьте все за и против.

Подобно типу Pascal Variant, наличие типа  WideString приводит к автоматиче скому  импортированию некоторых функций  из  библиотеки oleaut32.dll даже  в том случае,  если  программист и не использует этот  тип.  При  изучении таблицы им порта “ничего  не делающего”  приложения Delphi оказывается, что функции Sys- StringLen(),  SysFreeString(),  SysReAllocStringLen() и  SysAllocString- Len() задействованы библиотекой  RTL  Delphi для  обеспечения  поддержки типа WideString.

Интерфейсы

Возможно, наиболее важным  элементом реализации модели  COM  в языке  Object Pascal  является встроенная поддержка интерфейсов. Ирония судьбы состоит в том, что если менее  масштабные средства (имеются в виду типы  Variant и WideString) реализуются  непосредственным использованием функций интерфейса API COM,  то при  реализации интерфейсов в языке  Object  Pascal  функции  интерфейса API COM вообще  не нужны. То есть в языке  Object  Pascal имеется абсолютно самодостаточная реализация интерфейсов, которая полностью соответствует спецификации COM,  но при этом не использует ни одной функции интерфейса API COM.

В плане  соответствия спецификации COM все интерфейсы в Delphi  косвенно происходят от интерфейса IUnknown. А интерфейс IUnknown, как известно, обеспе чивает средства определения типа  и поддержку  учета  ссылок,  что  является основой основ  модели  COM.  Это  означает, что  знание особенностей интерфейса IUnknown встроено непосредственно в компилятор, а сам  интерфейс IUnknown определен  в модуле System. Сделав  интерфейс IUnknown полноправным членом языка  програм мирования, среда Delphi приобрела способность организовывать автоматический подсчет ссылок,  обязав  компилятор генерировать обращения к функциям IUn- known.AddRef() и  IUnknown.Release() в  соответствующие  моменты  времени. Кроме  того,  оператор as может  быть  использован в качестве ускоренного варианта определения типа  интерфейса,  обычно реализуемого с помощью метода  QueryIn- terface(). Но встроенная поддержка интерфейса IUnknown оказывается просто не значительным фрагментом, если рассмотреть весь объем низкоуровневой поддержки, обеспечиваемой языком и компилятором для интерфейсов в целом.

На рис. 15.17 показана упрощенная схема внутренней поддержки интерфейсов со стороны классов.  В действительности объект Delphi  — это ссылка,  которая указывает на физический экземпляр. Первых четыре байта  экземпляра объекта представляют собой указатель на таблицу виртуальных методов объекта (VMT — Virtual Method Table). При  положительном смещении от значения VMT находятся все виртуальные методы объекта. С отрицательным смещением размещаются те указатели  на методы  и данные, которые важны  для внутреннего функционирования объекта. В частности, на уровне смещения –72 от значения VMT содержится указатель  на таблицу  интерфейсов объ екта. Таблица интерфейсов представляет собой  список  записей типа PInterfaceEn- try (определенных в модуле System), которые, по сути, содержат идентификаторы интерфейса IID и информацию о том, где найти указатель  vtable для данного иден тификатора интерфейса IID.

Рис.  15.17. Поддержка  интерфейсов  внутренними  средствами  языка

Object Pascal

Рассмотрев показанную на рис. 15.17 схему, можно понять, как увязаны друг с другом отдельные элементы. Например, метод  QueryInterface() обычно реализуется в объ ектах  Object  Pascal с помощью вызова  метода  TObject.GetInterface(). Метод  Get- Interface() просматривает таблицу  интерфейсов в надежде  найти нужный  иденти фикатор интерфейса IID и возвращает указатель  виртуальной таблицы (указатель vtable)  для  этого  интерфейса. Теперь понятно, почему  новые   типы   интерфейсов должны  быть определены с помощью уникального идентификатора GUID — ведь в про тивном случае метод GetInterface() не сможет  найти их при просмотре таблицы ин терфейсов, и, следовательно, получение интерфейса с помощью метода  QueryInter- face() будет невозможным. Приведение типов  интерфейсов с помощью оператора as просто создает  обращение к методу QueryInterface(), поэтому  и здесь применяются те же самые правила.

Последняя запись  в таблице интерфейсов (см.  рис. 15.17)  представляет собой внутреннюю реализацию интерфейса на основе применения директивы implements. Вместо  прямого указателя  для виртуальной таблицы (указателя  vtable) запись  таб лицы интерфейсов содержит адрес небольшой функции, создаваемой компилятором, которая возвращает виртуальную таблицу интерфейса из свойства, для которого была использована директива implements.

Диспинтерфейсы

Диспинтерфейс  обеспечивает инкапсуляцию недвойственного  (non  dual)  интер фейса IDispatch, т.е. интерфейса IDispatch, в котором методы  могут быть  вызва ны только  через метод  Invoke(), но не через виртуальную таблицу.  В этом  отноше нии диспинтерфейс аналогичен автоматизации с вариантами. Однако  диспинтерфей сы чуть более эффективны, чем варианты, поскольку  объявления dispinterface содержат диспетчерский идентификатор DispID для каждого  поддерживаемого свой ства или метода. Это означает,  что  метод  IDispatch.Invoke() можно  вызвать  на прямую, без предварительного вызова  метода IDispatch.GetIDsOfNames(), как это происходит в случае с вариантами. В остальном же механизм работы диспинтерфей сов  аналогичен механизму  работы вариантов: при  вызове  метода  через диспинтер фейс  компилятор генерирует обращение к функции _IntfDispCall из модуля Sys- tem. Данный метод  передает управление указателю  DispCallByIDProc, который по умолчанию возвращает только  ошибку. Но при  включении в раздел  uses модуля Co- mObj указатель   DispCallByIDProc инициализируется  адресом   процедуры  Disp- CallByID(), которая объявлена в модуле ComObj следующим образом:

procedure DispCallByID(Result: Pointer; const Dispatch: IDispatch; DispDesc: PDispDesc; Params: Pointer); cdecl;

Источник: Тейксейра, Стив, Пачеко, Ксавье.   Borland Delphi 6. Руководство разработчика. : Пер.  с англ. — М. : Издательский дом “Вильямс”, 2002. —  1120 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии