Главная » Delphi » COM и Object Pascal

0

После  краткого обзора основных концепций и терминологии технологий COM, ActiveX и OLE можно  переходить к рассмотрению способов реализации этих  концеп ций в Delphi. В настоящем разделе более  детально рассматривается как сама техноло гия COM, так и ее согласование с языком Object  Pascal и библиотекой VCL.

Интерфейсы

COM определяет стандарты для расположения в памяти функций объектов. Функции располагаются в виртуальных таблицах (virtual tables — vtables),  т.е. таблицах адресов функций, аналогичных таблицам виртуальных методов (VMT — Virtual Method Table) клас сов в Delphi. Описание каждой виртуальной таблицы на языке программирования называ ется интерфейсом (interface).

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

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

Введенное в Delphi 3 ключевое слово  interface языка  Object Pascal  позволяет достаточно просто определять интерфейсы COM.  Объявление интерфейсов семан тически подобно объявлению классов, за исключением того,  что  интерфейсы могут содержать только  свойства и методы, но не данные. Поскольку интерфейсы не могут содержать данные, их свойства должны записываться и  считываться  только  с  помо щью методов. Однако самое важное то, что интерфейсы не  имеют раздела реализа

ции, поскольку они лишь определяют соглашение.

Интерфейс IUnknown

Подобно тому, как все классы  Object  Pascal в конечном счете  являются потомками класса  TObject, все  интерфейсы COM  (а  следовательно, и  все  интерфейсы Object Pascal) происходят от интерфейса IUnknown. Интерфейс IUnknown определен в модуле System следующим образом:

type

IUnknown = interface

[‘{00000000-0000-0000-C000-000000000046}’]

function QueryInterface(const IID: TGUID;

out Obj): Integer; stdcall;

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

end;

Как видно из приведенного фрагмента кода, помимо ключевого слова interface, между объявлениями интерфейса и класса  существует  еще одно  существенное разли чие,  заключающееся в присутствии глобального  уникального идентификатора  (GUID — Globally Unique Identifier), используемого в технологии COM.

CОВЕТ

Чтобы создать в IDE Delphi новый GUID, достаточно нажать в окне редактора кода комбинацию клавиш <Ctrl+Shift+G>.Глобальный уникальный идентификатор (GUID)

GUID (произносится гу-ид  (goo-id)) представляет собой 128-разрядное целое число, используемое в технологии COM для уникальной идентификации интерфейсов, компо- нентных классов и других объектов. GUID практически гарантирует настоящую гло- бальную уникальность благодаря использованию достаточно больших чисел и превос- ходного алгоритма их генерации. GUID создается с помощью функции API CoCre- ateGUID(),  а  алгоритм  его  генерации  основан  на  комбинации  следующей информации: текущая дата и время, частота процессора, номер сетевой карты, оста- ток на банковском счете Билла Гейтса (ну, хорошо, со счетом мы несколько погорячи- лись). Если на компьютере установлена сетевая карта, созданный на этом компьютере GUID будет действительно уникальным, поскольку уникальность каждой сетевой карты гарантируется встроенным в нее глобальным идентификатором (ID). Если же на ком- пьютере нет сетевой карты, ее номер можно заменить другим, синтезировав его с по- мощью параметров другого установленного в компьютере оборудования.

Поскольку не существует типа данных, позволяющего хранить число из 128 двоичных разрядов, для представления GUID используется запись с типом TGUID, которая оп- ределена в модуле System следующим образом:

type

PGUID = ^TGUID;

TGUID = record

D1: LongWord;

D2: Word;

D3: Word;D4: array[0..7] of Byte;

end;

Из-за того что присваивать переменным и константам значения GUID в таком формате записи достаточно сложно, Object Pascal позволяет также определить запись TGUID как строку следующего формата:

‘{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}’

Благодаря этому следующие объявления эквивалентны:

MyGuid: TGUID = ( D1:$12345678;D2:$1234;D3:$1234;D4:($01,$02,$03,$04,$05,$06,$07,$08));

MyGuid: TGUID = ‘{12345678-1234-1234-12345678}';

В COM каждый интерфейс или класс имеет соответствующий GUID, который является уникальным определителем интерфейса. В этом случае два интерфейса или класса, имеющих одинаковые имена и созданных двумя независимыми разработчиками, нико- гда не будут конфликтовать, потому что соответствующие им GUID всегда будут раз- личны. При определении интерфейса GUID обычно называют идентификатором ин- терфейса (IID — Interface ID), а при определении класса его называют идентифика- тором класса (CLSID — Class ID).

Помимо своего  идентификатора IID,  в интерфейсе IUnknown объявлены три  ме тода:  QueryInterface(), _AddRef() и _Release(). В следствии того,  что  интер фейс  IUnknown является базовым интерфейсом COM, все остальные интерфейсы не избежно будут реализовать интерфейс IUnknown и  его  методы.   Метод  _AddRef() следует вызывать в том случае, если клиент получает  и желает  использовать указатель на данный интерфейс. Вызов  этого  метода  должен  сопровождаться вызовом метода

_Release(), когда клиент заканчивает работу с интерфейсом. В таком случае объект, реализующий данный интерфейс, сможет  поддерживать счетчик клиентов, хранящих ссылку на этот  объект, или  вести  счетчик ссылок (reference count). Когда  количество ссылок  станет равным нулю, объект должен  будет выгрузить себя  самого  из памяти. Функция  QueryInterface() используется для  выполнения запроса о том,  поддер живает ли  данный объект некоторый интерфейс.  Если  требуемый интерфейс  под держивается, то клиенту  возвращается указатель  на него. Предположим, что объект O поддерживает интерфейсы I1 и I2, и клиент уже имеет  указатель  на интерфейс I1 объекта O. Для получения от объекта O указателя  на его интерфейс I2 необходимо вы звать метод I1.QueryInterface().

НА ЗАМЕТКУ

Опытный разработчик COM может заметить, что символ подчеркивания перед мето- дами _AddRef() и _Release() не используется в других языках программирования или даже в документации Microsoft по COM. Поскольку Object Pascal “знает” интерфейс IUnknown, данные методы нельзя вызывать напрямую (об этом чуть позднее), поэтому символ подчеркивания существует главным образом для того, чтобы обратить внима- ние разработчика и заставить его задуматься, прежде чем выполнять вызов таких ме- тодов.

Поскольку каждый  интерфейс в Delphi  косвенно происходит от интерфейса IUn- known, каждый  класс  Delphi, реализующий интерфейсы,  также  будет поддерживатьэти три  метода  интерфейса IUnknown. Такую “грязную работу” можно  выполнить вручную или  же поручить ее подпрограммам библиотеки VCL, сделав  свой  класс по томком  класса TInterfacedObject, в котором интерфейс IUnknown уже реализован.

Использование интерфейсов

В главе 2, “Язык программирования Object  Pascal”, и в документации Delphi  описы вается  семантика использования интерфейсов,  поэтому  приводить ее  здесь  не  имеет смысла.  Вместо  этого  рассмотрим, как интерфейс IUnknown косвенно интегрирован с языком Object Pascal.

При  присваивании значения переменной интерфейса компилятор автоматически создает  вызов метода интерфейса _AddRef(), чтобы  увеличить содержимое счетчика ссылок.  Когда  переменная интерфейса выходит за пределы области видимости или принимает значение nil, компилятор автоматически создает  вызов  метода  интер фейса _Release(). Рассмотрим такой фрагмент кода:

var

I: ISomeInteface; // Некоторый интерфейс

begin

// Эта функция возвращает интерфейс

I := FunctionThatReturnsAnInterface;

I.SomeMethod;          // Вызов некоторого метода интерфейса

end;

А сейчас обратите внимание на следующий код. Здесь код, вводимый разработчи

ком, выделен полужирным шрифтом, а код, созданный компилятором, обычным.

var

I: ISomeInterface;

begin

// Интерфейс автоматически инициализируется равным nil

I := nil;

try

// Ваш код должен быть здесь

I := FunctionThatReturnsAnInterface;

// Метод _AddRef() вызывается косвенно при

// присваивании значения переменной I

I._AddRef;

I.SomeMethod;

finally

// Блок завершения гарантирует, что ссылки

// на интерфейс будут удалены.

if I <> nil I._Release;

end;

end;

Компилятор Delphi также  достаточно интеллектуален и “знает”,  когда  вызывать методы  _AddRef() и _Release(). Это  делается при  переназначении интерфейсов экземплярам другого интерфейса или при присваивании интерфейсам значения nil. Рассмотрим, к примеру, следующий фрагмент кода:

var

I: ISomeInteface; // Некоторый интерфейсbegin

// Присвоить I

I := FunctionThatReturnsAnInterface;     // Получить интерфейс

I.SomeMethod;                            // Вызов метода интерфейса

// Переприсвоить I

I := OtherFunctionThatReturnsAnInterface;     // Другой интерфейс

I.OtherMethod;                   // Вызов метода другого интерфейса

// Установить I в nil

I := nil;

end;

Обратите внимание на комбинацию, состоящую из кода, написанного разработчи

ком (полужирный шрифт), и кода, созданного компилятором (обычный шрифт):

var

I: ISomeInterface;

begin

// Интерфейс автоматически инициализируется равным nil

I := nil;

try

// Ваш код должен быть здесь

// Присвоить I

I := FunctionThatReturnsAnInterface;

// Метод _AddRef() вызывается косвенно при

// присваивании I значения

I._AddRef;

I.SomeMethod;

// Переприсвоить I

I._Release;

I := OtherFunctionThatReturnsAnInterface;

I._AddRef;

I.OtherMethod;

// Установить I в nil

I._Release;

I := nil;

finally

// Блок завершения гарантирует, что ссылки

// на интерфейс будут удалены.

if I <> nil I._Release;

end;

end;

Этот  пример помогает понять, из за чего  в Delphi используется символ  подчерки вания  в методах  _AddRef() и _Release(). Об увеличении или уменьшении количе ства  ссылок  интерфейса часто  забывали, и это  являлось классической ошибкой при программировании с применением технологии COM в те времена, когда не существо вало ключевого слова interface. Интерфейсы Delphi  как раз и призваны помочь разработчикам избавиться от  такого   рода  проблем с  помощью косвенного  вызова этих методов.

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

_AddRef() и _Release(), резонно предположить, что он “знает” и о третьем методе интерфейса IUnknown, QueryInterface(). Конечно же, это так. Получив  указательна   интерфейс  от   некоторого   объекта,  можно    использовать  оператор   as для “приведения”  его  к  типу  другого  интерфейса,  который  поддерживается объектом COM. Понятие “приведение типа” наилучшим образом подходит для описания данно го процесса, хотя  такое  применение оператора as является на самом  деле не приве дением  типа  в буквальном  смысле  этого  выражения, а внутренним обращением к ме тоду QueryInterface(). Вот как выглядит демонстрация описанного процесса:

var

I1: ISomeInterface;            // Некоторый интерфейс

I2: ISomeOtherInterface;       // Другой интерфейс

begin

// Присвоить I1

I1 := FunctionThatReturnsAnInterface; // Функция, возвращающая

// интерфейс.

// Метод QueryInterface I1 для интерфейса I2

I2 := I1 as ISomeOtherInterface;         // Приведение к типу

// другого интерфейса.

end;

Если  объект, на который ссылается интерфейс I1, не  поддерживает интерфейс

ISomeOtherInterface, то оператор as породит исключение.

Одно дополнительное языковое правило, касающееся интерфейсов, заключается в

следующем:  переменные интерфейса  совместимы по  присвоению с классом  Object Pascal, который реализует этот  интерфейс. Например, рассмотрим следующие  объяв ления  интерфейса и класса:

type

IFoo = interface

// Определение IFoo

end;

IBar = interface(IFoo)

// Определение IBar

end;

TBarClass = class(TObject, IBar)

// Определение TBarClass

end;

При таких объявлениях следующий код является вполне  корректным:

var

IB: IBar;

TB: TBarClass;

begin

TB := TBarClass.Create;

try

// Получить указатель на интерфейс IBar объекта TB:

IB := TB;

// Использование объекта TB и интерфейса IB

finally

IB := nil;         // Явное освобождение интерфейса IB

TB.Free;end;

end;

Несмотря на то что  такой  подход,  казалось бы,  нарушает традиционные правила языка  Object  Pascal, связанные с совместимостью при выполнении операций при своения, он делает интерфейсы более естественными и простыми в работе.

Существует  важное, но не вполне очевидное следствие из этого  правила —  интер фейсы совместимы по присвоению только  с теми  классами, которые явно  поддержи вают  данный интерфейс. Например, класс  TBarClass, определенный выше,  объяв ляет  явную поддержку  интерфейса IBar. Поскольку интерфейс IBar происходит от интерфейса IFoo, резонно было бы предположить, что интерфейс TBarClass также поддерживает интерфейс IFoo. Однако это вовсе не так, что и проиллюстрировано в приведенном ниже фрагменте.

var

IF: IFoo;

TB: TBarClass;

begin

TB := TBarClass.Create;

try

// В следующей строке находится ошибка времени компиляции,

// поскольку класс TBarClass не поддерживает

// интерфейс IFoo явно

IF := TB;

// Использование объекта TB и интерфейса IF

finally

IF := nil;         // Явное освобождение интерфейса IF

TB.Free;

end;

end;

Интерфейсы и идентификаторы интерфейсов

Поскольку идентификатор  интерфейса  (ID)  описывается как  часть  объявления интерфейса, компилятор Object  Pascal знает  о том,  как получить его. Следовательно, можно  передать тип  интерфейса процедуре или  функции,  которой необходимы па раметры типа  TIID или  TGUID. Предположим, существует  функция,  подобная  сле дующей:

procedure TakesIID(const IID: TIID);

В этом случае приведенная ниже строка кода синтаксически правильна:

TakesIID(IUnknown);

Такая  возможность предотвращает необходимость использования констант IID_ТипИнтерфейса, определенных для  каждого  типа  интерфейса. Эти  константы хорошо знакомы всем,  кому приходилось иметь  дело  с разработкой или  использова нием объектов COM в языке  C++.

Псевдоним метода

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

type

IIntf1 = interface

procedure AProc;

end;

IIntf2 = interface procedure AProc;

end;

Каждый  из описываемых интерфейсов содержит метод  AProc(). Как объявить в таком  случае класс реализующий оба интерфейса? Оказывается, для этого  можно  ис пользовать псевдоним метода (method aliasing). С помощью псевдонима метод  интер фейса  в классе можно  поставить в соответствие методу с другим именем. Рассмотрим фрагмент, демонстрирующий объявление класса,  в котором реализованы интерфей сы IIntf1 и IIntf2:

type

TNewClass = class(TInterfacedObject, IIntf1, IIntf2)

protected

procedure IIntf2.AProc = AProc2;

procedure AProc;          // Связывает с IIntf1.AProc

procedure AProc2;         // Связывает с IIntf2.AProc

end;

В данном  объявлении метод  AProc() интерфейса IIntf2 устанавливается в соот ветствие методу по имени AProc2(). Создание псевдонима в этом  случае позволяет реализовать любой интерфейс в любом классе, избежав конфликта имен методов.

Тип возвращаемого значения HResult

Как можно  заметить, метод QueryInterface() интерфейса IUnknown возвращает результат типа  HResult. Это наиболее популярный тип значений, возвращаемых мно гими  методами различных интерфейсов ActiveX и OLE,  а также  функциями API COM. Тип HResult определен в модуле System как тип LongWord. Возможные значения типа HResult перечислены в модуле Windows (если  есть исходный код библиотеки VCL, то описания его  значений можно  найти под  заголовком { HRESULT value defini- tions }). Значение S_OK или  NOERROR (0) типа  HResult говорит об успешном  вы полнении. Если же старший бит значения типа  HResult установлен равным единице, значит, при  выполнении произошла ошибка.  В модуле  Windows есть  две  функции — Succeeded() и Failed(), — которые принимают значение типа  HResult в качестве параметра и возвращают значение типа  BOOL, означающее успешное  выполнение или наличие ошибки. Синтаксис вызова этих методов имеет следующий вид:

if Succeeded(FunctionThatReturnsHResult) then

\\ Нормальное продолжениеif Failed(FunctionThatReturnsHResult) then

\\ Код обработки ошибки

Естественно, проверка значения, возвращаемого при  каждом  отдельном вызове функции, — занятие скучное.  Кроме  того,  работа над ошибками, возвращаемыми функ циями, относится к “компетенции” методов обработки исключений Delphi. А потому  в модуле ComObj определена процедура OleCheck(), с помощью которой ошибки типа HResult преобразуются в исключения. Синтаксис вызова  данного метода  имеет  сле дующий вид:

OleCheck(FunctionThatReturnsHResult);

Эта  процедура  удобна  в  применении,  к  тому  же  значительно  упрощает  код  про

граммы.

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

По теме:

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