Большинство разработчиков давно привыкли использовать ту или иную IDE и не задумываются о том, как их исходный код превращается в исполняемый модуль. Современные средства разработки содержат UI и автоматизацию сборки для огромного числа типов проектов на все случаи жизни. И это очень здорово, так как позволят не задумываться над вещами, не имеющими непосредственного отношения к решаемой задаче. Но иногда задача бывает настолько сложной, что уже не получается использовать стандартные типы проектов. И тут появляется "Система сборки проектов".
Ярким примером такого типа проектов является .NET Micro Framework — реализация платформы Microsoft .NET для микроконтроллеров. В этой статье будет сделан обзор его системы сборки и особенности ее реализации.
Постановка задачи
Прежде чем говорить о том, как устроен проект и как происходит его сборка, нужно понять, какие перед проектом стоят цели и какие из этого следуют требования.
Идея .Net Micro Framewok заключается в том, чтобы разрабатывать приложения на .Net для самых маленьких устройств, управляемых микроконтроллерами. Такие устройства обладают несколькими сотнями килобайт Flash и несколькими десятками килобайт RAM. Из-за ограниченности ресурсов на них нельзя использовать полноценные операционные системы, включая IoT редакции.
Обычно разработка для таких устройств ведется на C\C++ со вставками на ассемблере и тесно связана с конкретным микроконтроллером. Все управление "железом" происходит через запись и чтение множества регистров. При этом используются компиляторы и линковщики из специализированного набора для платформы — Toolchain.
Раньше на рынке было представлено множество архитектур
микроконтроллеров. Каждый производитель имел несколько семейств устройств и у любого из них могла быть своя схема регистров. Кроме того, существовало множество разных toolchain. Поэтому выбор микроконтроллера являлся крайне важной вехой проекта, так как потом перейти на другое устройство было очень сложно.
Сейчас миром микроконтроллеров правит ARM. Многие производители отказались от собственных ядер и перешли на эту архитектуру. Это значительно унифицировало разработку и облегчило миграцию с одного устройства на другое. Кроме того, огромными темпами развивается CMSIS — не зависящий от конкретного производителя набор стандартных программных интерфейсов для работы с ARM микроконтроллерами. Каждый организация поставляет реализацию CMSIS для своих устройств, что позволяет, теоретически, абстрагироваться от особенностей реализации того или иного микроконтроллера.
Но тем не мене на рынке еще существует достаточно большое количество устройств как с отличной от ARM архитектурой, так и не имеющих реализации CMSIS.
.Net Micro Framework — это попытка поднять разработку для микроконтроллеров на более высокий уровень абстракции. Если CMSIS унифицирует устройства с ARM архитектурой, то .NetMF пытается унифицировать работу с микроконтроллерами в принципе. И заодно позволяет использовать мощь управляемого кода и удобство Visual Studio при разработке для встраиваемых систем. При этом никто не ограничивает использование .NetMF только микроконтроллерами. Например, существует его реализация для Windows, которая используется в эмуляторе.
Таким образом, основным требованием к .NetMF является возможность запуска на любом микроконтроллере, имеющем достаточное количество памяти (в документации указаны минимальные требования 256KB RAM и 512K Flash/ROM). Исходя из этого, компиляция должна выполняться разными toolchain, в зависимости от выбранного устройства. Архитектура системы должна учитывать то, что работа на низком уровне, с "железом" может вестись как по средствам одной из реализаций CMSIS, так и любой другой библиотеки или операционной системы (как в случае эмулятора для Windows).
Другие требования, связанные с реализацией .Net, системы безопасности, отладки и т.д., я не буду рассматривать, так как они не влияют на систему сборки проекта.
Архитектура решения
Итак, к системе предъявляются три основных требования:
- Возможность запуска на любом микроконтроллере, обладающем необходимым объемом памяти.
- Возможность использовать разные toolchain.
- Возможность использования как API операционной системы, так и широкий набор библиотек для работы с "железом".
Причем пункты 2 и 3 являются следствием первого пункта.
.Net Micro Framework имеет следующую архитектуру:
Система разделена на несколько слоев:
Два верхних слоя (приложения пользователя и системные библиотеки) написаны на управляем коде. Это то, что мы видим в Visual Studio. Слой аппаратного обеспечения — это и есть само «железо», на котором запущен .NetMF. Слой TinyCLR — это среда исполнения кода.
TinyCLR разделена на 3 части:
- CLR — тут все, что касается исполнения управляемого кода, типизации, сборки мусора и т.д.
- PAL (Platform Abstraction Layer) — классы и функции для работы с общими абстракциями, такими, как счетчики, таймеры, ввод-вывод. Эти классы одинаковы для всех аппаратных платформ.
- HAL (Hardware Abstraction Layer) — классы и функции дня работы непосредственно с «железом».
Разделение на PAL и HAL выполняет требование номер три (возможность использования как API операционной системы, так и широкий набор библиотек для работы с "железом").
Абстракция HAL представляет собой набор интерфейсов, с которыми работает PAL, и более высокие уровни. Это позволяет делать множество реализаций для разных платформ и использовать любые библиотеки или API.
Таким образом, разработчики разделяют код на следующие слои:
Часть, обозначенная на схеме как Native Code, написана на C/C++. Managed Code написан на C#. Соответственно, разные части репозитория компилируются разными компиляторами.
Чтобы реализовать возможность компиляции разными toolchain и при этом сохранить целостность проекта, нужна мощная система сборки, которая позволяет производить гибкую настройку всего процесса. Система сборки первых версий .NetMF была основана на MAKEFILE. Затем произошел переход на MSBuild. Причем на тот момент, сборка Visual C++ проектов в Visual Studio не использовала MSBuild (а большая часть NetMF написана на C\C++), поэтому пришлось делать "нестандартный" проект. В результате получилась преобразованная в формат MSBuild копия системы сборки, основанной на MAKEFILE.
В итоге, это позволило выполнить все требования, связанные с мультиплатформенностью .NetMF.
Особенности реализации
Недавно была опубликована статья о системе сборки .NetMF. Автор описывает проблемы, с которыми сталкивается проект.
Имеется несколько сценариев работы:
- Сборка компонентов, необходимых для построения остальной части репозитория. А именно любых расширений, которые требуются для сборки, но не входят в стандартный набор MSBuild.
- Сборка и настройка утилит, необходимых для сборки проекта. Этот пункт важно отличить от предыдущего. Если в первом пункте собирались именно расширения системы сборки, то во втором собираются именно дополнительные утилиты. Например, ПО для цифровой подписи.
- Сборка SDK, которая будет использоваться разработчиками при создании приложения для .NetMF в Visual Studio. Сюда входят VSIX палагин и необходимые библиотеки, включаемые в проект.
- Сборка "портов" для аппаратного обеспечения — TinyCLR для конкретной платы.
Прежде чем рассмотреть каждый сценарий, нужно еще раз напомнить, что .Net Micro Framework является "архитектурно нейтральной" и включает как как big-endian, так и little-endian системы. Поэтому для сборки может быть использовано множество разных toolchain. Для этого применяется сложный скрипт для MSBuild. Причем при создании этого скрипта разработчики исходили из очень интересной интерпретации требования номер два: возможность использовать разные toolchain. Для них важно было, во-первых, иметь возможность добавлять новые toolchain, не "сломав" существующее решение. А, во-вторых, дать пользователю возможность выбора toolchain. Например, сейчас компиляцию можно делать как с помощью GCC ARM, так и с помощью MDK.
Сборка компонентов для сборки
На этом этапе из исходных кодов собираются расширения, использующиеся MSBuild при сборке остальной части репозитория. Причины, по которым этот сценарий отделен от остальных, заключаются в особенности реализации MSBuild. Дело в том, что перед началом процесса сборки MSBuild сразу загружает все необходимые компоненты. Это значит, что они должны уже существовать к этому моменту. Кроме того, у MSBuild есть система кэширования, из-за которой собранные компоненты могут быть недоступны, если попытаться собрать остальную часть репозитория сразу после сборки предварительных компонентов.
Поэтому нужно сначала собрать расширения, затем полностью выгрузить MSBuild и уже только потом собирать остальной репозиторий.
Сборка и настройка утилит
В процессе сборки .NetMF участвует множество утилит. Одни из них занимаются подписыванием модулей, другие сжатием, третьи преобразованием форматов и т.д. Эти утилиты в основном представляют собой консольные приложения Windows. Они участвуют как в сборке "портов" для устройств, так и в сборке SDK. Поэтому они должны быть собраны заранее. В MSBuild они используются с помощью скриптов-оберток(wrappers).
Сборка SDK
Процесс создания инфраструктуры для разработки приложения для .NetMF включает в себя множество шагов:
- Сборка предварительных частей. Этот этап включает в себя 1 и 2 пункты предыдущего списка.
- Компиляция исходного кода в DLL.
- Преобразования DLL в PE файлы, содержащие отладочную информацию как для big-endian, так и для little-endian систем.
- Сборка компонентов для интеграции с Visual Studio.
- Подписывание всех компонентов перед упаковкой в пакеты.
- Сборка VSIX пакетов для поддерживающихся версий Visual Studio.
- Подписывание VSIX пакетов.
- Сборка SDK MSI пакетов.
- Подписывание SDK MSI пакетов.
Стоит отметить, что пакеты требуется пописывать несколько раз на разных этапах. И тут так же проявляется проблема, описанная выше (предварительная сборка). В SDK включены asseblies, которые требуются для сборки SDK. Это решается выполнением первого пункта.
Сборка "портов"
Прошивка для конкретного устройства тоже появляется в результате выполнения большого количества шагов:
- Компиляция нативного кода в .obj файлы.
- Компиляция нативного кода в .lib файлы.
- Линковка нативных .obj и .lib файлов в бинарный файл.
- Выполнение Link/Locate над бинарным файлом для получения образа XIP для flash.
- Подписывание бинарных образов для поддержки безопасной загрузки.
- Создание специализированного сжатого и подписанного пакета, который может использоваться программой MFUpdate.
- Генерация образа раздела конфигурации для Flash устройства.
- Компиляция управляемого кода в управляемые сборки.
- Генерация необходимых компонентов для взаимодействия между нативным и управляемым кодом.
- Генерция PE файлов, которые исполняются в .NetMF, и соответствующих файлов с отладочной информацией для big-endian и little-endian систем.
- Генерация DAT файлов из управляемых сборок, которые будут загружены во Flash устройства.
Все это выполняется с помощью скриптов для MSBuild.
Разрешение зависимостей
Этот пункт следует из требования об использовании широкого набора библиотек для работы с "железом".
Любой проект так или иначе сталкивается с зависимостями между его составными частями. Это могут быть как ссылки между файлами, так и ссылки между программными модулями, такими, как .exe и .dll. В общем случае существует два типа зависимостей, которые нужно разрешать:
- Жесткие зависимости.
Этот тип появляется, когда одна часть программы явно зависит от другой. Например, приложение A использует библиотеку xyz. Такие зависимости легко могут быть автоматически разрешены. В этом случае системе сборки понятно, что сначала нужно собрать библиотеку xyz, а потом уже приложение A. В более современных системах сборки xyz и A могут собираться параллельно в разных потоках, но линковка все равно будет происходить после того, как будут собраны все компоненты. - Мягкие зависимости.
Этот тип возникает, когда одна часть программы ссылается на интерфейс или API, которые могут иметь множество равнозначных реализаций. В этом случае система сборки не может автоматически определить, какую из реализаций ей использовать. Исключением является случай, когда существует только одна реализация: система сборки сможет определить, что ей использовать. Если реализаций несколько, то ей нужно явно тем или иным способом указать, что нужно брать.
В .NetMF есть как жесткие, так и мягкие зависимости. И если жесткие зависимости разрешаются автоматически, то проблема мягких зависимостей является одной из самых сложных. Сейчас она решается путем указания определенных значений в переменных окружения и указания ссылок на реализации в скриптах MSBuild. Это не лучшее решение, хотя оно и работает. Скрипты настолько сложны и полны ссылок друг на друга, что понять, где именно нужно поменять ссылки, очень сложно.
Заключение
Подводя итог, можно сказать, что .NetMF из-за своей "архитектурной нейтральности" имеет весьма сложную структуру. Следствием этого является очень непростая система сборки проекта. Мы рассмотрели основные задачи, их общие решения и некоторые особенности системы сборки .NetMF. В следующей статья я расскажу о деталях реализации этой системы с помощью скриптов для MSBuild.