Інтеграція зовнішньої об'єктної системи в Delphi на прикладі IBM SOM

SOM Technology: Making the Pieces Fit4 року назад вийшла моя стаття про IBM SOM, де я констатував вкрай плачевну ситуацію, коли загублений значущий інструментарій, і чим далі, тим менше шансів відновити. За минулий час багато чого сталося, знайшлися і SOM 3.0 для Windows, і SOM 2.1, і відкритий клон somFree, і робочий компілятор DirectToSOM C++ для Windows, і міст в OLE Automation.

Один з моїх проектів реалізує підтримку SOM в Delphi. Розробка починалася на Delphi, довелося частину прив'язок робити вручну і не так красиво, в процедурному стилі, без перевірки типів. Використовуючи ці прив'язки, був написаний генератор прив'язок в об'єктному стилі, а потім і сам генератор був переписаний на нові прив'язки, ставши підтвердженням їх працездатності. Заради краси довелося хакнуть об'єктну систему Delphi, і, може бути, вам буде цікаво, як це взагалі можна робити.

Некрасиві («тонкі») прив'язки хоча б як-то дозволяють взаємодії з бібліотекою, а красиві («товсті») прагнуть зробити так, щоб це було природно з точки зору звичайного коду. IBM SOM (Модель системних об'єктів) — про об'єкти, а об'єкти методи викликаються через точку. Мені відомі наступні сутності в Delphi, у яких можна викликати методи в об'єктному стилі:

  • Об'єкти
  • Об'єкти зі старої об'єктної системи Borland Pascal with Objects
  • Класи
  • Інтерфейси
  • Диспинтерфейсы
  • Варіанти
  • Починаючи з Delphi 2006, запису
  • Починаючи з Delphi XE3, що завгодно, до чого видно помічник
Розробляю я зазвичай на Delphi або Ada, з перевагою до другого, і мій інтерес до технологій сполучення різнорідних компонент починався з того, що я намагався подружити ці дві мови різними способами. У питанні розробки прив'язок початок підтримки SOM саме з Delphi, а не Ada, пов'язано з тим, що саме на Delphi це складніше зробити. А всередині Delphi найскладніше — це зробити блок type, і саме з нього починалася розробка засоби імпорту. Перемогти type — значить, вирішити основні проблеми.

В принципі, це добре, що в Delphi є модульність. Це краще, ніж хаос в C++ або, особливо, C, яким користуються поставляються разом з SOM DTK емітери. А там, де їм не вистачає звичайного нестабільності, доливаються тонни макросів. Але при цьому у схожому на Delphi мові Ada з'являлися «with private», «type X;», «with limited», що дозволяють підключати обмежену інформацію про типах і тим самим мати і модульність, і свободу робити циклічні зв'язки між модулями, і тим більше робити цикли в межах одного package, а в Delphi розвиток у цьому напрямі рухалася слабо. Що ще гірше, основний компілятор комерційний (в кращому випадку одне замовлення на FPC проти 6 на Delphi, а Пеклі доводиться впарювати, сама не попадається, кудись валити заради Ади теж не хочеться), тому користуються старими версіями, досі актуальна Delphi 7, так що якщо мені хочеться написати бібліотеку на улюбленій Пекло і виставити її, щоб користувалися з Delphi, бажано, щоб це могла бути Delphi 7, так що останні 2 опції відкидаються. У варіантів немає контролю типів, не спливає список методів в підказці, ця опція теж відмітається.

Спочатку я хотів зробити RAII, щоб пам'ять управлялася автоматично, як у рядків і інтерфейсів, а для цього потрібно загорнути інтерфейс або варіант запис Delphi 2006 або старий об'єкт. У цього підходу є істотний недолік. За правилами Delphi вперед можна оголошувати лише класи, інтерфейси і покажчики, причому, в неподільному блоці type. І ось тепер, припустимо, ми робимо прив'язки до класу Container і класу Contained, і вони взаємопов'язані. Добре б написати Container = record private FSomething: Variant; end; і аналогічно для Contained, а потім уточнювати, які у них є методи, тому що методи можуть приймати посилання на класи SOM, спроектовані в запису, а якщо методи можна написати тільки всередині record, то одна із записів буде явно ще не оголошена.

Значить, і всю запис не вийде оголосити через аргументів методу. Частково ситуацію може врятувати, якщо зробити спочатку для кожного класу SOM object зі старої системи об'єктів з прихованим полем, але без методів, а потім ще раз отнаследовать, і в кожному методі на вхід приймати неотнаследованную версію без методів, до якої будуть автоматом наводитися отнаследованные з методами. А ось з результатом буде проблема, там, навпаки, з-за обмежень Delphi метод SOM класу, прив'язки для якого створюються раніше іншого, не зможе повернути в якості результату отнаследованную версію, порослу методами повертається SOM класу.

Container і Contained знаходяться в дещо більш складних відносинах, вони повертають не один одного, а sequence (корбовский аналог динамічного масиву) з один одного, значить, і sequence знадобилося б проектувати саме для неотнаследованных, не оброслих методами об'єктів. І тільки в Delphi XE3 з'явився трохи менше костыльный спосіб розділити оголошення структури і методів. Спочатку загортаємо інтерфейс або варіант у приватну частина запису, і так для кожного проектованого класу, а потім навішуємо методи за допомогою помічників. І ці методи помічників вже благополучно можуть приймати на вхід і вихід все, що потрібно.

Розбираючись з управлінням пам'яті в SOM, робити RAII я перехотів. Справа в тому, що в IBM версії SOM немає лічильника посилань для всіх об'єктів, як це COM і сучасному Objective-C, і що накажете робити, якщо посилання на об'єкт копіюють? В Apple SOM, до речі, було, а звідти перейшло в somFree, так що не безнадійно, але у мене при цьому в розпорядженні DirectToSOM C++ компілятор і міст в OLE Automation, з якими я б хотів, щоб моє рішення було на даний момент сумісно, і вони не розраховані на такий режим роботи.

З інтерфейсами і звичайними класами-обгортками виникають проблеми, аналогічні «і що накажете робити, якщо посилання на об'єкт копіюють», тільки у разі знищення. Адже обгортка може транзитивно знищити SOM об'єкт, а може — ні. Це як мінімум у обгорток потрібно прапор володіння робити. І для повного щастя ще й заплутатися в цьому всьому. От був би лічильник посилань, ми б його завжди смикали і не рефлексували, і не плуталися. Все б працювало як годинник. Добре б жили.

Якщо ж чіпати стару об'єктну систему, починають сипатися попередження. Ось так я прийшов до вирішення хакнуть об'єктну систему Delphi. Мій генератор проектує SOM класи в Delphi класи зі звичайними методами, і все це використовується приблизно звичним для Delphi розробника чином. Для класів чудово робиться відкладене оголошення, загортати їх потім ні за що не потрібно. Оскільки всі цикли потрібно замкнути в одному блоці type, всі модулі CORBA і всі типи, вкладені в класи, доводиться проектувати в один unit Delphi, щоб у цьому unit був єдиний блок type.

Починалося з того, що я вирішив змусити Delphi вважати SOM об'єкти об'єктами Delphi. До тих пір, поки Delphi не чіпає VMT, все добре. Кожен метод SOM класу проектується на метод Delphi класу. При цьому методи SOM, як правило, віртуальні, а проектуються на невиртуальные методи Delphi, які знають, як відправити виклик в SOM. Невиртуальные методи викликаються, не торкаючись до VMT, і Delphi невтямки, що там взагалі далеко не Delphi об'єкти обробляються.


Методи в SOM викликати нескладно. Тут ви бачите 8 інструкцій (домашнє завдання — спробувати зрозуміти, що вони роблять), з них для власне виклику досить двох. Перед останнім call два рази робиться mov/push, це передача аргументів, а не виклик. Перед цим записувати адресу в var_14, а потім по ньому викликати не обов'язково, можна було в першій інструкції записати адресу в edx і в кінці зробити виклик [edx + 1Ch]. Ще мінус 2, разом 2 інструкції на виклик методу SOM об'єкта подібно 2м інструкцій на виклик віртуальних методів з VMT в інших системах розробки. За отриманим адресою знаходиться динамічно створений фрагмент коду, який знає, як краще всього викликати зазначений метод, і він дасть додаткові «приховані» інструкції, зате яка відразу різниця! Про цю різницю можна прочитати в перекладі доповіді «Release to Release Binary Compatibility». Якщо вам коли-небудь хотілося зрозуміти, чому під кожну версію Delphi свої набори dcu і bpl, тепер ви знаєте.

Повернемося до генерації прив'язок. Спадкування в SOM множинне, і це активно використовується. Ось, наприклад, при розробці генератора я працюю з OperationDef (метаінформація для про метод), і він одночасно Contained всередині класу і Container своїх аргументів. А в Delphi класах — одиночне. Теоретично, можна в проекції на Delphi робити одиночне спадкування для перших батьків, а методи непервых додавати потім, як ніби вони заново з'явилися. Мені це здалося негарним, несиметричним рішенням. Адже OperationDef (а також ModuleDef, InterfaceDef) в рівній мірі і Container, і Contained, і я одразу навіть не згадаю, в якому порядку у них оголошені батьківські класи. Плюс, якщо дозволяти ієрархії класів Delphi розростатися, потім виникають інші проблеми, про це — в абзаці після наступного. Так що я спроецировал SOM класи так, що вони один одному в Delphi не є батьками. Вони всі походять від службового класу SOMObjectBase, який потрібен, щоб заховати методи TObject, а методи у них кожен раз заповнюються з нуля. Операції «as» і «is», ясна річ, не підтримуються, адже вони візьмуть VMT об'єкта SOM і будуть думати, що це VMT об'єкта Delphi. Їх, на жаль, не вдається заблокувати, але щоб все було хоч якось типизировано, для кожного батьківського класу генеруються функції As_ИмяКласса для приведення типу вгору і класові функції Supports для приведення типу вниз.

Уважних читачів повинно було напружити словосполучення «класова функція», адже раніше було написано «до тих пір, поки Delphi не чіпає VMT, все добре». Сховали класові методи TObject і виставили свої? Адже так можна з розкривного списку вибрати Supports у звичайного об'єкта, і Delphi полізе в VMT. Виявляється, це можна елегантно обробити. Нові класові методи теж є невіртуальними, і все, що Delphi робить, щоб їх викликати у об'єкта, — це бере покажчик за нульового зміщення об'єкта, і це стає Self у класовому методі. А якщо викликати класовий метод у класу, звернувшись до нього по імені, то Self — це VMT Delphi класу. І як відрізнити VMT SOM класу від VMT Delphi класу?

А ось є один спосіб. Всі класові методи щоразу заново додаються в черговий Delphi клас, від якого в нормі ніхто не успадковується засобами Delphi, і самі один від одного проектоване класи засобами Delphi не успадковуються. Таким чином, у нас може бути тільки один можливий варіант VMT Delphi класу в Self — це VMT того самого Delphi класу, в іншому випадку метод був викликаний у SOM об'єкта, і Self — це насправді SOM VMT. Пристрій SOM VMT невідомо, воно залежить від версії SOM.DLL і може змінюватися, зате відомо, що в ньому за нульового зсуву міститься посилання на клас-об'єкт, а він нам і потрібен. В SOM всі класи — це теж об'єкти, і класові методи — це методи класів-об'єктів в самому звичайному сенсі. Таким чином, зробивши порівняння Себе зі своїм ім'ям, можна далі вибрати, або отримати посилання на SOM клас з нульового зсуву в структурі ClassData, або, якщо не співпало, взяти посилання на SOM клас, разыменовав Self. І далі робити те, що має класовий метод. Власне, є два класових методу, роблять таке порівняння, це ClassObject і NewClass, другої відрізняється тим, що якщо в структурі ClassData нічого не виявилося, то клас не створюється автоматично. Інші класові методи викликають один з цих двох. Наприклад, якщо нам потрібно перевірити, чи є такий-то об'єкт спадкоємцем такого-то класу, то якщо клас не створено, то і не треба, і так зрозуміло, що ні, а якщо питають InstanceSize, то без створення класу вже не обійтися. Таким чином, будуть коректно працювати і «o.InstanceSize» (для «var o: SOMObject»), і «SOMObject.InstanceSize». Прямо як в Delphi.

Була ідея спроектувати всі методи класу-об'єкта SOM в класові методи Delphi, однак тут виявилися труднощі. Переборні, але було вирішено відмовитися від їх подолання.

Труднощі при проектуванні методів класу SOM в класові методи DelphiПо-перше, в Delphi класи не створюються динамічно, а в SOM — так, і все це супроводжується викликами методів, які метаклас може перевизначити, щоб зробити якесь хитре поведінку. Наприклад, так звані кооперативні метаклассы можуть перед певним методом вставляти свою реалізацію, яка завжди буде викликатися першої, як ні наследуй класи, а потім передавати управління звичайної реалізації способом, схожим на виклик батьківського методу. Before/After метаклассы можуть додати перед усіма методами незалежно від сигнатури якийсь загальний код, який спрацює до звичайного методу, і на зворотному шляху — після. Наприклад, вхід і вихід з м'ютексу. Або друк в консоль події входу і виходу з методу. У проксі для віддаленого виклику процедур теж усе непросто. І всі методи, які роблять це можливим, вивалити в класові методи Delphi класу здалося негарним рішенням. По-друге, прив'язки робляться до класів з SOM DTK, а вони, як правило, свої метаклассы ховають, і у всьому DTK зустрічається тільки синглтонный метаклас, видимий публічно. По-третє, SOM гарантує (аж до штучного схрещування), що метаклас будь-якого нащадка буде нащадком метакласу, і проблеми несумісності метакласів не виникне, але текст прив'язок, які б це красиво описали, буде вельми надмірний. Як з'ясовується, ми навіть тип результату somGetClass можемо поправити, тільки якщо явно вказаний метаклас.

Якщо якийсь гіпотетичний компілятор типізованого мови програмування, що підтримує SOM або аналогічну модель, бачить, що є змінна, в якій міститься якийсь нащадок класу X з батьками Y і Z, і у Y є явний метаклас MY, а у Z — MZ, а у X замовлений метаклас MX, ну а насправді буде мінімальний нащадок MY, MZ і MX, то типізований компілятор може тип результату somGetClass так хакнути, щоб це був «MY&MZ&MX» з усіма методами, які у них всіх є, а якщо і цього класу викликати somGetClass, то щоб і далі збиралося об'єднання, але коли генеруються прив'язки, генерувати кожне таке потенційне об'єднання було б занадто. І без того дублюється текст, щоб підтримувати множинне спадкування. А, значить, серед класів SOM DTK, у яких відомий метаклас, залишаються тільки ті, які зробили це явно і самі, а от їхні нащадки — вже немає, якщо тільки вони не повторять вказівку явного метакласу. Так що в загальному випадку треба писати «o.ClassObject.МетодОбъектаКласса», ну а для деяких класових методів, які були в TObject, все ж зроблено зручний доступ.

Create, натхненний тим, як працює TLIBIMP.exe я зробив класової функцією. Виходить, що, як в Delphi, пишемо «repo := Repository.Create;» Але ось виникла ідея, а що якщо і конструктори SOM (инициализаторы в термінології SOM) зробити конструкторами з точки зору Delphi. Щоб вони, будучи викликані у класу, створювали об'єкт, а об'єкт працювали як методи. Щоб показати, як тут можна хакнуть класи Delphi, я вирішив привести тимчасову діаграму, як взагалі конструюються і знищуються об'єкти в Delphi:

Outer-Create
Outer-Create => virtual NewInstance
Outer-Create => virtual NewInstance => _GetMem
Outer-Create => virtual NewInstance
Outer-Create => virtual NewInstance => non-virtual InitInstance
Outer-Create => virtual NewInstance => non-virtual InitInstance => FillChar(0)
Outer-Create => virtual NewInstance => non-virtual InitInstance
Outer-Create => virtual NewInstance
Outer-Create
Outer-Create => Create
Outer-Create
Outer-Create => virtual AfterConstruction
Outer-Create

Free
Free => Outer-Destroy
Free => Outer-Destroy => virtual BeforeDestruction
Free => Outer-Destroy
Free => Outer-Destroy => Destroy
Free => Outer-Destroy
Free => Outer-Destroy => virtual FreeInstance
Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance
Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance => _FinalizeRecord
Free => Outer-Destroy => virtual FreeInstance => non-virtual CleanupInstance
Free => Outer-Destroy => virtual FreeInstance
Free => Outer-Destroy => virtual FreeInstance => _FreeMem
Free => Outer-Destroy => virtual FreeInstance
Free => Outer-Destroy
Free

Outer-Create і Outer-Destroy — це той код, який автоматично обертаються виклики конструктора і деструктора.

Що стосується SOM, то, якщо потрібно викликати нестандартний конструктор (не somInit), то об'єкт класу замість somNew викликається функція somNewNoInit, що повертає об'єкт, і у нього потім викликається той конструктор, який потрібно, наприклад, somDefaultCopyInit. Або все той же somInit. Задумка полягає в тому, щоб як-небудь хакнуть всі методи TObject, щоб послідовність створення об'єкта згадувалась на реальсах Delphi. Зокрема, ми бачимо, що TObject.NewInstance — віртуальна класова функція. Трюками з іменами не вийде обдурити, компілятор Delphi викликає її з VMT за певною адресою. Але можна в SOMObjectBase, де ховаються методи TObject, NewInstance не тільки сховати, але і надати осмислену реалізацію, яка викличе somNewNoInit у відповідного класу SOM. Де вона цей клас візьме? Наприклад, можна через Delphi VMT протягнути protected віртуальну класову функцію, яка буде вміти повертати собі відповідний клас SOM. Тільки є одна проблема. В кінці Outer-Create викликається AfterConstruction, віртуальний метод. Він не спрацює, якщо у об'єкта вже SOM VMT. Можна, звичайно, в кінці Create тимчасово заміняти VMT об'єкта з SOM на Delphi, а в AfterConstruction — назад, але це якась занадто кисла схема виходить. Ось і в цьому питанні довелося відступити.

Але в іншому вийшли досить натуральні прив'язки.

Спадкування з Delphi не реалізовано, але якщо буде, то там зробити красиво буде трохи важко. Навіть, якщо розглянути звичайні емітери для C++, то у них робота з SOM об'єктами схожа на роботу з C++ об'єктами, перевантажуються operator new() і operator new(void*), а при спадкуванні реалізація методів класів SOM зовсім не виглядає як реалізація методів класу C++. Крім спеціально зміненого компілятора DirectToSOM C++, звичайно.

Ця діяльність ведеться в рамках винахідницького проекту і на даний момент має дослідницький та демонстраційний характер. Мені треба дізнатися від А до Я підводні камені, іншим треба показати принципову здійсненність. Може бути, де-то і стане в нагоді, але на чистову планується працювати з іншою, новою моделлю, яка вбере в себе кращі риси SOM, COM і Objective-C, і буде готова працювати на актуальні, не стояли перед колишніми авторами SOM завдання.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.