В настоящее время все меньше смысла в разработке программ, работающих на одной платформе. Собственно, если задуматься, то практически любая современная программа взаимодействует с облачными серверами, сервисами обновлений, продаж и т.д., и является, таким образом, распределенной.
Это понимание не является общепринятым. И мы, как правило,разрабатываем программные системы по частям, используя разные среды разработки, языки и библиотеки на разных платформах, и не видя программную систему, как целое.
В лучшем случае, мы идейно остаемся на уровне кроссплатформенного программирования, то есть изготовления программ, которые могут быть запущены на нескольких платформах.
На мой взгляд, чтобы соответствовать современным требованиям, мы должны перейти на мультиплатформенное программирование, то есть на разработку распределенных программ, части которых работают на разных платформах. Естественно при этом требовать, чтобы любая часть могла работать на любой платформе, обладающей достаточными ресурсами.
Постановка задачи
Около года назад мы поставили себе задачу создания экспериментальной технологии разработки мультиплатформенных программ, воплощенную в систему (среду) разработки «Вир-2», на основе ранее созданной среды разработки «Вир-1».
Говоря простыми словами, технология должна позволить разрабатывать мультиплатформенную программу, часть которой работает на сервере/в облаке, часть на мобильных устройствах, часть на специализированных устройствах (фитнес-браслет, смарт-часы), а часть на устройстве IoT. При этом разработка всех частей программы должна вестись в одной среде разработки, так, чтобы разработчик мог «почти» не обращать внимания на то, на каком устройстве части этой программы будут исполняться. Понятно, что нельзя не учитывать наличие специфичного оборудования и производительность, но все остальное должно быть несущественно.
Все части программы должны разрабатываться в рамках одного проекта и из одних и тех же компонент. При этом речь не идет о разработке нового языка программирования или о выборе одного из существующих языков.
Мы должны уметь использовать компоненты, написанные на любых языках, при условии, что они «совместимы» со средой разработки (или помещены в совместимую «обертку»). То есть речь идет не о переписывании всего, что уже сделано, а о некоторой объединяющей экосистеме, в которой могут использоваться сильные стороны и части уже существующего ПО.
Очевидно, что создание такой технологии и среды разработки является весьма сложным делом, поэтому надо использовать по максимуму все то, что уже есть.
В первую очередь, это наша среда разработки «Вир-1» [3] – среда сборочного программирования, которая показала свои достоинства в ходе 10-ти летней эксплуатации (подробнее ниже). «Вир-2» – это развитие среды «Вир» с добавлением нескольких новых свойств.
Во-вторых, не менее важным является использование наработок проекта LLVM [4], без которых разработка «Вир-2» в разумное время была бы невозможна.
В-третьих, мы учли опыт, полученный при изучении и использовании инструментов разработки кроссплатформенных программ (Xamarin, Marmelade, Lazarus) и кроссплатформенных библиотек (например, SDL2). Отдельно надо отметить экспериментальный проект Google NaCl (Native Client): технология запуска нативного кода в браузерах, основанная на LLVM и кроссплатформенном Pepper API.
Перед тем, как перейти непосредственно к описанию «Вир-2», опишем, на достаточном уровне, две основные составляющие части «Вир-2»: «Вир» и LLVM.
Среда разработки «Вир» – первая составляющая «Вир-2»
Около 10 лет назад мы начали делать «Вир», решая задачу ускорить и упростить разработку программ, а также повысить надежность за счет максимального использования сборочного программирования. Сборочное программирование позволяет собирать (большую часть) программы из стандартных компонент, дописывая недостающие и постоянно добавляя компоненты в репозиторий стандартных компонент.
Еще одно принципиально важное понимание, повлиявшее на «Вир» - это понимание того, что программа – это не одноразовое изделие, скорее это долгоживущий организм, который существенно модифицируется в ходе своего жизненного цикла. Соответственно, задача модификации программ, это более важная задача, чем начальная разработка. Отсюда несколько существенных требований к Виру с точки зрения модификации программ:
1. Иметь возможность максимально использовать то, что уже было сделано (нужна не только сборка, но и разборка программы на части в любой точке жизненного цикла),
2. Архитектурную целостность программы должна сохранятся в течении всего жизненного цикла, 3. Должно быть обеспечено упрощенное понимание программы за счет явной структуры. Уменьшая потребность в документации и избегая обычного расхождения между документацией и кодом программы и исходной архитектурой, и кодом.
Мы понимали экспериментальный характер разработки, так как идеи, которые проверялись, существенно выходили за рамки традиционных технологий программирования. И это, естественно, приводило к тому, что инструменты и подходы многократно менялись.
При этом основные идеи и подходы, заложенные в начале разработки среды «Вир», сохранялись неизменными, это:
1. Явная схема программы;
2. Сборка программ из бинарных компонент;
3. Репозиторий стандартных компонент;
4. Независимость программы от ОС.
Разберем подробнее эти идеи и их реализацию.
Явная схема программы
В большинстве случаев, современные программы в бинарной форме слабо структурированы. На стадии проектирования могут быть использованы различные структурные методы представления архитектуры. Далее, вручную или с помощью инструментов, строится исходный код, в котором архитектура размазана. И дальше, компилятор, особенно сильно оптимизирующий, удаляет оставшиеся следы архитектуры.
Как правило, из бинарной программы восстановить архитектуру программы невозможно. Более того, так как далее разработка продолжается, как правило, на уровне исходного кода, то соответствие между архитектурой программы и исходным кодом теряется полностью, в лучшем случае, частично.
Единственным средством явного структурирования программы являются DLL (SO), но использование DLL накладывает существенные ограничения на способы разработки и развития программ. Вплоть до того, что, например, в языке Go постулируется статическая сборка программ, чтобы не сталкиваться с проблемами динамической сборки.
Рисунок 1. Пример модульной схемы, модуль А – головной модуль программы
Альтернатива DLL существовала в реализациях модульных языков программирования, например, в системе Оберон [5] (язык реализации Оберон) или в OS Excelsior iV [6] (Модула-2). В обоих случаях, использовалась раздельная компиляция модулей и динамическая загрузка. При запуске программы, которая была представлена головным модулем программы, динамически подгружались используемые (импортируемые) модули, кроме тех, которые уже были загружены.
Схема работающей программы при этом сохранялась в виде таблиц импорта для каждого модуля. Схему можно было извлечь из бинарных образов модулей и симфайлов, в которых хранилась информация об экспорте/импорте каждого модуля.
К сожалению, вместо развития модульных языков, которое могло привести к устранению их недостатков, индустрия пошла по пути использования слабоструктурированных языков, ярким примеров которых является C++.
Вернемся к схеме программ, характерной для модульных языков. Для них схема – это дерево, корнем которого является головной модуль программы, а переходы к узлам – это использование модуля (импорт), рис. 1.
Перечислим недостатки модульной схемы:
1. Устройство схемы или направление роста дерева. Корень дерева в модульной схеме – это модуль верхнего уровня, то есть модуль, обращенный к пользователю (для нас не важно, пользователь – это человек или другая программная часть). А это приводит к тому, что при любом изменении функциональности, корень дерева меняется. И это еще полбеды, гораздо хуже, что головной модуль становится узким местом при изменениях программы. Гораздо естественнее для «живой» модифицируемой программы схема, в которой корень находится внизу, и из него разворачивается функциональность.
2. Статичность схемы. Во-первых, изменение схемы делается только добавлением импорта в модуль и компиляцией. Динамическое изменение/построение новой схемы невозможно. Во-вторых, для использования компоненты необходимо при разработке иметь доступ к описанию её интерфейса. А если интерфейс изменился, то необходима перекомпиляция. Казалось бы, что этот недостаток преодолевается использованием средств ООП – описанием базового класса и наследованием (и еще нужна фабрика объектов или подобные механизмы). Да, но только частично. Хотя бы базовый класс должен быть описан во время разработки использующей компоненты. А при использовании базового класса мы постоянно сталкиваемся с типичной проблемой CLOP (class-oriented programming) языков – изменение в базовом классе приводит к перекомпиляции всех наследников. Еще более важно, что как только добавляются классы, теряется простота и ясность модульной схемы.
3. Следующий недостаток модульной схемы, о котором подробнее будем говорить позже, это то, что все узлы в модульной схеме одного и того же уровня. Понятно, что модули могут быть разного размера, но это не создает вложенность. В схеме программы должна присутствовать вложенность иерархий (например, дерево деревьев), иначе применимость схемы будет существенно ограничена.
Реализуя явную схему программы в среде «Вир» нам удалось избежать перечисленных выше недостатков модульных схем. Рассмотрим подробнее.
Устройство схемы в среде «Вир»
Схема программы в «Вире» – это дерево деревьев, корень программы – это самая низкоуровневая компонента, содержащая инструменты программы, необходимые для запуска программы. Далее разворачивается дерево компонент высокого уровня. Компоненты высокого уровня мы называем «рабочими столами». При этом каждый рабочий стол – это дерево компонент. Дерево обращено к пользователю кроной. Ближе к корню располагаются более «сервисные» компоненты. Ближе к кроне компоненты, более ориентированные на пользователя, не важно, кто является пользователем – человек либо программа.
Принципиально важно то, что схема отделена от компонент. Схема первична, компоненты вторичны. Если у нас есть схема программы, то любую компоненту можно заменить на другую без изменения или компиляции. Программа продолжит работать, если эти компоненты, старая и новая, «совместимы» (о совместимости надо говорить отдельно).
Рассмотрим пример схемы программы, рис. 2. На верхнем уровне схема состоит из рабочих столов (РС). Каждый рабочий стол определяется поддеревом.
Схема в «Вире» позволяет добавлять функциональность не только статически (во время разработки), но и «на лету», например, за счет скачивания дополнительных компонент из облака. Так можно добавить как РС, так и компоненту в под-дерево РС.
Рисунок 2. Пример схемы программы
Динамический способ используется, например, для реализации механизма «дополнений» (add-ons), используемых в программах, сделанных в «Вире». Причем, этот механизм работает не для конкретной программы, а может быть применен в любой из создаваемых программ.
Взаимодействие компонент
Так как, возможно, не все хорошо знакомы с проектом LLVM, приведу краткое описание с официального сайта (llvm.org): "The LLVM Project is a collection of modular and reusable compiler and toolchain technologies".
Любопытно, что изначально название проекта «LLVM» было аббревиатурой от "Low-Level Virtual Machine". Но проект ушел так далеко, что исходное название ему совсем не подходит. LLVM теперь – это не аббревиатура, а полное название проекта, как бы странно оно не звучало без гласных. Заметим, что подход авторов к названию хоть и, несомненно, оригинальный, но не дотягивает до оригинальности рекурсивных аббревиатур. Например: GNU, как известно, это рекурсивная аббревиатура GNU's Not Unix. Впрочем, llvm звучит вполне похоже на команды внутреннего представления (LLVM IR), которое является основой всего проекта, например: lshr или cmpxchng. Так что, на самом деле, чувство вкуса авторам LLVM не изменило.
Чтобы от шуток перейти к серьезному разговору, напомню, что в 2012 году основные разработчики LLVM (Chris Lattner, Evan Cheng, Vikram Adve) были удостоены премии ACM Software System Award, которая присуждается только одной программной системе в год.
Что же все-таки такое LLVM? Говоря простым языком, LLVM – это набор технологий, библиотек и утилит для построения компиляторов, оптимизаторов, верификаторов и других подобных программ. Основой LLVM является внутреннее представление – LLVM IR (Intermediate Representation) [8].
С использованием LLVM для реализации любого языка программирования на любую, поддерживаемую проектом платформу (а это все современные платформы), достаточно разработать только анализатор (front-end), который строит IR. Далее, достаточно подключить оптимизаторы и генераторы кода, входящие в LLVM Core Library.
Известным примером использования LLVM для построения компилятора является Clang (LLVM native C/C++/Objective-C compiler), который, собственно, и есть анализатор, строящий IR, и, далее, запускающий оптимизатор и генератор кода для целевой платформы.
Кроме C/C++/Objective-C LLVM используется в компиляторах языков Ruby, Python, Haskell, Java, D, PHP, Pure, Lua и других.
Как архитектор многоязыковой компилирующей системы XDS (eXtensible Development System) [9] я могу оценить высокое качество решений, заложенных в основу LLVM. В том числе, качество IR, уровень которого, на мой взгляд, удачно выбран (не слишком высокий и не слишком низкий). В XDS, в качестве внутреннего представления, использовалось абстрактное синтаксическое дерево, что приводило к сложностям из-за слишком высокого уровня промежуточного языка.
LLVM существенным образом упрощает разработку мультиплатформенных программ, так как избавляет от необходимости искать решения для генерации кода на разные CPU.
Вир-2
После того, как рассмотрели составляющие части, вернемся к разговору о технологии разработки мультиплатформенных программ.
В «Вир-2» мы используем отработанную технологию сборки программ, с очевидными изменениями, связанным с использованием LLVM. Перечислим их кратко:
• На этапе разработки словарные статьи переводятся в LLVM IR (а не в код CPU)
• Каждый инструмент переводится в IR для специфического кода со ссылками на IR для используемых словарных статей
• На этапе сборки готовой программы, для каждой кодовой части проверяется наличие и актуальность кода в кэше для конкретной платформы и при необходимости выполняется генерация кода по IR.
Соответственно, в репозитории хранится IR, а готовый код компонент кэшируется для ускорения сборки программы.
Работа, связанная с переходом на LLVM является достаточно простой с точки зрения понимания. Существенно более сложной является задача по созданию среды исполнения, которая позволит исполнять полученные программы (части программ) на разных платформах.
Очевидно, что:
- Среду исполнения нельзя сделать, её можно только делать, добавляя новое, и убирая совсем старое;
- Для разных предметных областей среда приложения будет частично пересекаться, а частично различаться;
- Среда исполнения должна легко расширяться;
- В ней должны быть разные варианты интерфейсов, не должно быть единственной альтернативы — только так и не иначе. То есть, мы изначально должны говорить не о наборе неких функций или модулей (POSIX, WIN32, NaCl Pepper), а о том, каким способом должен строиться «переходник» (abstraction layer) между приложением и базовым слоем софта на устройстве. Мы полагаем, что переходник не должен быть монолитом, а должен собираться (автоматически) под каждое приложение (часть приложения) с грануляцией на уровне функций. Некоторый функционал добавляется в переходник, если он нужен для работы хотя бы одной компоненты приложения. На первом этапе мы предполагаем в «Вир-2» создание сред исполнения для трех платформ: Windows, Linux, Android. Для реализации используются насколько возможно кроссплатформенные библиотеки, покрывающие нужную функциональность. После проверки работы среды «Вир-2» на этих платформах, мы планируем увеличить количество платформ.
Обучение программированию
Одно из потенциальных использований «Вир-2», которое мы собираемся активно продвигать – это обучение программированию, начиная с детского возраста – «программирование вторая грамотность».
Принципиальная особенность Вира – это сборка, которой можно обучать раньше, чем традиционному программированию. Упомянем, в качестве примера, Scratch-2 – среду визуального сборочного программирования для детей.
В отличие от систем, подобных Scratch-2, мы собираемся сделать открытую среду разработки для детей, то есть среду, позволяющую использовать окружающие детей устройства. Частично об этом говорится в [2].
Любопытно, что использование Вир-2 позволит ребенку оставаться в одной среде разработки.
Если привести аналогию, то ребенок сначала делает детскую машинку из LEGO, а потом собирает настоящий автомобиль из настоящих деталей. В виртуальном мире это возможно, как и возможна программа, которая начата, как система управления детской машинкой, а потом развивается до управления автомобилем или космическим кораблем.
Переходя от аналогии к Вир-2: ребенок сначала собирает программу из готовых компонент в визуальном редакторе, а потом переходит к программированию компонент и сборке их любым удобным для себя образом.
Заключение
Разговор о технологии разработки мультиплатформенных программ должен быть практическим. В целом мы понимаем, что комбинация проверенных технологий, подходов и инструментов позволит сделать систему разработки мультиплатформенных программ. Но только на практике можно убедиться в качестве решения и в том, что технология даст существенный прирост скорости разработки и качества программ. Задачу проверки подхода мы решаем разработкой «Вир-2».
Разработка «Вир-2» носит экспериментальный характер, и мы намерены выкладывать результаты наших экспериментов в открытый доступ для того, чтобы желающие могли подключится к работе, как с критическими замечаниями, так и с практическим вкладом.
Для начала, мы выкладываем ознакомительную версию «Вир-1» [10]. Для получения версии, напишите заявку на сайте: http://vir.synergetic-lab.ru
Литература
- Недоря А.Е., Буняк В.В. "Интернет — в поиске чистого воздуха" // http://digital- economy.ru/stati/internet-v-poiske-chistogo-vozdukha, 2017
- Недоря А.Е. "Забытое 40 лет назад новое, и как оно может изменить нашу жизнь" // Сборник трудов SoRuCom-2017, М.:ФГБОУ ВО «РЭУ им. Г. В. Плеханова», 2017, стр. 243-250.
- Недоря А.Е. «Вир» // заметки в блоге http://алексейнедоря.рф/?cat=13
- The LLVM Compiler Infrastructure, http://llvm.org/, 2017
- N. Wirth and J. Gutknecht. Project Oberon. Addison-Wesley, 1992. 488 p