Рекламодатель: АО «Топ Системы»

ИНН 7726601967 ОГРН 1087746953557

Рекламодатель:
ООО «С3Д Лабс»

ИНН 7715938849 ОГРН 1127747049209

7 - 2022

Разработка кроссплатформенного ядра геометрического моделирования

Анна Ладилова, 
руководитель отдела IТ-инфраструктуры C3D Labs
Анна Ладилова,
руководитель отдела IТ-инфраструктуры C3D Labs

В статье рассматриваются основные этапы и особенности портирования геометрического ядра C3D на различные ОС и платформы, а также результаты по его запуску на платформе «Эльбрус».

С самого основания C3D Labs 10 лет назад мы занимаемся вопросами кроссплатформенности наших программных компонентов. Тема эта бесконечна: год за годом мы улучшаем процессы разработки, добавляем поддержку всё новых операционных систем и сред разработки. Так, в августе 2021 года мы впервые выпустили версию геометрического ядра C3D для отечественной операционной системы Astra Linux, пополнив список поддерживаемых дистрибутивов Linux. Не останавливаясь на достигнутом, в конце того же года мы начали процесс портирования ядра на российскую платформу «Эльбрус» и уже получили первые результаты.

На данный момент ядро геометрического моделирования C3D может быть использовано в разработке ПО на широком спектре операционных систем: кроме Windows — это MacOS, IOS, FreeBSD и несколько Linux­дистрибутивов (рис. 1). Также SDK ядра предоставляет большое разнообразие компиляторов: MSVC 2012 — 2019, GCC 4.8 — 7.2, Clang 6.0 — 10.0.

Рис. 1. Поддерживаемые ядром C3D Toolkit ОС, платформы и компиляторы

Рис. 1. Поддерживаемые ядром C3D Toolkit ОС, платформы и компиляторы

Основные этапы и особенности портирования ядра на различные ОС и платформы

Рабочее окружение

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

На этапе подготовки уже могут возникнуть первые препятствия: например, нужной версии компилятора нет в поставке операционной системы, версия CMake может быть ниже требуемой, некорректно определяются пути к зависимостям и т.д. К счастью, здесь отсутствуют какие­либо подводные камни, однако с упомянутыми выше проблемами все же приходится разбираться.

Следующим этапом является генерация проекта с помощью CMake. Здесь появляется очередной блок проблем, связанный с тем, что в некоторых окружениях требуются специальные настройки CMake. В частности, такая ситуация возникала на MacOS, поэтому приходилось специально создавать целый блок настроек RPATH. Как оказалось, поведение линковщика в MacOS отличается от стандартного последовательностью путей, в которой он ищет зависимые библиотеки. Также через CMake настраиваются различные флаги компиляции, но необходимость флагов обычно определяется уже последующими шагами, которые описываются ниже.

Наконец, мы подошли к сборке библиотеки. Проблемы, которые возникали у нас на этом этапе, можно разделить на два блока, обусловленных особенностями операционной системы и компилятора.

Особенности операционных систем

От операционной системы в основном зависят пути к стандартным библиотекам. Мы используем реализацию стандартных библиотек libc/stdlibc++. Например, в коде ядра при низкоуровневой отладке выделения памяти используются функции, которые в одной операционной системе определяются в файле malloc/malloc.h, в другой — в stdlib.h, в третьей — в malloc.h, либо могут вызываться разные функции в зависимости от ОС. Подобные проблемы были решены путем использования макросов препроцессора, например:

#if defined(C3D_MacOS)
#include <malloc/malloc.h>
#elif defined(C3D_FreeBSD)
#include <stdlib.h>
#else
#include <malloc.h>
#endif

Кроме того, геометрическое ядро отвечает за чтение и сохранение данных, поэтому неизбежно приходится учитывать особенности систем, чтобы корректно открывать файл по заданной строке. В одних системах используется WCHAR­представление путей, в других — TCHAR. Кроме того, вызовы операций чтения/записи находятся в разных заголовочных файлах. Более серьезная проблема заключается в том, что данные, в том числе строковые, должны читаться и записываться в файл одинаково, независимо от операционной системы и ее разрядности. Однако, как известно, размер типа wchar_t зависит от платформы, а сама стандартная строка определяется опциями компиляции.

Для удобства разработчиков принято соглашение о введении собственного типа для работы со строками и предоставлены методы преобразования c3d­строк в std::string и обратно, в пути и т.д. Эти методы скрывают в себе директивы препроцессора.

Аналогичным образом решена проблема для встроенных типов данных, допускающих разный размер в зависимости от платформы.

Особенности компиляторов

Зависимость от компилятора проявляется в основном в различной поддержке стандартов разными компиляторами и различной обработке выражений, не оговоренных стандартом. Так, на первых этапах работы по переносу кода на Linux мы столкнулись с проблемой, когда написанный код не соответствовал стандарту, но при этом великолепно работал с компилятором от Microsoft и совершенно не компилировался GCC. В связи с этим приходилось вносить в него правки, делать определенные выводы и более внимательно относиться к стандартам языка.

Поскольку мы предлагаем пользователю широкий выбор средств компиляции, то приходится поддерживать совместимость со старыми стандартами языка C++. Однако писать весь код на старом стандарте тоже неправильно, поэтому пришлось искать некоторый компромисс. В результате нашими разработчиками была проделана большая работа по выявлению кода различных стандартов — c++17, c++14, c++11 и более ранних, после чего внедрен механизм, позволяющий писать универсальный код.

Приведем пример. Спецификатор constexpr поддерживается, начиная со стандарта c++11. Соответственно, если для современного кода допустима запись

constexpr size_t VAR = 100;

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

const size_t VAR = 100;

и никак иначе. Нашим решением является определение макросов стандартов и макроса препроцессора, зависящего от стандарта:

#ifdef C3D_STANDARD_CXX_11
#define c3d_constexpr constexpr
#else
#define c3d_constexpr const
#endif

Теперь при корректном определении параметра C3D_STANDARD_CXX_11 код c3d_constexpr size_t VAR = 100; будет работать при любом используемом стандарте, и разработчику не нужно об этом задумываться или загромождать код препроцессорными директивами — это делается единожды. Кстати, подходящий макрос стандарта определяется автоматически из опроса компилятора.

Особое место занимает многопоточность. Библиотека C3D использует стандарт OpenMP для реализации многопоточности. Однако не во всех операционных системах OpenMP включена в базовую поставку, поэтому приходится писать код, универсально подходящий как для сборки с включенной опцией openmp, так и без нее. Так, например, возникла проблема при портировании ядра на платформу E2K: как оказалось, используемый компилятор LCC понимает не все директивы OpenMP — и с этим тоже пришлось разбираться.

Вообще проблемы с компиляцией в основном возникают при портировании на совершенно новую для нас систему или платформу. На данный момент, например, сборка для Linux у нас налажена так, что добавить еще один дистрибутив или компилятор к списку поддерживаемых не составляет никакого труда. Так, к примеру, поддержку Astra Linux мы реализовали буквально за один день. В то же время с портированием на «Эльбрус» пришлось изрядно повозиться.

Проверка результата

Наконец, когда сборка завершена и файл библиотеки получен, наступает время тестирования итогового продукта. Бывает, что тесты выявляют в исходном коде какие­то неочевидные ошибки, которые приводят к некорректной работе приложения. К сожалению, мы не обладаем знанием общих методов устранения таких ошибок, поэтому каждая из них решается в индивидуальном порядке. Бывает и другая ситуация, когда результат слегка отличается от ожидаемого. Дело в том, что геометрическое ядро, построенное на собственных сложных алгоритмах и вычислительных методах, а также использующее возможности стандартной библиотеки STL и встроенные математические функции, является довольно хрупким продуктом. Практически невозможно обеспечить абсолютно идентичную работу библиотеки, собранной разными компиляторами. Поэтому мы считаем результат удовлетворительным, если он с заданной точностью совпадает с некоторым эталоном.

Доставка ядра до конечного пользователя

Все описанные выше шаги выполняются вручную лишь однократно, после чего налаживается схема автоматической сборки и тестирования ядра C3D. Автоматизация сборки для Linux­систем основана на технологии Docker. Таким образом, когда нужно добавить поддержку очередного компилятора с определенным системным окружением, мы создаем контейнер с базовой системой, устанавливаем в него все зависимости, настраиваем и запускаем в эксплуатацию. Сборки для Windows, MacOS и FreeBSD выполняются на целевой операционной системе.

В наших планах — реализация кроссплатформенной сборки, которая должна ускорить процесс, так как сможет задействовать более мощные машины. Не без оснований полагаем, что на этом пути могут встретиться подводные камни.

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

В качестве инструмента непрерывной интеграции мы используем BuildBot, который по сигналу от системы управления версиями запускает все необходимые сборки, а по их завершении — различные тесты: от юнит­тестирования до длительных регрессионных тестов. Пока такая схема себя оправдывает и уже позволила значительно сократить сроки предоставления дистрибутива конечному пользователю.

Особенности портирования на платформу «Эльбрус»

Процесс портирования геометрического ядра C3D на отечественную программно­аппаратную платформу «Эльбрус», разрабатываемую компанией МЦСТ, мы инициировали в конце 2021 года. Процессоры данной модели используют набор команд типа RISC (Reduced Instruction Set Computer) и имеют собственную архитектуру E2K. Последняя относится к типу VLIW, то есть имеет длинную машинную команду.

Стоит отметить, что большинство современных процессоров основано на наборе команд типа CISC (Complicated Instruction Set Computer) и имеет архитектуры x86_64 или arm. Из сказанного выше следует, что архитектура E2K отличается рядом особенностей по сравнению с другими архитектурами, что создает определенные сложности при портировании. Далее будут перечислены некоторые из этих сложностей.

Техника кросс­компиляции

Впервые мы столкнулись с техникой так называемой кросс­компиляции в связи с отсутствием у нас собственного компьютера на базе «Эльбрус». В результате возникло требование к собираемой на базе «Эльбрус Линукс» (рис. 2) с архитектурой x86_64 библиотеки ядра: она должна работать на машинах с архитектурой E2K. Поставленную задачу можно было решить путем использования кросс­компиляции: на машине­хосте с архитектурой x86_64 устанавливается окружение целевой машины с архитектурой E2K, после чего с помощью специального кросс­компилятора собирается библиотека с системными зависимостями целевой архитектуры. С задачей мы справились и теперь планируем применять полученные навыки при сборке ядра на других дистрибутивах Linux.

Рис. 2. «Эльбрус Линукс»

Рис. 2. «Эльбрус Линукс»

Собственный компилятор

«Эльбрус» использует собственный системный компилятор LCC. Несмотря на уверения разработчиков платформы, что командный интерфейс компилятора совпадает с интерфейсом GCC, полная совместимость все равно отсутствует. Кроме того, LCC не может «угнаться» за поддержкой новых стандартов C++ и поддерживает только c++14. Еще одна проблема заключается в том, что нет четкого описания поддерживаемых особенностей. Например, по заданной версии GCC можно определить, какие особенности языка поддерживаются, но в документации (руководстве по эффективному программированию на платформе «Эльбрус») указана совместимость с GCC лишь для старой версии компилятора, а своего полного списка «фич» у LCC нет. Нельзя не отметить и ограничения (описанные в документации) при использовании прагм или встроенных команд GCC.

Особое внимание обратим на проблему многопоточности, связанную с неполной поддержкой платформой E2K стандарта OpenMP. Так как компилятор LCC понимает далеко не все его директивы, отдельные участки кода пришлось менять для обеспечения корректной компиляции. Например, после num_threads не может идти вычисляемое выражение. Поэтому блоки типа:

#pragma omp parallel num_threads ((int)viewCount) if (useParallel)

пришлось заменить на:

int vc = (int)viewCount;
#pragma omp parallel num_threads (vc) if (useParallel)

Кроме того, к примеру, не поддерживается директива #pragma omp for и т.д.

Поведение компилятора значительно отличается от GCC, причем логика работы не всегда понятна. В логе компиляции можно увидеть неожиданные уведомления, которые не возникают в других компиляторах. При линковке мы также получали необъяснимые ошибки. Одна из них, например, заключалась в «ругани» на отсутствие символов, которых в принципе не было и не должно было быть в коде. В конечном счете, после необходимой адаптации исходных текстов нам удалось собрать и запустить библиотеку C3D на «Эльбрус Линукс» с архитектурой E2K (рис. 3).

Рис. 3. C3D умеет работать на «Эльбрусе»

Рис. 3. C3D умеет работать на «Эльбрусе»

Работоспособность ядра под «Эльбрус»

Следующий этап, к которому мы приступили, — проверка элементарной работоспособности ядра. Для начала нам было необходимо обеспечить работу базовой функциональности по построению геометрических объектов. И здесь как раз проявились основные сложности, связанные с особенностями архитектуры E2K. В качестве примера можно привести организацию работы с памятью: по всей видимости, на уровне низкоуровневых операций очень много нюансов, приводящих к некорректному поведению библиотеки. Работы в этом направлении, а также в части достижения требуемой производительности еще продолжаются.

***

В заключение отметим, что мы не останавливаемся на достигнутом результате: постоянно расширяется список поддерживаемых систем, улучшается качество кода, ускоряется процесс доставки ядра пользователю. В ближайших планах у нас стоит поддержка последних версий UNIX­подобных систем (Ubuntu или Debian, Scientific Linux, Astra Linux) и «свежих» версий компиляторов GCC и Clang. Кроме того, мы собираемся к имеющемуся «зоопарку» Linux­систем добавить еще несколько, например Alpine Linux. 

Регистрация | Войти

Мы в телеграм:

Рекламодатель:
ООО «Нанософт разработка»

ИНН 7751031421 ОГРН 5167746333838

Рекламодатель: АО «Топ Системы»

ИНН 7726601967 ОГРН 1087746953557