Как стать автором
Обновить

Комментарии 35

хорошая статья, спасибо за проделанную работу.
Спасибо на добром слове!
Прекрасный материал, не переставайте писать!
Постараюсь в следующем посте написать про саму ОС, как дойдут руки. И спасибо за отзыв :-)
Очень хорошая статья и очень знакомая проблема, которую мы в своем проекте (тоже, кстати, embedded ось) решали вот так: habrahabr.ru/post/144935/
Те же интерфейсы и абстрактные модули, те же glue-header'ы, то же самое внедрение зависимостей (правда, еще и в рантайме). Но у вас, конечно, здорово получилось, что не нужно поддерживать никаких дополнительных файлов с метаданными для билд-системы. Этакие self-hosted исходники, очень круто!
Спасибо! Embox я видел, но, правда, очень давно, надо как-то посмотреть новые версии :-)
У нас все ориентировано в данный момент на более deep, так сказать, embedded, уровня cortex m0/m3 без MMU. Как FreeRTOS.
Я правильно понял, что это решение возникло из-за того, что все исходники (относящиеся к разным компонентам) находятся в одной папке (а заголовочные файлы — в другой, но тоже одной)?
Может быть я неправильно понял вопрос, но исходники могут быть расположены как угодно, скрипту дается «папка проекта» в которой лежат вообще все исходники, а он сам должен в них найти все интерфейсы и реализации. Как исходники раскладываются по папкам — дело вкуса разработчика. Можно менять структуру исходников и имена файлов произвольным образом.
Нет, вопрос был про изначальную ситуацию, до появления описанного в статье решения.

Просто на мой взгляд, если бы у вас программные компоненты действительно были компонентами (т.е. достаточно изолированными, без «жадных» зависимостей, унифицированным подключением любого компонента к проекту) это всё не пришлось бы делать вообще.

Вот и интересно узнать, насколько я ошибаюсь.
Когда все начиналось, никакой структуры папок не было вовсе, нужно было решать вопрос о комбинировании модулей на уровне исходников с сохранением интерфейсов и без поддержки какой-либо согласованности между исходниками и системой билда вручную. А потом уже все писалось изначально так, как описано.
В свою очередь, мне любопытно, как можно сделать «унифицированное подключение компонентов к проекту» без всего этого.
В свою очередь, мне любопытно, как можно сделать «унифицированное подключение компонентов к проекту» без всего этого

Хм…

У нас было так:
— все модули генерируют библиотеки со унифицированными названиями, которые состоят из имени модуля с добавлением суффиксов/префиксов, которые зависят от платформы,
— соответственно, добавление очередного компонента в проект — это плюс одна строчка в перечислении зависимостей,
— каждый модуль имел унифицированную структуру: заголовки в папке inc/, исходники — в src/, в корне модуля лежит файл, отвечающий за его сборку,
— все модули максимально обособлены, например, обёртка для платформно-специфичных функций — это один компонент, инициализация и работа с OpenGL ES — другой, и т.п.,
— все модули «торчат» наружу через простой C/C++ интерфейс, никакие потроха (из FreeType, OpenGL, DSP/BIOS) в глобальное пространство имён не попадают. Вообще, если требуется какой-то функционал, который присутствует в «third-party library», значит будет компонент, который её внутри будет содержать, который будет уметь строить её на все нужные платформы и торчать наружу 2-5 понятными функциями. Чтобы кто угодно потом мог брать и использовать, не вникая в тонкости инициализации этой библиотеки и как забирать от неё данные,
— все компоненты проекта «лежат рядом»,
— более высокоуровневые компоненты ссылаются (в исходниках) на более базовые однотипно — типа
#include "egl_utils/inc/egl_program.h"
#include "xtree/inc/node.h"
(здесь egl_utils и xtree — названия внешних компонентов)

Никаких проблем с тем, чтобы понять к какому модулю относится какой-то данный конкретный файл — нет в принципе. Подход прекрасно себя зарекомендовал и на малых проектах и на больших. Компилировалось на Windows/Linux, target platforms — windows/linux(ARM и x86)/Texas Instruments.
Пытаюсь сейчас построить очень похожую модульную систему в организации, выпускающей линейку продуктов, но очень быстро встал вопрос о транзитивных зависимостях. К примеру, каждый модуль имеет свой набор тестов, для сборки и выполнения которых нужен соответствующий фреймворк. Получается, некая управляющая система должна сообщать модулям при сборке, где искать модули-зависимости.

В maven этот вопрос решается наличием локального репозитория модулей, и все сборки проходят через этот репозиторий, а транзитивные зависимости выкачиваются в него прозрачно для разработчика. Мне этот вариант не очень по душе, т.к. сборки получаются менее воспроизводимыми и герметичными, иногда проект сложно собрать «с нуля».

А как вы решили этот вопрос?
Это вопрос мне или предыдущему комментатору?
Вопрос адресовался qehgt
У нас были очень легковесные тесты (на gtest основанные), поэтому держать в каталоге проекта ещё и его — сложностей не вызывало. Кроме того, у нас же embedded специфика, многие компоненты «протестировать» можно только на реальном железе, автоматическое тестирование «каждую ночь» не получится.

То есть, резюмируя, у нас не было «множества разных тестирующих фреймворков», а те тесты, что были — использовали только gtest или были вообще автономны.
Кажется, теперь я понял, что моих проблем у вас просто не возникло. Когда проект один, с модулями всё довольно просто: они «знают», где лежат зависимости.

В моём случае есть линейка продуктов, или даже один продукт, состоящий из нескольких проектов (располагающихся в разных репозиториях). В таких условиях очень хочется повторно использовать одни и те же модули в разных продуктах и проектах, держать их в отдельных репозиториях и назначать им их собственные версии, встраивая в проект через некий механизм вроде svn:externals.
Вот тут как раз возникает вопрос: где должны быть зависимости модуля, чтобы модуль можно было собрать и отдельно, и в рамках всего проекта…

В любом случае, спасибо за ответ.
Дело не в организации структуры папок, а в том, чтоб инклуд был абстрактным. В Вашем примере инклуд все равно подключает какой-то конкретный компонент, заменить его на другой, без изменения исходников нельзя.
Тут просто ситуация «вам шашечки или ехать?».

Если я использую некий функционал (пусть это будет поворот картинки, для примера), то мне без разницы как именно он будет делаться — с помощью CPU, DSP или задействуя GPU. Главное, чтобы мне было удобно картинку ему передать и удобно результат получить. А как он это будет делать — это внутреннее дело этого компонента, ему лучше знать, как это сделать быстрее на данной платформе.

То есть никакого «изменения исходников» в моём случае нет — если появится новая платформа, или заоптимизируют реализацию уже существующей платформы — это никак не затронет остальные исходники. Зачастую, они даже не потребуют перекомпиляции.
«хидер»

По-английски это слово читается /ˈhɛdə/ — созвучно слову head.
Пожалуйста, будьте грамотными.
Возможно здесь какая-то недоступная мне специфика embeded мира.

В привычном мне контексте (сервер/десктоп/autotools/CMake) как правило во главе угла стоит система сборки. Имеется некий набор переменных, которые можно менять на этапе ./configure. В зависимости от значений этих переменных, в компиляцию попадают или не попадают те или иные файлы. Обычно также генерируется config.h, куда вставляются значения переменных, заданных на этапе конфигурации, для использования в директивах условной компиляции (#ifdef). Информация о зависимостях строится и поддерживается автоматически.
Я как-раз написал о разнице, между закрытой и открытой системой конфигурирования. Когда есть набор _заранее_определенных_ переменных, которые включают или отключают что-то, это не совсем то. Например, как можно подменить реализацию какого-то модуля добавив новый модуль (на уровне исходников)? Нужно, во-первых, прописать его в файлы CMake, зависимости «его» и «от него» и имена его собственных файлов. Если повезет, и структура каталогов подобрана удачно, тогда, перенастройкой default include dir, вероятно, можно добиться того, чтоб старые компоненты проинклудили его, иначе придется менять исходники старых компонентов.
Например, как можно подменить реализацию какого-то модуля добавив новый модуль (на уровне исходников)?

Например AC_CONFIG_LINKS.
Это во-первых не на уровне исходников, а на уровне библиотек, а во-вторых тут опять же речь о конфигурировании в заданных рамках и без отслеживания зависимостей.
то во-первых не на уровне исходников, а на уровне библиотек
Это на уровне файлов. Задачу подменить клиентам header.h на другой, не меняя их исходного кода — решает.

а во-вторых тут опять же речь о конфигурировании в заданных рамках и без отслеживания зависимостей
Если я правильно понимаю вашу концепцию, у вас точно так же есть «заданные рамки». Просто переменные, которые управляют конфигурацией, задаются непосредственно в исходном коде.

Кстати множественные конфигурации тестируются все равно «в заданных рамках». Даже просто на компиляемость.

По поводу зависимостей. Зависимости — исходников от хидеров — отслеживаются автоматически. Зависимость между объектными файлами и либами надо прописывать явно; после чего они отслеживаются :) У вас ведь зависимости тоже указываются явно, просто непосредственно в исходниках.

У вас любопытный подход. Но на лицо определенная потеря гибкости — как быть с генеренными исходными файлами (допустим в проекте есть парсер на flex/bison), как быть если код должен быть включен в сборку, даже если ничто его явно не использует (допустим используется что-то вроде linkerset)?
Я может невнимательно прочитал, но там речь о подключении библиотек в зависимости от процессоров. Где там меняется header я не нашел. По поводу зависимостей я тоже не понял, как можно отследить зависимость исходников от хидеров (помимо этих зависимостей есть еще зависимости модулей от модулей, а не только хидеров от исходников)?
Про то, что переменные в исходниках, речь о том и идет, что надо содержать в исходниках то, что по смыслу в исходниках, иначе надо согласовывать исходники и метаданные вручную.
Про то, что негибко где-то, согласен, у нас при разработке ОС была такая проблема, что некоторые файлы, вроде тех, которые содержат таблицы прерываний по определенным адресам, явно никем не используются, но должны быть включены в билд принудительно. Мы решаем эту проблему тем, что обычно собирается некоторое «ядро», которое «корневой модуль» и в нем перечисляется то, что должно входить в сборку, и вот туда включаются модули (инклудами), которые должны быть включены принудительно. Ну или можно задавать какой-то список «принудительных модулей» самому скрипту, но это криво как-то и выглядит костылем, поэтому пока все только на дереве зависимостей.
Все, нашел про хидеры :-) Надо было по ссылкам дальше пройти. Ну, это одно из решений «виртуального хидера», с помощью внешних средств (файловой системы). Зависимости, тем не менее, между модулями не отслеживаются автоматически и закомменчивание инклуда, который тянет за собой кучу всего, не влечет исключение этой «кучи всего» из билда.
Недоосилил. Можно я сразу спрошу, а потом еще раз попробую прочитать.

Т.е. суть всего этого поста, это способ уйти от
func.h

void func();

func.cpp

#ifdef X86
	void func() {}
#elif X64
	void func() {}
#elif ARM
	void func() {}
#else
	// unsuported system
	void func() {*(NULL);}

Так как это сложно сопровождать при добавлении новые #elif

К вот этому

func.h
	void func();

func_x86.cpp
	void func() {}
	
func_x64.cpp
	void func() {}
	
func_arm.cpp
	void func() {}

	
func.cpp
	// unsuported system
	void func() {*(NULL);}


Так как тут достаточно для новой target системы просто добавить новый файл и при сборке выбрать его по меткам?
Нет, пост не совсем об этом. Точнее, совсем не об этом. Он он том, что можно написать #include INTERFACE(CPU, DEFAULT) и не задумываться ни об именах файлов, ни об путях для include. В большинстве случаев, в конечном итоге все выльется во второй вариант, но можно написать такой заголовочный файл, чтоб там был макрос #define func(). Если назначить его как дефолтный интерфейс, тогда все вызовы функций func исчезнут из всех исходников, которые включают INTERFACE(CPU, DEFAULT), то есть конфигурирование не на уровне подлинковки нужной либы, содержащей реализацию нужной функции, а на уровне исходников.
тогда все вызовы функций func исчезнут из всех исходников

Эм… Я так понимаю это тот кейс когда нужно например логирование и трассировку выключить в релизе, но мне кажется что модульная система уж точно не для этого нужна.

Вариант, когда одна и та-же функция под разные системы реализована по разному более нагляден. Но этого можно добиться и просто рассовыванием реализации по разным папкам и при сборке подтягивать нужные сорсы. И видимо это тоже не тот случай когда модули вот прям ТАК нужны.

Ну почему, и для этого тоже нужна, многие вещи имеют «аспектную» природу: логирование, дебаг, поддержка многопроцессорности, безопасность и т.д. и т.п… Модули нужны много для чего, понятно, что каждую конкретную задачу в конкретном проекте можно и без них решить. Просто речь о том, что можно в исходниках хранить ту информацию которая там уже есть по смыслу, и это упрощает жизнь. Если более удобно поддерживать согласованность исходников и билда вручную или с помощью каких-то соглашений, то никто с этим не спорит. Если вдруг заккоментируешь какой-то инклуд, и модуль становится не нужен, надо вручную подкрутить систему сборки, установить какие-то переменные и т.д., а тут говорится о том, что можно просто закомментить инклуд и все.
По факту, информации о связях между заголовочными файлами и исходниками нет нигде. Даже распарсив исходники ее не установишь, т.к. может быть несколько реализаций в разных файлах. Если эту информацию зарыть в исходники — все сведется к тому что тут, либо к тому что в Pascal. Если информации в исходниках нет, значит она будет лежать снаружи в виде метаданных для системы сборки, и поддерживать согласованность исходников и метаданных надо будет вручную. Вот в этом поинт.
Мне вся эта конструкция очень напоминает идеи Александреску о параметризации политиками. Нужно только рассматривать класс как интерфейс модуля, а «инжектируемые» через параметры шаблонов политики — как интерфейсы модулей-зависимостей (которые, возможно, тоже параметризованы, и т. д.). Возможность задания параметры по умолчанию даёт «стандартные» реализации. Схожее торжество неявных интерфейсов — концептов.

Но у вас C, а не C++. Да и с инкапсуляцией у подхода Александреску явные проблемы: все детали реализации вылезают в клиентский код.
Ничто не мешает использовать наш подход и с С++, только он (наш инструмент), конечно, не будет ничего знать о классах, тут нужны какие-то соглашения, например имя модуля = имя класса, и один класс — один файл, как в java.
Концепты тоже крайне интересная тема, для меня, по крайней мере. В планах на будущее — сделать проверку соответствия хидера и «интерфейса», то есть интерфейс должен быть описан каким-то IDL-ом, который для сборки исходников не нужен и может в ней не участвовать, но, когда кто-то написал реализацию модуля, чтоб он мог проверить, что его модуль точно соответствует интерфейсу, который он собрался из него экспортировать.
Сложность в том, что самый простой случай, вроде совпадения имен функций и структур данных не дает гарантий, что модуль «подойдет», тут нужен концепт и какое-то его описание + возможность автоматической проверки соответствия кода концепту.
«Нечто похожее было несколько десятков лет назад в Pascal» — не в Pascal, а в TurboPascal фирмы Borland. TurboPascal довольно сильно расширял учебно-минималистичный Pascal, в том числе, классной идеей 1) хранить вместе интерфейс и реализацию, и 2) хранить интерсейс явно в скомпилированном файле.

Привет! Отключи пожалуйста блокировку личных сообщений )

Прошу прощения, отключил. Проверь.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории