Навіщо і як я писав BOSS'а і що з цього вийшло. Кроссплатформенная система плагінів на C + +11

  Кілька разів я вже посилався на свій пост про кроссплатформенной системі плагінів на C + +11 [1] . Цей пост виник з бажання спробувати деякі нововведення стандарту C + +11. На чому пробувати — довго не роздумував. У мене вже був пост про кроссплатформенной компонентної моделі [2] на C + +03 і він з'явився з невеликого раніше розроблюваного проекту.
 
 
З появою gcc 4.7.2 мені випала можливість поекспериментувати вдосталь з C + +11. Трохи пізніше вирішив довести матеріал до логічного завершення: зробити код більш закінченим, трохи написати документації та створити невеликий ресурс [3] , де все це можна було б розмістити.
 
Що і як було реалізовано і чому прийнято те чи інше рішення було розписано в попередніх постах. Цим же постом мені хотілося розповісти не про всілякі спеціалізаціях шаблонів і інших принади C + +, а про результати: з чим довелося зіткнутися при бажанні підтримати кілька платформ (Windows, Linux, FreeBSD) і при бажанні зробити збірку різними компіляторами, а так само про плани розвитку.
 
 Зміст:
  

Виникнення ідеї

Десь у 2001-му році мені до рук потрапили дві книги: «Мова програмування С + +» (Бьерн Страуструп, 3-е видання) і «Модель COM і застосування ATL 3.0» (Трельсен, 2001 р.). У той час я починав програмувати на C + +. Книга Трельсена мені дуже сподобалася. У ній було добре описана робота з MS COM, а так само описані основні принципи програмування на основі інтерфейсів. Попрацювавши деякий час з MS COM, я з часом перейшов до програмування під Linux / Unix і до кроссплатформенной розробці. Якщо ж говорити про досвід, то можна сказати, що більшу його частину складає саме кроссплатформенная розробка з написанням власних всіляких обгорток, наприклад, із застосуванням pImpl, а так само використання різних кроссплатформенних бібліотек.
 
При розробці кроссплатформенного ПО мені стало не вистачати деякої компонентної моделі. MS COM — це Windows орієнтована розробка. А як же кроссплатформенная? Захотілося створити щось своє, кроссплатформне, засноване на тих же структурах з чисто віртуальними функціями, якось декомпозировать систему з розбиттям на модулі, а надалі і організувати взаємодію процесів, прокинув через їхні кордони ті ж інтерфейси, і в той же час мати можливість отримати якомога більше тонкий прошарок для створення компонент. Хотілося мінімізувати роботу з побудови компонент так, щоб на їх побудову витрачалося мінімум зусиль, а більшу частину часу можна було витратити на розробку логіки, яка повинна була поміщатися в ці самі компоненти / плагіни. Не страждати комплексом Наполеона, тобто чи не намагатися з невеликої компонентної моделі зробити всеосяжний framework, який би крім основної роботи зі створення компонент ще робив би все, що тільки на думку спаде. Для цього є інші бібліотеки: boost, Qt, wxWidgets, poco і т.д. Цього не завжди легко домогтися. Завжди є бажання додати чогось ще на випадок «може стати в нагоді». Цим грішать багато, я не виняток. З цим бажанням борюся. Крім легкості самої прошарку хотілося зробити і якомога коротший шлях зі створення компонент, уникаючи складних ритуалів, які треба здійснити, щоб хоч щось почало працювати.
 
Попрацювавши в декількох великих і не дуже компаніях зауважив, що моє бажання створити подібне не перша. У багатьох компаніях є свої «компонентні корпоративні великі framework'і». У певний момент я написав свій невеликий компонентний движок з видами на кроссплатформенность, і з реалізацією тільки під Windows. Для Linux частини були заглушки. На ньому ж розроблявся один з невеликих проектів. Ця версія не була успішна хоч якось. Через деякий час я написав уже більш просунуту версію компонентної моделі з повною підтримкою багатоплатформеності. Врахував проблеми першої версії і з чистого аркуша приступив до других. Движок паралельно розвивався під Windows і Linux. І на його основі розвивався один з проектів. До цього часу десь в безмежному просторі Інтернету його можна знайти. Ця версія була більш успішна. Вона і взята за основу проекту «BOSS», як ідея і як деякі випробувані раніше принципи. Так само була зроблена деяка вижимка, як матеріал для поста на Хабре [2] .
 
Деяка захопленість шаблонами і прочитанням праць Александреску дали основу для реалізації принципів мінімалізму в розробляється компонентної моделі. Списки типів в стилі Александреску далеко не всіма розробниками сприймаються позитивно. А ось поява стандарту C + +11 дало можливість реалізувати бажане вже на variadic templates. До того ж стандарт C + +11 дав і таку річ як decltype, що дало можливість в дусі мінімалізму створювати Proxy / Stub для організації маршалинга інтерфейсів між процесами. В цілому C + +11 дав мені багато корисних інструментів.
 
Отримавши певний досвід подібної розробки для невеликих проектів, подивившись як це роблять інші, вирішив зробити свій компонентний движок зі своїми особливостями і довести його до продуктового виду. Іноді в «личку» приходили питання як за допомогою пропонованої моделі щось реалізувати. Були й питання про плани розвитку проекту та підтримки.
 
Одного разу я все ж вирішив довести проект до продуктового види: довести код до продуктового виду, написати документацію, написати приклади, зробити сайт проекту і т. д. Тобто довести хоча б до мінімальної точки завершеності, коли цим вже можна скористатися, а не тільки прочитати як про нудною теорії. З'явився продукт «BOSS». Можна сказати, що його зачатки ідей були на рубежі 2000-2001 років, перша реалізація була в 2007 році, в 2009 році з'явилася друга версія і проект на її основі. Зараз в 2014 році «матеріали минулого» здобули рамки проекту хоч і на одному ентузіазмі і моєму інтересі розвиваються.
 
 

Про назву проекту

Якось звик розкладати код по просторах імен. Спроба підібрати щось милозвучне і легко промовлене як ім'я простору імен швидко знайшла рішення. Крутя в голові щось типу: COM, std, stl, ATL, boost, component, model, service, plugin, model і т.д. склався пазл: придумано трохи жартівлива назва BOSS-Base Objects for Service Solutions.
 
BOSS — це деяка абревіатура, яка не має ніякого відношення до керівників (хоча така асоціація в коментарях раніше була). Якщо все ж є стійка асоціація BOSS'а з начальством, то давайте його сприймати в кращих традиціях західного підходу керівник-слуга. Про такий підхід до управління можна почитати в книзі «Лідерство: до вершин успіху» (Кен Бланшар, 2011 р.) або «Від ефективності до величі» (Стівен Кові, 2004 р.). На жаль, серед керівників є і шанувальники Макіавеллі (можна прочитати на цю тему книги: «Государ» (Нікколо Макіавеллі, 1532 р.) і «Менеджер мафії. Керівництво для корпоративного Макіавеллі» (V., 2010 р.)). Це для деяких може стати причиною стійкої асоціації того, що BOSS — товстий, злий дядько з палицею, що стоїть за спиною співробітника. Але все ж давайте розвіємо цю негативну асоціацію на користь більш ефективної та позитивної!
 
І якщо від асоціації того, що BOSS — це керівник, звільнитися не вдається, то пропоную розглядати його з боку керівник-слуга. В рамках даної компонентної моделі BOSS якраз буде слугою: деякій тонким прошарком між співробітниками (в світлі розглянутої моделі «компонентами»), налагоджуючи між ними комунікації для досягнення максимального результату рішення бізнес завдань (логіки, що розташовується в компонентах). А що як і комунікації можна ще віднести до однієї з ключових обов'язків керівника…
 
 

Навіщо потрібна компонентізація

При розробці проектів на C + + в певний момент з'являється бажання, а іноді і необхідність декомпозиції системи на окремі компоненти з рознесенням їх по окремих модулях. Розробляється система може від невеликого додатки з одним виконуваним модулем вирости до складної багатомодульної системи. Можливий і варіант початкової орієнтації на великий програмний комплекс. У такі моменти виникає питання: «Що використовувати для декомпозиції системи на компонентному рівні і як організувати взаємодію компонент?».
 
Основна ідея компонентної моделі BOSS — дати можливість декомпозировать систему на компоненти з мінімальними витратами на забезпечення взаємодії між компонентами.
 
Розробка моделі BOSS була спочатку орієнтована на втілення концепції «Мінімалізм у всьому, де це тільки можливо». Мінімально необхідна реалізація, мінімальна кількість кроків для створення і використання компонент.
 
BOSS не намагається надати всілякі допоміжні сервіси, які могли б бути корисні. Для цього є інші бібліотеки. BOSS — це можливість налагодити комунікації між частинами програмного комплексу і нічого більше.
 
 

Підтримка різних ОС і компіляторів

При використанні однієї і тієї ж ОС, одного і того ж компілятора і до того ж не змінюючи версії, розробка стає все швидше і швидше, тому що стають відомі особливості використовуваних продуктів, їх недоліки, а так само стають відомі і їх «плюшки», якими вони прикормлюють. Розробка стає швидше, а в довгостроковій перспективі веде до проблем.
 
При розробці кроссплатформенного коду робота трохи ускладнюється. Треба якось реалізовувати одне і те ж на різних платформах. Благо тут маються Кросплатформені бібліотеки. Не завжди вони можуть використовуватися в тому чи іншому проекті, як з технічних, так і з «політичних» причин. Тут згадується pImpl та інші ідіоми. Так само треба підтримувати в адекватному стані гілки для кожної з платформ. Бувають моменти, що здається код не містить нічого залежного від платформи, повинно збиратися і на інших платформах. Знайшовши часові ресурси, робиться спроба зібрати на іншій платформі і виявляється, що це не так. Не збирається з першого разу. Можливо в одній з ОС деякі функції розміщені в одній бібліотеці, а в іншій зовсім інший (наприклад, при використанні функції dlopen для Linux компоновка повинна бути з-ldl, а для FreeBSD це розташовано в libc; здавалося б один і той же інтерфейс). Ці дрібниці швидко правляться. Виникають і аналогічні, які так само не викликають великих труднощів.
 
Ще одна проблема — це при складанні під різні операційні системи доводиться користуватися іноді компіляторами від різних виробників. І тут починається найнеприємніше: мова C + + він ніби один і має стандарт, а компілятори різні і кожен з них трохи по своєму трактує стандарт. В цілому якщо ж писати якомога примітивніше, використовуючи якомога менший підмножина мовних конструкцій і мінімум із стандартної бібліотеки, то код з великою ймовірністю збереться на різних компіляторах майже з першого разу. Але це все одно, що бути емігрантом в англомовних країнах з запасом слів у три з половиною сотні. Жити можна, але повноцінно спілкуватися проблемно. Хочеться-то більшого. При бажанні отримати більше починаються пошуки пересічний множин підтримуваних можливостей у використовуваних компіляторах.
 
Стандарт C + +11 багато обговорювали. Чекали підтримки тієї чи іншої фічі в черговий версії компілятора і пробували її якось використовувати. Цей шлях могли пройти багато захоплені C + + програмісти. Для себе я знайшов прийнятний варіант в gcc 4.7.2, який покривав мої потреби реалізації C + +11 для написання задуманого. Або хоча б 4.7. На даний же момент я збирав BOSS 'а за допомогою gcc 4.8 / gcc 4.8.1.
 
Коли у мене все запрацювало на gcc 4.7.2 я вирішив спробувати зібрати на Microsoft Visual C + + 2013. Одна з поширених операційних систем — Windows. Де факто один з основних інструментів розробки на C + + під неї — Microsoft Visual C + +. Виходить, що цей компілятор дуже добре було б підтримати при складанні BOSS, щоб дати можливість використання системи плагінів BOSS у звичній для багатьох Windows-розробників середовищі. Благо версія 2013 вже позиціонувалася з підтримкою C + +11 і найцікавіше — це в ній з'явилася нормальна робота з шаблонами з перемінним кількістю параметрів. А це основне, що було потрібно мені для складання моєї системи плагінів.
 
Натхнений цим я спробував зробити збірку на Microsoft Visual C + + 2013 і тут мене чекало перше розчарування. Шаблони-то з перемінним кількістю параметрів працюють, а constexpr'а немає. Не підтримується. А він мені був необхідний для розрахунку ідентифікаторів сутностей, які обчислюються в момент компіляції як CRC32. Вирішивши відмовитися від блага розрахунку CRC32 в момент компіляції від переданої рядка, я прибрав його з коду. Мені потрібні були константи для ідентифікації інтерфейсів, класів-реалізацій і сервісів, які використовувалися як параметри шаблонів. Розрахувавши їх окремо, я замінив весь код розрахунку в момент компіляції на готові значення. Спробував ще раз зібрати. Тут було друге розчарування — в одному з ключових місць компілятор просто видав повідомлення про внутрішню помилку компілятора. Кілька невеликих спроб щось поміняти так само не увінчалися успіхом. Кажуть, що один раз — це випадковість, два — збіг, а три — закономірність. Не доходячи до виведення закономірності, я на час відклав спроби зробити збірку на Microsoft Visual C + + 2013. Для складання під Windows використовував MinGW тієї ж версії gcc, що і для * nix.
 
Другим компілятором, на якому пробував зробити збірку був clang. З версією 3.4 не заладилося відразу. На версії 3.5 є деякі поки проблеми. Але доріжка намацаний і можливо в недалекому майбутньому буде збірка під clang. А ось чи потрібна вона…
 
Зібравши всі успішно під Windows і Linux (Ubuntu), вирішив спробувати зробити збірку під FreeBSD. Поставивши на віртуальну машину FreeBSD 9.2, знайшов у портах компілятор gcc 4.9. Зацікавився збіркою на більш нової версії (раніше як було сказано я експериментував з gcc 4.8.1 / gcc 4.8 та gcc 4.7.2). Встановивши компілятор gcc 4.9, він мені підніс неприємний сюрприз відсутністю std :: to_string. Швидко заглянувши в інтернет, зрозумів, що баг такий є. Поставив більш ранню версію gcc 4.8 і поміняв код, що використовує цю функцію. Аналогічно була проблема і з std :: stoul. Версія 4.9 тепер може бути використана для складання.
 
Після всіх експериментів поки тільки на gcc і зупинився, незважаючи на те, що раніше багато працював з Microsoft Visual C + +. Можливо хтось скаже, що поки що не варто використовувати C + +11. На мій погляд, все ж варто використовувати новий стандарт, тому що він дає багато можливостей. Мова стала трохи складніше, а використання його простіше. До того ж стандартна бібліотека стала «ще більш кроссплатформенной». Так в BOSS використовуються потоки і примітиви синхронізації, яких раніше не було і доводилося писати або свою обгортку або використовувати щось готове, наприклад, boost. До того ж і з підтримкою однаковості на різних компіляторах від різних виробників для C + +03 не все обстоит гладко.
 
Зупинившись на gcc, як середовище розробки я вибрав QtCreator. Він дозволяє не тільки проекти з використанням Qt розробляти, але можна і будь-який проект з наявністю Makefile в ньому використовувати, ніж я користувався як для * nix, так і для Windows.
 
 

Основні ідеї та рішення

Від загальних слів пропоную перейти до конкретних особливостей пропонованої системи плагінів. Основні ідеї, які спочатку були закладені і які призвели до появи проекту — це
 
 
     
  • Зробити систему плагінів кроссплатформенной
  •  
  • Зробити якомога більш тонкої прошарок для реалізації компонент
  •  
  • Дати можливість здійснювати якомога менше «ритуальних» дій при створенні компонент
  •  
Про багатоплатформеності вже сказано вище. Можу лише додати, що більша частина раніше залежного коду від ОС тепер підтримується стандартної бібліотекою. Залишився невеликий фрагмент із завантаження динамічних бібліотек, в яких розміщуються плагіни.
 
Про те наскільки тонкою вийшла прошарок кожен може судити сам. І може бути написати цікавий конструктивний коментар з пропозиціями про поліпшення. Я ж при реалізації намагався позбутися бажання додати чогось ще, що може бути колись в рідкісних випадках буде корисним або те, що можна знайти в тому ж boost, poco і т. д.
 
Коли я раніше використовував MS COM або користувався аналогічними «корпоративними framework'амі» мені завжди хотілося звільнитися від купи макросів і особливо від карти інтерфейсів, в якій треба вказувати які з інтерфейсів я хочу експортувати з класу-реалізації. За підтримки чергового інтерфейсу класом-реалізацією, треба було від нього успадковуватися і його ж помістити в карту інтерфейсів. Як не дивно, останнє я дуже часто забував. Згадував тільки при розробці клієнтського коду, коли потрібний мені інтерфейс не запитували з розробленого ж мною компонента, так як я його забув включити в експортовані інтерфейси (в карту). Цей, на мою думку, недолік я вирішив прибрати в запропонованій реалізації. Вийшло так, що від яких інтерфейсів клас-реалізація успадкований, ті інтерфейси автоматично експортуються. Як це технічно реалізовано було мною докладно описано в [1] і раніше в [2] (відповідно засобами C + +11 і С + +03). Тут є невелика плата за цю автоматизацію — не можна наслідувати інтерфейс і заборонити його запитувати у об'єкта. Потреба в цьому мені здається дуже рідкісної якщо взагалі потрібною, а якщо і виникне, то її можна легко вирішити майже нульовими витратами без модифікації системи плагінів.

C + + має такий наріжний камінь, як множинне спадкування реалізацій. Деякі сучасні мови програмування таке виключають, пропонуючи натомість користуватися множинним спадкування інтерфейсів. Не хочу розпалювати священної війни з посяганням на чий-небудь в цьому відношенні релігійний катехезіс. Моє ставлення до множинного спадкоємства реалізацій позитивно. Воно дає можливість економити час, наприклад, при вирішенні такого завдання: є пара інтерфейсів, реалізованих в своїх класах-реалізаціях і виникає потреба реалізувати клас із підтримкою цих же інтерфейсів з тією ж реалізацією і додати ще один інтерфейс до нього. Писати клас успадкований від трьох інтерфейсів, в ньому в якості полів вказувати готові реалізації і до них перенаправляти всі методи двох інтерфейсів, реалізуючи по справжньому тільки третій, ліниво. Та у множинного спадкоємства реалізацій є і свої недоліки, але при використанні інструменту при потребі, а не тому що він є, від них можна сховатися при цьому ще й отримавши вигоду.

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

Всі ідентифікатори інтерфейсів — CRC32, обчислене від рядка. Раніше в [2] я використовував рядок. З появою C + +11 з'явилася можливість від цього рядка обчислити CRC32 в момент компіляції. Це так само дало можливість трохи скоротити необхідні дії. Для інтерфейсів особливого скорочення не вийшло в [1] на відміну від [2] , а от для ідентифікації класів-реалізацій, в яких реалізуються інтерфейси вийшло скорочення і можливість позбавити від забудькуватості. Якщо в [2] при розробці класу-реалізації забути вказати його ідентифікатор, то це може виявитися невеликою неприємністю в момент виконання програми. У [1] ж якщо не вказати ідентифікатор класу-реалізації, то компілятор про це не пам'ятає недвозначно. До того ж тепер ідентифікатор треба вказувати як параметр базового шаблонного класу для класів-реалізацій, а не десь окремо. А використання числа замість рядка дає це зробити без особливих проблем. Більш докладно про переваги і здаються проблемах використання числа як ідентифікатора замість рядка розказано в [1] .

Використання BOSS'а

Всі компоненти системи плагінів BOSS — це реалізації інтерфейсів. Кожен інтерфейс повинен мати свій ідентифікатор і бути успадкований або від базового інтерфейсу Boss :: IBase, або від іншого іншого інтерфейсу, який так само веде до Boss :: IBase. При описі інтерфейсів можливо і множинне спадкування інтерфейсів.
Приклад опису простого інтерфейсу:

namespace MyNs
{
  struct ISimpleObject
    : public Boss::Inherit<Boss::IBase>
  {
    BOSS_DECLARE_IFACEID("MyNs.ISimpleObject")
    virtual Boss::RetCode BOSS_CALL HelloWorld() = 0;
  };
}

Деяка сутність Boss :: Inherit дає можливість обходити список переданих інтерфейсів при пошуку потрібного в разі множинного успадкування. Як це реалізовано і чому в ній з'явилася необхідність описано в [1] , а так само як це можна використовувати більш докладно описано в документації [3] . Під макросом BOSS_DECLARE_IFACEID прихована робота з ідентифікатором інтерфейсу (вона надалі може змінюватися в залежності від компілятора, так як є проблеми, описані вище). Макрос BOSS_CALL так само переважно використовувати при описі методів інтерфейсу. Це дасть можливість у майбутньому скористатися деякими перевагами, які поки в розробці.

Всі методи інтерфейсу — це чисто віртуальні функції, які можуть приймати і повертати будь-які типи, які розробник визнає за можливе передавати між динамічними бібліотеками. Проте в рамках даної системи плагінів було б добре використовувати в якості типу значення, що повертається Boss :: RetCode, а в якості переданих параметрів використовувати тільки інтегральні типи, типи з плаваючою точкою, покажчики і посилання на них і покажчики і подвійні покажчики на інтерфейси і cv- кваліфікатор. Про це розказано в документації [3] і можливо стане матеріалом одного з подальших постів, коли вийшов продукт [4] з напівготової реалізації взаємодії між процесами плагінів буде доведений до кінця, куди [4] назад буде поміщений як база для більш складної взаємодії плагінів через кордони процесів.

Приклад інтерфейсів з множинним спадкуванням
namespace MyNs
{
  
  namespace IFaces
  {
    struct IFace1
      : public Boss::Inherit<Boss::IBase>
    {
      BOSS_DECLARE_IFACEID("MyNs.IFaces.IFace1")
      virtual Boss::RetCode BOSS_CALL Method1() = 0;
    };

    struct IFace2
      : public Boss::Inherit<Boss::IBase>
    {
      BOSS_DECLARE_IFACEID("MyNs.IFaces.IFace2")
      virtual Boss::RetCode BOSS_CALL Method2() = 0;
    };

    struct IFace3
      : public Boss::Inherit<IFace1, IFace2>
    {
      BOSS_DECLARE_IFACEID("MyNs.IFaces.IFace3")
      virtual Boss::RetCode BOSS_CALL Method3() = 0;
    };
  }
}

Створені інтерфейси повинні бути десь реалізовані. Нижче наведено приклад простої реалізації інтерфейсу

class SimpleObject
  : public Boss::CoClass<Boss::MakeId("MyNs.SimpleObject"), ISimpleObject>
{
public:
  // ...
private:
  // ISimpleObject
  virtual Boss::RetCode BOSS_CALL HelloWorld()
  {
    // ...
    return Boss::Status::Ok;
  }
};

З прикладу видно, що клас-реалізація успадкований від класу Boss :: CoClass. Це клас є базовим для всіх класів-реалізацій інтерфейсів. Першим параметром передається ідентифікатор (CRC32) класу-реалізації. Будь інтерфейс може мати різні реалізації і для вибору потрібної необхідний ідентифікатор. Отримати його з рядка можна за допомогою Boss :: MakeId. Рядки в коді більш читані, а компілятор і кінцевий програмний продукт користуватимуться числами (CRC32), розрахованими від цих рядків. Наступними параметрами шаблону передаються інтерфейси, які реалізує клас. Так само в цьому списку можуть бути передані і готові класи-реалізації, якщо розробляється клас захоче їх підтримати без реалізації у себе (збірка компонента з готових кубиків).

Приклад більш складного компонента
namespace MyNs
{
  namespace Impl
  {
    
    class Face4
      : public Boss::CoClass
          <
            Boss::MakeId("MyNs.Impl.Face4"),
            IFaces::IFace4
          >
    {
    public:
      // ...      
    private:
      // IFace4
      virtual Boss::RetCode BOSS_CALL Method4()
      {
        // ...
        return Boss::Status::Ok;
      }
    };
    
    class Face_1_2_3_4
      : public Boss::CoClass
          <
            Boss::MakeId("MyNs.Impl.Face_1_2_3_4"),
            IFaces::IFace3,
            Face4
          >
    {
    public:
      // ...
    private:
      // IFace1
      virtual Boss::RetCode BOSS_CALL Method1()
      {
        // ...
        return Boss::Status::Ok;
      }
      // IFace2
      virtual Boss::RetCode BOSS_CALL Method2()
      {
        // ...
        return Boss::Status::Ok;
      }
      // IFace3
      virtual Boss::RetCode BOSS_CALL Method3()
      {
        // ...
        return Boss::Status::Ok;
      }
    };
  }

}


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

Інтерфейси, реалізації… Думаю варто трохи повернутися назад і подивитися, що представляє з себе базовий інтерфейс Boss :: IBase. І якщо його показати у спрощеному вигляді, то він виглядатиме так:

struct IBase
{
  BOSS_DECLARE_IFACEID("Boss.IBase")
  virtual ~IBase() {}
  virtual Boss::UInt BOSS_CALL AddRef() = 0; \
  virtual Boss::UInt BOSS_CALL Release() = 0; \
  virtual Boss::RetCode BOSS_CALL QueryInterface(Boss::InterfaceId ifaceId, Boss::Ptr *iface) = 0;
};

У реальності він трохи інакше виглядає, але зводиться до наведеного вище. Про тонкощі реалізації цього інтерфейсу написано в [1] . Інтерфейс має методи управління часом життя об'єкта (AddRef і Release), а воно базується на підрахунку посилань. Ці методи бажано ніколи не повинні викликатися користувачем, їх використання заховано в розумні покажчики. Користувальницький код повинен орієнтуватися на використання розумних покажчиків. Третій метод (QueryInterface) призначений для отримання потрібного інтерфейсу за його ідентифікатором з наявного покажчика на інший інтерфейс.

Так само при створенні компонент в BOSS є можливість використовувати для більш тонкої настройки складних об'єктів механізм «постконструкторов» і «преддеструкторов». Більш докладно в [1] описано як це реалізовано, а в [3] для чого і як цим можна скористатися (див. FinalizeConstruct і BeforeRelease). Тут лише коротко скажу, що він потрібен при реалізації компонент з готових кубиків і при необхідності зробити щось коли об'єкт вже повністю створений або поки він ще не почав руйнуватися, тобто то, що не можна зробити в конструкторах і деструктори (наприклад викликати віртуальний метод з конструктора / деструктора, перевизначення в ієрархії нижче).

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

Приклад точки входу
#include "face1.h"
#include "face2.h"
#include "plugin/module.h"
namespace
{
  
  typedef std::tuple
    <
      MyNs::Face1,
      MyNs::Face2
    >
    ExportedCoClasses;
}
BOSS_DECLARE_MODULE_ENTRY_POINT("MultyComponentExample", ExportedCoClasses)

З прикладу видно, для створення точки входу створюється список експортованих класів-реалізацій (ExportedCoClasses) і передається в макрос BOSS_DECLARE_MODULE_ENTRY_POINT. Цей макрос так само бере ідентифікатор модуля (рядок для отримання CRC32).

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

Підводячи підсумки цієї частини поста можна сказати, що для створення і використання компонент / плагінів потрібно наступне.

  1. Визначити інтерфейси
  2. Створити класи-реалізації
  3. Помістити класи-реалізації в модулі або модуль, де створити і точку входу
  4. Зареєструвати модулі
  5. У частині коду клієнта завантажити ядро ​​і створювати потрібні об'єкти, використовуючи їх ідентифікатори реалізацій.
Хотілося відзначити, що при завантаженні фабрикою в усі модулі передається глобальний ServiceLocator, в якому є покажчик на фабрику класів. Раніше була реалізація без такого глобального ServiceLocator'а. Доводилося з компонента в компонент передавати покажчик на фабрику класів, якщо компонент потребував створенні інших об'єктів за допомогою фабрики. Але як відомо, більш живучими є змішані рішення. У чистому вигляді щось одне іноді проблемно використовувати. Тому мається компроміс — глобальний ServiceLocator. Він так само може бути використаний для розміщення користувача об'єктів, а не тільки фабрики класів. Водночас зловживати використанням глобального ServiceLocator'а не варто, можна легко заплутатися і зробити неможливою коректне вивантаження модулів в момент коли вони не потрібні або при завершенні програми. Це проблема систем з підрахунком посилань, коли з'являються циклічні посилання.

Приклади

Як перший приклад, пропоную розглянути створення простого інтерфейсу, реалізувати його і написати код клієнта без використання інфраструктури (фабрики класів, реєстру сервісів).

Приклад створення і використання простого об'єкта
#include "core/ibase.h" // Интерфейс IBase
#include "core/co_class.h"  // Базовый класс для всех классов-реализаций
#include "core/base.h"  // Реализация базового интерфейса IBase
#include "core/ref_obj_ptr.h" // Умный указатель RefObjPtr
#include "core/module.h"  // Этот файл содержит определение некоторых
                          // методов системы плагинов BOSS. Должен быть
                          // включен один раз в проект если не используется
                          // инфраструктура. Если используется инфраструктура,
                          // то должно быть единственное включение в каждый
                          // модуль файла "plugin/module.h" вместо "core/module.h"

  #include <iostream>
namespace MyNs
{
  
  // Определение интерфейса с базовым интерфейсом IBase
  struct ISimpleObject
    : public Boss::Inherit<Boss::IBase>
  {
    // Идентификатор интерфейса
    BOSS_DECLARE_IFACEID("MyNs.ISimpleObject")
    virtual Boss::RetCode BOSS_CALL HelloWorld() = 0;
  };
    
    
  // Класс-реализация интерфейса ISimpleObject
  class SimpleObject
    : public Boss::CoClass  // Базовый класс для всех классов-реализаций
        <
          Boss::MakeId("MyNs.SimpleObject"),  // Идентификатор реализации
          ISimpleObject // Реализуемый интерфейс
        >
  {
  public:
    SimpleObject()
    {
      std::cout << "SimpleObject" << std::endl;
    }
    ~SimpleObject()
    {
      std::cout << "~SimpleObject" << std::endl;
    }
    
  private:
    // ISimpleObject
    virtual Boss::RetCode BOSS_CALL HelloWorld()
    {
      // Реализация метода интерфейса
      std::cout << "BOSS. Hello World!" << std::endl;
      // Возвращает один из статусов завершения выполнения метода
      return Boss::Status::Ok;
    }
  };
}
int main()
{
  try
  {
    // Создание объекта, реализующего интерфейс ISimpleObject
    Boss::RefObjPtr<MyNs::ISimpleObject> Inst = Boss::Base<MyNs::SimpleObject>::Create();
    // Вызов метода интерфейса
    Inst->HelloWorld();
  }
  catch (std::exception const &e)
  {
    std::cerr << "Error: " << e.what() << std::endl;
  }
  return 0;
}

Все, що наведено в прикладі вже розглядалося вище. Здавалося б навіщо в системі плагінів створювати об'єкти без використання фабрики класів. Причому тут плагіни? Так, плагіни — це кінцева мета, а створювати об'єкти без фабрики потрібно, наприклад, при створенні об'єкта, що повертається з методу інтерфейсу. Метод інтерфейсу створює деякий невеликий об'єкт, і повертає його як вихідний параметр, при цьому цей клас-реалізація не реєструється ніде, в цьому немає потреби. Щоб продемонструвати це наведу приклад, в якому метод інтерфейсу повертає колекцію рядків в якості вихідного параметра.

Приклад роботи з колекцією рядків
#include "core/module.h"
#include "core/ibase.h"
#include "core/base.h"
#include "core/co_class.h"
#include "core/ref_obj_ptr.h"
#include "common/string.h"  // Реализация интерфейса IString
#include "common/enum.h"  // Реализация интерфейса IEnum
#include "common/string_helper.h" // Вспомогательный класс для IString
#include "common/enum_helper.h" // Вспомогательный класс для IEnum
  #include <iostream>
namespace MyNs
{
  // Описание интерфейса
  struct ISimpleObject
    : public Boss::Inherit<Boss::IBase>
  {
    // Идентификатор интерфейса
    BOSS_DECLARE_IFACEID("MyNs.ISimpleObject")
    // Метод, возвращающий коллекцию строк
    virtual Boss::RetCode BOSS_CALL GetStrings(Boss::IEnum **strings) const = 0;
  };
  // Реализация интерфейса
  class SimpleObject
    : public Boss::CoClass  // Базовый класс для всех классов-реализаций
        <
          Boss::MakeId("MyNs.SimpleObject"),  // Идентификатор реализации
          ISimpleObject // Реализуемый интерфейс
        >
  {
  public:
    SimpleObject()
    {
      // Создание коллекции
      auto StringEnum = Boss::Base<Boss::Enum>::Create();
      
      // Добавление строк в коллекцию
      StringEnum->AddItem(Boss::Base<Boss::String>::Create("String 1"));
      StringEnum->AddItem(Boss::Base<Boss::String>::Create("String 2"));
      StringEnum->AddItem(Boss::Base<Boss::String>::Create("String 3"));
      
      Strings = std::move(StringEnum);
      
      std::cout << "SimpleObject" << std::endl;
    }
    ~SimpleObject()
    {
      std::cout << "~SimpleObject" << std::endl;
    }
    
  private:
    // Коллекция строк
    mutable Boss::RefObjPtr<Boss::IEnum> Strings;
        
    // ISimpleObject
    virtual Boss::RetCode BOSS_CALL GetStrings(Boss::IEnum **strings) const
    {
      // Проверка входного параметра. Должен быть пуст, чтобы исключить возможность
      // присвоения уже существующему объекту нового и минимизировать ошибки,
      // связанные с утечками
      if (!strings)
        return Boss::Status::InvalidArgument;
      // Возвращение коллекции строк, созданной в конструкторе.
      // Конструкция Strings.QueryInterface(strings) для возврата выходного
      // параметра предпочтительнее, конструкции
      //  *strings = Strings.Get();
      //  (*strings)->AddRef();
      //  retutn Boss::Status::Ok;
      // так как более короткая и исключает возможность забыть увеличить
      // счетчик ссылок на объект, что могло бы привести к долгому поиску
      // ошибки в работающей программе.
      return Strings.QueryInterface(strings);
    }
  };
    
}
int main()
{
  try
  {
    // Создание объекта
    Boss::RefObjPtr<MyNs::ISimpleObject> Obj = Boss::Base<MyNs::SimpleObject>::Create();
    // Умный указатель на IEnum, в который будет передано выходное значение
    Boss::RefObjPtr<Boss::IEnum> Strings;
    // Запрос коллекции строк
    if (Obj->GetStrings(Strings.GetPPtr()) != Boss::Status::Ok)
    {
      std::cerr << "failed to get strings." << std::endl;
      return -1;
    }
    // Использование вспомогательного класса для удобства работы с коллекцией
    Boss::EnumHelper<Boss::IString> Enum(Strings);
    // Проход по коллекции строк
    for (Boss::RefObjPtr<Boss::IString> i = Enum.First() ; i.Get() ; i = Enum.Next())
    {
      // Использование вспомогательного класса для работы с интерфейсом строки
      std::cout << Boss::StringHelper(i).GetString<Boss::IString::AnsiString>() << std::endl;
    }
  }
  catch (std::exception const &e)
  {
    std::cerr << "Error: " << e.what() << std::endl;
  }
  return 0;
}

Можливо з цього прикладу виникне питання: «А навіщо використовувати IString, чому просто не повертати char const *?». Так, можна і просто рядок повернути (char const *), але надалі якщо планується перехід на межпроцессное взаємодія плагінів (яке повинно скоро з'явитися; про нього постараюся написати пост) то це рішення буде проблемним. При орієнтації лише на використання плагіна в рамках одного процесу такої проблеми немає і можна повертати char const *. Так же приклад з IString добре підходить для демонстрації роботи з об'єктами.

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

Визначення інтерфейсу
// Файл isum.h
#include "core/ibase.h"

namespace MyNs
{
  struct ISum
    : public Boss::Inherit<Boss::IBase>
  {
    BOSS_DECLARE_IFACEID("MyNs.ICalc")
    virtual Boss::RetCode BOSS_CALL CalcSum(int a, int b, int *sum) = 0;
  };
}

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

Визначення ідентифікатора реалізації
// Файл class_ids.h
#include "core/utils.h"

namespace MyNs
{
  namespace Service
  {
    namespace Id
    {
      enum
      {
        Sum = Boss::MakeId("MyNs.Service.Id.Sum")
      };
    }
  }
}

Реалізація інтерфейсу ISum
// calc_service.h
#include "isum.h"
#include "class_ids.h"
#include "core/co_class.h"
namespace MyNs
{
  class Sum
    : public Boss::CoClass
          <
            Service::Id::Sum,
            ISum
          >
  {
  public:
    // ISum
    virtual Boss::RetCode BOSS_CALL CalcSum(int a, int b, int *sum);
  };
  
}


// calc_service.cpp
#include "calc_service.h"
#include "core/error_codes.h"
namespace MyNs
{
  
  Boss::RetCode BOSS_CALL Sum::CalcSum(int a, int b, int *sum)
  {
    if (*sum)
      return Boss::Status::InvalidArgument;
    *sum = a + b;
    return Boss::Status::Ok;
  }
}

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

Точка входу
// Файл  module.cpp

#include "calc_service.h"
#include "plugin/module.h"
namespace
{
  
  typedef std::tuple
    <
      MyNs::Sum
    >
    ExportedCoClasses;
}
BOSS_DECLARE_MODULE_ENTRY_POINT("Calc.Sum", ExportedCoClasses)

Після створення плагіна його реєстрація робиться утилітою regtool.
Для * nix систем: . / Regtool-reg Registry.xml. / Libcalc_service.so
Для Windows: regtool.exr-reg Registry.xml calc_service.dll

Залишилося тільки код клієнта написати…

Клієнт
#include "isum.h"
#include "class_ids.h"
#include "plugin/loader.h"  // Загрузчик системы плагинов

#include <iostream>
int main()
{
  try
  {
    // Загрузка ядра системы плагинов. Указывается файл реестра и модули
    // реестра сервисов и фабрики классов. После чего пользователь полностью
    // абстрагируется от модулей, путей к ним и их загрузке.
    Boss::Loader Ldr("Registry.xml", "./" MAKE_MODULE_NAME("service_registry"),
                     "./" MAKE_MODULE_NAME("class_factory"));
    // Создание объекта через фабрику классов, доступ к которой возможен через
    // глобальный ServiceLocator. Вспомогательная функция Boss::CreateObject
    // получает глобальный ServiceLocator, находит в нем фабрику классов,
    // создает через нее нужную реализацию, запрашивает требуемый интерфейс
    // и возвращает умный указатель на созданный объект с требуемым интерфейсом.
    // Так же эта функция может быть использована внутри любого из плагинов.
    auto Obj = Boss::CreateObject<MyNs::ISum>(MyNs::Service::Id::Sum);
    int Res = 0;
    // Вызов метода интерфейса
    if (Obj->CalcSum(10, 20, &Res))
      std::cerr << "Failed to calc sum." << std::endl;
    std::cout << "Sum: " << Res << std::endl;
  }
  catch (std::exception const &e)
  {
    std::cerr << "Error: " << e.what() << std::endl;
  }
  return 0;
}

Для підрахунку суми двох чисел коду дуже багато. У аналогію можна навести додавання 2 + 2 з використанням ООП. Але це всього лише демонстрація функціональності і в реальному розробці код логіки куди більшого розміру, на тлі якого компонентна прошарок вже не так помітна. Все ж компонентізація призначена не для «складання 2 +2» і коротких програм, а її існування обумовлено бажанням декомпозировать системи, які насилу можуть існувати в рамках одного виконуваного файлу.

Висновок

У висновку хотілося б сказати що і де знаходиться і планах розвитку.

Всі приклади, наведені в цьому пості, а так само інші, невелика документація, сама система плагінів (компонентна модель), інформація по складанню і т.д. доступні на www.t-boss.ru Так само вихідні файли доступні для скачування у вигляді zip-архіву . При складанні потрібно в Makefile розкоментувати рядок, визначальну цільову платформу, закоментувавши всі інші. Або можна збирати з командного рядка наприклад так «make OS = Windows», наприклад при наявності MSYS під Windows. Так само можна використовувати QtCreator. З його допомогою можна створити проект з Makefile: Файл → Новий файл або проект → Імпортувати проект → Імпорт існуючого проекту → далі вказати ім'я проекту і шлях до папки з Makefile'ом, при цьому попередньо відредагувати для цільової ОС Makefile.

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

Хотілося б розвинути ресурс проекту. Він поки не блищить оригінальністю, я не веб-розробник, так що строго не судіть :) Намагаюся його розвивати. Він уже пережив одне серйозне зміна за пару з невеликим місяців існування. А в силу того, що я схильний до C + + розробці, то і сайт так само на ньому розвивається. Цікаво в логах бачити запити провідні в нікуди зі спробами запостити щось недобре в розрахунку на стандартні для вебу технології… C + + не зовсім той інструмент, який використовується повсякденно для розробки веб frontend'а.

Компонентна модель — це для мене не сферичний кінь у вакуумі, на ній так само з інтересу в даний момент дещо розробляю. Можливо в осяжному майбутньому з'явиться пост про архітектуру та реалізації сайту проекту на самій же компонентної моделі, так сказати про BOSS'е на BOSS'е.

Крім прикладного програмування з використанням системи плагінів в планах ще й її розвиток. В недалекому майбутньому думаю опублікувати пост про інтеграції BOSS і Qt. Навести кілька прикладів створення користувальницького інтерфейсу на Qt, складові якого будуть плагінами BOSS. Що буде прикладом розширення інтерфейсу користувача програми без її зміни, простим додаванням плагіна.

Так само планую опублікувати пост про розташування полігонів в різних процесах. Для чого і як розповім там же. Потреба така є і для мене вона дуже цікава. Деякі вичавки з цієї розробки вже публікував у [4] , але це всього лише невеликий інструмент був для розробки клієнт-серверного ПЗ. У рамках системи плагінів ідея опису Proxy / Stub так само залишиться в тому ж дусі мінімалізму, а так само буде додана інфраструктура з підтримкою різного транспорту для використання компонент на різних робочих станціях, і більше легковагого транспорту для використання в рамках однієї робочої станції.

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

Дякую за увагу.

Посилання

  1. Система плагінів як вправу на C + +11
  2. Своя компонентна модель на C + +
  3. Base Objects for Service Solutions (BOSS)
  4. Мінімалізм віддаленого взаємодії на C + +11


Джерело: Хабрахабр

0 коментарів

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