уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

В одном из постов я рассказывал о своей дипломной работе, которая частично перекочевала в программный комплекс для экологов. Сейчас появилась идея значительно усовершенствовать комплекс, как для самих пользователей (внедрение новых методик расчёта, анализ данных и т.д.), так и в плане новых возможностей для разработчиков – сделать поддержку плагинов (plug-in’s).
Так как до нынешнего момента я детально не разбирался с подобными задачами, хотя и использовал в работе OLE и пр. технологии, то решил более детально разобраться с тем, что представляет из себя COM, как реализуются интерфейсы в Delphi и как с помощью них можно реализовать поддержку плагинов.
Вначале немного теории, а именно разберемся с тем, что такое интерфейс.

Чтобы установить обмен данными между двумя разнородными системами (клиентом и сервером), необходимо создать нечто общее, т. е. заранее «объяснить» компонентам, как они будут общаться. Это осуществляется с помощью одного из центральных элементов модели СОМ, называемого интерфейсом.
Интерфейс в модели СОМ — это средство, которое позволяет клиенту правильно обратиться к объекту СОМ, а объекту позволяет ответить клиенту так, чтобы он (клиент) его (сервер) понял.
То есть интерфейс – это то посредством чего что-то соединяется с чем-то.
Каждый интерфейс обязательно должен иметь два атрибута. Первый атрибут —название интерфейса, составленное в соответствии с правилами языка программирования. Имя должно начинаться с буквы “I” — так принято, так же как в Delphi название любого класса должно начинаться с буквы «Т».
Второй атрибут представляет собой глобальный уникальный идентификатор (Globally Unique IDenitfier, GUID), который задается уникальным сочетанием символов. Для его генерации используются такие сильные алгоритмы, что однажды сгенерированное сочетание никогда не повторится в будущем ни на одном компьютере мира.
Как обратиться к методам объекта с помощью интерфейса?
Для этого необходимо получить указатель на соответствующий интерфейс, после чего клиент имеет право использовать службы объекта, вызывая его методы как методы обычного объекта.
Поскольку объект может иметь несколько интерфейсов, то при получении интерфейса для каждого из них будет получен собственный указатель.
Возникает закономерный вопрос — как быть, если клиент не знает, какие интерфейсы имеются у объекта? Для получения их перечня нужно использовать интерфейс IUnknown или IInterface, который есть у любого объекта СОМ.
IUnknown является базовым интерфейсом СОМ-объектов. Через этот интерфейс можно получить все остальные интерфейсы, которые поддерживает объект. В нем присутствуют всего три метода, но они играют самую важную роль в функционировании объекта:
1. Querylnterface
2. _AddRef
3. _Release
Метод Querylnterface возвращает указатель на интерфейс объекта по его идентификатору. Если передан идентификатор несуществующего интерфейса, то метод возвратит Null.
СОМ реализует автоматическое управление памятью СОМ-объектов, основанное на идее подсчета ссылок на объект. Объект существует, пока его использует хотя бы один клиент. Поэтому любой класс после создания объекта должен увеличить счетчик ссылок, а после завершения его использования уменьшить счетчик на единицу. Когда счетчик достигнет нулевого значения, СОМ-объект автоматически будет удален из памяти. Именно для этого предназначены метод _AddRef, увеличивающий счетчик на единицу, и метод _Release, уменьшающий его.
Используя Querylnterface для получения указателя на интерфейс в Delphi, метод запускает процедуру увеличения счетчика ссылок. Вызвать его вручную может потребоваться в том случае, если один клиент попытается передать другому указатель на интерфейс объекта. Тогда без вызова _AddRef счетчик будет хранить неверные сведения о количестве использующих его клиентов. То же справедливо и для метода _Release. При выходе переменной, ссылающейся на интерфейс, за область видимости (либо при присвоении ей другого значения) компилятор генерирует код для вызова метода _Release, информируя реализацию COM-объекта о том, что ссылка на нее больше не нужна. Поэтому нет надобности постоянно вызывать этот метод, за исключением особых случаев.
Что делать, если клиенту требуется получить интерфейс объекта, который еще не иcпoльзoвaлся и, следовательно, может быть и не создан. В этом случае клиенту необходимо обратиться к библиотеке СОМ. Она обеспечивает выполнение базовых функций и интерфейсов в операционной системе. К ней обращаются посредством специальных функций, имена которых согласно спецификации начинаются с приставки Co.
При установке СОМ-приложения в системный реестр записываются данные обо всех реализуемых им объектах.
CLSID (class identifier) — идентификатор класса, однозначно идентифицирует класс объекта в системе;
тип сервера объекта (внутренний, локальный, удаленный).
Поэтому для получения указателя на требуемый класс и интерфейс необходимо вызвать метод CoCreateInstance, передав в качестве параметров CLSID нужного класса, IID интерфейса (Interface Identifier) и тип требуемого сервера.
Возвращение указателя происходит по следующей схеме.
1. Библиотека через диспетчер управления службами обращается к системному реестру.
2. Находит информацию о сервере по идентификатору класса CLSID.
3. Запускает сервер.
4. Сервер создает экземпляр класса.
5. Объект возвращает библиотеке указатель на запрошенный интерфейс.
6. Библиотека СОМ передает указатель клиенту.
Теперь, немного разобравшись с теорией, попробуем реализовать простенький интерфейс.

Реализация интерфейса. Вариант №1 – использование TInterfacedObject

Реализацией интерфейса в Delphi всегда выступает класс. Поэтому в объявлении класса необходимо указать, какие интерфейсы он реализует.
Рассмотрим реализацию интерфейса, который имеет всего один метод Hello() в результате выполнения которого мы получим сообщение «Hello World!». Вначале, объявим сам интерфейс:

type
  IMyInterface = interface(IUnknown)
  ['{A2CF248C-7FC9-43B0-A503-F0AD53FBA325}']
  procedure Hello();stdcall;
end;

Для создания GUID я воспользовался “горячими” клавишами Ctrl+Shift+G.
Теперь создадим класс, реализующий наш интерфейс. Здесь, прежде всего следует отметить следующее: класс должен иметь методы, точно соответствующие по именам и спискам параметров всем методам, объявленным в его заголовке интерфейсов. Как «обойти» это требование мы рассмотрим чуть позже.
Итак, наш класс, реализующий интерфейс IMyInterface может выглядеть так:

type
  TMyClass = class(TInterfacedObject,IMyInterface)
    procedure Hello();stdcall;
    destructor Destroy; override;
end;
 
destructor TMyClass.Destroy;
begin
  ShowMessage('Запускаем деструктор класса');
  inherited;
end;
 
procedure TMyClass.Hello;
begin
  ShowMessage('Hello World!')
end;

Новый класс является наследником от TInterfacedObject. TInterfacedObject описан в модуле System.pas и именно этот класс рекомендуется использовать при создании классов, реализующих какие-либо интерфейсы.
Теперь рассмотрим работу приложения в котором будет использоваться интерфейс IMyInterface. Пусть по клику на кнопке Button1 будет выполняться метод Hello(). Код процедуры может выглядеть следующим образом:

procedure TForm3.Button1Click(Sender: TObject);
var MyInterface : IMyInterface;
 begin
  MyInterface:=TMyClass.Create;
  MyInterface.Hello;
end;

Теперь запустите приложение и увидите, что после того, как на экране появится сообщение «Hello World!» сразу же сработает деструктор класса. Все дело в том, что приведение класса к интерфейсу:

 MyInterface:=TMyClass.Create;

неявно увеличивает счетчик ссылок на единицу. А при выходе переменной, ссылающейся на интерфейс, за область видимости (либо при присвоении ей другого значения) компилятор Delphi генерирует код для вызова метода _Release, информируя реализацию о том, что ссылка на нее больше не нужна. Таким образом, после выполнения первой строки кода счетчик увеличится на единицу, а при выходе из процедуры уменьшится на единицу, и объект удаляется из памяти. В некоторых случаях подобный «автоматизм» может быть и не нужен. Что делать, если, например, нам необходимо, чтобы объект уничтожился только после того как будет закрыто наше приложение? Именно на этом вопросе мы переходим ко второму варианту реализации интерфейсов.

Реализация интерфейса. Вариант №2 – использование TComponent

В базовом классе TComponent имеется полный набор методов, позволяющий реализовать интерфейс IUnknown, хотя сам класс данный интерфейс не реализует. Это позволяет наследникам TComponent реализовывать интерфейсы, не заботясь о реализации IUnknown.
Методы TComponent._AddRef и TComponent._Release на этапе выполнения программы не реализуют механизм подсчета ссылок, т. е. в отношении классов-наследников TComponent, реализующих интерфейсы, не действует автоматическое управление памятью. Это позволяет запрашивать у них интерфейсы, не опасаясь, что объект будет удален из памяти при выходе переменной за область видимости.
Изменим наш класс, следующим образом:

type
  TMyClass = class(TComponent,IMyInterface)
    procedure Hello();stdcall;
    destructor Destroy; override;
end;

В обработчике OnClick необходимо теперь приводить класс к интерфейсу следующим образом:

MyInterface:=TMyClass.Create(self);

То есть указывать «хозяина» (Owner:TComponent). Теперь снова запустите приложение и убедитесь, что класс не будет уничтожен до тех пор пока Вы не закроете приложение или вручную его не «убьете».
Теперь обратим свой взор на следующее обстоятельство: объект может иметь несколько интерфейсов.
Что делать, если у нас есть два интерфейса, которые имеют одноименные методы и эти интерфейсы необходимо реализовать одним классом? Например, два интерфейса с методами Hello():

type
  IMyInterface = interface(IUnknown)
  ['{A2CF248C-7FC9-43B0-A503-F0AD53FBA325}']
  procedure Hello();stdcall;
end;
 
type
  IMyInterface2 = interface(IUnknown)
  ['{C3B70F90-7BE7-4AC1-91BF-3EAD9FE56734}']
  procedure Hello();stdcall;
end;

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

type
  IMyInterface = interface(IUnknown)
  ['{A2CF248C-7FC9-43B0-A503-F0AD53FBA325}']
  procedure Hello();stdcall;
end;
 
type
  IMyInterface2 = interface(IUnknown)
  ['{C3B70F90-7BE7-4AC1-91BF-3EAD9FE56734}']
  procedure Hello();stdcall;
end;
 
type
  TMyClass = class(TComponent,IMyInterface,IMyInterface2)
    procedure Hello();stdcall;
    procedure Hello2();stdcall;
    procedure IMyInterface.Hello = Hello;
    procedure IMyInterface2.Hello = Hello2;
    destructor Destroy; override;
end;
[]
implementation
[]
procedure TForm3.Button1Click(Sender: TObject);
var MyInterface : TMyClass;
begin
  MyInterface:=TMyClass.Create(self);
  MyInterface.Hello;
  MyInterface.Hello2;
end;
 
{ TMyClass }
 
destructor TMyClass.Destroy;
begin
  ShowMessage('Запускаем деструктор класса');
  inherited;
end;
 
procedure TMyClass.Hello;
begin
  ShowMessage('Hello World!')
end;
 
procedure TMyClass.Hello2;
begin
  ShowMessage('Hello World FROM IMyInterface2!')
end;
end.

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

type
  TMyClass = class(TComponent,IMyInterface,IMyInterface2)
    procedure Hello();stdcall;
    procedure IMyInterface.Hello = Hello;
    procedure IMyInterface2.Hello = Hello;
    destructor Destroy; override;
end;

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

4.3 4 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
14 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
Алексей (Тамбов)
Алексей (Тамбов)
11/05/2010 15:37

Очень интересно. Хочется побольше примеров как это использовать на практике.

mamont80
mamont80
14/05/2010 14:17

Хорошо пишите. Хотя ничего нового не узнал, но в голове освежил. В книгах часто слишком много путаницы и воды. Никак не решусь перевести свой проект на интерфейсы.

Odysseos
15/05/2010 22:39

Вообще-то, хоть вся система интерфейсов и создавалась для поддержки в Delphi COM — непосредстенно от COM интерфейсы в Delphi, тем не менее, не зависят, и в тех примерах, которые Вы привели, система COM никак не используется. Собственно — для интерфейсов, которые используются только в Delphi, даже GUID задавать не обязательно (если не передавать интерфейсы между exe и dll).

crystalbit
30/05/2010 19:32

Опечатка в названии поста?)

meligo
meligo
23/07/2014 17:25

Допустим, на Button1 есть есть обработчик:

procedure TForm1.Button1Click(Sender: TObject);
begin
with TXMLDocument.Create(Self) do begin
Active := true;
Options := Options + [doNodeAutoIndent];

with AddChild(‘My’) do begin
Attributes[‘Att1’] := ‘AAA’;
Attributes[‘Att2’] := ‘BBB’;
with AddChild(‘My1’) do begin
Attributes[‘Att1’] := ‘AAA’;
Attributes[‘Att2’] := ‘BBB’;
end
end;

Memo1.Lines.Assign(XML);
end
end;

Вопрос: Как вынести в отдельную процедуру, например Foo; две повторяющиеся строчки:

procedure Foo;
begin
Attributes[‘Att1’] := ‘AAA’;
Attributes[‘Att2’] := ‘BBB’;
end;

meligo
meligo
23/07/2014 17:40
Ответить на  Vlad

А что Вы будете передавать в качестве «ANode»? :))

В том то и задача — вызвать эту функцию «изнутри», не имея явного значения ANode!
Эта тема в точности пересекается с вашей статьёй в смысле наследования и расширения интерфейсов.

meligo
meligo
23/07/2014 17:35

Предыдущий вопрос возник, потому что ниже приведенный вариант попытки решения этой задачи
при использовании: with AddChild(‘My’) as IXMLNode_ do … выдает ошибку: «Interface not suppotrted»

type
IXMLNode_ = interface(IXMLNode)
['{F3364035-74D4-499E-9D65-11E26B04DDD1}']
procedure Foo stdcall;
end;

TXMLNode_ = class(TXMLNode,IXMLNode_)
procedure Foo stdcall;
end;

{ TXMLNode_ }

procedure TXMLNode_.Foo;
begin
SetAttribute('Att1','AAA');
SetAttribute('Att2','BBB');
end;

Прошу помощи…

meligo
meligo
23/07/2014 17:58

Изложенный случай, мне кажется, очень похож на то, что описывает Тенцер в своей статье: «Delphi и COM» ссылка: http://www.delphisources.ru/pages/faq/base/delphi_com.html Цитата: «В рассмотренных примерах код для получения интерфейса у класса генерировался (с проверкой типов) на этапе компиляции. Если класс не реализует требуемого интерфейса, то программа не откомпилируется. Однако существует возможность запросить интерфейс и во время выполнения программы. Для этого служит оператор as, который вызывает QueryInterface и, в случае успеха, возвращает ссылку на полученный интерфейс. В противном случае генерируется исключение. Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку «Interface not supported»: » Мне кажется, что это в точности наш… Подробнее »

meligo
meligo
28/07/2014 14:30
Ответить на  Vlad

У Александра Алексеева, к которому вы посоветовали обратиться, почта на сайте «отражает» все письма…
Есть какой нибудь другой способ связи с ним?