Вторник, 14.05.2024
Королевство Delphi
Главное меню
Статьи
Наш опрос
Как часто ви на этот сайт заходите?
Всего ответов: 159
Статистика
Онлайн всего: 1
Гостей: 1
Пользователей: 0
Форма входа
Главная » Статьи » Система » CORBA и COM

Delphi и COM

Введение

COM (Component Object Model) — модель объектных компонентов — одна из основных технологий, на которых основывается Windows. Более  того, все новые технологии в Windows (Shell, Scripting, поддержка HTML и т.п.) реализуют свои API именно в виде COM-интерфейсов. Таким образом, в настоящее время профессиональное программирование требует понимания модели COM и умения с ней работать. В этой главе мы рассмотрим основные понятия COM и особенности их поддержки в Delphi.

Базовые понятия

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

Интерфейс

Интерфейс, образно говоря,  является «контрактом» между программистом и компилятором.

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

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

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

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

Таким образом, необходимо понимать следующее:

  • Интерфейс не является классом. Класс может выступать реализацией интерфейса, но класс содержит код методов на конкретном языке программирования, а интерфейс — нет.
  • Интерфейс строго типизирован. Как клиент, так и реализация интерфейса должны использовать точно те же методы и параметры, что указаны в описании интерфейса.
  • Интерфейс является «неизменным контрактом». Нельзя определять новую версию того же интерфейса с измененным набором методов (или их параметров), но с тем же идентификатором.

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

Реализация интерфейса — это код, который реализует эти методы. При этом, за несколькими исключениями, не накладывается никаких ограничений на то, каким образом будет выглядеть реализация. Физически реализация представляет собой массив указателей на методы, адрес которого и используется в клиенте для доступа к COM-объекту. Любая реализация интерфейса имеет метод QueryInterface, позволяющий запросить ссылку на конкретный интерфейс из числа реализуемых.

Автоматическое управление памятью и подсчет ссылок

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

Объявление интерфейсов

Для поддержки интерфейсов Delphi расширяет синтаксис языка Pascal дополнительными ключевыми словами. Объявление интерфейса в Delphi реализуется ключевым словом interface:

type
 IMyInterface = interface
 ['{412AFF00-5C21-11D4-84DD-C8393F763A13}']
 procedure DoSomething(var I: Integer); stdcall;
 function DoSomethingAnother(S: String): Boolean;
 end;
 
 IMyInterface2 = interface(IMyInterface)
 ['{412AFF01-5C21-11D4-84DD-C8393F763A13}']
 procedure DoAdditional(var I: Integer); stdcall;
 end;

Для генерации нового значения GUID в IDE Delphi служит сочетание клавиш Ctrl+Shift+G.

IUnknown

Базовым интерфейсом в модели COM является IUnknown. Любой интерфейс наследуется от IUnknown и обязан реализовать объявленные в нем методы. IUnknown объявлен в модуле System.pas следующим образом:

type
 IUnknown = interface
 ['{00000000-0000-0000-C000-000000000046}']
 function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
 function _AddRef: Integer; stdcall;
 function _Release: Integer; stdcall;
 end;

Рассмотрим назначение методов IUnknown более подробно.

Последние два метода предназначены для реализации механизма подсчета ссылок.

function _AddRef: Integer; stdcall;

Эта функция должна увеличить счетчик ссылок на интерфейс на единицу и вернуть новое значение счетчика.

function _Release: Integer; stdcall;

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

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

function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;

Эта функция получает в качестве входного параметра идентификатор интерфейса. Если объект реализует запрошенный интерфейс, то функция:

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает 0.

В противном случае — функция возвращает код ошибки E_NOINTERFACE.

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

В модуле System.pas объявлен класс TInterfacedObject, реализующий IUnknown и его методы. Рекомендуется использовать этот класс для создания реализаций своих интерфейсов.

Кроме того, поддержка интерфейсов реализована в базовом классе TObject. Он имеет метод

function TObject.GetInterface(const IID: TGUID; out Obj): Boolean;

Если класс реализует запрошенный интерфейс, то функция:

  1. возвращает ссылку на него в параметре Obj;
  2. вызывает метод _AddRef полученного интерфейса;
  3. возвращает TRUE.

В противном случае — функция возвращает FALSE.

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

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

Реализацией интерфейса в Delphi всегда выступает класс. Для этого в объявлении класса необходимо указать, какие интерфейсы он реализует.

type
 TMyClass = class(TComponent, IMyInterface, IDropTarget)
 // Реализация методов
 end;

Класс TMyClass реализует интерфейсы IMyInterface и IDropTarget. Необходимо понимать, что реализация классом нескольких интерфейсов не означает множественного наследования и вообще наследования класса от интерфейса. Указание интерфейсов в описании класса означает только то, что в данном классе реализованы все эти интерфейсы.

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

Рассмотрим более подробный пример.

type
 ITest = interface
 ['{61F26D40-5CE9-11D4-84DD-F1B8E3A70313}']
 procedure Beep;
 end;
 
 TTest = class(TInterfacedObject, ITest)
 procedure Beep;
 destructor Destroy; override;
 end;
 
 …
 
procedure TTest.Beep;
 begin
 Windows.Beep(0,0);
 end; 
 
destructor TTest.Destroy;
 begin
 inherited;
 MessageBox(0, 'TTest.Destroy', NIL, 0);
 end;

Здесь класс TTest реализует интерфейс ITest. Рассмотрим использование интерфейса из программы.

procedure TForm1.Button1Click(Sender: TObject);
 var
 Test: ITest;
 begin
 Test := TTest.Create;
 Test.Beep;
 end;

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

Во-первых, оператор присваивания при приведении типа данных к интерфейсу неявно вызывает метод _AddRef. При этом количество ссылок на интерфейс увеличивается на единицу.

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

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

Так, следующий код приведет к ошибке в момент выхода из функции:

var
 Test: ITest;
 T: TTest;
 begin
 T := TTest.Create;
 Test := T;
 Test.Beep;
 T.Free;
 end; // в этот момент произойдет ошибка

Если вы хотите уничтожить реализацию интерфейса немедленно, не дожидаясь выхода переменной за область видимости, – просто присвойте ей значение NIL:

var
 Test: ITest;
 T: TTest;
 begin
 T := TTest.Create;
 Test := T;
 Test.Beep;
 Test := NIL; // Неявно вызывается IUnknown._Release;
 end;

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

  1. При приведении типа объекта к интерфейсу вызывается метод _AddRef.
  2. При выходе переменной, ссылающейся на интерфейс, за область видимости либо при присвоении ей другого значения вызывается метод _Release.
  3. Единожды запросив у объекта интерфейс, в дальнейшем вы не должны освобождать объект вручную. Вообще начиная с этого момента лучше работать с объектом только через интерфейсные ссылки.

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

Например, следующий код будет успешно откомпилирован, но при выполнении вызовет ошибку «Interface not supported»:

var
 Test: ITest;
 begin
 Test := TInterfacedObject.Create as ITest;
 Test.Beep;
 end;

В то же время код

var
 Test: ITest;
 begin
 Test := TTest.Create as ITest;
 Test.Beep;
 end;

будет успешно компилироваться и выполняться.

Реализация интерфейсов (расширенное рассмотрение)

Рассмотрим вопросы реализации интерфейсов подробнее.

Объявим два интерфейса:

type
 ITest = interface
 ['{61F26D40-5CE9-11D4-84DD-F1B8E3A70313}']
 procedure Beep;
 end;
 
 ITest2 = interface
 ['{61F26D42-5CE9-11D4-84DD-F1B8E3A70313}']
 procedure Beep;
 end; 

Теперь создадим класс, который будет реализовывать оба этих интерфейса:

 TTest2 = class(TInterfacedObject, ITest, ITest2)
 procedure Beep1;
 procedure Beep2;
 procedure ITest.Beep = Beep1;
 procedure ITest2.Beep = Beep2;
 end;

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

Если реализация методов TTest2.Beep1 и TTest2.Beep2 идентична, то можно не создавать два разных метода, а объявить класс следующим образом:

 TTest2 = class(TInterfacedObject, ITest, ITest2)
 procedure MyBeep;
 procedure ITest.Beep = MyBeep;
 procedure ITest2.Beep = MyBeep;
 end;

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

type
 TBeeper = class
 procedure Beep;
 end;
 
 TMessager = class
 procedure ShowMessage(const S: String);
 end; 
 
 TTest3 = class(TInterfacedObject, ITest, IAnotherTest)
 private
 FBeeper: TBeeper;
 FMessager: TMessager;
 property Beeper: TBeeper read FBeeper implements ITest;
 property Messager: TMessager read FMessager implements IAnotherTest;
 public
 constructor Create;
 destructor Destroy; override;
 end; 

Для делегирования реализации интерфейса другому классу служит ключевое слово implements.

{ TBeeper }
 
 procedure TBeeper.Beep;
 begin
 Windows.Beep(0,0);
 end;
 
{ TMessager }
 
 procedure TMessager.ShowMessage(const S: String);
 begin
 MessageBox(0, PChar(S), NIL, 0);
 end;
 
{ TTest3 }
 
 constructor TTest3.Create;
 begin
 inherited;
 // Создаем экземпляры дочерних классов
 FBeeper := TBeeper.Create;
 FMessager := TMessager.Create;
 end; 
 
destructor TTest3.Destroy;
 begin
 // Освобождаем экземпляры дочерних классов
 FBeeper.Free;
 FMessager.Free;
 inherited;
 end;

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

Обращаться к полученному классу можно точно так же, как и к любому классу, реализующему интерфейсы:

var
 Test: ITest;
 Test2: IAnotherTest;
 begin
 Test2 := TTest3.Create;
 Test2.ShowMessage('Hi');
 Test := Test2 as ITest;
 Test.Beep;
 end;

Интерфейсы и TComponent

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

type
 IGetData = interface
 ['{B5266AE0-5E77-11D4-84DD-9153115ABFC3}']
 function GetData: String;
 end;
 
 TForm1 = class(TForm, IGetData)
 private
 function GetData: String;
 end;
 
....
 
var
 I: Integer;
 GD: IGetData;
 S: String;
 begin
 S := '';
 for I := 0 to Pred(Screen.FormCount) do begin
 if Screen.Forms[I].GetInterface(IGetData, GD) then
 S := S + GD.GetData + #13;
 end;
 ShowMessage(S);
 end;

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

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

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

В качестве примера рассмотрим MDI-приложение, имеющее много различных форм и единую панель инструментов. Предположим, что на этой панели инструментов имеются команды «Сохранить», «Загрузить» и «Очистить», однако каждое из окон реагирует на эти команды по-разному.

Создадим модуль с объявлениями интерфейсов:

unit ToolbarInterface;
 
 interface
 
 type
 TCommandType = (ctSave, ctLoad, ctClear);
 TCommandTypes = set of TCommandType;
 TSaveType = (stSave, stSaveAS);
 
 IToolBarCommands = interface
 ['{B5266AE1-5E77-11D4-84DD-9153115ABFC3}']
 function SupportedCommands: TCommandTypes;
 function Save(AType: TSaveType): Boolean;
 procedure Load;
 procedure Clear;
 end;
 
 implementation
 
 end.

Интерфейс IToolBarCommands описывает набор методов, которые должны реализовать формы, поддерживающие работу с панелью кнопок. Метод SupportedCommands возвращает список поддерживаемых формой команд.

Создадим три дочерние формы — Form2, Form3 и Form4 — и установим им свойство FormStyle = fsMDIChild.

Form2 умеет выполнять все три команды:

type
 TForm2 = class(TForm, IToolBarCommands)
 private
 function SupportedCommands: TCommandTypes;
 function Save(AType: TSaveType): Boolean;
 procedure Load;
 procedure Clear;
 end;
 
{ TForm2 }

procedure TForm2.Clear;
 begin
 ShowMessage('TForm2.Clear');
 end;
 
procedure TForm2.Load;
 begin
 ShowMessage('TForm2.Load');
 end;
 
function TForm2.Save(AType: TSaveType): Boolean;
 begin
 ShowMessage('TForm2.Save');
 Result := TRUE;
 end;
 
function TForm2.SupportedCommands: TCommandTypes;
 begin
 Result := [ctSave, ctLoad, ctClear]
 end;

Form3 умеет выполнять только команду Clear:

type
 TForm3 = class(TForm, IToolBarCommands)
 private
 function SupportedCommands: TCommandTypes;
 function Save(AType: TSaveType): Boolean;
 procedure Load;
 procedure Clear;
 end;
 
{ TForm3 }

procedure TForm3.Clear;
 begin
 ShowMessage('TForm3.Clear');
 end;
 
procedure TForm3.Load;
 begin
 // Метод ничего не делает, но должен присутствовать
 // для корректной реализации интерфейса
 end;
 
function TForm3.Save(AType: TSaveType): Boolean;
 begin
 end;
 
function TForm3.SupportedCommands: TCommandTypes;
 begin
 Result := [ctClear]
 end;

И наконец, Form4 вообще не реализует интерфейс IToolBarCommands и не откликается ни на одну команду.

На главной форме приложения поместим ActionList и создадим три компонента TAction. Кроме того, разместим на ней TToolBar и назначим ее кнопкам соответствующие TAction.

type
 TForm1 = class(TForm)
 ToolBar1: TToolBar;
 ImageList1: TImageList;
 ActionList1: TActionList;
 acLoad: TAction;
 acSave: TAction;
 acClear: TAction;
 tbSave: TToolButton;
 tbLoad: TToolButton;
 tbClear: TToolButton;
 procedure acLoadExecute(Sender: TObject);
 procedure ActionList1Update(Action: TBasicAction;
 var Handled: Boolean);
 procedure acSaveExecute(Sender: TObject);
 procedure acClearExecute(Sender: TObject);
 end;

Наиболее интересен метод ActionList1Update, в котором проверяются поддерживаемые активной формой команды и настраивается интерфейс главной формы. Если нет активной дочерней формы либо она не поддерживает интерфейс IToolBarCommands, все команды запрещаются, в противном случае — разрешаются только поддерживаемые формой команды.

procedure TForm1.ActionList1Update(Action: TBasicAction;
 var Handled: Boolean);
 var
 Supported: TCommandTypes;
 TC: IToolBarCommands;
 begin
 if Assigned(ActiveMDIChild)
 and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
 Supported := TC.SupportedCommands
 else
 Supported := [];
 acSave.Enabled := ctSave in Supported;
 acLoad.Enabled := ctLoad in Supported;
 acClear.Enabled := ctClear in Supported;
 end;

При активизации команд проверяется наличие активной дочерней формы, у нее запрашивается интерфейс IToolBarCommands и вызывается соответствующий ему метод:

procedure TForm1.acLoadExecute(Sender: TObject);
 var
 TC: IToolBarCommands;
 begin
 if Assigned(ActiveMDIChild)
 and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
 TC.Load;
 end;
 
procedure TForm1.acSaveExecute(Sender: TObject);
 var
 TC: IToolBarCommands;
 begin
 if Assigned(ActiveMDIChild)
 and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
 if not TC.Save(stSaveAS) then
 ShowMessage(‘Not Saved !!!’);
 end;
 
procedure TForm1.acClearExecute(Sender: TObject);
 var
 TC: IToolBarCommands;
 begin
 if Assigned(ActiveMDIChild)
 and ActiveMDIChild.GetInterface(IToolBarCommands, TC) then
 TC.Clear;
 end; 

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

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

Использование интерфейсов для реализации Plug-In

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

В качестве примера реализуем несложную программу, использующую Plug-In для загрузки данных.

Объявим интерфейсы модуля расширения и внутреннего API программы.

unit PluginInterface;
 
 interface
 
 type
 IAPI = interface
 ['{64CFF1E0-61A3-11D4-84DD-B18D6F94141F}']
 procedure ShowMessage(const S: String);
 end;
 
 ILoadFilter = interface
 ['{64CFF1E1-61A3-11D4-84DD-B18D6F94141F}']
 procedure Init(const FileName: String; API: IAPI);
 function GetNextLine(var S: String): Boolean;
 end;
 
 implementation
 
 end.

Этот модуль должен использоваться как в Plug-In, так и в основной программе и гарантирует использование ими идентичных интерфейсов.

Plug-In представляет собой DLL, экспортирующую функцию CreateFilter, возвращающую ссылку на интерфейс ILoadFilter. Главный модуль сначала должен вызвать метод Init, передав в него имя файла и ссылку на интерфейс внутреннего API, а затем вызывать метод GetNextLine до тех пор, пока он не вернет FALSE.

Рассмотрим код модуля расширения:

library ImpTxt;
 
 uses
 ShareMem, SysUtils, Classes, PluginInterface;
 
 {$R *.RES}
 
 type
 TTextFilter = class(TInterfacedObject, ILoadFilter)
 private
 FAPI: IAPI;
 F: TextFile;
 Lines: Integer;
 InitSuccess: Boolean;
 procedure Init(const FileName: String; API: IAPI);
 function GetNextLine(var S: String): Boolean;
 public
 destructor Destroy; override;
 end;
 { TTextFilter }
 
 procedure TTextFilter.Init(const FileName: String; API: IAPI);
 begin
 FAPI := API;
 {$I-}
 AssignFile(F, FileName);
 Reset(F);
 {$I+}
 InitSuccess := IOResult = 0;
 if not InitSuccess then
 API.ShowMessage('Ошибка инициализации загрузки');
 end;

Метод Init выполняет две задачи: сохраняет ссылку на интерфейс API главного модуля для дальнейшего использования и пытается открыть файл с данными. Если файл открыт успешно – выставляется внутренний флаг InitSuccess.

function TTextFilter.GetNextLine(var S: String): Boolean;
 begin
 if InitSuccess then begin
 Inc(Lines);
 Result := not Eof(F);
 if Result then begin
 Readln(F, S);
 FAPI.ShowMessage('Загружено ' + IntToStr(Lines) + ' строк.');
 end; 
 end else
 Result := FALSE;
 end;

Метод GetNextLine считывает следующую строку данных и возвращает либо TRUE, если это удалось, либо FALSE — в случае окончания файла. Кроме того, при помощи API, предоставляемого главным модулем, данный метод информирует пользователя о ходе загрузки.

destructor TTextFilter.Destroy;
 begin
 FAPI := NIL;
 if InitSuccess then
 CloseFile(F);
 inherited;
 end;

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

function CreateFilter: ILoadFilter;
 begin
 Result := TTextFilter.Create;
 end;

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

exports
 CreateFilter; // Функция должна быть экспортирована из DLL
 
 begin
 end.

Теперь полученную DLL можно использовать из основной программы.

type
 TAPI = class(TInterfacedObject, IAPI)
 procedure ShowMessage(const S: String);
 end;
 
{ TAPI } 

 procedure TAPI.ShowMessage(const S: String);
 begin
 with (Application.MainForm as TForm1).StatusBar1 do begin
 SimpleText := S;
 Update;
 end;
 end; 

Класс TAPI реализует API, предоставляемый модулю расширения. Функция ShowMessage выводит сообщения модуля в Status Bar главной формы приложения.

type
 TCreateFilter = function: ILoadFilter;
 
 procedure TForm1.LoadData(FileName: String);
 var
 PluginName: String;
 Ext: String;
 hPlugIn: THandle;
 CreateFilter: TCreateFilter;
 Filter: ILoadFilter;
 S: String;
 begin

Подготавливаем TMemo к загрузке данных:

 Memo1.Lines.Clear;
 Memo1.Lines.BeginUpdate;

Получаем имя модуля с фильтром для выбранного расширения файла. Описания модулей хранятся в файле plugins.ini в секции Filters в виде строк формата:

 <расширение> = <имя модуля>, например:

[Filters]
 TXT=ImpTXT.DLL
 
 try
 Ext := ExtractFileExt(FileName);
 Delete(Ext, 1, 1);
 with TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'plugins.ini') do
 try
 PlugInName := ReadString('Filters', Ext, '');
 finally
 Free;
 end;

Теперь попытаемся загрузить модуль и найти в нем функцию CreateFilter:

 hPlugIn := LoadLibrary(PChar(PluginName));
 try
 CreateFilter := GetProcAddress(hPlugIn, 'CreateFilter');
 if Assigned(CreateFilter) then begin

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

 Filter := CreateFilter;
 try
 Filter.Init(FileName, TAPI.Create);

Загружаем данные при помощи созданного фильтра:

 while Filter.GetNextLine(S) do
 Memo1.Lines.Add(S);

Перед выгрузкой DLL из памяти необходимо обязательно освободить ссылку на интерфейс Plug-In, иначе это произойдет по выходе из функции и вызовет Access Violation.

 finally
 Filter := NIL;
 end;
 end else raise Exception.Create('Не могу загрузить фильтр');

Выгружаем DLL и обновляем TMemo:

 finally
 FreeLibrary(hPlugIn);
 end;
 finally
 Memo1.Lines.EndUpdate;
 end;
 end;

Таким образом, реализовать модули расширения для загрузки данных из форматов, отличающихся от текстового, и единообразно работать с ними несложно.

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

Внимание! Поскольку в EXE и DLL используются длинные строки, не забудьте включить в список uses обоих проектов модуль ShareMem. Другим вариантом решения проблемы передачи строк является использование типа данных WideString. Для них распределением памяти занимается OLE, причем делает это независимо от модуля, из которого была создана строка.

COM-сервер, структура и использование

Модель COM предоставляет возможность создания многократно используемых компонентов, независимых от языка программирования. Такие компоненты называются COM-серверами и представляют собой исполняемые файлы (EXE) или динамические библиотеки (DLL), специальным образом оформленные для обеспечения возможности их универсального вызова из любой программы, написанной на поддерживающем COM языке программирования. При этом COM-сервер может выполняться как в адресном пространстве вызывающей программы (In-Process-сервер), так и в виде самостоятельного процесса (Out-Of-Process-сервер) или даже на другом компьютере (Distributed COM). COM автоматически разрешает вопросы, связанные с передачей параметров (Marshalling) и согласованием потоковых моделей клиента и сервера.

Далее будут рассмотрены некоторые архитектурные вопросы, знание которых необходимо для работы с COM.

COM-сервер

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

Сервер в виде DLL

Такой сервер всегда выполняется в адресном пространстве активизировавшего его приложения (In-Process). За счет этого, как правило, снижаются накладные расходы на вызов методов сервера. В то же время такой сервер менее надежен, поскольку его память не защищена от ошибок в вызывающем приложении. Кроме того, он не может выполняться на удаленной машине без исполнимого модуля-посредника, способного создать процесс, в который может быть загружена DLL. Примером такого модуля может служить Microsoft Transaction Server.

Сервер в виде исполнимого файла

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

Регистрация сервера

COM реализует механизм автоматического поиска серверов по запросу клиента. Каждый COM-объект имеет уникальный идентификатор, Class Identifier (CLSID). Windows ведет в реестре базу данных зарегистрированных объектов, индексированную при помощи CLSID. Она расположена в ветке реестра HKEY_CLASSES_ROOT\CLSID. 

Для каждого сервера прописывается информация, необходимая для нахождения и загрузки его модуля. Таким образом, клиентское приложение не должно беспокоиться о поиске сервера: достаточно зарегистрировать его на компьютере — и COM автоматически найдет и загрузит нужный модуль. Кроме того, объект может зарегистрировать свое «дружественное» имя, или Programmatic Identifier (PROGID). Обычно оно формируется как комбинация имени сервера и имени объекта, например Word.Application. Это имя содержит ссылку на CLSID объекта. Когда он создается с использованием PROGID, COM просто берет связанное с ним значение CLSID и получает из него всю необходимую информацию.

Серверы в виде исполняемых файлов автоматически регистрируются при первом запуске программы на компьютере. Для регистрации серверов DLL служит программа Regsvr32, поставляемая в составе Windows, либо TRegSvr из  поставки DELPHI.

Потоки и «комнаты»

Windows — многозадачная и многопоточная среда с вытесняющей многозадачностью. Применительно к COM это означает, что клиент и сервер могут оказаться в различных процессах или потоках приложения, что к серверу могут обращаться множество клиентов, причем в непредсказуемые моменты времени. Технология COM решает эту проблему при помощи концепции «комнат» (Apartments), в которых и выполняются COM-клиенты и COM-серверы. «Комнаты» бывают однопоточные (Single Threaded Apartment, STA) и многопоточные (Multiple Threaded Apartment, MTA).

STA

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

  1. Программист может не заботиться о синхронизации методов. Гарантируется, что до окончания выполнения текущего метода никакой другой метод объекта вызван не будет.
  2. Программист может не заботиться о синхронизации доступа к полям класса, реализующего объект. Поскольку одновременно может выполняться только один метод, одновременный доступ к полю из двух методов невозможен.

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

Недостатки STA напрямую вытекают из ее реализации:

  1. Дополнительные (и иногда излишние) затраты на синхронизацию при вызове методов.
  2. Невозможность отклика на вызов метода, пока не исполнен предыдущий. Например, если в настоящее время выполняется метод, требующий одну минуту на исполнение, то до его завершения COM-объект будет недоступен.

Тем не менее STA, как правило, является наиболее подходящим выбором для реализации COM-сервера. Использовать MTA есть смысл только в том случае, если STA не подходит для конкретного сервера.

MTA

Многопоточная «комната» не реализует автоматический сервис по синхронизации и не имеет его ограничений. Внутри нее может быть создано сколько угодно потоков и объектов, причем ни один из объектов не привязан к какому-то конкретному потоку. Это означает, что любой метод объекта может быть вызван в любом из потоков в MTA. В то же самое время в другом потоке может быть вызван любой другой (либо тот же самый) метод COM-объекта по запросу другого клиента. COM автоматически ведет пул потоков внутри MTA, при вызове со стороны клиента находит свободный поток и в нем вызывает метод требуемого объекта. Таким образом, даже если выполняется метод, требующий длительного времени, то для другого клиента он может быть вызван без задержки в другом потоке. Очевидно, что COM-сервер, работающий в MTA, обладает потенциально более высокими быстродействием и доступностью для клиентов, однако он значительно сложнее в разработке, поскольку даже локальные данные объектов не защищены от одновременного доступа и требуют синхронизации.

Передача интерфейсов и параметров

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

Эту работу берет на себя COM. Для доступа к серверу в другой «комнате» клиент должен запросить у COM создание в своей «комнате» представителя, реализующего запрошенный интерфейс. Такой представитель в терминах COM называется proxy и представляет собой объект, экспортирующий запрошенный интерфейс. Одновременно COM создает в «комнате» сервера объект-заглушку (stub), принимающий вызовы от proxy и транслирующий их в вызовы сервера. Таким образом, клиент в своей «комнате» может рассматривать proxy в качестве сервера и работать с ним так, как если бы сервер был создан в его «комнате». В то же время сервер может рассматривать stub как расположенного с ним в одной «комнате» клиента. Всю работу по организации взаимодействия proxy и stub берет на себя COM. При вызове со стороны клиента proxy получает от него параметры, упаковывает их во внутреннюю структуру и передает в «комнату» сервера. Stub получает параметры, распаковывает их и производит вызов метода сервера. Аналогично осуществляется передача параметров обратно. Этот процесс называется Marshalling. При этом «комнаты» клиента и сервера могут иметь разные потоковые модели и физически находиться где угодно. Разумеется, по сравнению с вызовом сервера в своей «комнате» такой вызов требует значительных накладных расходов,  однако это единственный способ обеспечить корректную работу любых клиентов и серверов. Если необходимо избежать накладных расходов, сервер надо создавать в той же «комнате», где расположен клиент.

Для обеспечения возможности корректного создания proxy в клиентской «комнате» COM должен узнать «устройство» сервера. Сделать это можно несколькими способами:

  1. Реализовать на сервере интерфейс IMarshal и, при необходимости, — proxy-DLL, которая будет загружена на клиенте для реализации proxy. Подробности реализации описаны в документации COM и MSDN.
  2. Описать интерфейс на языке IDL (Interface Definition Language) и при помощи компилятора MIDL фирмы Microsoft сгенерировать proxy-stub-DLL.
  3. Сделать сервер совместимым с OLE Automation. В этом случае COM сам создаст proxy, используя описание сервера из его библиотеки типов — специального двоичного ресурса, описывающего COM-интерфейс. При этом в интерфейсе можно использовать только типы данных, совместимые с OДУ Automation.
Инициализация COM

Каким же образом клиенты и серверы COM могут создавать «комнаты» в соответствии со своими требованиями? Для этого они должны соблюдать одно правило: каждый поток, желающий использовать COM, должен создать «комнату» при помощи вызова функции CoInitializeEx. Она объявлена в модуле ActiveX.pas следующим образом:

const
 COINIT_MULTITHREADED = 0; // OLE calls objects on any thread.
 COINIT_APARTMENTTHREADED = 2; // Apartment model

function CoInitializeEx(pvReserved: Pointer;
 coInit: Longint): HResult; stdcall;

Параметр pvReserved зарезервирован  для будущего использования и должен быть равен NIL, а параметр coInit определяет потоковую модель создаваемой комнаты. Он может принимать следующие значения:

COINIT_APARTMENTTHREADED — для потока создается STA. Каждый поток может иметь (или не иметь) свою STA;
COINIT_MULTITHREADED       — если в текущем процессе еще не создана MTA, создается новая MTA; если она уже создана другим потоком, поток «подключается» к ранее созданной. Иными словами, каждый процесс может иметь только одну MTA.

Функция возвращает S_OK в случае успешного создания «комнаты».

По завершении работы с COM (или перед завершением работы) поток должен уничтожить «комнату» при помощи вызова процедуры CoUninitialize, также описанной в модуле ActiveX:

procedure CoUninitialize; stdcall;

Каждый вызов CoInitializeEx  должен иметь соответствующий вызов CoUninitialize, то есть, используя COM в приложении, необходимо вызвать CoInitializeEx до первого использования функций COM и CoUninitialize перед завершением работы приложения. VCL реализует автоматическую инициализацию COM при использовании модуля ComObj. По умолчанию создается STA. При желании необходимость использовать другую потоковую модель следует установить флаг инициализации COM до оператора Application.Initialize:

program Project1;
 
 uses
 Forms,
 ComObj,
 ActiveX,
 Unit1 in 'Unit1.pas' {Form1};
 
 {$R *.RES}
 
 begin
 CoInitFlags := COINIT_MULTITHREADED;
 Application.Initialize;
 Application.CreateForm(TForm1, Form1);
 Application.Run;
 end.

Если COM используется в потоке, то эти функции должны быть вызваны в методе Execute:

procedure TMyThread.Execute;
 begin
 CoInitializeEx(NIL, COINIT_MULTITHREADED);
 …
 CoUninitialize
 end;

Инициализация COM необходима и для вызова любых функций Windows API, связанных с COM, за исключением CoGetMalloc, CoTaskMemAlloc, CoTaskMemFree и CoTaskMemReAlloc.

Отдельного обсуждения заслуживает инициализация потоковой модели COM для сервера, расположенного в DLL. Дело в том, что DLL может быть загружена любым потоком, который уже ранее создал свою «комнату». Поэтому сервер в DLL не может сам инициализировать требуемую ему потоковую модель. Вместо этого сервер при регистрации прописывает в реестре параметр ThreadingModel, который и указывает, в какой потоковой модели способен работать данный сервер. При создании сервера COM анализирует значение этого параметра и потоковой модели «комнаты» запросившего создание сервера потока и при необходимости создает для сервера «комнату» с требуемой потоковой моделью.

Параметр ThreadingModel может принимать следующие значения:

Apartment — сервер может работать только в STA. Если он создается из STA, то он будет создан в «комнате» вызывающего потока, если из MTA — COM автоматически создаст для него «комнату» c STA и proxy в «комнате» клиента;
Free — сервер может работать только в MTA. Если он создается из MTA, то он будет создан в «комнате» вызывающего потока, если из STA — COM автоматически создаст для него «комнату» c MTA и proxy в «комнате» клиента;
Both — сервер может работать как в STA, так и MTA. Объект всегда создается в вызывающей «комнате».

Если этот параметр не задан, сервер имеет потоковую модель Single. В этом случае он создается в Primary STA (то есть в STA потока, который первым вызвал CoInitialize), даже если создание сервера запрошено из потока, имеющего свою отдельную STA.

Получить ссылку на материал

Категория: CORBA и COM | Добавил: Барон (08.12.2011)
Просмотров: 2137 | Теги: com, delphi | Рейтинг: 0.0/0
[ Пожертвования для сайта ] [ Пожаловаться на материал ]

Если вам помог материал сайта кликните по оплаченной рекламе размещенной в центре

Поиск
Категории раздела
ActiveX [10]
CORBA и COM [16]
Kol и MCK [23]
WinAPI [28]
Компоненты [27]
Работа с Bluetooth [4]
Железо [8]
Текст [18]
Разное [98]
Королевство Delphi © 2010-2024
Яндекс цитирования