Как скомпилировать rust

Обновлено: 08.05.2024

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

Cargo имеет два основных профиля: профиль dev используемый Cargo при запуске cargo build и профиль release используемый Cargo при запуске cargo build --release . Профиль dev определён со значениями по умолчанию для разработки, а профиль release имеет значения по умолчанию для релиз сборок.

Эти имена профилей могут быть знакомы по результатам ваших сборок:

Выводы для dev и release , показанные при сборке указывают, что компилятор использует разные профили.

Cargo имеет настройки по умолчанию для каждого из профилей, которые применяются, когда в файле проекта Cargo.toml нет разделов [profile.*] . Добавляя разделы [profile.*] для любого профиля, который вы хотите настроить, вы можете переопределить любое подмножество настроек по умолчанию. Например, вот значения по умолчанию для параметра opt-level для профилей dev и release :

Параметр opt-level управляет количеством оптимизаций, которые Rust будет применять к вашему коду, в диапазоне от 0 до 3. Применение дополнительных оптимизаций увеличивает время компиляции, поэтому, если вы находитесь в разработке и часто компилируете свой код, вам понадобится быстрая компиляция, даже если полученный код работает медленнее. Вот почему по умолчанию opt-level для dev равно значению 0 . Когда вы будете готовы выпустить свой код, лучше потратить больше времени на компиляцию. Вы будете компилировать в режиме релиза только один раз, но вы будете запускать скомпилированную программу много раз, поэтому режим релиза тратит больше времени на компиляцию кода, который работает быстрее. Вот почему по умолчанию opt-level для профиля release является значением 3 .

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

Этот код переопределяет настройку по умолчанию 0 . Теперь, когда мы запустим cargo build , Cargo будет использовать значения по умолчанию для профиля dev плюс нашу настройку для opt-level . Поскольку мы установили для opt-level значение 1 , Cargo будет применять больше оптимизаций, чем по умолчанию, но не так много, как при сборке релиза.

Полный список параметров конфигурации и значений по умолчанию для каждого профиля вы можете найти в документации Cargo.

Шак третий: rustc-interface

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

Так, осматриваемся и находим

Кстати тут же, недалеко, мы можем найти настройку механизма кодогенерации.

Быстренько посмотрим на наши сорцы и увидим что у нас прямо в сорцах есть 3 различных модуля кодогенерации. Что они делают? Превращают MIR в конечный код для системы компиляции. Открываем rustc-codegen-llvm и смотрим в README:

Ок, ну тут всё понятно, мы берём MIR и переделываем его в LLVM IR. После этого LLVM может скомпилировать код в конечный бинарник. Но погодите, помимо LLVM бекенда у нас есть ещё два других! Смотрим туда. rustc-codegen-ssa согласно документации, позволяет генерировать низкоуровневый код, который не будет привязан к определённому бекэнду (например, LLVM) и позволит в дальнейшем использовать другие системы компиляции.

Собственно говоря, прямо там же вы найдёте rustc-codegen-cranelift. То есть MIR в будущем может компилироваться через cranelift, который в идеале ускорит процесс компиляции. Ну это в будущем, пока что проект в процессе тестирования и работает не лучше, чем Газель без мотора.

Открываем модуль и смотрим, что происходит внутри:

Ага, вот тут мы берём быка за рога и начинаем разбирать исходный код на части. Далее, создаём и проверяем AST

И вот тут у нас начинается полное мясо. Прикол вот в чём, обычно компиляторы делают достаточно простую вещь - берёшь сорцы, проходишься по ним несколько раз, парсишь синтаксис, находишь ошибки, разбираешь на куски макросы, всё хорошо. rust в стародавние времена начинал именно так же. Но, со временем, была предложена новая модель компиляции. Запросы. Вместо того чтобы делать всю работу несколько раз подряд, давайте просто превратим проходы в запросы. Результат запроса можно сохранить в кеш. Если пользователь (программист) не менял ничего в определённом файле, то и компилировать его не надо. Если мы видим что делать это не надо, мы просто возвращаем данные из кеша запроса.

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

Давайте посмотрим на запросы, которые создаёт компилятор:

Парсинг, создание крейта, сбор HIR - всё это делается через запросы. Один момент про который полезно знать, это то что ещё не всё переписано на запросах.

В итоге у нас на выходе получается большая и толстая структура:

И как раз её можно дёргать для выполнения необходимых запросов.

Шаг седьмой: rustc_ty

И .\rust-master\compiler\rustc_middle\src\ty\mod.rs . Одна из самых больших частей компилятора занимается проверками системы типов после того, как у нас есть HIR. Какой тип будет у let mut a = 5; ? Вот на этот вопрос и ответит наша система работы с типами. Две основных структуры здесь:

Последняя тянется через весь процесс компиляции.

Файл просто огромный. Нам надо вычислить типы каждой переменной, замыкания и трейта. Сам модуль занимает более 3000 строк, не считая остальные файлы в директории.

Кстати, смотрим в rust-master\compiler\rustc_typeck\src\check\expr.rs

Хмм.. Если мы натыкаемся на брейк, после которого есть только один лейбл - rust то нужно запустить функцию fatally_break_rust .

Компилируем и запускаем:

Пасхалки они выглядят именно вот так.

Так, вычислили типы и теперь можем проверить что никто не пытается запихнуть строку в Int. Хорошо. Можно идти дальше.

Пробуем ручками

Ну что же, напоследок осталось написать простенькую программку, типа этого:

И начать её компилировать, только показывая все внутренности. Для начала есть замечательная опция компилятора, которая работает на любой версии:

Значит, запуская компиляцию следующим образом:

Мы получаем на выходе мириады различных форматов, включая сгенерированный ассемблеровский код, байткод и IR для LLVM, и даже челвоеко-читаемый MIR.

А если у вас есть nightly компилятор, то вы можете запустить

И полюбоваться вашим HIR, в то время как

Даст вам возможность посмотреть на то, как выглядит AST.

Постскриптум

А почему бинарник такой-то большой? Ну, на этот вопрос можно ответить легко. Залезаем вашим любимым дебаггером в .pdb файл и смотрим на указатели функций, которые тянутся в наш бинарник. Их много. Подсистема работы со строками, макросами и памятью. Плюс система ввода-вывода. Что, считаете что 150 килобайт - слишком много?

Шаг пятый: rustc-expand

В результате работы парсера мы получаем наш самый великий и могучий AST.

Всё это создаётся огромным макросом astfragments! в \compiler\rustcexpand\src\expand.rs

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

Шаг девятый: Проверка заимствования

Самая "страшная" функция rust это всем известный borrow cheker. Сам он живёт в

В rust-master\compiler\rustc_mir\src\borrow_check\mod.rs . Да, сам модуль такой же огромный и страшный, как и borrow checker. А вот тут, например, можно найти всю логику проверки заимствования при перемещении переменных rust-master\compiler\rustc_mir\src\borrow_check\diagnostics\move_errors.rs

Погружаемся в логово ржавчины. Как работает компилятор rust


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

Шаг шестой: rustc-middle

Куда ты завёл нас? Не видно ни зги! Простите, ребята, не варят мозги. Вернее, мозг начинает вариться. Сложность процесса увеличивается настолько, что просто читая коды дальше ходить страшно. Ладно, обратимся к инструкции для разработчиков - смотрим. Видим что после того как у нас появился AST мы можем заняться приведением его в приличный вид. Вернее, в HIR.

Этим как раз и занимается rustc-middle. Вернее, не только этим. Залезаем в исходники и видим что тут у нас есть HIR, MIR и Types.

Что же происходит в реальности? Ну, для начала мы начинаем обработку AST. Этим, кстати занимается ещё один модуль, rust_ast_lowering . Смотрим туда и находим достаточно длинный файл, в котором и происходит преобразование каждого элемента AST в HIR.

Здесь весь синтаксический сахар растворяется в чае и перестаёт быть сахаром. Так моя любимая for node in data превращается в

А вот здесь, как раз, всеми любимый оператор ? первращается в Try::into_result :

С HIR теперь можно работать…

Шаг седьмой: rustc_ty

И .\rust-master\compiler\rustc_middle\src\ty\mod.rs . Одна из самых больших частей компилятора занимается проверками системы типов после того, как у нас есть HIR. Какой тип будет у let mut a = 5; ? Вот на этот вопрос и ответит наша система работы с типами. Две основных структуры здесь:

Последняя тянется через весь процесс компиляции.

Файл просто огромный. Нам надо вычислить типы каждой переменной, замыкания и трейта. Сам модуль занимает более 3000 строк, не считая остальные файлы в директории.

Кстати, смотрим в rust-master\compiler\rustc_typeck\src\check\expr.rs

Хмм.. Если мы натыкаемся на брейк, после которого есть только один лейбл - rust то нужно запустить функцию fatally_break_rust .

Компилируем и запускаем:

Пасхалки они выглядят именно вот так.

Так, вычислили типы и теперь можем проверить что никто не пытается запихнуть строку в Int. Хорошо. Можно идти дальше.

Пробуем ручками

Ну что же, напоследок осталось написать простенькую программку, типа этого:

И начать её компилировать, только показывая все внутренности. Для начала есть замечательная опция компилятора, которая работает на любой версии:

Значит, запуская компиляцию следующим образом:

Мы получаем на выходе мириады различных форматов, включая сгенерированный ассемблеровский код, байткод и IR для LLVM, и даже челвоеко-читаемый MIR.

А если у вас есть nightly компилятор, то вы можете запустить

И полюбоваться вашим HIR, в то время как

Даст вам возможность посмотреть на то, как выглядит AST.

Шаг второй: rustc-driver

Ладно, всё выглядит слишком уж просто. Погружаемся дальше. rustc тянет за собой rustc-driver. Ныряем туда.

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

Да, в этом крейте файлов не так-то много, но что бы тут не творилось, на самом деле всё сводится к вызову методов в крейте под названием interface. Вышеприведённый код это и показывает. interface::run_compiler и поехали.

Что же произошло в rustc-driver? Мы собрали все конфиги. Подгрузили все файлы и нашли их местоположение в файловой системе. Создали замыкание, которое следит за процессом компиляции и запускает линкер после успешной компиляции. Запустили линтеры (если такие имелись) и приготовили сам компилятор к запуску. Давайте запускать.

Шак одиннадцатый: прощай, rust!

Полученный оптимизированный MIR можно теперь переделать в LLVM IR. Поехали. rustc-codegen-llvm создаёт LLVM-IR на базе MIR, который мы сгенерировали на предыдущем этапе. Здесь заканчивается rust и начинается llvm. Хотя, мы ещё не закончили с сорцами компилятора.

Тут можно найти пару интересных моментов, например rust-master\compiler\rustc_codegen_llvm\src\asm.rs содержит код для компилирования ассемблера напрямую из rust. Даже не замечал этого. Смотрим в документацию - есть такая поддержка в этом компиляторе!

Копаемся чуть глубже и находим rustc-target в котором видим различные дополнительные классы для работы с определённым ассемблером.

После того как кодогенерация завершена, мы можем передать IR в сам LLVM. rustc_llvm нам в помощь.

Вот, собственно говоря, и всё, ребята! LLVM за пределами нашей видимости. На моей операционной системе Visual Studio Build Tools берут на себя контроль и перегоняют LLVMIR в обычный бинарник.

Процесс компиляции в rust - это вам не мешки ворочать. Надо проверить неимоверное количество разных вещей. Вот что происходит с вашим кодом:

Он парсится из текста в AST.

AST обрабатывается и оптимизируется в HIR

HIR обрабатывается и оптимизируется в MIR.

MIR делает проверки заимствования и оптимизацию и перегоняется в LLVMIR.

LLVMIR компилируется на конечной платформе.

Шаг девятый: Проверка заимствования

Самая "страшная" функция rust это всем известный borrow cheker. Сам он живёт в

В rust-master\compiler\rustc_mir\src\borrow_check\mod.rs . Да, сам модуль такой же огромный и страшный, как и borrow checker. А вот тут, например, можно найти всю логику проверки заимствования при перемещении переменных rust-master\compiler\rustc_mir\src\borrow_check\diagnostics\move_errors.rs

Шаг пятый: rustc-expand

В результате работы парсера мы получаем наш самый великий и могучий AST.

Всё это создаётся огромным макросом astfragments! в \compiler\rustcexpand\src\expand.rs

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

Шаг десятый: Оптимизации

Про систему оптимизаций в rust можно писать отдельную книгу. Всё аккуратно сложено в rust-master\compiler\rustc_mir\src\transform . LLVM сам по себе не сможет оптимизировать некоторые высокоуровневые примитивы, о которых знает только rust. И вот тут мы как раз и занимаемся оптимизацией этих примитивов.

Шаг четвёртый: rustc-parse и rustc-lexer

Далее по тексту вы найдёте простую логику всех этих запросов. "Простая" логика заключается в вызове крейтов, которые её обрабатывают. Например, rustc-parse. Это крейт, который использует rustc-lexer. Лексер читает строки из файлов и преобразовывает их в очень простые токены. Токены передаются парсеру, который превращает их в Span и продолжает работу с кодом. Основной момент этого Span заключается в том, что к каждому элементу в дереве кода будет добавлена информация о том, в каком конкретно месте этот элемент записан в исходном файле. Когда компилятор будет сообщать об ошибке, вы увидите, где именно эта ошибка произошла.

Основная часть парсера запускается через вызов parse_crate_mod в rustc_parse\src\parser\item.rs . А дальше по тексту вы найдёте невероятное количество проверок синтаксиса, который этот парсер делает. Вот, например:

Напоследок

Понятно? Ну и хорошо.

Шаг первый: rustc

Открываем сорцы и наслаждаемся. Всё выглядит очень прилично и чисто. Тут, понятное дело, можно учиться тому как правильно разделять свой проект на куски и как правильно управлять кодом на rust. Собственно говоря, сразу понятно куда идти. Забираемся в compiler/rustc/src/main.rs и смотрим.

Всё только начинается. Держитесь.

Всё только начинается. Держитесь.

Хм. То есть точка входа в программу просто тянет jemalloc вызовы и запускает ещё две функции. Ну вот, всё. Теперь понятно как работает компилятор rust. Делов-то! Кстати, jemalloc это специальный менеджер памяти, изначально разработанный для FreeBSD в 2005 году. Основной упор был сделан на то, чтобы избежать фрагментации памяти при работе с этим аллокатором. В оригинальной версии он просто заменяет malloc. В 2007 году Firefox начал использовать этот менеджер для снижения расхода памяти, а ещё через пару лет он попал в Facebook.

Постскриптум

А почему бинарник такой-то большой? Ну, на этот вопрос можно ответить легко. Залезаем вашим любимым дебаггером в .pdb файл и смотрим на указатели функций, которые тянутся в наш бинарник. Их много. Подсистема работы со строками, макросами и памятью. Плюс система ввода-вывода. Что, считаете что 150 килобайт - слишком много?

Погружаемся в логово ржавчины. Как работает компилятор rust


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

Словарь

Далее по тексту я буду разбрасываться следующими терминами без удержи. Если вы знаете что это всё значит - хорошо. Если нет, убедитесь что подтянули свои знания, перед тем как заныривать.

LLVM - система компиляции которая состоит из компилятора и набора инструментов, позволяющая создавать фронт-энд для любого языка и компилировать его на множество различных платформ.

AST - (abstract syntax tree) древовидная репрезентация семантической структуры исходного кода. Каждый узел обычно показывает конструкцию, встречающуюся в коде.

IR (intermediate representation) - Структура данных, обычно используемая в кишках компилятора или виртуальной машины, для представления исходного кода программы. Такую структуру обычно оптимизируют и перегоняют в конечный код.

HIR (High Level IR) - IR высокого уровня. Это основная репрезентация кода, используемая в rust. Фактически это представление AST, которым компилятору удобно пользоваться.

MIR (Mid Level IR) - Это репрезентация HIR, которая намного ближе к LLVMIR.

LLVMIR (Language Independent IR) - фактически это высокоуровневый ассемблер, который не привязан к определённому языку или системе. Такой код удобно оптимизировать и после он передаётся компилятору.

Крейт, crate - Это то, что будет скомпилировано либо в библиотеку или бинарник. На выходе будет одна библиотека или бинарник, вне зависимости от того, сколько файлов входят в крейт.

ICE (Internal compiler error), ошибка компилятора.

Дальнейший текст подразумевает, что вы умеете программировать. Можно и не на rust.

Шаг первый: rustc

Открываем сорцы и наслаждаемся. Всё выглядит очень прилично и чисто. Тут, понятное дело, можно учиться тому как правильно разделять свой проект на куски и как правильно управлять кодом на rust. Собственно говоря, сразу понятно куда идти. Забираемся в compiler/rustc/src/main.rs и смотрим.

Всё только начинается. Держитесь.

Всё только начинается. Держитесь.

Хм. То есть точка входа в программу просто тянет jemalloc вызовы и запускает ещё две функции. Ну вот, всё. Теперь понятно как работает компилятор rust. Делов-то! Кстати, jemalloc это специальный менеджер памяти, изначально разработанный для FreeBSD в 2005 году. Основной упор был сделан на то, чтобы избежать фрагментации памяти при работе с этим аллокатором. В оригинальной версии он просто заменяет malloc. В 2007 году Firefox начал использовать этот менеджер для снижения расхода памяти, а ещё через пару лет он попал в Facebook.

Шаг шестой: rustc-middle

Куда ты завёл нас? Не видно ни зги! Простите, ребята, не варят мозги. Вернее, мозг начинает вариться. Сложность процесса увеличивается настолько, что просто читая коды дальше ходить страшно. Ладно, обратимся к инструкции для разработчиков - смотрим. Видим что после того как у нас появился AST мы можем заняться приведением его в приличный вид. Вернее, в HIR.

Этим как раз и занимается rustc-middle. Вернее, не только этим. Залезаем в исходники и видим что тут у нас есть HIR, MIR и Types.

Что же происходит в реальности? Ну, для начала мы начинаем обработку AST. Этим, кстати занимается ещё один модуль, rust_ast_lowering . Смотрим туда и находим достаточно длинный файл, в котором и происходит преобразование каждого элемента AST в HIR.

Здесь весь синтаксический сахар растворяется в чае и перестаёт быть сахаром. Так моя любимая for node in data превращается в

А вот здесь, как раз, всеми любимый оператор ? первращается в Try::into_result :

С HIR теперь можно работать…

The Rust Programming Language

Итак, когда Rust уже установлен можно приступать к написанию вашей первой программы. Общая традиция при изучении нового языка программирования - писать маленькую программу которая печатает в строке вывода "Hello, world!" . Давайте сделаем тоже самое.

Обратите внимание: данная книга подразумевает, что читатель должен быть знаком с командной строкой. Однако, Rust не выдвигает каких-либо специальных требований к тому, как вы пишете и по какому принципу храните код своих программ. По этому, если вы предпочитаете использовать интегрированную среду разработки (IDE) взамен командной строки терминала, чувствуйте себя спокойно и пользуйтесь тем, чем вам удобно. Разные IDE имеют разный уровень поддержки Rust, по этому не забывайте проверять документацию к выбранной IDE. Однако, не так давно команда Rust сфокусировалась на задаче большей поддержки языка в разных IDE и однозначно сделала определённый прогресс в данном направлении!

Создание папки проекта

Прежде всего начнём с создания директории, в которой будем сохранять наш код на языке Rust. На самом деле не важно, где сохранять наш код. Однако, для упражнений и проектов, обсуждаемых в данной книге, мы советуем создать директорию projects в вашем домашнем каталоге, там же и хранить в будущем код программ из книги.

Откройте терминал и введите следующие команды для того, чтобы создать директорию projects для хранения кода разных проектов, и, внутри неё, директорию hello_world для проекта “Hello, world!”.

Для Linux, macOS и PowerShell на Windows, введите:

Для Windows в CMD, введите:

Написание и запуск первой Rust программы

Теперь создадим новый файл, в котором сохраним исходный код программы, назовём его main.rs. Заметьте, что файлы с кодом на языке программирования Rust всегда заканчиваются расширением .rs. В случае, если вы захотите использовать более одного слова в названии файла, используйте знак подчёркивания в качестве разделителя. Например, возможно использовать именование hello_world.rs, но не рекомендуется использовать вариант helloworld.rs.

Теперь откроем файл main.rs для редактирования и введём следующие строки кода:

Название файла: main.rs

Листинг 1-1: Программа которая печатает Hello, world!

Сохраните файл и вернитесь обратно в окно терминала, чтобы скомпилировать и запустить нашу первую программу. На Linux или macOS для этого введите следующие команды:

В Windows, введите команду .\main.exe вместо ./main :

Независимо от операционной системы, строка Hello, world! должна напечататься в окне вашего терминала. Если вы не увидели вывода, вернитесь в часть "Решение проблем" "Troubleshooting" раздела "Установка" для получения помощи.

Если напечаталось Hello, world! , то примите наши поздравления! Вы написали программу на Rust, что делает вас Rust программистом — добро пожаловать!

Анатомия программы на Rust

Давайте рассмотрим в деталях, что происходит в программе “Hello, world!”. Вот первый кусок пазла:

Эти строки определяют функцию в Rust. Функция main имеет специальный смысл: она всегда является первым кодом, который запускается в исполняемой программе на Rust. Первая строка объявляет функцию с именем main у которой нет параметров и она ничего не возвращает. Если бы тут были параметры, они были бы записаны внутри круглых скобок, () .

Также заметим, что тело функции обёрнуто в фигурные скобки <> (curly brackets). В Rust тело любой функции оборачивается в фигурные скобки. Хорошим стилем является размещение открывающей скобки в строке объявления функции, оставляя пробел между ними.

На момент написания этих строк инструмент, выполняющий автоматическое форматирование, называется rustfmt и находится ещё в разработке. Если вы хотите использовать стандартный стиль во всех Rust проектах, то rustfmt отформатирует код в нужном стиле. Команда Rust планирует в конечном счёте включить данный инструмент в стандартный дистрибутив Rust, подобно rustc компилятору. Так что в зависимости от того, когда вы читаете данную книгу, rustfmt уже может быть инсталлирован на ваш компьютер! Загляните в документацию для деталей.

Содержимое функции main :

Эта строка делает всю работу в этой маленькой программе: печатает текст на экран. Можно заметить четыре важных детали.

Первая, не столь заметная, - в стиле Rust для отступа используются четыре пробела, а не знак табуляции.

Вторая - вызов println! является макросом. Если бы вызывалась функция, то она была бы введена как println (без восклицательного знака ! ). Более детального обсуждения макросов Rust мы коснёмся в 19 главе. Сейчас вам достаточно знать, что использование символа ! означает вызов макроса, а не обычной функции.

Третья - вы видите строку "Hello, world!" . Мы передаём эту строку как аргумент в макрос println! и, благодаря этому, строка выводится макросом на экран.

Четвёртая - в конце строки стоит точка с запятой ( ; ), которая означает, что выражение закончилось и следующее выражение можно начинать опять. Большая часть строк в Rust коде заканчивается точкой с запятой.

Компиляция и выполнение кода являются отдельными шагами

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

Перед запуском программы, её необходимо скомпилировать компилятором Rust с помощью ввода команды rustc и передачи имени вашего файла с исходным кодом, например так:

Если вы знакомы с C или C++, то заметили, что это весьма похоже на вызов компиляции при помощи gcc или clang . После успешной компиляции, Rust выдаст двоичный исполняемый файл.

Для того, чтобы посмотреть на исполняемый файл на Linux, macOS и в PowerShell на Windows достаточно выполнить команду ls в командной строке. На Linux или macOS будет отображено два файла. В PowerShell на Windows, как и в Windows CMD - три файла.

В CMD на Windows следует ввести следующие команды:

В обоих случаях видно: файл исходного кода с расширением .rs, исполняемый двоичный файл (main.exe в Windows, но main на всех других платформах), а в случаем использования Windows, ещё и файл включающий отладочную информацию с расширением .pdb.

Отсюда мы можем запустить нашу программу (исполняемый двоичный файл) main или main.exe соответствующей командой для Windows или иных ОС:

Если main.rs был с текстом “Hello, world!”, то строка Hello, world будет напечатана в терминале.

Если вы знакомы с динамическими языками вроде Ruby, Python или JavaScript, то, возможно, вам не доводилось выполнять компиляцию и запуск программы отдельными шагами. Rust является "заранее скомпилированным" (ahead-of-time compiled) языком. Это означает, что можно скомпилировать программу и передать исполняемый файл ещё кому-то, и получатели смогут его запустить, не имея локально установленного Rust. А если вы отдаёте файл .rb, .py или .js, то получателям необходимо иметь установленную реализацию для Ruby, Python или JavaScript соответственно. Но в этих языках нужна только одна команда для компиляции и запуска программы. Что ж, всё является компромиссом в дизайне языков.

Простая компиляция с помощью rustc подходит только для простых программ, но по мере того, как ваши проекты становятся сложнее, вы захотите управлять всеми опциями и упростить обмен своим кодом с другими. Далее мы представим инструмент Cargo, который поможет в написании настоящих, а значит и более сложных, программ на Rust.

Начало

Поехали. Мы будем лезть нашими ручками в сам компилятор и смотреть на его исходники. Для начала нам понадобятся кое-какие инструменты. Ставим чистую виртуальную машину с Windows 10. Идём в интернеты и льём следующее:

Сорцы компилятора. Достаются с github. Можно лить просто zip, ибо обратно коммитить мы ничего не будем.

Установщик компилятора. Любая свежая стабильная версия подойдёт.

Не будем мучиться, давайте, заодно, установим nightly компилятор.
rustup toolchain install nightly --allow-downgrade --profile minimal --component clippy rustup default nightly

Guide to Rustc Development. Инструкция по разработке компилятора. 460 страниц. Не хило. Сохраняем pdf.

Ну и хорошо. Этого, для начала достаточно. Отключаемся от проводного интернета, хватаем ноутбук и идём на веранду, сидеть и погружаться. Начинаем погружаться, понимаем что будет глупо говорить о компиляторе, если мы не скомпилируем хоть что-то. Ок, так и сделаем.

Ок, это было просто. Но мы не будем использовать cargo для самой компиляции. Используем компилятор напрямую. Но я же на надо cargo издеваюсь, так ведь?

Отступление по теме

Как не надо устанавливать rust

Как не надо устанавливать rust

Чего? Так, сам по себе компилятор всё собрал, но ругается на отсутствие линкера. От жеж, зараза. То есть, линкер ему нужен внешний. Ругаемся на компилятор, встаём с удобного кресла и идём обратно, подключаться к проводному интернету, потому что палить 5 гигов установщика Visual Studio Build Tools не хочется на хотспоте.

Билдим всё ещё раз и смотрим.

Ширина и жирина файлов.

Ширина и жирина файлов.

Ах, ты, ржавая банка! Какого чёрта?? Я уже как две недели рассказываю всем обитателям Хабра о том, какой ты прекрасный компилятор, и как хорошо ты собираешь минимальные бинарники, а ты. 150 килобайт исполняемого кода из-за одной только линии текста на экране?

Пытаемся скомпилировать с -C opt-level=3 и получаем то же самое. Что случилось с бинарником? Сейчас на этот вопрос отвечать не будем. Мотаем на Ус и едем дальше.

Копирайт - сдесь же.

Копирайт - сдесь же.

Ус недоволен. На него и так уже много чего намотано, он не понимает, почему ему надо разбираться с исходниками раста теперь.

Ладно, что мы знаем? Компилятор не работает без внешнего линкера и исходник для вывода одной строки текста раздувается до 150 килобайт. Ну, по крайней мере мы это можем скомпилировать. Давайте пока распакуем исходники компилятора и начнём рыться. (Собирать компилятор я не собираюсь. Если вам очень хочется - это можно сделать, но процесс это долгий и утомительный.)

Шаг десятый: Оптимизации

Про систему оптимизаций в rust можно писать отдельную книгу. Всё аккуратно сложено в rust-master\compiler\rustc_mir\src\transform . LLVM сам по себе не сможет оптимизировать некоторые высокоуровневые примитивы, о которых знает только rust. И вот тут мы как раз и занимаемся оптимизацией этих примитивов.

Шаг восьмой: rustc_mir и rustc_mir_build

Теперь наш HIR можно преобразовать в MIR. Берём ранее созданный TyCtxt и начинаем преобразовывать его в

И так далее по всем нодам. MIR это намного более генерализированная версия HIR. Она очень близка к тому что требует от нас LLVM для компиляции. В результате этой генерализации мы можем намного более эффективно работать над оптимизацией написанного вами кода и заниматься проверками заимствований и оптимизацией.

Шаг восьмой: rustc_mir и rustc_mir_build

Теперь наш HIR можно преобразовать в MIR. Берём ранее созданный TyCtxt и начинаем преобразовывать его в

И так далее по всем нодам. MIR это намного более генерализированная версия HIR. Она очень близка к тому что требует от нас LLVM для компиляции. В результате этой генерализации мы можем намного более эффективно работать над оптимизацией написанного вами кода и заниматься проверками заимствований и оптимизацией.

Шаг второй: rustc-driver

Ладно, всё выглядит слишком уж просто. Погружаемся дальше. rustc тянет за собой rustc-driver. Ныряем туда.

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

Да, в этом крейте файлов не так-то много, но что бы тут не творилось, на самом деле всё сводится к вызову методов в крейте под названием interface. Вышеприведённый код это и показывает. interface::run_compiler и поехали.

Что же произошло в rustc-driver? Мы собрали все конфиги. Подгрузили все файлы и нашли их местоположение в файловой системе. Создали замыкание, которое следит за процессом компиляции и запускает линкер после успешной компиляции. Запустили линтеры (если такие имелись) и приготовили сам компилятор к запуску. Давайте запускать.

Словарь

Далее по тексту я буду разбрасываться следующими терминами без удержи. Если вы знаете что это всё значит - хорошо. Если нет, убедитесь что подтянули свои знания, перед тем как заныривать.

LLVM - система компиляции которая состоит из компилятора и набора инструментов, позволяющая создавать фронт-энд для любого языка и компилировать его на множество различных платформ.

AST - (abstract syntax tree) древовидная репрезентация семантической структуры исходного кода. Каждый узел обычно показывает конструкцию, встречающуюся в коде.

IR (intermediate representation) - Структура данных, обычно используемая в кишках компилятора или виртуальной машины, для представления исходного кода программы. Такую структуру обычно оптимизируют и перегоняют в конечный код.

HIR (High Level IR) - IR высокого уровня. Это основная репрезентация кода, используемая в rust. Фактически это представление AST, которым компилятору удобно пользоваться.

MIR (Mid Level IR) - Это репрезентация HIR, которая намного ближе к LLVMIR.

LLVMIR (Language Independent IR) - фактически это высокоуровневый ассемблер, который не привязан к определённому языку или системе. Такой код удобно оптимизировать и после он передаётся компилятору.

Крейт, crate - Это то, что будет скомпилировано либо в библиотеку или бинарник. На выходе будет одна библиотека или бинарник, вне зависимости от того, сколько файлов входят в крейт.

ICE (Internal compiler error), ошибка компилятора.

Дальнейший текст подразумевает, что вы умеете программировать. Можно и не на rust.

Начало

Поехали. Мы будем лезть нашими ручками в сам компилятор и смотреть на его исходники. Для начала нам понадобятся кое-какие инструменты. Ставим чистую виртуальную машину с Windows 10. Идём в интернеты и льём следующее:

Сорцы компилятора. Достаются с github. Можно лить просто zip, ибо обратно коммитить мы ничего не будем.

Установщик компилятора. Любая свежая стабильная версия подойдёт.

Не будем мучиться, давайте, заодно, установим nightly компилятор.
rustup toolchain install nightly --allow-downgrade --profile minimal --component clippy rustup default nightly

Guide to Rustc Development. Инструкция по разработке компилятора. 460 страниц. Не хило. Сохраняем pdf.

Ну и хорошо. Этого, для начала достаточно. Отключаемся от проводного интернета, хватаем ноутбук и идём на веранду, сидеть и погружаться. Начинаем погружаться, понимаем что будет глупо говорить о компиляторе, если мы не скомпилируем хоть что-то. Ок, так и сделаем.

Ок, это было просто. Но мы не будем использовать cargo для самой компиляции. Используем компилятор напрямую. Но я же на надо cargo издеваюсь, так ведь?

Отступление по теме

Как не надо устанавливать rust

Как не надо устанавливать rust

Чего? Так, сам по себе компилятор всё собрал, но ругается на отсутствие линкера. От жеж, зараза. То есть, линкер ему нужен внешний. Ругаемся на компилятор, встаём с удобного кресла и идём обратно, подключаться к проводному интернету, потому что палить 5 гигов установщика Visual Studio Build Tools не хочется на хотспоте.

Билдим всё ещё раз и смотрим.

Ширина и жирина файлов.

Ширина и жирина файлов.

Ах, ты, ржавая банка! Какого чёрта?? Я уже как две недели рассказываю всем обитателям Хабра о том, какой ты прекрасный компилятор, и как хорошо ты собираешь минимальные бинарники, а ты. 150 килобайт исполняемого кода из-за одной только линии текста на экране?

Пытаемся скомпилировать с -C opt-level=3 и получаем то же самое. Что случилось с бинарником? Сейчас на этот вопрос отвечать не будем. Мотаем на Ус и едем дальше.

Копирайт - сдесь же.

Копирайт - сдесь же.

Ус недоволен. На него и так уже много чего намотано, он не понимает, почему ему надо разбираться с исходниками раста теперь.

Ладно, что мы знаем? Компилятор не работает без внешнего линкера и исходник для вывода одной строки текста раздувается до 150 килобайт. Ну, по крайней мере мы это можем скомпилировать. Давайте пока распакуем исходники компилятора и начнём рыться. (Собирать компилятор я не собираюсь. Если вам очень хочется - это можно сделать, но процесс это долгий и утомительный.)

Шак третий: rustc-interface

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

Так, осматриваемся и находим

Кстати тут же, недалеко, мы можем найти настройку механизма кодогенерации.

Быстренько посмотрим на наши сорцы и увидим что у нас прямо в сорцах есть 3 различных модуля кодогенерации. Что они делают? Превращают MIR в конечный код для системы компиляции. Открываем rustc-codegen-llvm и смотрим в README:

Ок, ну тут всё понятно, мы берём MIR и переделываем его в LLVM IR. После этого LLVM может скомпилировать код в конечный бинарник. Но погодите, помимо LLVM бекенда у нас есть ещё два других! Смотрим туда. rustc-codegen-ssa согласно документации, позволяет генерировать низкоуровневый код, который не будет привязан к определённому бекэнду (например, LLVM) и позволит в дальнейшем использовать другие системы компиляции.

Собственно говоря, прямо там же вы найдёте rustc-codegen-cranelift. То есть MIR в будущем может компилироваться через cranelift, который в идеале ускорит процесс компиляции. Ну это в будущем, пока что проект в процессе тестирования и работает не лучше, чем Газель без мотора.

Открываем модуль и смотрим, что происходит внутри:

Ага, вот тут мы берём быка за рога и начинаем разбирать исходный код на части. Далее, создаём и проверяем AST

И вот тут у нас начинается полное мясо. Прикол вот в чём, обычно компиляторы делают достаточно простую вещь - берёшь сорцы, проходишься по ним несколько раз, парсишь синтаксис, находишь ошибки, разбираешь на куски макросы, всё хорошо. rust в стародавние времена начинал именно так же. Но, со временем, была предложена новая модель компиляции. Запросы. Вместо того чтобы делать всю работу несколько раз подряд, давайте просто превратим проходы в запросы. Результат запроса можно сохранить в кеш. Если пользователь (программист) не менял ничего в определённом файле, то и компилировать его не надо. Если мы видим что делать это не надо, мы просто возвращаем данные из кеша запроса.

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

Давайте посмотрим на запросы, которые создаёт компилятор:

Парсинг, создание крейта, сбор HIR - всё это делается через запросы. Один момент про который полезно знать, это то что ещё не всё переписано на запросах.

В итоге у нас на выходе получается большая и толстая структура:

И как раз её можно дёргать для выполнения необходимых запросов.

Напоследок

Понятно? Ну и хорошо.

Шаг четвёртый: rustc-parse и rustc-lexer

Далее по тексту вы найдёте простую логику всех этих запросов. "Простая" логика заключается в вызове крейтов, которые её обрабатывают. Например, rustc-parse. Это крейт, который использует rustc-lexer. Лексер читает строки из файлов и преобразовывает их в очень простые токены. Токены передаются парсеру, который превращает их в Span и продолжает работу с кодом. Основной момент этого Span заключается в том, что к каждому элементу в дереве кода будет добавлена информация о том, в каком конкретно месте этот элемент записан в исходном файле. Когда компилятор будет сообщать об ошибке, вы увидите, где именно эта ошибка произошла.

Основная часть парсера запускается через вызов parse_crate_mod в rustc_parse\src\parser\item.rs . А дальше по тексту вы найдёте невероятное количество проверок синтаксиса, который этот парсер делает. Вот, например:

Шак одиннадцатый: прощай, rust!

Полученный оптимизированный MIR можно теперь переделать в LLVM IR. Поехали. rustc-codegen-llvm создаёт LLVM-IR на базе MIR, который мы сгенерировали на предыдущем этапе. Здесь заканчивается rust и начинается llvm. Хотя, мы ещё не закончили с сорцами компилятора.

Тут можно найти пару интересных моментов, например rust-master\compiler\rustc_codegen_llvm\src\asm.rs содержит код для компилирования ассемблера напрямую из rust. Даже не замечал этого. Смотрим в документацию - есть такая поддержка в этом компиляторе!

Копаемся чуть глубже и находим rustc-target в котором видим различные дополнительные классы для работы с определённым ассемблером.

После того как кодогенерация завершена, мы можем передать IR в сам LLVM. rustc_llvm нам в помощь.

Вот, собственно говоря, и всё, ребята! LLVM за пределами нашей видимости. На моей операционной системе Visual Studio Build Tools берут на себя контроль и перегоняют LLVMIR в обычный бинарник.

Процесс компиляции в rust - это вам не мешки ворочать. Надо проверить неимоверное количество разных вещей. Вот что происходит с вашим кодом:

Он парсится из текста в AST.

AST обрабатывается и оптимизируется в HIR

HIR обрабатывается и оптимизируется в MIR.

MIR делает проверки заимствования и оптимизацию и перегоняется в LLVMIR.

LLVMIR компилируется на конечной платформе.

Читайте также: