Компилятор Rust считается одним из самых современных и безопасных инструментов для разработки программного обеспечения. Традиционно Rust использует LLVM как базовый backend для генерации кода, но команда разработчиков не останавливается на достигнутом и исследует возможности создания компилятора с использованием GCC — альтернативного инфраструктурного проекта для компиляции. Интерес к созданию компилятора Rust с использованием GCC обоснован желанием повысить гибкость, улучшить совместимость и расширить возможности самого Rust. В этой статье подробно рассмотрим процесс сборки Rust с кодогенерацией через GCC, выявим ключевые проблемные моменты и объясним, как их удалось преодолеть. Процесс, известный как bootstrap или загрузка компилятора, в данном контексте подразумевает построение Rust-компилятора без использования привычного LLVM, а вместо него с применением GCC.
Эта задача далеко не тривиальна, ведь необходима глубокая интеграция с ядром компилятора, поддержка всех возможностей Rust и корректный перевод кода в промежуточное представление для GCC. Bootstrap представлен в трех последовательных этапах. Сначала (Stage 1) исходно используется традиционный LLVM-компилятор Rust для создания rustc с поддержкой генерации кода через GCC. Затем (Stage 2) на базе этого GCC-кодогенератора производится повторная сборка самого Rust-компилятора, после чего результат запускается в Третьей стадии (Stage 3) для проверки консистентности и корректности работы. Идея, заложенная в трехступенчатом процессе, очень проста — если бинарные файлы, полученные на Stage 1 и Stage 2, совпадают, значит, компилятор, собранный с помощью GCC, эквивалентен LLVM-версии по функциональности.
Это важный критерий качества и стабильности нового подхода. Впрочем, на практике существует множество технических сложностей и уникальных багов, препятствующих полноценной работе на Stage 3. Особое внимание уделялось отладке проблем совместимости на уровне генерации кода. Одним из сложных препятствий стала поддержка рекурсивных функций с директивой inlining. В частности, атрибут #[inline(always)], который требует всегда инлайнировать вызов функции, оказался проблемным при использовании GCC, так как для рекурсивных функций обоснованно невозможно бесконечное встраивание самого себя.
LLVM пропускает рекурсивные вызовы при невозможности инлайна, воспринимая #[inline(always)] лишь как рекомендацию, а GCC же явно жаловался на такую ситуацию. Это привело к разработке решения, при котором для рекурсивных функций и их непрямых вызовов атрибут #[inline(always)] автоматически понижается до более мягкой версии #[inline]. Методика основывается на анализе промежуточного представления MIR (Mid-level Intermediate Representation), где просматриваются все вызовы внутри функции и проверяется, помечены ли они похожим образом. Если обнаруживаются циклы вызовов функций с #[inline(always)], то такие атрибуты демаркируются для предотвращения невозможных бесконечных инлайнов. Еще одной значимой проблемой стала некорректная реализация обработки 128-битных целочисленных switch-конструкций.
MIR в Rust представляет условный переход через SwitchInt, аналогичный оператору switch в языке С. Несмотря на то, что C обеспечивает поддержку 128-бит через сложные механизмы, API libgccjit, используемый для взаимодействия с GCC, не поддерживал создание 128-битных констант напрямую. В результате разработчики применили нестандартное решение — заменили switch на цепочку if-else, что хотя и менее эффективно, но компилятор GCC оптимизировал, превратив данные конструкции в эквивалентную низкоуровневую структуру переходов. Такое решение оказалось достаточно приемлемым на временном этапе, позволив продвинуться к успешной сборке Stage 2. Необходмиость обходных путей и компромиссов показала непростую природу интеграции сложных языковых конструкций Rust с GCC, у которого API для JIT-компиляции сравнительно ограничен.
В процессе возникали ситуации с внезапными аварийными завершениями (segfaults), причинами которых служили проблемы с выравниванием данных и неопределенным поведением при оптимизациях. Отладка таких багов требовала глубокого погружения в данные структуры, используемые внутри компилятора, такие как ValTree и ScalarInt — типы для представления констант в виде деревьев значений. Например, из-за использования атрибута #[repr(packed(1))] для структуры ScalarInt, которая содержит поле u128, возникает ситуация, при которой данные могут быть выровнены не по стандартным требованиям, что приводит к ошибкам при попытке выполнения векторных инструкций, которые требуют строгого выравнивания. Анализ показал, что при генерации кода для 128-битных типов не всегда корректно учитывается выравнивание, что приводит к падениям на этапе выполнения. Исправление заключалось в корректировке механизма установки выравнивания типа при генерации кода в libgccjit.
Вместо того, чтобы слепо применять выравнивание, привязку к 128-битным типам пришлось обрабатывать с учетом уже заданных атрибутов выравнивания, избегая избыточных преобразований. Это позволило устранить часть сбоев и приблизиться к стабильному построению Stage 3. Стоит отметить, что подобные подробности показывают не только сложность задач по кросс-компиляции и поддержки различных backend, но и важность понимания внутренней архитектуры Rust-компилятора. MIR и прочие абстракции дают мощные инструменты для трансформации и оптимизации кода, но их точная корректная реализация для каждого backend требует больших усилий и тщательного тестирования. Наконец, лишь преодолев все перечисленные трудности, авторы сумели достичь значимого прогресса в проекте, доводя Rust-компилятор с GCC-кодогенерацией до состояния, когда он успешно проходит большую часть процесса bootstrap.
Несмотря на то, что остаются вызовы с высокой загрузкой памяти и редкими переполнениями стеков, эти проблемы считаются поддающимися решению в дальнейшем. Подобная работа является ярким примером стремления к расширению экосистемы Rust, обеспечивая альтернативу в лице GCC и уменьшая зависимость от одного LLVM. Открытый характер проекта, а также активное взаимодействие в сообществах разработки Rust предоставляют площадку для обмена опытом и совместного поиска решений. Для заинтересованных разработчиков и исследователей рекомендуется следить за обновлениями в репозитории rustc_codegen_gcc, где публикуются последние улучшения и патчи. Также можно участвовать в обсуждениях на тематических каналах в Zulip, где ведется живая коммуникация между командами по развитию компилятора, что способствует оперативному обмену идеями и решениями.
Таким образом, создание компилятора Rust с использованием GCC открывает новые горизонты для развития языка и инструментов разработки, обеспечивая большую универсальность, устойчивость и возможности оптимизации. Каждый шаг в этой области — это не только вызов, но и вклад в будущее индустрии программирования.