Delphi world - delphi і com

COM (Component Object Model) - модель об'єктних компонентів - одна з основних технологій, на яких грунтується Windows. Більш того, всі нові технології в Windows (Shell, Scripting, підтримка HTML і т.п.) реалізують свої API саме у вигляді COM-інтерфейсів. Таким чином, в даний час професійне програмування вимагає розуміння моделі COM і вміння з нею працювати. У цьому розділі ми розглянемо основні поняття COM і особливості їх підтримки в Delphi.

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

Інтерфейс, образно кажучи, є «контрактом» між програмістом і компілятором.

Програміст зобов'язується реалізувати всі методи, описані в інтерфейсі, і слідувати вимогам, що пред'являються до реалізації деяких з них.

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

Інтерфейси можуть успадковуватися. Спадкування інтерфейсів - це декларація, яка вказує, що успадкований інтерфейс повинен включати в себе всі методи предка.

Таким чином, необхідно розуміти наступне:

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

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

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

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

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

Базовим інтерфейсом в моделі COM є IUnknown. Будь-інтерфейс успадковується від IUnknown і зобов'язаний реалізувати оголошені в ньому методи. IUnknown оголошений в модулі System.pas наступним чином:

Розглянемо призначення методів IUnknown більш докладно.

Останні два методи призначені для реалізації механізму підрахунку посилань.

Ця функція повинна збільшити лічильник посилань на інтерфейс на одиницю і повернути нове значення лічильника.

Ця функція повинна зменшити лічильник посилань на інтерфейс на одиницю і повернути нове значення лічильника. Після досягнення лічильником нульового значення вона повинна звільнити пам'ять, зайняту реалізацією інтерфейсу.

Перший метод дозволяє отримати посилання на реалізований класом інтерфейс.

Ця функція отримує в якості вхідного параметра ідентифікатор інтерфейсу. Якщо об'єкт реалізує запитаний інтерфейс, то функція:

  1. повертає посилання на нього в параметрі Obj;
  2. викликає метод _AddRef отриманого інтерфейсу;
  3. повертає 0.

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

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

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

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

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

  1. повертає посилання на нього в параметрі Obj;
  2. викликає метод _AddRef отриманого інтерфейсу;
  3. повертає TRUE.

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

Таким чином, є можливість запросити у будь-якого класу Delphi реалізований їм інтерфейс. Детальніше використання цієї функції розглянуто нижче.

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

Клас повинен мати методи, точно відповідні по іменах і списками параметрів всім методам всіх оголошених в його заголовку інтерфейсів.

Розглянемо більш детальний приклад.

Тут клас TTest реалізує інтерфейс ITest. Розглянемо використання інтерфейсу з програми.

Оскільки даний код виглядає досить дивно, зупинимося на ньому докладніше.

По-перше, оператор присвоювання при приведенні типу даних до інтерфейсу неявно викликає метод _AddRef. При цьому кількість посилань на інтерфейс збільшується на одиницю.

Увага! Якщо у класу запитано хоча б один інтерфейс - не викликає його метод Free (або Destroy). Клас буде звільнений тоді, коли відпаде необхідність в останньому посиланню на його інтерфейси. Якщо ви до цього моменту знищили екземпляр класу вручну - відбудеться помилка доступу до пам'яті.

Так, наступний код призведе до помилки в момент виходу з функції:

Якщо ви хочете знищити реалізацію інтерфейсу негайно, не чекаючи виходу змінної за область видимості, - просто надайте їй значення NIL:

Зверніть особливу увагу, що виклики методів інтерфейсу IUnknown здійснюються Delphi неявно і автоматично. Тому не викликайте методи інтерфейсу IUnknown самостійно. Це може порушити нормальну роботу автоматичного підрахунку посилань і привести до незвільнена пам'яті або до порушень захисту пам'яті при роботі з інтерфейсами. Щоб уникнути цього необхідно просто пам'ятати наступне.

  1. При приведенні типу об'єкта до інтерфейсу викликається метод _AddRef.
  2. При виході змінної, що посилається на інтерфейс, за область видимості або при присвоєнні їй іншого значення викликається метод _Release.
  3. Одного разу запитавши у об'єкта інтерфейс, в подальшому ви не повинні звільняти об'єкт вручну. Взагалі починаючи з цього моменту краще працювати з об'єктом тільки через інтерфейсні посилання.

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

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

буде успішно компілюватися і виконуватися.

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

Розглянемо питання реалізації інтерфейсів докладніше.

Оголосимо два інтерфейси:

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

Як видно, клас не може містити відразу два методи Beep. Тому Delphi надає спосіб для вирішення конфліктів імен, дозволяючи явно вказати, який метод класу буде служити реалізацією відповідного методу інтерфейсу.

Якщо реалізація методів TTest2.Beep1 і TTest2.Beep2 ідентична, то можна не створювати два різних методу, а оголосити клас наступним чином:

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

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

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

Звертатися до отриманого класу можна точно так само, як і до будь-якого класу, який реалізує інтерфейси:

Інтерфейси і TComponent

У базовому класі VCL TComponent є повний набір методів, що дозволяють реалізувати інтерфейс IUnknown, хоча сам клас даний інтерфейс не реалізує. Це дозволяє спадкоємцям TComponent реалізовувати інтерфейси, не піклуючись про реалізацію IUnknown. Однак методи TComponent._AddRef і TComponent._Release на етапі виконання програми не реалізують механізм підрахунку посилань, і, отже, для класів-спадкоємців TComponent, що реалізують інтерфейси, не діє автоматичне керування пам'яттю. Це дозволяє запитувати у них інтерфейси, не побоюючись, що об'єкт буде видалений з пам'яті при виході змінної за область видимості. Таким чином, наступний код абсолютно коректний і безпечний:

Цей код перевіряє наявність у всіх форм в додатку можливості реалізації інтерфейсу IGetData і в разі, якщо форма реалізує цей інтерфейс, викликає його метод.

Використання інтерфейсів всередині програми

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

Як приклад розглянемо MDI-додаток, що має багато різних форм і єдину панель інструментів. Припустимо, що на цій панелі інструментів є команди «Зберегти», «Завантажити» і «Очистити», проте кожне з вікон реагує на ці команди по-різному.

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

Створимо три дочірні форми - Form2, Form3 і Form4 - і встановимо їм властивість FormStyle = fsMDIChild.

Form2 вміє виконувати всі три команди:

Form3 вміє виконувати тільки команду Clear:

І нарешті, Form4 взагалі не реалізує інтерфейс IToolBarCommands і не відгукується ні на одну команду.

На головній формі програми помістимо ActionList і створимо три компонента TAction. Крім того, розмістимо на ній TToolBar і призначимо її кнопок відповідні TAction.

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

При активізації команд перевіряється наявність активної дочірньої форми, у неї запитується інтерфейс IToolBarCommands і викликається відповідний йому метод:

Робота програми представлена ​​на малюнку.

Того ж ефекту можна домогтися і іншими методами (наприклад, успадкувавши всі дочірні форми від єдиного предка або обмінюючись з ними повідомленнями), проте ці методи мають ряд істотних недоліків.

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

Використання інтерфейсів для реалізації Plug-In

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

Як приклад реалізуємо нескладну програму, яка використовує Plug-In для завантаження даних.

Оголосимо інтерфейси модуля розширення і внутрішнього API програми.

Цей модуль повинен використовуватися як в Plug-In, так і в основній програмі і гарантує використання ними ідентичних інтерфейсів.

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

Розглянемо код модуля розширення:

Метод Init виконує два завдання: зберігає посилання на інтерфейс API головного модуля для подальшого використання і намагається відкрити файл з даними. Якщо файл відкритий успішно - виставляється внутрішній прапор InitSuccess.

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

У деструкції ми Обнуляємо посилання на API головного модуля, знищуючи його, і закриваємо файл.

Ця функція створює екземпляр класу, що реалізовує інтерфейс ILoadFilter. Посилань на екземпляр зберігати не потрібно, він буде звільнений автоматично.

Тепер отриману DLL можна використовувати з основної програми.

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

Готуємо TMemo до завантаження даних:

Отримуємо ім'я модуля з фільтром для обраного розширення файлу. Описи модулів зберігаються в файлі plugins.ini в секції Filters у вигляді рядків формату:

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

Тепер спробуємо завантажити модуль і знайти в ньому функцію CreateFilter:

Функція знайдена, створюємо екземпляр фільтра і инициализируем його. Оскільки внутрішній API реалізований теж як інтерфейс - немає необхідності зберігати посилання на нього.

Завантажуємо дані за допомогою створеного фільтра:

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

Вивантажуємо DLL і оновлюємо TMemo:

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

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

Увага! Оскільки в EXE і DLL використовуються довгі рядки, не забудьте включити в список uses обох проектів модуль ShareMem. Іншим варіантом вирішення проблеми передачі рядків є використання типу даних WideString. Для них розподілом пам'яті займається OLE, причому робить це незалежно від модуля, з якого була створена рядок.