Підкачуємі модулі (plugins) в delphi

Мелкософта на дві фірми розпиляли. Тепер одна робитиме продукти, а друга буде робити для них патчі.

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

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

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

У той же час мене часто запитували, яким чином можна створити зручний механізм plugin'ов і я описував свій метод. Метод, запропонований мною, заснований на використанні механізму, за допомогою якого сама Delphi IDE - пакети (packages).

Проблема (недоліки DLL-plugin'ов)

  • Всі використовувані модулі компілюються в DLL.

Уявіть, що вам треба зробити модуль, який виводить форму з настройками. Як тільки ви впишете в DLL вираз uses Forms. модуль Forms, а також всі модулі, що використовуються модулем Forms будуть прілінкованние до вашої DLL, що катастрофічно збільшить її розмір. Уявіть тепер, що вам потрібно підключати кілька plugin'ов, кожен з яких буде надавати форму або вкладку для редагування параметрів. Як писав класик, трагічна картина.

Попередній недолік є кількісним, тобто просто збільшує розмір проекту. Але з нього випливає якісний недолік. Розглянемо його на прикладі. Нехай вам треба створити підкачуємі разборщікі пакетів. Ви визначаєте абстрактний клас TParser в модулі UParser і хочете, щоб все разборщікі успадковували від нього. Але для того, щоб ви могли описати в DLL нащадок від TParser, ви повинні включити модуль UParser в список uses для DLL. А для того, щоб основна програма могла звертатися з TParser і його нащадками, в неї також потрібно включити uses UParses. Неприємність полягає в тому, що ці модулі будуть знаходитися в пам'яті двічі і той TParser, про який знає основна програма не збігається з тим, який знає plugin.

Завдання (чого б нам хотілося)

Все просто. Нам би хотілося, щоб основна програма могла без особливих хитрувань працювати із зовнішніми модулями як з нащадками деякого абстрактного класу і при цьому б не було надмірності коду. При цьому бажано, щоб зміни в основну програму вносити доводилося якомога рідше, навіть при дуже розвиненою функціональності plugin'а.

Засіб (пакети і функції для роботи з ними)

Пакети з'явилися в третій версії Delphi. Що таке пакет? Пакет - це набір компілювати модулів, об'єднаних в один файл. Оригінальний текст пакета, який зберігає я в файлах .dpk містить тільки вказівки на те, які модулі містить (contains) цей пакет (тут "містить" означає також "надає") і на каіе інші пакети він посилається (requires). При комптляціі пакета виходить два файли - * .dcp і * .dpl. Перший використовується просто як бібліотека модулів. Нам же більше цікавий другий.

Основною особливістю пакетів є те, що не включають в себе код, яким користуються. Тобто якщо деякі модулі використовують велику бібліотеку функцій і класів, то можна вимагати їх наявності, але не включати в пакет. Ви запитаєте, що ж тут нового, адже звичайні модулі теж не включають в .dcu-файл весь використовуваний код? А нового тут те, що dpl-пакет є повноправною DLL спеціального формату (тобто з обумовленими розробниками Delphi іменами експортованих процедур). При завантаженні пакета в пам'ять автоматично встановлюються зв'язки з уже завантаженими пакетами, а якщо завантаження пакет вимагає наявності ще якихось пакетів, то завантажуються і вони. Крім того, на відміну від звичайних модулів, програма, яка використовує модулі з зовнішнього пакета теж не повинна мати його код. Таким чином, можна писати EXE-програми розмірів в кілька десятків кілобайт (природно, буде вимагатися наявність на диску відповідного пакета, який потім підвантажиться).

Функції для роботи з пакетами зосереджені в модулі SysUtils. Нас будуть цікавити наступні з них:

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

- вивантажує заданий пакет з пам'яті.

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

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

  1. На відміну від dll-plugin'ов, ви прив'язується до Delphi і C ++ Builder'у (або це плюс.)).
  2. Звичайно, існують деякі накладні витрати на забезпечення інтерфейсу пакета - найменший пакет має не нульову довжину. Крім того, розумний линкер Delphi не зможе викинути зайві процедури з спільно використовуваних пакетів - адже будь-який метод може бути затребуваний пізніше, якимось іншим зовнішнім пакетом. Тому можливе збільшення розміру сумарного коду програми. Це збільшення практично не помітно, якщо розділяється пакет містить тільки інтерфейс для plugin'а і істотно більше, якщо необхідно розділити стандартні пакети VCL. Втім, це легко окупається, якщо plugin'ов багато. Крім того, стандартні пакети можуть використовуватися різними програмами.
  3. Можливо найістотніший недолік, що випливає з попереднього. Пакет неподільний, тому що невідомо, які його процедури знадобляться, тому він вантажиться в пам'ять цілком. Навіть якщо ви використовуєте одну єдину функцію з пакета, що не викликає інші і не посилається на інші ресурси пакета, пакет вантажиться в пам'ять цілком. Це, знову ж таки, не дуже помітно, якщо в пакеті тільки голий інтерфейс з невеликою кількістю процедур. Але якщо це кілька стандартних пакетів VCL, то яку займає програмою пам'ять може збільшитися дуже суттєво (на кілька мегабайт). Втім, це знову окупається, якщо ви використовуєте велику кількість plugin'ов - якби вони були оформлені у вигляді dll, то кожна з них містила б пристойну частину стандартних модулів і вони трималися б в пам'яті одночасно. Фактично, пропонований метод є більш масштабованим, тобто витрати починають знижуватися при збільшенні кількості plugin'ов.

Метод (що робимо, і що отримаємо)

Предлагемая структура побудови піложенія виглядає наступним чином: Виконуємо код = Основна програма + Інтерфейс plugin'ов + plugin. Всі три перераховані компоненти повинні знаходитися в різних файлах (програма - у EXE, решта - в пакетах BPL). Програма вміє завантажувати пакети в пам'ять і звертатися до підвантаженим нащадкам абстрактного plugin'а. Plugin є нащадок абстрактного класу, оголошеного в интерфейсном модулі. Програма та plugin використовують модуль інтерфейсу, але він знаходиться в окремому пакеті і в пам'яті буде присутній в єдиному екземпляр.

Залишився єдине питання - як основна програма отримає посилання на об'єкти або на класи (class references) потрібного типу? Для цього в интерфейсном модулі зберігається диспетчер plugin'ов або, в простому випадку, просто TList, в який кожен модуль заносить стали доступними класи. У більш розвиненому випадку диспетчер класів може забезпечувати пошук підвантаженими класів, які є нащадками заданого, пріоритети при завантаженні, і.т.д.

Ясно, що ми досягли поставленої мети - надмірності коду немає (за умови, що всі бібліотеки, в тому числі і стандартні бібліотеки VCL, використовуються у вигляді пакетів), написання plugin'а спрощено до краю. Чого можна домогтися ще?

А можна домогтися ще більш цікавою речі. Якщо ми всю основну програму помістимо в пакет, а EXE-файл буде включати в себе тільки процедуру створення та відкриття основної форми, то зовнішній plugin може отримати повний доступ до всіх модулів програми, в тому числі і до головної формі. Таким чином ми можемо написати plugin, який самостійно, без будь-яких зусиль з боку головного програми, помістить свій пункт в головне меню і кнопку на панель інструментів, за командою яких буде викликатися зовнішній код. Це те, заради чого варто задуматися над використанням запропонованого методу - поклавши в певний каталог маленький (в ньому тільки ваш код) plugin, ви додаєте до програми чергову можливість, не перекомпіліруя основної програми.

Підкачуємі модулі (plugins) в Delphi: Приклад 1

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

Ми створимо один зумовлений клас, що експортує рядки в текствий файл і один зовнішній plugin, що містить клас, який вміє експортувати рядки. ну, скажімо, в HTML. Експорт в Excel або в БД виведе нас за тонку межу прикладу.

Отже, розглянемо визначення абстрактного класу:

Я сподіваюся, ніхто не дорікне мені за надмірне ускладнення прикладу :). А тих, хто, прочитавши цей шматочок коду, закричить гучним голосом "Це можна було зробити і в dll!" я відсилаю до роздумів про розміри dll. Адже нащадки TExporter в методі BeginExport запросто можуть виводити форму налаштування експорту.

Наступним номером нашої програми буде менеджер завантажених класів. Як я і говорив, це може бути просто TList:

У цьому коді, по моєму, пояснювати нічого.

Експорт в простий текстовий файл

Тепер напишемо стандартний нащадок від TExporter, що забезпечує виведення рядків в звичайний текстовий файл.

Ми вважаємо, що коррестность виклику методів BeginExport і EndExport забезпечить головний програма і не замислюємося про можливі неприємності з відкритим файлом. Крім того, слід зазначити, що використовується модуль Dialogs, який використовує Forms і т.п. І нарешті, зверніть увагу на розділи initialization і finalization модуля - ми використовуємо можливість Delphi посилатися на клас, як на об'єкт.

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

Ця процедура переглядає список зареєстрованих класів (мається на увазі, що там тільки нащадки TExporter) і виводить їх "читабельні" імена в ListBox.

Ця процедура завантажує пакет з "зашитим" ім'ям (ну це ж просто приклад :)) і запам'ятовує його handle. Після чого відбувається оновлення списку, щоб ви могли переконатися, що новий клас зареєструвався.

Ну тут, я думаю, все ясно.

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

Розгорніть архів в якийсь каталог (наприклад c: \ bebebe :)) і відкрийте групу проектів Demo1ProjectGroup.bpg. Використання групи корисно, так як вам часто доведеться перемикатися між основною програмою і двома пакетами - це різні проекти. Я сподіваюся, що якщо ви натиснете "Build All Projects" то все успішно скомпілітся.

Подивившись на опції головного проекту, ви побачите, що на сторінці Packages зазначено, які з використовуваних пакетів не прілінковивают до exe-файлу. Слід зазначити, що навіть якщо ви дозволите туди тільки PluginInterfaceProject, то автоматом будуть вважатися зовнішніми і все, він використовував пакети - в нашому випадку Vcl5.dpl. Зате якщо ви покладете на основну форму якийсь компонент роботи з BDE, то пакет VclDB5.bpl може бути прікомпілірован (з оптимізацією, природно) до EXE-файлу.

Що ще можна сказати? Мабуть, варто відзначити, що "метушня" з пакетами нерідко буває втомлює і чревата "незрозумілими помилками" аж до зависання Delphi. Однак всі вони в підсумку виявляються наслідком недбалості розробника - адже зв'язування на етапі виконання це не проста штука. Тому стежте, куди ви компілюєте пакети, стежте за своєчасною перекомпиляцией plugin'ов, якщо змінився абстрактний клас, стежте, щоб у вас на машині не валялося 10 копій dpl-пакета, тому як ви можете думати, що програма завантажить лежить там-то і помилитеся.