Delphi і 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;
Ця функція отримує в якості вхідного параметра ідентифікатор інтерфейсу. Якщо об'єкт реалізує запитаний інтерфейс, то функція:



a) повертає посилання на нього в параметрі Obj;
b) викликає метод _AddRef отриманого інтерфейсу;
c) повертає 0.


В іншому випадку – функція повертає код помилки E_NOINTERFACE.


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


У модулі System.pas оголошений клас TInterfacedObject, який реалізує IUnknown і його методи. Рекомендується використовувати цей клас для створення реалізацій своїх інтерфейсів.


Крім того, підтримка інтерфейсів реалізована в базовому класі TObject. Він має метод



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


Якщо клас реалізує запитаний інтерфейс, то функція:



a) повертає посилання на нього в параметрі Obj;
b) викликає метод _AddRef отриманого інтерфейсу;
c) повертає 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_ROOTCLSID.



Для кожного сервера прописується інформація, необхідна для знаходження і завантаження його модуля. Таким чином, клієнтське додаток не повинно турбуватися про пошук сервера: достатньо зареєструвати його на комп'ютері – і 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.


Активація сервера


Для активації COM-сервера клієнт повинен викликати функцію CreateComObject, описану в модулі ComObj.pas:



function CreateComObject(const ClassID: TGUID): IUnknown;


Функція отримує як параметр CLSID необхідного об'єкту і повертає посилання на його інтерфейс IUnknown. Далі клієнт може запитати потрібний інтерфейс і працювати з ним:



var
     COMServer: IComServer;


/ / Створюємо COM-об'єкт і запитуємо у нього інтерфейс
     ComServer := CreateComObject(IComServer) as IComServer;
/ / Працюємо з інтерфейсом
     ComServer.DoSomething;
/ / Звільняємо інтерфейс
     ComServer := NIL;


Що ж робить COM при запиті на створення сервера?



  1. У реєстрі по запрошенням CLSID ведеться пошук запису реєстрації сервера.
  2. З цього запису виходить ім'я исполнимого модуля сервера.
  3. Якщо це здійснимий файл, то він запускається на виконання. Додаток, що реалізує COM-сервер, при старті реєструє в системі інтерфейс «фабрики об'єктів». Після запуску та реєстрації COM отримує посилання на «фабрику об'єктів».
  4. Якщо це DLL, то вона завантажується в адресний простір викликав процесу і викликається її функція DllGetClassObject, що повертає посилання на реалізовану в DLL «фабрику об'єктів».
  5. Фабрика об'єктів – це COM-сервер, який реалізує інтерфейс IClassFactory. Ключовим методом цього інтерфейсу є метод CreateInstance, який і створює екземпляр необхідного об'єкта.
  6. COM викликає метод CreateInstance і передає отриманий інтерфейс клієнта.

По завершенні роботи з COM-об'єктом клієнт звільняє посилання на нього (що призводить до виклику методу Release). У цей момент COM-сервер перевіряє, чи є ще посилання на створені їм об'єкти. Якщо всі об'єкти звільнені, то COM-сервер завершує свою роботу. У разі якщо він реалізований у вигляді DLL, він повинен експортувати функцію DllCanUnloadNow, яка викликається COM по таймеру або при виконанні функції API CoFreeUnusedLibraries. Якщо всі об'єкти з цієї DLL звільнені, вона вивантажується з пам'яті.


Вся робота по створенню та реєстрації «фабрики об'єктів» та експорту відповідних функцій з DLL в Delphi вже реалізована в складі стандартних бібліотек, і створення COM-сервера насправді є дуже простим завданням.


Створення COM-сервера


Для створення COM-сервера Delphi надає широкий набір майстрів, автоматизують виконання рутинних завдань і дозволяють програмісту сконцентруватися на реалізації функціональності. Майстри доступні за допомогою команди меню File-> New, на закладці ActiveX.



Щоб зробити COM-сервером EXE-файл, необхідно просто додати до нього модуль з COM-об'єктом. Для створення COM-сервера у вигляді DLL потрібно спочатку створити бібліотеку, оформлену з урахуванням вимог COM. Це робиться за допомогою майстра ActiveX Library. При його виборі буде створено новий проект, який реалізує DLL, і згенерований наступний код:



library Project1;

     uses
          ComServ;

     exports
          DllGetClassObject,
          DllCanUnloadNow,
          DllRegisterServer,
          DllUnregisterServer;

     {$R *.RES}

     begin
     end.


Створена DLL експортує функції, необхідні для роботи COM, при цьому можна не відволікатися на рутинну роботу і відразу приступити до реалізації COM-сервера.


Для цього виберіть майстер «COM-об'єкт».



Від заповнення полів цієї форми залежить реалізація створюваного COM-об'єкту:


























ClassName

Ім'я класу Delphi, що реалізує COM-сервер.
Майстер створить заготівлю класу з цим ім'ям,
під яким COM-сервер також буде зареєстрований в реєстрі.

Instansing


Визначає режим створення COM-об'єктів. Параметр може приймати такі значення:












Internal  – Об'єкт може використовуватися тільки усередині цього додатка;
Single Instance – Створення кожного примірника об'єкта призводить до запуску нового екземпляра додатку-сервера. Після створення об'єкту «фабрика об'єктів» програми видаляє інформацію про себе з системного списку зареєстрованих «Фабрик», що змушує COM при створенні нового об'єкта запустити додаток-сервер в новому процесі;
Multiple 
Instance
– Після створення екземпляра об'єкту «фабрика» не видаляє себе зі списку зареєстрованих.
При створенні запиті на створення нового об'єкта COM виявить її в цьому списку і запросить створення у тій же «фабрики» – при цьому новий екземпляр об'єкта буде створений у тому ж додатку. Іншими словами, для створення всіх об'єктів даного типу буде запущено не більше одного примірника сервера.

Цей параметр має сенс тільки для EXE-серверів, для DLL він ігнорується.


Threading Model


Потокова модель сервера. Дія цього параметра залежить від типу сервера (EXE або DLL):















Single – Немає ніякої підтримки потоків. Для DLL-сервера при реєстрації не буде створений параметр ThreadingModel. Для EXE-сервера вказівку цього параметра (на відміну від будь-якого іншого) не призведе до встановлення прапора IsMultiThread, а буде створена STA. Зазвичай ця модель використовується для Internal-серверів;
Apartment – Для DLL-сервера в реєстрі буде створено параметр ThreadingModel = Apartment, для EXE – створена STA;
Free – Для DLL-сервера в реєстрі буде створено параметр ThreadingModel = Free, для EXE – створена MTA;
Both – Для DLL-сервера в реєстрі буде створено параметр ThreadingModel = Both, для EXE – створена MTA.
 

Include Type Library


Установка цього прапорця призводить до включення в сервер бібліотеки типів – спеціального двійкового ресурсу, що описує реалізовані сервером інтерфейси, їх методи і параметри виклику. COM надає стандартні засоби роботи з бібліотеками типів. Зокрема, Delphi може імпортувати наявну в сервері бібліотеку типів і автоматично побудувати по ній інтерфейсний модуль для роботи з ним. При використанні бібліотеки типів інтерфейси описуються за допомогою Type Library Editor. Об'єкт успадковується від TTypedComObject Якщо цей прапорець вимкнений, то об'єкт успадковується від TComObject. Це більш «легка» реалізація сервера.

Description


Примітка до об'єкта.

Implemented Interfaces


Це поле дозволено, тільки якщо об'єкт не використовує бібліотеку типів. У такому випадку ви повинні самі описати інтерфейси в коді своєї програми і перерахувати їх у цьому полі, наприклад «ITest, IAnotherTest».


Mark interface Oleautomation


Установка цього прапора робить COM-сервер сумісним з OLE Automation. Ви повинні використовувати в методах інтерфейсу лише сумісні з OLE Automation типи даних. Це необхідно, якщо ви хочете передавати посилання на інтерфейс між різними «кімнатами». Така операція, яка називається маршалинга інтерфейсів, вимагає написання спеціальної proxy / stub-DLL. Однак якщо інтерфейс позначений як OleAutomation, то цю роботу виконає маршалер OLE, що позбавить вас від зайвої роботи.


Для підтримки OleAutomation-маршалинга необхідно:



  • щоб сервер був успадкований від TTypedComObject (реалізація IDispatch не обов'язкова);
  • всі методи інтерфейсу були оголошені як safecall. Якщо ви створюєте інтерфейс, успадкований від IUnknown, то за замовчуванням всі його методи оголошуються як stdcall. Щоб створити safecall-методи, необхідно в діалозі Tools> Environment Options на закладці Type Library встановити перемикач Safecall function mapping в значення All v-table interfaces.

Сервер без бібліотеки типів


Такий сервер, якщо він не реалізує інтерфейс IMarshall, може працювати лише в одній «кімнаті» з клієнтом, тому його варто використовувати тільки для In-Process-серверів з потокової моделлю, ідентичною клієнту.


При створенні сервера, не включає бібліотеку типів, необхідно вказати майстру реалізовані їм інтерфейси. Зазначимо ім'я інтерфейсу ITest. По завершенні роботи майстра буде створено наступний модуль:



unit Unit1;

     interface

     uses
               Windows, ActiveX, Classes, ComObj;

     type
               TTest = class(TComObject, ITest)
               protected
               end;

     const
Class_Test: TGUID = "{1302FB00-703F-11D4-84DD-825B45DBA617}";

     implementation

     uses ComServ;

     initialization
          TComObjectFactory.Create(ComServer, TTest, Class_Test,
               “Test”, “”, ciMultiInstance, tmApartment);
     end.


Увага! COM-сервер, який може використовуватися різними клієнтами (а не тільки в рамках конкретного проекту, в якому специфікації клієнтів жорстко задані), не рекомендується створювати без підтримки маршалинга даних, оскільки в цьому випадку неможливо забезпечити гарантоване знаходження її в одній «кімнаті» з клієнтом. Якщо ви все ж робите такий сервер, в документації на нього необхідно відобразити необхідні специфікації клієнта.


Розглянемо згенерований код докладніше. Особливий інтерес представляє секція Initialization. У ній створюється екземпляр «фабрики об'єктів» – COM-сервера, що реалізує інтерфейс IClassFactory2. До нього COM буде звертатися для створення екземпляра об'єкта Test. VCL автоматично виконує всю рутинну роботу по взаємодії з COM.


Для реалізації сервера потрібно написати інтерфейсний модуль з описом реалізованого інтерфейсу. Крім того, винесемо в нього опис константи Class_Test і додамо його в рядок uses модуля Unit1:



unit TestInterface;

     interface

     const
Class_Test: TGUID = "{1302FB00-703F-11D4-84DD-825B45DBA617}";

     type
          ITest = interface
          [“{1C986802-6D6D-11D4-84DD-996A491CE716}”]
               procedure ShowIt(S: String);
     end;

  implementation

  end.


Цей модуль містить всю необхідну інформацію для роботи сервера і повинен використовуватися при компіляції клієнта.


Доповнимо код COM-об'єкта реалізацією методів реалізованого інтерфейсу:



unit Unit1;

          interface

          uses
               Windows, ActiveX, Classes, ComObj, TestInterface;

          type
               TTest = class(TComObject, ITest)
               protected
                    procedure ShowIt(S: String);
               end;

          implementation

          uses ComServ;

          { TTest }

     procedure TTest.ShowIt(S: String);
     begin
          MessageBox(0, PChar(S), NIL, 0);
     end;

     initialization
          TComObjectFactory.Create(ComServer, TTest, Class_Test,
               “Test”, “”, ciMultiInstance, tmApartment);
          end.


Откомпилирован проект, ми отримаємо файл Project1.dll.


Останнім кроком є ​​реєстрація COM-сервера.


Введемо в командному рядку «regsvr32 project1.dll».


Якщо все було зроблено правильно, на екрані з'явиться повідомлення про успішну реєстрацію: «DllRegisterServer in Project1.dll succeeded».


Тепер можна приступати до написання клієнта. Для цього створимо новий проект, додамо в модуль з його головною формою рядок uses TestInterface і напишемо наступний код:



uses TestInterface, ComObj;

     procedure TForm1.Button1Click(Sender: TObject);
     var
               Test: ITest;
     begin
               Test := CreateComObject(Class_Test) as ITest;
               Test.ShowIt(“Hi”);
     end;


Як видно з цього прикладу, створення і використання COM-сервера не складніше, ніж робота із звичайними класами Delphi. Сервер без бібліотеки типів є хорошим вибором для реалізації COM-серверів, використовуваних всередині проекту, оскільки для його роботи потрібен інтерфейсний модуль. При передачі сервера іншим розробникам вам доведеться передати їм цей модуль і при необхідності перевести його на іншу мову (наприклад, С).


Сервер з бібліотекою типів


Бібліотека типів – це спеціальний двійковий ресурс, що описує інтерфейси і методи, реалізовані COM-сервером. Окрім наявності бібліотеки типів сервер повинен підтримувати інтерфейс IProvideClassInfo. У Delphi такий сервер реалізується шляхом успадкування його від TTypedComObject. Для цього залиште прапорець Include Type Library в майстрі створення COM-об'єкта включеним.


Створимо COM-сервер у вигляді EXE (зрозуміло, він може бути також створений і вигляді DLL).


Спочатку створимо новий проект – File-New Application, а потім додамо в нього COM-об'єкт.


Якщо не відключати прапорець Include Type Library, то майстер створить вже не один, а два модулі. Перший з них нагадує створений раніше.



unit Unit1;

     interface

     uses
Windows, ActiveX, Classes, ComObj, Project1_TLB, StdVcl;

     type
          TTest1 = class(TTypedComObject, ITest1)
          protected
               {Declare ITest1 methods here}
          end;

     implementation

     uses ComServ;

     initialization
TTypedComObjectFactory.Create (ComServer, TTest1, Class_Test1,
               ciMultiInstance, tmApartment);
     end.


Найбільш цікава рядок: uses … Project1_TLB. Це автоматично згенерований інтерфейсний модуль до нашого COM-об'єкту (аналогічно TestInterface.pas в попередньому прикладі). Він містить опис всіх необхідних для роботи з сервером інтерфейсів. На відміну від попереднього прикладу, вам не доведеться редагувати його вручну. Для цього Delphi відкриє редактор бібліотеки типів:



Це спеціалізований редактор для опису інтерфейсів COM-об'єктів. Ви повинні описати всі необхідні інтерфейси, методи і т.п. в цьому редакторі, після чого можна натиснути кнопку «Оновити» – і зміни будуть автоматично внесені в усі необхідні модулі. Вам залишиться лише дописати реалізацію методів.


Додамо опис нового методу. Для цього клацнемо правою кнопкою мишки на інтерфейсі ITest і виберемо з контекстного меню опцію New-> Method. Введемо ім'я методу – ShowIt.



На закладці Parameters задамо параметр S і тип BSTR. Після цього натиснемо кнопку «оновити» і подивимося, що сталося з вихідними текстами нашої програми. У модулі Project1_TLB в описі інтерфейсу ITest1 з'явився метод ShowIt:



ITest1 = interface(IUnknown)
     [“{1302FB06-703F-11D4-84DD-825B45DBA617}”]
     function ShowIt(const S: WideString): HResult; stdcall;
end;


А в модулі Unit1:



type
     TTest1 = class(TTypedComObject, ITest1)
          protected
function ShowIt (const S: WideString): HResult; stdcall;
     end;

implementation

uses ComServ;

function TTest1.ShowIt(const S: WideString): HResult;
begin

end;


Нам залишається лише написати реалізацію методу:



function TTest1.ShowIt(const S: WideString): HResult;
     begin
          MessageBoxW(0, PWideChar(S), NIL, 0)
Result: = S_OK; / / Стандартний код успішного завершення
     end;


Для реєстрації сервера достатньо один раз запустити його на комп'ютері клієнта.


Перейдемо до написання програми-клієнта. При наявності модуля Project_TLB воно нічим не буде відрізнятися від попереднього прикладу. Більш цікавий випадок, коли ми маємо тільки исп

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*