Зачем и как я писал BOSS’а и что из этого получилось. Кроссплатформенная система плагинов на C++11

в 20:56, , рубрики: Без рубрики

Несколько раз я уже ссылался на свой пост о кроссплатформенной системе плагинов на C++11 [1]. Этот пост возник из желания попробовать некоторые новшества стандарта C++11. На чем пробовать — долго не размышлял. У меня уже был пост о кроссплатформенной компонентной модели [2] на C++03 и он появился из небольшого ранее разрабатываемого проекта.

Зачем и как я писал BOSSа и что из этого получилось. Кроссплатформенная система плагинов на C++11
С появлением 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а и что из этого получилось. Кроссплатформенная система плагинов на C++11
На рисунке приведена упрощенная иерархия наследования интерфейсов и классов-реализаций без отображения промежуточных классов системы плагинов. В приведенном примере можно видеть как класс-реализация наследуется от пары интерфейсов, реализует их и пользуется уже готовой реализацией одного из интерфейсов, просто унаследовав ее. При этом интерфейс, расположенный в наследованной реализации так же доступен для механизма получения интерфейсов.

Интерфейсы, реализации… Думаю стоит немного вернуться назад и посмотреть, что представляет из себя базовый интерфейс 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» и коротких программ, а ее существование обусловлено желанием декомпозировать системы, которые с трудом могут существовать в рамках одного исполняемого файла.

Зачем и как я писал BOSSа и что из этого получилось. Кроссплатформенная система плагинов на C++11

Заключение

В заключении хотелось бы сказать что и где находится и планах развития.

Все примеры, приведенные в этом посте, а так же иные, небольшая документация, сама система плагинов (компонентная модель), информация по сборке и т.д. доступны на 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

Автор: NYM

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js