Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Модуль 1.1 — Введение

Слайды

О проекте. Курс основан на открытых учебных материалах Tweede golf. Лицензия оригинала: CC BY-SA 4.0. Русский перевод и адаптация выполнены участниками проекта RustTraining-Rus и распространяются по той же лицензии CC BY-SA 4.0.

Упражнения. Исходный код упражнений находится в репозитории оригинала. Пути вида exercises/... в тексте указывают на каталоги в этом репозитории.

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

Мы разрабатываем этот материал открыто с 2023 года и с тех пор провели на его основе более 15 индивидуальных корпоративных тренингов.

Материал состоит из нескольких модулей, основанных на материалах проекта teach-rs (теперь часть Trifecta Tech Foundation). Мы ранее развивали этот проект совместно и возвращаем в него улучшения.

Слайды и упражнения

Все наши программы обучения построены на интерактивном сочетании теории и практических упражнений.

Теорию преподаватели объясняют по слайдам. Ссылки на них приведены в начале большинства модулей, в том числе вверху этой страницы.

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

Улучшения

Если вы заметили ошибку или хотите что-то предложить, откройте issue или pull request в репозитории оригинала на GitHub. Замечания по русскому переводу присылайте в репозиторий RustTraining-Rus.

Лицензия

Материал опубликован по лицензии CC BY-SA 4.0. Вы можете свободно использовать и распространять его при условии указания авторства. Если вы публикуете изменённую версию, её нужно распространять по той же лицензии.

Обучение с нами

Самостоятельное изучение — это хорошо, но занятия с наставником по программе, подобранной под вашу команду, продуктивнее и интереснее!

Отзывы, знакомство с нашими преподавателями и подробности о программах смотрите на странице профессиональных тренингов.


Модуль 1.2 — Введение

Упражнение 1.2.1: Настройка окружения

В этом файле описано, как установить инструменты, которые понадобятся на курсе.

Все эти инструменты доступны для Linux, macOS и Windows. Они нужны, чтобы писать и компилировать код на Rust, а также для удалённого наставничества. Важно: выполните эти инструкции до начала первого занятия. Если у вас возникли проблемы с установкой, заранее свяжитесь с преподавателями. Мы бы не хотели тратить на проблемы с установкой время первого занятия.

Rust и Cargo

Сначала нам понадобится rustc, стандартный компилятор Rust. rustc обычно не вызывают напрямую, а работают через cargo, менеджер пакетов Rust. rustup устанавливает rustc и cargo.

Здесь всё просто: перейдите на https://rustup.rs и следуйте инструкциям. Убедитесь, что вы устанавливаете последний стабильный тулчейн по умолчанию. После установки выполните

rustc -V && cargo -V

Вывод должен выглядеть примерно так:

rustc 1.79.0 (129f3b996 2024-06-10)
cargo 1.79.0 (ffa9cf99a 2024-06-03)

С помощью Rustup можно устанавливать тулчейны и компоненты Rust. Подробнее:

Rustfmt и Clippy

Чтобы не спорить о стиле кода, Rust предоставляет собственный инструмент форматирования, Rustfmt. Мы также будем использовать Clippy, набор линтов для анализа кода, который помогает ловить распространённые ошибки. Clippy — очень полезный помощник при изучении Rust. И Rustfmt, и Clippy по умолчанию устанавливаются вместе с Rustup.

Чтобы отформатировать проект с помощью Rustfmt, выполните:

cargo fmt

Чтобы запустить Clippy:

cargo clippy

Подробнее:

Visual Studio Code

На курсе мы будем писать код в Visual Studio Code (VS Code). Конечно, вы можете использовать любой редактор, но если возникнут проблемы, мы не сможем гарантированно помочь с его настройкой. Кроме того, VS Code позволяет совместно работать и проводить наставничество во время дистанционных занятий.

Инструкции по установке: https://code.visualstudio.com/.

Понадобится несколько расширений. Первое — Rust-Analyzer. Инструкции по установке: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer. Rust-Analyzer много помогает при разработке и незаменим на старте.

Второе расширение — CodeLLDB. Оно позволяет отлаживать код Rust прямо в VS Code. Инструкции: https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb.

Если вы проходите курс дистанционно, установите также расширение Live Share. С его помощью мы будем делиться кодом и помогать во время дистанционных занятий. Инструкции по установке: https://marketplace.visualstudio.com/items?itemName=MS-vsliveshare.vsliveshare

Подробнее:

Совет

В репозитории много проектов на Rust, и из-за сложной структуры Rust-Analyzer не может найти их автоматически.

Поэтому мы указали проекты вручную в файле .vscode/settings.json. Чтобы снизить нагрузку на компьютер, можно закомментировать проекты, которые не используются на курсе.

Git

На курсе понадобится система контроля версий Git. Если Git ещё не установлен, инструкции здесь: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git. Если вы новичок в Git, пригодится вводный материал GitHub https://docs.github.com/en/get-started/using-git/about-git, а также видео о работе с Git в VS Code: https://www.youtube.com/watch?v=i_23KUAEtUM.

Подробнее: https://www.youtube.com/playlist?list=PLg7s6cbtAD15G8lNyoaYDuKZSKyJrgwB-

Код курса

Когда всё установлено, склонируйте репозиторий с исходным кодом через Git. Репозиторий находится здесь: https://github.com/tweedegolf/rust-training.

Инструкции по клонированию: https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls

Пробный запуск

Когда код у вас на машине, перейдите в его каталог в терминале и выполните:

cd exercises/1-course-introduction/1-introduction/1-setup-your-installation
cargo run

В первый раз эта команда может выполняться долго: Cargo сначала загрузит индекс крейтов из реестра. Затем он соберёт и запустит пакет intro, который находится в exercises/1-course-introduction/1-introduction/1-setup-your-installation. Если всё прошло успешно, вы увидите такой вывод:

   Compiling intro v0.1.0 ([/path/to/rust-workshop]/exercises/1-course-introduction/1-introduction/1-setup-your-installation)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.15s
     Running `target/debug/intro`
🦀 Hello, world! 🦀
You've successfully compiled and run your first Rust project!
X: 2; Y: 2

Если Rust-Analyzer настроен правильно, над функцией main в exercises/1-course-introduction/1-introduction/1-setup-your-installation/src/main.rs появится кнопка «▶️ Run». Если установлен CodeLLDB, рядом с ней появится кнопка «Debug», которая запускает отладочную сессию. Попробуйте поставить точку останова, кликнув по номеру строки (появится красный кружок), и пошагово пройтись по коду: через функции, в них и из них, с помощью кнопок управления. Значения переменных можно посмотреть, наведя на них курсор во время паузы, или раскрыв раздел «Local» в панели «Variables» слева во время отладки.

Инструкции для модуля по FFI

Этот раздел нужен, только если вы проходите один из модулей о FFI в Rust.

Для работы с FFI нам понадобится скомпилировать C-код, поэтому нужен установленный компилятор C. В упражнениях мы используем clang.

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

Linux

Для Debian и его производных (например, Ubuntu):

sudo apt update
sudo apt install clang

Для Arch:

sudo pacman -S clang

Для Fedora:

sudo dnf install clang

Windows

Обязательно выберите опцию добавления установки в PATH.

С помощью winget:

winget install -i -e --id LLVM.LLVM

С помощью Chocolatey:

choco install llvm

Ручная установка:

  • Перейдите на страницу релизов: https://github.com/llvm/llvm-project/releases
  • Откройте один из свежих релизов
  • Найдите файл LLVM-[ВЕРСИЯ]-win64.exe и скачайте его
  • Запустите установщик

macOS

С помощью Homebrew:

brew install llvm

Затем добавьте LLVM в PATH, например так:

echo 'export PATH="$(brew --prefix llvm)/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

Инструкции для встраиваемых систем

Этот раздел нужен, только если вы проходите один из модулей о встраиваемом Rust.

Аппаратура

Мы будем использовать BBC micro:bit V2. Если он у вас уже есть, отлично; если нет, мы привезём его с собой.

Понадобится также кабель Micro-USB, но уверены, что он у вас найдётся.

Проверьте, что всё на месте. Если чего-то не хватает, свяжитесь с нами.

Программное обеспечение

Теперь установим инструменты для прошивки микроконтроллера и просмотра кода.

Установите тулчейн thumbv7em-none-eabihf командой:

rustup target add thumbv7em-none-eabihf

Также установим пару инструментов для анализа бинарников:

rustup component add llvm-tools
cargo install cargo-binutils

Теперь установим probe-rs. Следуйте инструкции по установке. Probe-rs работает с отладочным интерфейсом micro:bit и позволяет прошивать приложение, выводить лог-сообщения, ставить точки останова и читать память устройства.

Для Linux команда такая:

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/probe-rs/probe-rs/releases/latest/download/probe-rs-tools-installer.sh | sh

Если вы работаете в Linux, нужно обновить правила udev. В Ubuntu или Fedora выполните следующие команды в папке с тренингом, которую вы только что клонировали:

sudo cp 99-microbit-v2.rules /etc/udev/rules.d
sudo udevadm control --reload-rules
sudo udevadm trigger

Может оказаться, что probe-rs находит два отладочных интерфейса. Такое бывает в Windows. В этом случае откройте .cargo/config.toml и измените runner так, чтобы он указывал на конкретный отладчик, который нужно использовать. Значения должны совпадать с тем, что probe-rs показывает для одного из интерфейсов. Не уверены? Выполните probe-rs list, чтобы получить список всех подключённых отладчиков.

Пробный запуск

Перед началом проверим оборудование. Мы проверим микроконтроллер nRF52833 и датчик движения LSM303AGR, которые есть на micro:bit V2. Убедитесь, что у вас последняя версия исходного кода тренинга.

Проверка оборудования

Подключите micro:bit V2 к компьютеру, включите его и выполните

cd ./exercises/1-course-introduction/1-introduction/2-embedded
cargo run --release

Если всё работает правильно, на дисплее должны появиться показания акселерометра. Если нет, не переживайте и свяжитесь с нами.

Документация

Даташиты, руководства и схемы компонентов, которые мы используем в модулях по встраиваемым системам.

BBC micro:bit V2

nRF52833

LSM303AGR

Модуль 2.1 — Базовый синтаксис

Слайды

Упражнение 2.1.1: Базовый синтаксис

Откройте exercises/2-foundations-of-rust/1-basic-syntax/1-basic-syntax в редакторе. В этой папке собраны упражнения, с помощью которых можно потренировать базовый синтаксис Rust.

Чтобы начать, находясь в папке exercises/2-foundations-of-rust/1-basic-syntax/1-basic-syntax, выполните:

cargo run --bin 01

Эта команда попытается скомпилировать упражнение 1. Добейтесь, чтобы пример запустился, и переходите к следующему упражнению, заменив номер в команде cargo run.

Некоторые упражнения содержат модульные тесты. Чтобы запустить тесты из src/bin/01.rs, выполните

cargo test --bin 01

Убедитесь, что все тесты проходят!

Модуль 2.2 — Владение и ссылки

Слайды

Упражнение 2.2.1: Семантика перемещения

Упражнение адаптировано из упражнения на семантику перемещения проекта Rustlings.

Находясь в папке exercises/2-foundations-of-rust/2-ownership-and-references/1-move-semantics, чтобы начать, выполните:

cargo run --bin 01

Эта команда попытается скомпилировать упражнение 1. Добейтесь, чтобы пример запустился, и переходите к следующему упражнению, заменив номер в команде cargo run.

Некоторые упражнения содержат модульные тесты. Чтобы запустить тест из src/bin/01.rs, выполните

cargo test --bin 01

Убедитесь, что все тесты проходят!

01.rs должен компилироваться без изменений, но остальные примеры тоже нужно заставить компилироваться. В некоторых упражнениях инструкции записаны в виде doc-комментариев в начале файла. Обязательно следуйте им.

Упражнение 2.2.2: Заимствование

Исправьте два примера в крейте exercises/2-foundations-of-rust/2-ownership-and-references/2-borrowing! Не забывайте, что отдельные бинарники можно запускать командой cargo run --bin 01 в этой папке. Обязательно следуйте инструкциям в комментариях!

Модуль 2.3 — Продвинутый синтаксис

Слайды

Упражнение 2.3.1: Распространение ошибок

Следуйте инструкциям в комментариях файла exercises/2-foundations-of-rust/3-advanced-syntax/1-error-propagation/src/main.rs!

Упражнение 2.3.2: Обработка ошибок

Следуйте инструкциям в комментариях файла exercises/2-foundations-of-rust/3-advanced-syntax/2-error-handling/src/main.rs!

Упражнение 2.3.3: Срезы

Следуйте инструкциям в комментариях файла exercises/2-foundations-of-rust/3-advanced-syntax/3-slices/src/main.rs! Не тратьте много времени на дополнительное задание. Лучше вернитесь к нему позже, когда выполните остальные упражнения.

Упражнение 2.3.4: Кольцевой буфер

Это бонусное упражнение! Следуйте инструкциям в комментариях файла exercises/2-foundations-of-rust/3-advanced-syntax/4-ring-buffer/src/main.rs!

Упражнение 2.3.5: Данные в Box

Следуйте инструкциям в комментариях файла exercises/2-foundations-of-rust/3-advanced-syntax/5-boxed-data/src/main.rs!

Модуль 2.4 — Трейты и обобщения

Слайды

Упражнение 2.4.1: Векторная математика

В этом упражнении мы реализуем базовые операции векторной математики для нашего двумерного типа вектора Vec2D.

2.4.1.A Сложение векторов

Начнём с реализации сложения для Vec2D через трейт std::ops::Add. Замените todo!() так, чтобы возвращался новый Vec2D, у которого компонента x равна сумме компонент x двух входных векторов, и аналогично для y. Если всё реализовано правильно, тест integer_addition должен пройти!

2.4.1.B Скалярное произведение

Теперь реализуем скалярное произведение для нашего типа вектора. Скалярное произведение двух векторов — это одно число, равное сумме произведений их компонент. Например, [1, 2] * [3, 4] = 1 * 3 + 2 * 4 = 11. Реализуйте его, добавив новый блок impl, который реализует трейт std::ops::Mul для Vec2D. Раскомментируйте тест integer_dot_product, чтобы проверить код!

2.4.1.C Делаем Vec2D обобщённым

Сейчас компоненты x и y нашего Vec2D могут быть только 32-битными целыми. А что, если нужны другие числовые типы, например числа с плавающей точкой? Сделаем Vec2D обобщённым по типу T, изменив определение Vec2D так, чтобы x и y имели обобщённый тип.

Также нужно обновить реализации сложения и скалярного произведения, чтобы они работали с обобщённым типом T. Вместо того чтобы реализовывать add и mul отдельно для каждого числового типа, можно сделать реализации обобщёнными: реализовать их для любого типа T, который можно складывать и/или умножать (то есть для любого T, реализующего трейты Add и/или Mul). Тогда достаточно поменять только начало блока impl, без изменения самих функций!

Подсказка 1 (обобщённые реализации с ограничениями трейтов) Обобщённый тип можно добавить в блок `impl`, написав `impl<T>`. Тип можно ограничить трейтом, написав, например, `impl<T: Display>`, что означает, что тип `T` должен реализовывать `Display` (то же самое можно записать как `impl<T> ... where T: Display`). Несколько ограничений можно перечислить через `+`, например `impl<T: Clone + Display>`.
Подсказка 2 (трейты `Add` и `Mul`) У трейтов `Add` и `Mul` есть обобщённый associated-тип `Output`. Именно поэтому мы смогли реализовать скалярное произведение как трейт `Mul` для `Vec2D`, задав в качестве результата число. В обобщённой реализации мы хотим, чтобы тип `T` можно было складывать и/или умножать. Для этого добавляем к `T` ограничения `Add` и/или `Mul`, но также нужно указать, какой результат ожидаем от сложения или умножения двух значений `T`, например так: `T: Add<Output = T>`.

Раскомментируйте тесты float_addition и float_dot_product, чтобы проверить, что Vec2D теперь работает и с числами с плавающей точкой!

Упражнение 2.4.2: Вектор с локальным хранилищем (LocalStorageVec)

Это бонусное упражнение! Мы создадим тип LocalStorageVec, обобщённый список элементов, который хранится либо на стеке, либо в куче, в зависимости от размера. Если элементы достаточно малы, чтобы поместиться на стеке, буфер LocalStorageVec реализован массивом. LocalStorageVec обобщён не только по типу (T) элементов списка, но и по размеру (N) массива на стеке, используя относительно новую возможность языка, константные обобщения (const generics). Как только в LocalStorageVec оказывается больше элементов, чем помещается в массив, выделяется Vec в куче, где и будут храниться элементы.

В этом упражнении цели помечены звёздочками (⭐), которые показывают сложность. Скорее всего, вы не успеете выполнить все упражнения во время занятия.

Вопросы

  1. Когда такая структура данных эффективнее обычного Vec?
  2. Каковы её недостатки по сравнению с обычным Vec?

Откройте крейт exercises/2-foundations-of-rust/4-traits-and-generics/1-local-storage-vec. В нём есть файл src/lib.rs, то есть этот крейт — библиотека. В lib.rs есть набор тестов, которые запускаются командой cargo test. Не беспокойтесь, если они пока не проходят или даже не компилируются: в этом упражнении ваша задача — это исправить. Большинство тестов сейчас закомментированы, чтобы можно было идти шаг за шагом. Прежде чем начать, посмотрите код и комментарии в нём: там много полезных подсказок.

2.4.2.A Определяем тип ⭐

Сейчас перечисление LocalStorageVec (enum) не завершено. Добавьте в него два варианта: Stack и Heap. Stack содержит два именованных поля: buf и len. buf — массив, вмещающий N элементов типа T; len — поле типа usize, которое хранит количество реально сохранённых элементов. Вариант Heap содержит безымянное поле с Vec<T>. Если варианты определены правильно, запуск cargo test должен вывести примерно следующее:

running 1 test
test test::it_compiles ... ignored, This test is just to validate the definition of `LocalStorageVec`. If it compiles, all is OK

test result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s

Этот тест не запускается (и не должен), он нужен только для проверки определения вариантов.

Подсказка 1 Определение `LocalStorageVec` можно восстановить, глядя на код тестового случая `it_compiles`.

Подсказка 2 (если застряли, но постарайтесь ещё немного сопротивляться)

Определение ниже работает. Прочитайте комментарии в коде и убедитесь, что понимаете, что происходит.

#![allow(unused)]
fn main() {
// Определяем перечисление `LocalStorageVec`, обобщённое
// по типу `T` и константе `N` типа `usize`
pub enum LocalStorageVec<T, const N: usize> {
    // Вариант в стиле структуры с именем `Stack`, содержащий два именованных поля:
    // - `buf` — массив элементов типа `T` размером `N`
    // - `len` — поле типа `usize`
    Stack { buf: [T; N], len: usize },
    // Вариант в стиле кортежа с именем `Heap`, содержащий одно поле
    // типа `Vec<T>` — растущий непрерывный список `T` в куче
    Heap(Vec<T>),
}
}

2.4.2.B Реализуем From<Vec<T>> ⭐

Раскомментируйте тест it_from_vecs и добавьте реализацию From<Vec<T>> для LocalStorageVec<T>. Для этого скопируйте следующий код в файл lib.rs и замените вызов макроса todo! на ваш код, который создаёт LocalStorageVec в куче с переданным Vec<T>.

#![allow(unused)]
fn main() {
impl<T, const N: usize> From<Vec<T>> for LocalStorageVec<T, N> {
    fn from(v: Vec<T>) -> Self {
        todo!("Implement me");
    }
}
}

Вопрос

  1. Как бы вы по-английски прочитали первую строку кода, которую только что скопировали?*

Запустите cargo test, чтобы проверить реализацию.

2.4.2.C AsRef и AsMut ⭐⭐

AsRef и AsMut используются для реализации дешёвого преобразования ссылки в ссылку. Например, наш LocalStorageVec<T, N> похож на срез &[T]: оба представляют непрерывную последовательность значений T. Это верно и тогда, когда буфер LocalStorageVec находится на стеке, и тогда, когда он в куче.

Раскомментируйте тестовый случай it_as_refs и реализуйте AsRef<[T]> и AsMut<[T]>.

Подсказка При создании среза для варианта `Stack` у `LocalStorageVec` обязательно учитывайте значение `len`.

2.4.2.D impl LocalStorageVec ⭐⭐

Чтобы LocalStorageVec был полезнее, добавим в него больше методов. Создайте блок impl для LocalStorageVec. Не забудьте объявить обобщённые параметры и передать их. Чтобы упростить реализацию, пока добавим ограничение на T: он должен реализовывать Copy и Default. Сначала раскомментируйте тест it_constructs. Сделайте его компилируемым и проходящим, создав ассоциированную функцию new для LocalStorageVec, которая создаёт новый пустой экземпляр LocalStorageVec без выделения памяти в куче.

Далее реализуем методы len, push, pop, insert, remove и clear:

  • len возвращает длину LocalStorageVec;
  • push добавляет элемент в конец LocalStorageVec и увеличивает длину. Если содержимое больше не помещается на стеке, оно может быть перенесено в кучу;
  • pop удаляет элемент с конца LocalStorageVec, возвращает его (опционально) и уменьшает длину. Если длина равна 0, pop возвращает None;
  • insert вставляет элемент по заданному индексу и увеличивает длину LocalStorageVec;
  • remove удаляет элемент по заданному индексу и возвращает его;
  • clear сбрасывает длину LocalStorageVec в 0.

Раскомментируйте соответствующие тестовые случаи и добейтесь, чтобы они компилировались и проходили. Обязательно изучите методы, которые предоставляют срезы [T] и Vec<T>. В частности, могут пригодиться [T]::copy_within и Vec::extend_from_slice.

2.4.2.E Iterator и IntoIterator ⭐⭐

Теперь LocalStorageVec можно использовать на практике, но останавливаться рано. В стандартной библиотеке есть трейты, которые можно реализовать для LocalStorageVec, и пользователи вашего крейта будут им рады.

Для начала реализуем трейты IntoIterator и Iterator. Раскомментируйте тестовый случай it_iters. Определим новый тип:

#![allow(unused)]
fn main() {
pub struct LocalStorageVecIter<T, const N: usize> {
    vec: LocalStorageVec<T, N>,
    counter: usize,
}
}

Именно для этого типа мы реализуем трейт Iterator. Вам нужно указать тип элемента, который выдаёт реализация Iterator, а также реализовать Iterator::next, который возвращает следующий элемент. Задача упростится, если при реализации Iterator ограничить T трейтом Default: тогда можно использовать функцию std::mem::take, чтобы забрать элемент из LocalStorageVec и заменить его значением по умолчанию для T.

Посмотрите список методов в разделе «provided methods». Там много полезных методов, которые доступны бесплатно после реализации Iterator и выражаются через метод next. Если держать их в голове, писать код на Rust будет заметно эффективнее. Какие из этих методов можно переопределить, чтобы реализация LocalStorageVecIter стала эффективнее, учитывая, что мы имеем доступ к полям и методам LocalStorageVec?

Чтобы создать LocalStorageVecIter, реализуйте для него трейт IntoIter так, чтобы вызов into_iter возвращал LocalStorageVecIter.

2.4.2.F Index ⭐⭐

Чтобы пользователи LocalStorageVec могли читать элементы и срезы из его буфера, можно реализовать трейт Index. Этот трейт обобщён по типу индекса. Чтобы сделать LocalStorageVec универсальным, реализуйте:

  • Index<usize> — позволяет получить один элемент через vec[1];
  • Index<RangeTo<usize>> — позволяет получить первые n элементов (не включая элемент n) через vec[..n];
  • Index<RangeFrom<usize>> — позволяет получить элементы с позиции n до конца через vec[n..];
  • Index<Range<usize>> — позволяет получить элементы между n и m (не включая элемент m) через vec[n..m].

Каждую из этих реализаций можно выразить через реализацию as_ref, поскольку срезы [T] поддерживают индексирование всеми перечисленными типами. То есть [T] тоже реализует Index для этих типов. Раскомментируйте тестовый случай it_indexes и запустите cargo test, чтобы проверить реализацию.

2.4.2.G Снимаем ограничения ⭐⭐

Когда мы реализовывали итератор, заимствующий данные, мы видели, что методы можно определять в разных блоках impl с разными ограничениями на типы. Часть написанного нами функционала опиралась на допущение, что T одновременно реализует Copy и Default. Но из-за этого каждый такой метод определён только для LocalStorageVec, содержащих элементы типа T, которые действительно реализуют Copy и Default, а это неидеально. Сколько методов можно переписать, убрав одно или оба из этих ограничений?

2.4.2.H Заимствующий Iterator ⭐⭐⭐

У LocalStorageVec уже есть итератор, но у него есть ограничение: чтобы его создать, LocalStorageVec нужно поглотить (consume). А что, если мы хотим только перебирать элементы, не забирая их? Понадобится другой тип итератора, который содержит неизменяемую ссылку на LocalStorageVec и, значит, потребует аннотацию времени жизни. Добавьте в LocalStorageVec метод iter, который принимает разделяемую ссылку &self и создаёт заимствующий итератор. Реализуйте трейт Iterator с подходящим типом ссылки Item для вашего заимствующего итератора. Чтобы проверить код, раскомментируйте и запустите тестовый случай it_borrowing_iters.

Обратите внимание: на этот раз тест не скомпилируется, если потребовать, чтобы элементы LocalStorageVec были Copy! Значит, метод LocalStorageVec::iter нужно определить в новом блоке impl, который не накладывает это ограничение на T:

#![allow(unused)]
fn main() {
impl<T: Default + Copy, const N: usize> LocalStorageVec<T, N> {
    // Методы, которые вы реализовали до сих пор
}

impl<T, const N: usize> LocalStorageVec<T, N> {
    pub fn iter(&self) -> /* TODO */
}
}

Определение методов в разных блоках impl означает, что часть методов недоступна для некоторых экземпляров обобщённого типа. В нашем случае метод new доступен только для LocalStorageVec, содержащих элементы типа T, которые реализуют и Copy, и Default, а iter доступен для всех LocalStorageVec.

2.4.2.I Обобщённый Index ⭐⭐⭐⭐

Скорее всего, в упражнении 2.4.2.F вы продублировали много кода. Его можно сократить, определив пустой трейт:

#![allow(unused)]
fn main() {
trait LocalStorageVecIndex {}
}

Сначала реализуйте этот трейт для usize, RangeTo<usize>, RangeFrom<usize> и Range<usize>.

Затем замените несколько реализаций Index одной. Словами это звучит так:

«Для каждого типа T, типа I и константы N типа usize реализовать Index<I> для LocalStorageVec<T, N>, где I реализует LocalStorageVecIndex и [T] реализует Index<I>»

Если вы всё сделали правильно, it_indexes снова должен компилироваться и проходить.

2.4.2.J Deref и DerefMut ⭐⭐⭐⭐

Следующие трейты, которые сделают LocalStorageVec гибче, — это Deref и DerefMut. Они используют возможность Rust «deref coercion», которая позволяет обращаться с типом так, будто это тот тип, на который он похож. Благодаря этому мы сможем вызывать на LocalStorageVec любой метод, определённый для [T]. Прежде чем продолжить, прочитайте раздел «Treating a Type Like a Reference by Implementing the Deref Trait» из The Rust Programming Language (TRPL) — раздел о том, как относиться к типу как к ссылке, реализуя трейт Deref. Не путайте deref coercion с наследованием! Использовать Deref и DerefMut для имитации наследования в Rust не принято.

Ниже приведена реализация Deref и DerefMut через реализации AsRef и AsMut. Обратите внимание на то, как именно вызываются as_ref и as_mut.

#![allow(unused)]
fn main() {
impl<T, const N: usize> Deref for LocalStorageVec<T, N> {
    type Target = [T];

    fn deref(&self) -> &Self::Target {
        <Self as AsRef<[T]>>::as_ref(self)
    }
}

impl<T, const N: usize> DerefMut for LocalStorageVec<T, N> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        <Self as AsMut<[T]>>::as_mut(self)
    }
}
}

Вопрос

  • Если заменить реализацию deref на self.as_ref(), то при запуске неоптимизированной сборки возникает переполнение стека. Почему? (Подсказка: deref coercion)

Модуль 2.5 — Замыкания и динамическая диспетчеризация

Слайды

Упражнение 2.5.1: Чтение конфигурации

В этом упражнении вы будете работать с динамической диспетчеризацией: десериализовать файл с помощью serde_json или serde_yaml в зависимости от его расширения. Стартовый код находится в exercises/2-foundations-of-rust/5-closures-and-dynamic-dispatch/1-config-reader. Исправьте все места, помеченные todo.

Чтобы запустить программу, нужно передать файл для десериализации самому бинарнику, а не Cargo. Для этого выполните

cargo run -- <FILE_PATH>

Успешная десериализация и config.json, и config.yml должна корректно выводить Config.

Модуль 3.1 — Инженерия крейтов

Слайды

Упражнение 3.1.1: Моё приложение на serde

Упражнение адаптировано из упражнения serde_lifetimes от Ferrous Systems.

Откройте exercises/3-crate-engineering/1-crate-engineering/1-my-serde-app/src/main.rs. Там находится код на Rust, с которым мы будем работать в этом упражнении.

Мы использовали макросы todo!(), чтобы отметить места, куда нужно вставить код, чтобы программа заработала. За справкой обращайтесь к API serde_json.

Подсказка Serde предоставляет два трейта: `Serialize` и `Deserialize`. Их можно получить через `derive` для ваших типов `struct` и `enum`. Другие крейты `serde-*` используют эти трейты, чтобы преобразовывать наш тип данных в соответствующее представление и обратно (`serde_json` — в JSON, `serde_yaml` — в YAML и т. д.).

Почему main возвращает anyhow::Result<()>? Если main возвращает результат, ошибки можно передавать вверх до самого рантайма. Подробнее об этом — в Rust By Example. Тип anyhow::Result — более гибкий вариант Result, который позволяет легко преобразовывать типы ошибок.

Что за конструкция r#"...? Префикс r перед строковым литералом означает «сырую» строку (raw string). Escape-последовательности (\n, \" и т. п.) в ней не работают, поэтому такие строки удобны для регулярных выражений, JSON-литералов и подобных случаев.

После r можно поставить один или несколько символов (в нашем примере это #), и строка закончится, когда встретится закрывающая двойная кавычка, за которой следует такое же количество тех же символов. Это удобно, если внутри строки нужны двойные кавычки. Для нашего примера r#" ... "# отлично подходит для JSON. В редких случаях может понадобиться два и более знака #, например, когда в JSON-строках хранятся значения цвета CSS:

#![allow(unused)]
fn main() {
// здесь `"#` не завершит строку
r##"
    {
        "color": "#ff00ff"
    }
"##
}

Упражнение 3.1.2: Викторина

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

Упражнение выполняется в группах по 2 человека.

3.1.2.A Подготовка ⭐

Создайте новый проект командой cargo new --name quizzer. Убедитесь, что он одновременно является и бинарником, и библиотекой. Это значит, что в крейте будут файлы src/lib.rs и src/bin/quizzer/main.rs, где quizzer — имя бинарника:

$ tree
.
├── Cargo.toml
├── quiz.json
└── src
    ├── bin
    │   └── quizzer
    │       └── main.rs
    └── lib.rs

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

Ваш Cargo.toml должен выглядеть так:

[package]
name = "quizzer"
version = "0.1.0"
edition = "2021"

### Подробнее о ключах и их значении: https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
anyhow = "1.0.66"
clap = { version = "4.0.18", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.87"

Для clap и serde включена нестандартная фича derive этих крейтов. В clap она позволяет вывести (derive) трейт Parser, что сильно упрощает создание CLI. Фича derive из serde позволяет вывести трейты Serialize и Deserialize для любой структуры, которую мы хотим сериализовать или десериализовать с помощью serde и его бэкендов, в нашем случае serde_json.

3.1.2.B Викторина ⭐⭐⭐

Это упражнение и о проектировании, и о поиске информации. Вам придётся придумать модель для представления вопросов викторины, а также способ сохранять их в JSON-файл и загружать его. Кроме того, нужно выяснить, как разбирать аргументы программы.

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

  • Она работает как инструмент командной строки в терминале.
  • У неё два режима: режим ввода вопросов и режим викторины. Режим выбирается подкомандой, которая передаётся программе первым аргументом.
    • Режим ввода вопросов: позволяет вводить вопросы с множественным выбором, по четыре варианта ответа в каждом, ровно один из которых правильный. Вопросы сохраняются на диск в виде JSON-файла.
    • Режим викторины: загружает сохранённые вопросы из JSON-файла, по одному показывает их игроку, читает и проверяет его ответы, а в конце игры выводит счёт.
  • Ошибки обрабатываются корректно, то есть приложение не паникует при любой неожиданной ситуации. Используйте anyhow и оператор ?, чтобы передача ошибок вверх была лаконичной. Об операторе ? можно почитать здесь: https://doc.rust-lang.org/reference/expressions/operator-expr.html#the-question-mark-operator
  • Логика создания, хранения и загрузки вопросов викторины определена в библиотечной части крейта.
  • Функциональность, связанная с пользовательским вводом (разбор аргументов, чтение из stdin), определена в коде приложения, а не в библиотеке.
  • Логические части крейта разделены на модули.

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

Упражнение 3.1.3: BSN

BSN (Burgerservicennummer) — голландский персональный идентификационный номер. По назначению он отчасти похож на американский Social Security Number. BSN подчиняется определённым правилам. В этом упражнении мы создадим тип Rust, который гарантирует, что представляет собой корректный BSN.

3.1.3.A Newtype ⭐⭐

В этой части мы реализуем проверку номера BSN, а также конструктор, который может завершиться ошибкой.

BSN корректен тогда и только тогда, когда соответствует следующим критериям:

  • Он состоит из 8 или 9 цифр.
  • Он проходит вариант проверки «одиннадцатью» (elfproef, нидерл.):

Для 8-значных BSN справа дописывается 0. Цифры номера обозначаются как ABCDEFGHI. Например, для BSN 123456789 имеем A = 1, B = 2, C = 3 и так далее до I.

Тогда (9 × A) + (8 × B) + (7 × C) + (6 × D) + (5 × E) + (4 × F) + (3 × G) + (2 × H) + (-1 × I) должно делиться на 11.

Откройте exercises/3-crate-engineering/1-crate-engineering/3-bsn в редакторе. Там вы найдёте заготовку кода и два файла:

  • valid_bsns.in со списком корректных BSN;
  • invalid_bsns.in со списком некорректных BSN.

В src/lib.rs реализуйте Bsn::validate, чтобы прошёл тест test_validation. Также реализуйте Bsn::try_from_string. Чтобы запустить только тест test_validation, выполните:

cargo test -- test_validation

3.1.3.B Visitor и serde ⭐⭐⭐

Далее нужно реализовать трейты serde::Serialize и serde::Deserialize, чтобы поддержать сериализацию и десериализацию Bsn. В этом случае просто вывести (derive) эти трейты не получится: после сериализации мы хотим представлять BSN строкой. Мы также хотим десериализовать строки напрямую в Bsn, сохраняя гарантию, что созданный экземпляр Bsn представляет корректный BSN. Поэтому Bsn::validate нужно включить в реализацию посетителя (visitor) для десериализации.

Подробнее о реализации трейтов:

  • serde::Serialize: https://serde.rs/impl-serialize.html
  • serde::Deserialize: https://serde.rs/impl-deserialize.html

Если всё получилось, все тесты должны проходить.

Упражнение 3.1.4: 3D-принтер

Воображаемый 3D-принтер использует филамент, чтобы создавать самые разные изделия. Его состояния можно представить следующей диаграммой:

                   ┌─────────────────┐
                   │                 │
                   │                 │   Reset
                   │      Idle       │◄────────────────────────────┐
         ┌────────►│                 │                             │
         │         │                 │                             │
         │         │                 │                             │
         │         └────────┬────────┘                             │
         │                  │                                      │
         │                  │                                      │
         │                  │ Start                                │
         │                  │                                      │
         │                  ▼                                      │
         │         ┌─────────────────┐                    ┌────────┴────────┐
         │         │                 │                    │                 │
         │         │                 │   Out of filament  │                 │
Product  │         │    Printing     ├──────────────────► │      Error      │
retrieved│         │                 │                    │                 │
         │         │                 │                    │                 │
         │         │                 │                    │                 │
         │         └────────┬────────┘                    └─────────────────┘
         │                  │
         │                  │ Product ready
         │                  │
         │                  ▼
         │         ┌─────────────────┐
         │         │                 │
         │         │                 │
         │         │  Product Ready  │
         └─────────┤                 │
                   │                 │
                   │                 │
                   └─────────────────┘

Состояния на диаграмме сохранены на английском: это те же имена, которые используются в коде (Idle, Printing, Error, Product Ready).

Принтер при включении находится в состоянии Idle (ожидание). Когда задание запущено, принтер переходит в состояние Printing (печать). В состоянии печати принтер продолжает печатать изделие, пока оно не будет готово или пока у принтера не закончится филамент. Если филамент закончился, принтер переходит в состояние Error (ошибка), из которого можно выйти только после сброса устройства. Если изделие готово, принтер переходит в состояние Product Ready (изделие готово), а когда пользователь заберёт изделие, принтер возвращается в Idle.

Принтер можно представить в Rust с помощью паттерна typestate, как описано на лекции. Это позволит написать простой драйвер 3D-принтера. В exercises/3-crate-engineering/1-crate-engineering/4-3d-printer создаётся структура Printer3D. Добавьте методы, соответствующие каждому из трейтов, которые имитируют переходы между состояниями, выводя текущее состояние. Метод, имитирующий проверку того, закончился ли филамент, уже предоставлен.

Конечно, чтобы сделать принтер реалистичнее, можно добавить больше состояний и переходов.

Модуль 3.2 — Тестирование и фаззинг

Слайды

Упражнение 3.2.1: Декодирование DNS

В этом упражнении вы потренируетесь в модульном тестировании, измерении покрытия кода и написании небольшого фаззера с помощью cargo-fuzz.

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

Например, mailcrab.tweedegolf.nl можно закодировать как b"\x08mailcrab\x0Atweedegolf\x02nl\0". При этом действуют два ограничения:

  • Максимальный размер одной части — 63 байта.
  • Максимальный размер полного доменного имени (включая точки) — 255 байт.

Мы подготовили «первую попытку» в exercises/3-crate-engineering/2-testing/1-dns-decode. Определение функции находится в src/lib.rs, а рядом есть src/main.rs, чтобы можно было запускать функцию интерактивно. В src/lib.rs даже есть несколько модульных тестов, так что старт хороший! Однако функция далеко не полностью протестирована и содержит ошибки. Возможно, вы их найдёте?

Упражнение 3.2.1.A: Улучшаем покрытие кода

Если вы ещё этого не делали, установите cargo-llvm-cov:

cargo install cargo-llvm-cov

После этого посмотреть покрытие можно, запустив cargo llvm-cov из каталога крейта. Также можно выполнить cargo llvm-cov --open, чтобы открыть отчёт о покрытии в браузере.

Хотя покрытие строк высокое, как видно, оно не равно 100%. Напишите модульный тест, чтобы покрытие строк для src/lib.rs действительно достигло 100% (на практике этого редко удаётся добиться).

Упражнение 3.2.1.B: Фаззинг

Даже при 100% покрытии в коде могут скрываться ошибки. Найдём их с помощью cargo-fuzz. Чтобы его установить, нужно не только установить cargo-fuzz, но и использовать тулчейн «nightly», который содержит все экспериментальные возможности. Установить оба инструмента можно так:

rustup toolchain install nightly
cargo install cargo-fuzz

Затем из каталога exercises/3-crate-engineering/2-testing/1-dns-decode выполните cargo fuzz init. Эта команда создаст подкаталог fuzz/ с шаблоном цели для фаззинга. (Обратите внимание: подкаталог fuzz/ — это небольшой проект на Rust со своим Cargo.toml! Поэтому в него тоже можно добавлять зависимости, как в любой другой проект на Rust.)

Список доступных целей можно посмотреть командой:

cargo +nightly fuzz list

Цель fuzz_target_1 уже можно запустить командой

cargo +nightly fuzz run fuzz_target_1

Но она не найдёт ошибок. Отредактируйте fuzz/fuzz_targets/fuzz_target_1.rs, чтобы она делала что-то интересное! Также подумайте о добавлении других целей командой cargo +nightly fuzz add <TARGETNAME>.

Подсказки:

  • Чтобы обратиться к функции dns_decode, импортируйте её в цели фаззинга:
    #![allow(unused)]
    fn main() {
    use dns_parse::decode_dns_name;
    }
  • Начните с простого: напишите фаззер, который просто вызывает dns_decode для входных данных &[u8], и проверьте, находит ли он падения.
  • Попробуйте проверить два других свойства, которые должны выполняться для dns_decode:
    • максимальная длина части — 63 байта;
    • максимальная длина доменного имени — 255 байт.
  • Как и в модульных тестах, assert! — ваш лучший друг.

Модуль 3.3 — Инструментарий Cargo

Слайды

Упражнение 3.3.1: FizzBuzz

В этом упражнении вы попрактикуетесь в написании модульного теста и используете средства бенчмаркинга Rust, чтобы оптимизировать приложение FizzBuzz. Понадобится cargo-criterion — инструмент, который запускает бенчмарки и создаёт наглядные отчёты. Установить его можно командой

cargo install cargo-criterion --version=1.1.0

3.3.1.A Тестируем FizzBuzz ⭐

Откройте exercises/3-crate-engineering/3-cargo-tooling/1-fizzbuzz/src/lib.rs. Создайте модульный тест, который проверяет корректность функции fizz_buzz. Для этого можно использовать макрос include_str, чтобы включить файл exercises/3-crate-engineering/3-cargo-tooling/1-fizzbuzz/fizzbuzz.out в бинарник как &str. Каждая строка fizzbuzz.out содержит ожидаемый результат функции fizz_buzz для номера строки, переданного в качестве входных данных. Запустить тест можно командой

cargo test

По умолчанию тестовый харнесс (test harness) перехватывает весь вывод и отбрасывает его. Если вы хотите отлаживать тестовый код с помощью печати, можно запустить

cargo test -- --nocapture

чтобы харнесс не перехватывал вывод.

3.3.1.B Бенчмаркинг FizzBuzz ⭐⭐

Вероятно, вы заметили, что реализация fizz_buzz не очень оптимизирована. Мы используем criterion, чтобы измерить производительность fizz_buzz. Чтобы запустить бенчмарк, выполните следующую команду из каталога exercises/3-crate-engineering/3-cargo-tooling/1-fizzbuzz/:

cargo criterion

Эта команда запустит бенчмарки и выведет статистику в терминал. Она также создаёт HTML-отчёты с графиками, которые находятся в target/criterion/reports. Например, target/criterion/reports/index.html — это сводка по всем бенчмаркам. Откройте её в браузере и посмотрите.

Ваша задача — оптимизировать функцию fizz_buzz и с помощью cargo-criterion измерить эффект каждого изменения. Не бойтесь менять сигнатуру fizz_buzz, если, например, хотите уменьшить количество аллокаций. Однако убедитесь, что функция по-прежнему выдаёт правильный результат. Насколько быстро вы сможете сделать FizzBuzz?

Модуль 4.1 — Введение в многозадачность

Слайды

В этом модуле нет упражнений.

Модуль 4.2 — Параллельная многозадачность

Слайды

Упражнение 4.2.1: TF-IDF

Следуйте инструкциям в комментариях файла exercises/4-multitasking/2-parallel-multitasking/1-tf-idf/src/main.rs!

Упражнение 4.2.2: Mutex

Простой mutex при ожидании захвата блокировки крутится в цикле (spin-loop). Это крайне неэффективно. К счастью, операционная система умеет ждать, пока блокировка освободится, и в это время просто усыпляет поток.

Эта возможность доступна в крейте atomic_wait. Раздел о реализации mutex из книги Rust Atomics and Locks объясняет, как им пользоваться.

  • Замените AtomicBool на AtomicU32.
  • Реализуйте lock. Будьте внимательны к ложным пробуждениям (spurious wakes): после того как wait вернул управление, условие всё равно нужно проверить.
  • Реализуйте разблокировку (Drop for MutexGuard<T>) с помощью wake_one.

В упомянутой главе mutex оптимизируется дальше. Формально это выходит за рамки курса, но мы не будем вас останавливать, если вы попробуете (и всё равно постараемся помочь, если застрянете)!

Модуль 4.3 — Асинхронная многозадачность

Слайды

Упражнение 4.3.1: От синхронного кода к асинхронному

Синхронный и асинхронный код на Rust выглядят не так уж по-разному. В этом упражнении мы превратим синхронный TCP-эхо-сервер на Rust в асинхронный.

Откройте exercises/4-multitasking/3-asynchronous-multitasking/1-sync-to-async в редакторе. Следуя шагам в main.rs, сначала проверьте, что программа работает, затем преобразуйте её в async и снова проверьте, что она работает.

Упражнение 4.3.2: Приёмник данных измерений

В этом сценарии есть набор IoT-датчиков, которые измеряют качество воздуха в разных комнатах. Они отправляют данные через TCP-сокет на сервер. Сервер агрегирует данные по комнатам и записывает их в CSV-файл. Сейчас эта функциональность реализована синхронно. Ваша задача — сделать серверный код async.

Откройте exercises/4-multitasking/3-asynchronous-multitasking/2-measurement-data-sink в редакторе.

В двух разных терминалах запустите:

cargo run --bin server

и

cargo run --bin sensor-nodes

Вы должны увидеть регулярные лог-сообщения о полученных измерениях. Каждые 60 секунд в database.csv будут добавляться новые строки.

Упражнение 4.3.2A: async-изация

Затем разберитесь с комментариями TODO: в src/bin/server.rs. Проверьте, что приложение по-прежнему работает, как и раньше.

Упражнение 4.3.2B: Изменение требований

Запустите клиентов с интервалом 10 секунд:

cargo run --bin sensor-nodes -- -i 10s

Разберитесь с возникающими ошибками и устраните их.

Упражнение 4.3.3: Асинхронные каналы

Каналы — очень удобный способ обмениваться сообщениями между потоками и async-задачами. Они позволяют разбить приложение на множество слабо связанных задач. Вы увидите, как это пригодится в упражнении E.2. В этом упражнении вы реализуете два варианта: oneshot-канал и канал «много отправителей — один получатель» (multi-producer-single-consumer, MPSC). Если хочется испытать себя, можно написать и broadcast-канал.

4.3.3.A Канал MPSC ⭐⭐

Канал MPSC (multi-producer-single-consumer) позволяет нескольким Sender отправлять много сообщений одному Receiver.

Откройте exercises/4-multitasking/3-asynchronous-multitasking/3-async-channels в редакторе. Там вы найдёте заготовку кода. Для части A работайте в src/mpsc.rs. Исправьте todo! в этом файле, чтобы тест прошёл. Для проверки выполните:

cargo test -- mpsc

Если тесты зависают, скорее всего, ваша реализация неправильно использует Waker или возвращает Poll::Pending там, где не должна.

4.3.3.B Oneshot-канал ⭐⭐⭐

Oneshot — канал, который позволяет одному Sender отправить ровно одно сообщение одному Receiver.

Для части B работайте в src/broadcast.rs. На этот раз больше придётся сделать самостоятельно. Ожидаемое поведение:

  • Receiver реализует Future. Он возвращает Poll::Ready(Ok(T)), если inner.data равно Some(T); Poll::Pending, если inner.data равно None; и Poll::Ready(Err(Error::SenderDropped)), если Sender был уничтожен.
  • Receiver::poll заменяет inner.waker на тот, что пришёл в Context.
  • Sender потребляет self при отправке, поэтому его можно использовать не более одного раза. Отправка устанавливает inner.data в Some(T). Если Receiver был уничтожен до отправки, возвращается Err(Error::ReceiverDropped(T)).
  • Sender::send будит inner.waker после того, как записал данные в inner.data.
  • Когда Sender уничтожается, он помечает себя уничтоженным в inner.
  • Когда Receiver уничтожается, он помечает себя уничтоженным в inner.
  • После успешной отправки потреблённый Sender не помечается как уничтоженный. Вместо этого используется std::mem::forget, чтобы не запускать деструктор.

Для проверки выполните:

cargo test -- broadcast

4.3.3.C Broadcast-канал (бонус) ⭐⭐⭐⭐

Broadcast-канал поддерживает несколько отправителей и несколько получателей. Каждое сообщение, отправленное любым из отправителей, получает каждый получатель. Поэтому реализация должна хранить сообщения, пока их не получит каждый ещё не уничтоженный получатель. Из этого также следует, что при рассылке сообщение нужно клонировать.

В этом бонусном упражнении заготовки нет. Вдохновляйтесь модулями mpsc и oneshot и реализуйте модуль broadcast самостоятельно.

Модуль 5.1 — Rust для веб-серверов

Слайды

Упражнение 5.1.1: Lettuce Crop

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

5.1.1.A Hello axum

В exercises/5-rust-for-web/1-rust-for-web/1-lettuce-crop уже настроено начало нашего веб-сервера. Сейчас он отдаёт «Hello, world!» только на GET-запросы к главной странице. Запустите программу и откройте в браузере http://[::]:7000/, чтобы проверить, что всё работает.

Адрес http://[::]:7000/ — это неуказанный (wildcard) адрес для IPv6. Если вы хотите использовать IPv4, откройте http://0.0.0.0:7000/. Если сервер нужен только на localhost, используйте http://127.0.0.1:7000/ или http://[::1]:7000/.

В main.rs вы увидите Router, который отдаёт «Hello, world!». Можно связать несколько маршрутов, чтобы обслуживать несколько конечных точек. Попробуйте добавить второй маршрут, который обрабатывает GET-запросы к другой странице (например, /hello).

5.1.1.B Отдача статических файлов

Пока наш веб-сервер отдаёт только статические строки. Чтобы отдавать статические HTML-документы, CSS-таблицы стилей, изображения и другие файлы, используем файловый сервер ServeDir из tower_http. Его можно добавить в роутер как fallback-сервис, который обработает любой запрос, не совпавший ни с одним из заданных маршрутов.

Добавьте в роутер fallback_service с ServeDir, который отдаёт файлы из папки assets.

Если теперь открыть http://0.0.0.0:7000/index.html, вы должны увидеть страницу Lettuce Crop с нужным оформлением и изображением салата.

По умолчанию ServeDir автоматически добавляет index.html, если запрошен путь к каталогу. Значит, если удалить маршрут «Hello, world!» для /, страница Lettuce Crop появится и на главной странице сайта!

5.1.1.C POST-запросы и динамические ответы

На странице Lettuce Crop есть HTML-форма, которая при отправке делает POST-запрос на /crop:

<form action="/crop" method="post" enctype="multipart/form-data">

POST-запросы могут содержать дополнительные данные, которые отправляются серверу. В нашем случае вместе с запросом отправляются данные формы: изображение и значение максимального размера.

Если выбрать изображение и нажать кнопку обрезки, браузер перенаправит вас на /crop, которого пока не существует. Если открыть инструменты разработчика браузера (правый клик → Inspect или ctrl+shift+i) и перейти на вкладку Network, вы увидите POST-запрос, который сейчас возвращает статус 405 (Method Not Allowed). Маршрут /crop пока обрабатывается fallback-сервисом, который не принимает POST-запросы. Если открыть http://0.0.0.0:7000/crop напрямую, минуя форму, браузер отправит обычный GET-запрос, и вернётся статус 404 (Not Found).

Добавьте в роутер маршрут для /crop, который будет обрабатывать POST-запросы из формы. Маршрут задаётся так же, как и для GET-запросов, только вместо get используйте post.

Вместо статической строки для ответа на запрос можно использовать функцию. Определите следующую функцию и передайте её в метод post для /crop:

#![allow(unused)]
fn main() {
async fn crop_image() -> String {
    format!("Hi! This is still a work in progress. {}", 42)
}
}

5.1.1.D Обработка загруженных файлов (multipart form data)

Как же получить данные формы из POST-запроса? В axum для получения информации о запросе (заголовков, путей, параметров запроса) используются экстракторы. Обычно для получения отправленной формы применяют экстрактор Form. Однако поскольку пользователь должен иметь возможность загрузить изображение, мы используем кодировку multipart form data, которая задаётся атрибутом enctype в HTML-теге form.

Чтобы извлечь multipart-данные формы в axum, используется экстрактор Multipart. В отличие от Form, экстрактор Multipart не разбирает данные автоматически в удобную структуру. Вместо этого нужно вручную пройтись по полям и десериализовать нужные данные.

Добавьте mut multipart: Multipart параметром в функцию crop_image, чтобы извлечь multipart-данные формы. Затем используйте следующий цикл, чтобы вывести все поля, которые были переданы в POST-запросе:

#![allow(unused)]
fn main() {
while let Some(field) = multipart.next_field().await.unwrap() {
    let name = field.name().unwrap().to_string();
    let bytes = field.bytes().await.unwrap();
    println!("{name}: {} bytes long", bytes.len());
}
}

После отправки формы вы должны увидеть поле image с данными изображения и поле max_size, которое соответствует полю ввода максимального размера в форме.

Теперь разберём два поля формы:

  • Поле image содержит байты, из которых состоит изображение. Чтобы прочитать данные изображения, используем ImageReader из крейта image:

    #![allow(unused)]
    fn main() {
    ImageReader::new(Cursor::new(bytes)).with_guessed_format().unwrap().decode()
    }

    Это вернёт DynamicImage, который может быть изображением одного из различных форматов. С помощью крейта image мы сможем обрезать и масштабировать это изображение.

  • Поле max_size содержит число в виде обычного текста. Текст можно получить через field.text() вместо field.bytes(), а затем распарсить в число с помощью .parse(). Пусть это будет u32.

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

Чтобы проверить, что всё работает, замените строку, которую возвращает crop_image, на следующую:

#![allow(unused)]
fn main() {
format!("Image size: {}x{}\nMax size: {}", image.width(), image.height(), max_size)
}

5.1.1.E Отправка обрезанного изображения в ответе

Обрежем DynamicImage до квадрата с помощью следующего кода:

#![allow(unused)]
fn main() {
let size = min(min(image.width(), image.height()), max_size);
let image = image.resize_to_fill(size, size, imageops::FilterType::Triangle);
}

Размер обрезанного квадратного изображения — это минимум из ширины, высоты и заданного максимального размера. Метод resize_to_fill обрежет и масштабирует изображение до нужного размера, аккуратно выровняв его по центру.

Теперь, когда изображение обрезано, его нужно вернуть клиенту. Кодируем изображение обратно в формат с помощью write_to; обрезанные изображения мы решили возвращать в формате WebP:

#![allow(unused)]
fn main() {
let mut image_buffer = Vec::new();
image
    .write_to(&mut BufWriter::new(Cursor::new(&mut image_buffer)), ImageFormat::WebP)
    .unwrap();
}

Чтобы отправить эти байты клиенту как изображение, нужно создать ответ с правильным заголовком типа содержимого и буфером изображения в качестве тела. Измените crop_image, чтобы она возвращала Response вместо String, и создайте ответ с помощью Response::builder(). Установите заголовок «content-type» в соответствии с выбранным форматом изображения (например, image/webp для WebP), а тело ответа сформируйте из буфера изображения с помощью Body::from.

Теперь, если отправить изображение на сайт, оно должно вернуться обрезанным до квадрата!

5.1.1.F Обработка ошибок и валидация входных данных

Сейчас обработчик, вероятно, содержит много .unwrap(), которые могут вызвать панику. К счастью, axum перехватывает такие паники в нашем обработчике и продолжает работать после вывода сообщения о панике. Однако в этом случае пользователь не получит от axum никакого корректного ответа. Чтобы сообщить клиенту, что пошло не так, можно реализовать более аккуратную обработку ошибок.

Изменим функцию crop_image, чтобы она возвращала Result<Response, (StatusCode, &'static str)>. Так мы сможем возвращать ошибки, состоящие из HTTP-кода статуса и статической строки.

Например, пусть пользователь загрузит повреждённое изображение. Тогда метод .decode() у ImageReader вернёт ошибку, и .unwrap() вызовет панику. Заменим .unwrap() на .map_err, который сообщает пользователю, что запрос некорректен:

.map_err(|_| (StatusCode::BAD_REQUEST, "Error: Could not decode image"))?

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

Сейчас размер обрезанного изображения задаётся как минимум из ширины и высоты исходного изображения и значения max_size. В HTML-форме значение max_size ограничено числом 2048. Однако данным от клиента нельзя доверять: HTML и JavaScript, которые выполняются на устройстве клиента, легко изменить, а клиент может отправить и изменённые HTTP-запросы. Поэтому также вернём StatusCode::BAD_REQUEST, если max_size больше 2048.

По умолчанию для тела запроса установлен лимит 2 МБ. Если пользователь отправит изображение больше этого лимита, вызов .bytes() для поля multipart вернёт ошибку. В таком случае можно вернуть StatusCode::PAYLOAD_TOO_LARGE. Если вы хотите принимать большие изображения, задайте больший лимит с помощью DefaultBodyLimit.

5.1.1.G Отдача файлов из памяти (бонус)

Сейчас статические файлы отдаются из папки assets. Вместо этого мы можем упаковать эти файлы в бинарник с помощью memory-serve. Это удобно: все файлы попадают в один бинарник, а кроме того, это может повысить производительность!

После добавления memory-serve в проект командой cargo add memory-serve можно определить роутер для памяти так:

#![allow(unused)]
fn main() {
let memory_router = MemoryServe::new(load_assets!("assets"))
    .index_file(Some("/index.html"))
    .into_router();
}

Теперь этот роутер можно использовать как fallback-сервис вместо ServeDir.

Если собрать проект в режиме release (cargo build --release), вы увидите, что файлы из папки assets включены в бинарник!

Упражнение 5.1.2: Pastebin

В этом упражнении нужно написать простой веб-сервер pastebin. Веб-сервер снова будет построен на axum. Для этого упражнения проект нужно настроить самостоятельно.

  • Данные хранятся в памяти. Бонус, если вы используете базу данных или sqlite, но сначала добейтесь, чтобы приложение работало без неё.
  • Реализуйте маршрут, на который можно отправить POST-запрос с простым текстом. Он сохраняет текст вместе со свежесгенерированным UUID, который возвращается в ответе. Для генерации UUID можно использовать крейт uuid.
  • Реализуйте маршрут, на который можно отправить GET-запрос с UUID. Он возвращает простой текст, соответствующий UUID, или ошибку 404, если такого текста нет.
  • Реализуйте маршрут, на который можно отправить DELETE-запрос с UUID. Он удаляет текст, соответствующий этому UUID.

Модуль 5.2 — Rust в облаке

В этом разделе мы покажем, как использовать Rust для создания облачных сервисов. Первое упражнение использует AWS от Amazon, а второе — Scaleway, европейского облачного провайдера.

Упражнение 5.2.1: Lettuce Crop на AWS

В этом упражнении мы перенесём наш сайт Lettuce Crop из упражнения 5.1.1 в облако с помощью AWS Lambda. AWS Lambda позволяет запускать код в облаке в бессерверной (serverless) конфигурации. Это значит, что машины в дата-центрах Amazon будут автоматически запускать ваш код при необходимости: заботиться о серверах не нужно, а платите вы только за фактически использованное время вычислений.

Для этого упражнения должно хватить бесплатного уровня AWS. Однако не забудьте отключить свои Lambda-функции после тестирования, чтобы избежать непредвиденных расходов! Посмотреть, сколько квот бесплатного уровня вы ещё не использовали в этом месяце, можно на странице бесплатного уровня в разделе Billing and cost management консоли AWS.

5.2.1.A Установка Cargo Lambda

Чтобы собирать код на Rust для AWS Lambda, будем использовать Cargo Lambda. Установить Cargo Lambda можно с помощью Cargo Binstall:

cargo binstall cargo-lambda

Может понадобиться и Zig, который используется для кросс-компиляции. Если Zig не установлен, Cargo Lambda сообщит об этом при сборке Lambda и попытается помочь установить его автоматически через pip или npm.

Также можно воспользоваться любым другим способом установки Cargo Lambda, описанным здесь.

5.2.1.B Роутер axum с lambda_http

Крейт lambda_runtime предоставляет рантайм для AWS Lambda, написанных на Rust. Крейт lambda_http добавляет слой абстракции поверх lambda_runtime, который упрощает разработку HTTP-серверов на AWS Lambda с помощью Rust. Это идеально подходит для небольших динамических сайтов и REST API.

Добавьте lambda_http в проект командой:

cargo add lambda_http

Поскольку lambda_http умеет запускать роутеры axum, почти ничего менять не нужно: достаточно изменить функцию main, чтобы превратить наш сервер Lettuce Crop в Lambda. Роутер создаём как обычно, но вместо того, чтобы отдавать его через axum::serve, запускаем его функцией run из lambda_http:

use lambda_http::{run, tracing, Error};

#[tokio::main]
async fn main() -> Result<(), Error> {
    // нужно, чтобы рантайм включил логирование ошибок в CloudWatch
    tracing::init_default_subscriber();

    let app = Router::new()
        .route("/crop", post(crop_image))
        .fallback_service(ServeDir::new("assets"));

    run(app).await
}

Обновите функцию main, как показано выше, и затем попробуйте Lambda командой:

cargo lambda watch

Эта команда эмулирует Lambda локально на вашем устройстве. По умолчанию сервер доступен по адресу http://localhost:9000/.

5.2.1.C Создание функции Lambda в консоли AWS

Теперь, когда мы протестировали Lambda локально, создадим функцию Lambda в консоли AWS. Перейдите на страницу AWS Lambda в консоли AWS и нажмите «Create a function». Затем настройте её так:

  1. Выберите «Author from scratch» (создать с нуля).
  2. Задайте имя, например lettuce-crop.
  3. Выберите рантайм «Amazon Linux 2023».
  4. Выберите архитектуру «arm64» (она дешевле, чем x86_64).
  5. В разделе «Additional Configurations» включите «Enable function URL» и выберите тип аутентификации «NONE», чтобы получить общедоступный URL для функции Lambda.

Наконец, нажмите «Create function» и подождите несколько секунд, пока Lambda будет создана.

5.2.1.D Сборка и развёртывание функции Lambda

Прежде чем развернуть Lambda, нужно собрать проект для нужной архитектуры:

cargo lambda build --release --arm64 --output-format zip

Эта команда создаст файл bootstrap.zip в папке target/lambda/{имя проекта}, который можно загрузить в консоль AWS, чтобы развернуть Lambda.

Однако этот zip-файл не содержит наши ресурсы. Если мы хотим, чтобы Lambda отдавала HTML-документ, а также соответствующие CSS-файл и изображение, эти ресурсы нужно включить в архив. Создадим конфигурационный файл CargoLambda.toml, в котором опишем, как собирать Lambda, и добавим в него следующее:

[build]
arm64 = true
output_format = "zip"
include = ["assets/index.html", "assets/styles.css", "assets/crop.webp"]

Теперь, если собрать Lambda командой cargo lambda build --release, мы получим zip, который содержит и наши ресурсы. Аргументы командной строки --arm64 и --output-format больше не нужны: они заданы в конфигурационном файле.

Также, если вы используете memory-serve для отдачи ресурсов, как описано в упражнении 5.1.1.G, включать ресурсы в zip не нужно: они уже входят в бинарник.

Чтобы развернуть Lambda, нажмите кнопку «Upload from» на вкладке «Code» для нашей Lambda в консоли AWS и загрузите файл bootstrap.zip. Теперь Lambda должна работать! Откройте URL функции, указанный в обзоре функции вверху страницы, чтобы её проверить!

Также можно развернуть Lambda через CLI командой cargo lambda deploy. Однако для этого сначала нужно настроить учётные данные AWS.

Обратите внимание, что AWS Lambda принимает файлы размером не более 50 МБ. Для больших проектов можно загружать файлы в бакет S3. У S3 нет бессрочного бесплатного уровня, но есть 12-месячный бесплатный пробный период.

5.2.1.E Анализ использования Lambda через CloudWatch

Теперь, когда наша Lambda работает, посмотрим на консоль AWS. Если открыть вкладку «Monitor», можно увидеть метрики по запросам, которые обработала функция. Эти базовые метрики CloudWatch собирает автоматически и бесплатно.

Если прокрутить страницу вниз до CloudWatch Logs, вы увидите недавние вызовы функции Lambda. Если открыть log stream одного из таких запросов, вы увидите логи, созданные при его обработке. Вывод println! и логи крейта tracing должны появиться здесь. Бесплатный уровень CloudWatch позволяет хранить до 5 ГБ логов.

На вкладке «Monitor» также можно увидеть список самых дорогих вызовов. Стоимость измеряется в гигабайт-секундах, то есть в объёме памяти, умноженном на время обработки запроса. Бесплатный уровень AWS Lambda включает 1 000 000 запросов и 400 000 гигабайт-секунд в месяц.

По умолчанию Lambda настроены на 128 МБ памяти. Значение можно увеличить на вкладке «Configuration» (меньше 128 МБ задать нельзя). На этой же вкладке можно настроить таймаут обработки запросов. По умолчанию Lambda прерывается через 3 секунды, но при необходимости это можно изменить.

Что дальше?

Не забудьте ограничить или удалить функцию Lambda после тестирования, чтобы избежать непредвиденных расходов!

Упражнение 5.2.2: Lettuce Crop на Scaleway

В этом упражнении мы перенесём наш сайт Lettuce Crop из упражнения 5.1.1 в облако с помощью Scaleway Serverless Functions. Scaleway Serverless Functions позволяет запускать код в облаке в бессерверной конфигурации: машины в дата-центрах Scaleway будут автоматически запускать ваш код при необходимости. О серверах заботиться не нужно, а платите вы только за фактически использованное время вычислений.

Для этого упражнения должно хватить бесплатных ресурсов Scaleway. Однако не забудьте отключить свои serverless-функции после тестирования, чтобы избежать непредвиденных расходов! Актуальную информацию о стоимости работы serverless-функций см. на странице цен serverless.

5.2.2.A Общая структура проекта

Serverless-функции в Scaleway работают так: вы предоставляете функцию-обработчик, которая принимает запрос и возвращает ответ. В exercises/5-rust-for-web/2-rust-in-the-cloud/2-lettuce-crop-scaleway уже подготовлена базовая структура проекта serverless-функции Scaleway. В src/handler.rs есть функция-обработчик, которая превращает любой запрос в ответ с телом «Hello, world!».

Чтобы Scaleway могла использовать эту функцию, настройте проект как библиотеку, указав src/handler.rs в качестве главной точки входа. Для этого добавьте в Cargo.toml следующее:

[lib]
path = "src/handler.rs"

5.2.2.B Развёртывание serverless-функции

Чтобы развернуть serverless-функцию, перейдите на страницу функций в консоли Scaleway, выберите подходящий регион и пространство имён (или создайте новое). В этом пространстве имён нажмите кнопку «Create function» и выберите Rust в качестве рантайма.

Чтобы загрузить код, сначала создайте zip-файл с каталогом src и Cargo.toml. Загрузите этот zip в Scaleway и укажите в качестве обработчика имя функции из src/handler.rs, которая будет обрабатывать запросы (в нашем случае my_handler).

Теперь можно задать имя и настроить ресурсы по необходимости, а затем создать функцию. Дождитесь развёртывания (это может занять несколько минут), после чего откройте URL эндпоинта функции, чтобы увидеть её в действии.

5.2.2.C Обработка запросов с маршрутизацией axum

Чтобы добавить этому обработчику более интересную функциональность, можно настроить роутер axum, как в упражнении 5.1.1. Однако serverless-функции нельзя запустить через axum::serve. Вместо этого можно вызвать роутер как сервис, передав ему текущий входящий запрос:

let mut router = Router::new()
    .route("/", get("Hello, world!"))
    .route("/crop", get("Lettuce crop!"));

router.as_service().call(req).await.unwrap()

Метод call вернёт ответ на основе запроса, используя заданные маршруты.

Измените обработчик так, чтобы он использовал роутер с несколькими маршрутами, и заново разверните его на Scaleway для проверки.

5.2.2.D Отдача файлов из serverless-функций

Чтобы разместить Lettuce Crop на serverless-функции Scaleway, нужно отдавать статические ресурсы сайта. Обычно для этого подошёл бы ServeDir, но у serverless-функций Scaleway во время работы нет доступа к файлам. Вместо этого можно включить файлы в бинарник с помощью memory-serve, как описано в упражнении 5.1.1.G.

Скопируйте папку assets и функцию crop_image из упражнения 5.1.1, настройте роутер так, чтобы он обрезал изображения, отправленные POST-запросами на /crop, и отдавал ресурсы через memory-serve как fallback-сервис. Если теперь заново развернуть код (не забудьте включить папку assets в zip), у нас должна заработать полноценная версия Lettuce Crop на Scaleway!

В реальных приложениях лучше (и дешевле!) размещать статическую часть страницы отдельно, например в бакетах хранилища, а в serverless-функции обрабатывать только интерактивную часть, то есть POST-запросы для обрезки изображений.

Модуль 5.3 — Rust в браузере

Упражнение 5.3.1: Lettuce Crop на WebAssembly

В упражнении 5.1.1 мы создали веб-сервер, который предоставляет сервис обрезки изображений. Но нужно ли делать обрезку на сервере? Не было бы гораздо приватнее, если бы обрезка выполнялась в браузере пользователя, а не загружала изображения на наш внешний сервер?

В этом упражнении мы создадим новую версию сайта Lettuce Crop, который обрезает изображения с помощью WebAssembly. WebAssembly позволяет запускать скомпилированный код в безопасной изолированной среде (песочнице) в браузере. Это значит, что выделенный сервер больше не понадобится: сайт будет состоять только из статических файлов, которые можно разместить на любом HTTP-сервере. Его даже можно бесплатно опубликовать на GitHub Pages!

5.3.1.A Сборка с помощью Wasm Pack

В exercises/5-rust-for-web/3-rust-in-the-browser/1-lettuce-crop-wasm уже настроен базовый проект WebAssembly. Как видно в Cargo.toml, проект сконфигурирован как динамическая библиотека ("cdylib"). Мы также добавили в зависимости крейт wasm-bindgen, который используется для генерации привязок WebAssembly.

Для сборки проекта используем wasm-pack. Сначала установите wasm-pack:

cargo install wasm-pack

Затем соберите проект с помощью wasm-pack. Поскольку мы хотим использовать его в браузере, задаём target web и указываем, что сгенерированные файлы нужно поместить в папку assets/pkg:

wasm-pack build --target web --out-dir assets/pkg

Теперь в папке assets/pkg должно появиться несколько файлов:

  • Файл .wasm, который содержит скомпилированный код WebAssembly;
  • Несколько файлов .d.ts, которые описывают типы TypeScript для сгенерированных привязок;
  • Файл .js, который содержит JavaScript-привязки к нашему бинарнику WebAssembly.

5.3.1.B Взаимодействие с JavaScript

Итак, какой функционал сейчас включён в скомпилированный WebAssembly? В lib.rs есть две функции: внешняя (extern) функция alert() и функция hello(). Обе помечены атрибутом #[wasm_bindgen], чтобы показать, что их нужно связать с WebAssembly. Внешние функции связываются с существующими JavaScript-методами; в нашем случае это функция alert() объекта window, которая показывает всплывающее диалоговое окно.

Добавим WebAssembly на наш сайт. Вставьте следующий JavaScript в <body> файла index.html. Он загрузит бинарник WebAssembly и будет вызывать функцию hello() при нажатии кнопки отправки:

<script type="module">
    import init, { hello } from "./pkg/lettuce_crop_wasm.js";
    init().then(() => {
        const submit_button = document.querySelector('input[type="submit"]');
        submit_button.onclick = () => {
            hello("WebAssembly");
        }
    });
</script>

Чтобы попробовать сайт, можно использовать любой HTTP-сервер, который умеет отдавать локальные файлы. Например, можно использовать axum, как в упражнении 5.1.1, а если у вас установлен npm, подойдёт, например, npx http-server.

5.3.1.C Обрезка изображений

Добавим в нашу библиотеку на Rust функцию crop_image(bytes: Vec<u8>, max_size: u32) -> Vec<u8>, которая будет обрезать изображения. Можно использовать ту же логику, что и в упражнении 5.1.1 (части D и E), чтобы создать DynamicImage из входных байтов, обрезать его и экспортировать в WebP. Пометьте функцию атрибутом #[wasm_bindgen] и пересоберите библиотеку, чтобы сгенерировать для неё привязки WebAssembly.

Если посмотреть на сгенерированные JavaScript-привязки, вы увидите, что Vec<u8> в функции crop_image превратился в Uint8Array. Нам понадобится написать JavaScript, который читает выбранное пользователем изображение и передаёт его в crop_image как Uint8Array.

Сначала получим два других входных элемента:

const max_size = document.querySelector('input[name="max_size"]');
const image = document.querySelector('input[name="image"]');

Затем в onclick кнопки отправки можно получить выбранный файл через image.files[0]. Чтобы получить содержимое файла, используем FileReader:

const file = image.files[0];
const reader = new FileReader();
reader.onload = (evt) => {
    const bytes = new Uint8Array(evt.target.result);
    const cropped_bytes = crop_image(bytes, max_size.value); // вызываем нашу функцию
    // TODO: что-то сделать с cropped_bytes
};
reader.readAsArrayBuffer(file);

Наконец, чтобы показать пользователю результат, создадим Blob из Uint8Array и превратим этот Blob в URL, на который перенаправим пользователя:

window.location.href = URL.createObjectURL(new Blob([cropped_bytes]));

Если выбрать некорректный файл, в консоли браузера появится ошибка. Можно добавить более аккуратную обработку ошибок с помощью try-catch, а также проверять, что image.files[0] существует, перед чтением файла. Было бы неплохо проверить и то, что значение max_size разумно.

5.3.1.D Использование крейта web-sys (бонус)

Вместо того чтобы использовать JavaScript для работы с HTML-документом или вручную связывать внешние JavaScript-функции через #[wasm_bindgen], как мы делали с alert(), можно использовать крейт web-sys. Он предоставляет привязки к JavaScript Web API, доступным в браузере. Однако большинство этих API нужно включать вручную, по отдельным фичам.

Добавьте крейт web-sys в проект, включив все нужные фичи:

cargo add web-sys --features "Window,Document,HtmlElement,HtmlImageElement,Blob,Url"

Теперь пусть функция crop_image не возвращает массив байтов, а добавляет изображение в HTML-документ:

  • Сначала получите элемент body HTML-документа:
    #![allow(unused)]
    fn main() {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();
    }
  • Затем создайте HTML-элемент изображения:
    #![allow(unused)]
    fn main() {
    let img = document.create_element("img").unwrap();
    let img: web_sys::HtmlImageElement = img.dyn_into().unwrap();
    }
  • Чтобы задать источник изображения, снова понадобится Blob, с помощью которого получим временный data URL. Для этого сначала создаём JavaScript-массив:
    #![allow(unused)]
    fn main() {
    let bytes = web_sys::js_sys::Array::new();
    bytes.push(&web_sys::js_sys::Uint8Array::from(&buffer[..]));
    }
  • Далее создаём Blob и URL:
    #![allow(unused)]
    fn main() {
    let blob = web_sys::Blob::new_with_u8_array_sequence(&bytes).unwrap();
    let url = web_sys::Url::create_object_url_with_blob(&blob).unwrap();
    }
  • И наконец задаём источник изображения и добавляем его в body документа:
    #![allow(unused)]
    fn main() {
    img.set_src(&url);
    body.append_child(&img).unwrap();
    }
  • Не забудьте соответствующим образом обновить и JavaScript-код в HTML-документе.

Модуль 6.1 — Интерфейс внешних функций (FFI)

Слайды

Упражнение 6.1.1: CRC на C

В этом упражнении мы вызовем из программы на Rust функцию вычисления контрольной суммы CRC, написанную на C.

Инструкции

Требования:

  • Компилятор C

Шаги:

  1. Добавьте build-зависимость cc, добавив в Cargo.toml строки:

    [build-dependencies]
    cc = "1.0"
    
  2. Создайте build.rs (в той же папке, что и Cargo.toml) со следующим содержимым:

    fn main() {
        println!("cargo:rerun-if-changed=crc32.h");
        println!("cargo:rerun-if-changed=crc32.c");
        cc::Build::new().file("crc32.c").compile("crc32");
    }

    build.rs — это скрипт сборки, который cargo запускает перед компиляцией вашего крейта. Он найдёт C-код, скомпилирует его и подключит к исполняемому файлу, который создаёт Rust.

  3. В main.rs объявите extern-функцию (укажите типы аргументов и возвращаемого значения):

    #![allow(unused)]
    fn main() {
    extern "C" {
        fn CRC32( ... ) -> ...; // подсказка: https://doc.rust-lang.org/std/ffi/
    }
    }
  4. Теперь создайте Rust-обёртку, которая вызывает extern-функцию:

    #![allow(unused)]
    fn main() {
    fn crc32( ... ) -> ... {
        ... // (подсказки: `unsafe`, `.as_ptr()`, `.len()`)
    }
    }
  5. Вызовите обёртку на каком-нибудь примере входных данных:

    fn main() {
        println!("{:#x}", crc32(b"abcde"));
    }

    В примере выше правильный вывод — 0x8587d865.

Упражнение 6.1.2: CRC на Rust

Теперь сделаем наоборот: используем написанную на Rust функцию вычисления CRC в программе на C.

Инструкции

Требования:

  • Компилятор C

Шаги:

  1. Добавьте в Cargo.toml следующее:

    [lib]
    name = "crc_in_rust"
    crate-type = ["staticlib"]
    
  2. Экспортируйте extern-функцию Rust в lib.rs:

    #![allow(unused)]
    fn main() {
    #[unsafe(no_mangle)]
    pub extern "C" fn crc32(...) -> ... {
    
        ...
    
        crc32_rust(...)
    }
    }
  3. Создайте заголовочный файл C crc_in_rust.h:

    #include <stdint.h> // uint32_t, uint8_t
    #include <stddef.h> // size_t
    
    uint32_t crc32(const uint8_t* data, size_t data_length);
    
  4. Создайте main.c и используйте в нём функцию crc32 на Rust:

    #include "crc_in_rust.h"
    #include <stddef.h> // size_t
    #include <inttypes.h> // uint32_t, uint8_t, PRIx32
    #include <stdio.h>  // printf
    
    int main() {
        uint8_t data[] = "abcde";
        size_t data_length = 5;
    
        uint32_t hash = crc32(data, data_length);
    
        printf("Hash: 0x%"PRIx32"\n", hash);
    
        return 0;
    }
    
  5. Задайте функции на Rust ту же сигнатуру, что и в заголовочном файле.

  6. Соберите крейт Rust, а затем запустите программу.

    Linux и macOS:

    # Собираем main.c, связываем его с библиотекой и получаем исполняемый файл main
    $ clang main.c target/debug/libcrc_in_rust.a -lpthread -ldl -omain
    # Запускаем исполняемый файл
    $ ./main
    Hash: 0x8587d865
    

    Windows:

    # Собираем main.c, связываем его с import-библиотекой DLL и получаем исполняемый файл main.exe
    ❯ clang main.c .\target\debug\crc_in_rust.lib -lkernel32 -lntdll -luserenv -lws2_32 -ldbghelp -o "main.exe"
    # Запускаем исполняемый файл
    ❯ .\main.exe
    Hash: 0x8587d865
    

Упражнение 6.1.3: QOI и bindgen

В этом упражнении мы используем bindgen, чтобы сгенерировать FFI-привязки к C-библиотеке. Bindgen читает заголовочный C-файл и на основе определений C создаёт функции, типы и константы для Rust.

Однако сгенерированный код, скорее всего, будет громоздким и не идиоматичным. Чтобы грамотно обернуть C-библиотеку, нужен хороший дизайн API и документация.

Справочная информация

Крейт image предоставляет функции для кодирования, декодирования и редактирования изображений на Rust. Он поддерживает множество форматов, например JPEG, PNG и GIF, а также QOI. QOI — это «Quite OK Image format», формат, который нацелен на быстрое кодирование и декодирование изображений при размере файла, сопоставимом с PNG. В этом упражнении мы проверим, даёт ли крейт image такие же результаты при декодировании QOI-изображений, как эталонная C-библиотека QOI.

Библиотека QOI на C — header-only, то есть реализации функций находятся прямо в заголовочном файле, а не в отдельном C-файле. Мы добавили отдельный C-файл, который подключает заголовок, чтобы было проще скомпилировать библиотеку и включить её в программу на Rust.

Генерация привязок

Требования:

  • В системе установлен компилятор C
  • Bindgen, который можно установить командой cargo install bindgen-cli

Шаги:

  1. Создайте привязки для Rust: bindgen qoi.h -o src/bindings.rs

  2. Используйте скрипт build.rs, чтобы скомпилировать и связать qoi.h. Создайте build.rs и вставьте:

    fn main() {
        cc::Build::new().file("qoi.c").compile("qoi"); // создаёт `qoi.a`
    }

    И добавьте в Cargo.toml такой раздел:

    [build-dependencies]
    cc = "1"
    
  3. Создайте src/lib.rs с содержимым pub mod bindings;. Это сделает модуль bindings доступным в main.rs.

  4. Выполните cargo check, чтобы убедиться, что всё компилируется правильно.

Изучаем привязки

В сгенерированном файле bindings.rs мы находим такую сигнатуру для C-функции qoi_read из QOI:

#![allow(unused)]
fn main() {
extern "C" {
    pub fn qoi_read(
        filename: *const ::std::os::raw::c_char,
        desc: *mut qoi_desc,
        channels: ::std::os::raw::c_int,
    ) -> *mut ::std::os::raw::c_void;
}
}

Кое-что из этого можно заметить:

  • Определение находится внутри блока extern "C" и не имеет тела. Поэтому функция помечена как extern, и Rust ожидает, что её свяжут при сборке.
  • Функция помечена pub, то есть её можно импортировать и использовать в других модулях (например, в нашем main.rs).
  • По сигнатуре типов можно в общих чертах понять поведение функции:
    • filename — C-строка с именем QOI-файла, который нужно прочитать;
    • desc описывает метаданные изображения, и функция запишет их в структуру qoi_desc. Эту структуру тоже сгенерировал bindgen:
      #![allow(unused)]
      fn main() {
      #[repr(C)]
      #[derive(Debug, Copy, Clone)]
      pub struct qoi_desc {
          pub width: ::std::os::raw::c_uint,
          pub height: ::std::os::raw::c_uint,
          pub channels: ::std::os::raw::c_uchar,
          pub colorspace: ::std::os::raw::c_uchar,
      }
      }
    • channels — количество каналов изображения: 3 для RGB или 4 для RGBA (у последнего есть альфа-канал для прозрачности). В этом упражнении мы считаем, что у изображений есть альфа-канал.
    • Возвращаемое значение — void-указатель. Если функция успешно прочитала пиксельные данные из QOI-изображения, этот указатель должен указывать на них.
  • Поскольку типы здесь — сырые C-типы, вызывать функцию напрямую из Rust неудобно.

Последнюю проблему решим, написав удобную Rust-обёртку вокруг сгенерированных привязок.

Пишем обёртку

Чтобы функцию qoi_read было проще использовать, напишем обёртку, которая принимает путь и возвращает буфер изображения:

#![allow(unused)]
fn main() {
fn read_qoi_image(filename: &Path) -> ImageBuffer<Rgba<u8>, &[u8]> {
    todo!()
}
}

Чтобы реализовать эту обёртку, нужно решить несколько задач:

  • Нужно превратить путь в C-строку. Подсказка: можно использовать std::ffi::CString::new, чтобы создать C-строку из последовательности байтов. Удобнее всего получить байты пути, сначала получив из него OsStr. Затем C-строку можно передать как указатель.
  • Нужно подготовить qoi_desc. Эту структуру можно импортировать из привязок. Передайте в функцию изменяемую ссылку на экземпляр этой структуры.
  • После вызова qoi_read нужно превратить возвращённый void-указатель в буфер изображения:
    • Сначала проверьте, не is_null() ли возвращённый void-указатель. Если он нулевой, при чтении изображения что-то пошло не так.
    • Затем определите длину возвращённых пиксельных данных. Если считать, что у изображения есть альфа-канал, на каждый пиксель приходится 4 байта. Количество пикселей можно определить по структуре qoi_desc.
    • Теперь можно превратить void-указатель в &[u8]. Сначала приведите его as *const u8, затем используйте std::slice::from_raw_parts с ранее вычисленной длиной.
    • Наконец, создайте буфер изображения с помощью ImageBuffer::from_raw.

Чтобы опробовать обёртку, прочитаем QOI-изображение и сохраним его как PNG:

fn main() {
    let image = read_qoi_image(Path::new("image.qoi"));
    image.save("image.png").unwrap();
}

Если всё реализовано правильно, получится красивая картинка!

Теперь, когда мы умеем декодировать изображения эталонной C-библиотекой QOI, можно проверить, что крейт image даёт те же результаты, с помощью следующего модульного теста:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use crate::read_qoi_image;
    use std::path::Path;

    #[test]
    fn test_qoi_read() {
        let filename = "image.qoi";
        let image = image::open(filename).unwrap().into_rgba8();
        let my_image = read_qoi_image(Path::new(filename));

        assert_eq!(image.width(), my_image.width());
        assert_eq!(image.height(), my_image.height());

        assert!(image.pixels().eq(my_image.pixels()));
    }
}
}

Если добавить этот тест в main.rs и запустить cargo test, мы увидим:

running 1 test
test tests::test_qoi_read ... ok

Освобождение пиксельных данных

Работая с данными из C, мы сами отвечаем за освобождение памяти, когда перестаём её использовать. Некоторые C-библиотеки предоставляют отдельную функцию для очистки структур данных. Для QOI же нужно вызвать libc::free, чтобы освободить память, как указано в документации функции qoi_read:

Возвращённые пиксельные данные нужно освободить через free() после использования.

Чтобы пользователь нашей обёртки не забыл освободить память, можно реализовать трейт Drop, который автоматически вызывает libc::free, когда переменная выходит из области видимости.

  • Сначала создайте обёртку struct MyImage<'a>(ImageBuffer<Rgba<u8>, &'a [u8]>);, которая хранит буфер изображения.
  • Затем реализуйте трейт Drop для MyImage, чтобы освобождать память. Указатель нужно получить из буфера изображения и привести обратно к void-указателю:
    #![allow(unused)]
    fn main() {
    impl Drop for MyImage<'_> {
        fn drop(&mut self) {
            todo!(); // вызовите здесь libc::free, передав указатель на буфер изображения
        }
    }
    }
  • Чтобы обёрткой MyImage было удобнее пользоваться, можно реализовать трейт Deref. Тогда методы внутреннего буфера изображения будут вызываться на ней напрямую:
    #![allow(unused)]
    fn main() {
    impl<'a> Deref for MyImage<'a> {
        type Target = ImageBuffer<Rgba<u8>, &'a [u8]>;
    
        fn deref(&self) -> &Self::Target {
            &self.0
        }
    }
    }
  • Теперь измените функцию read_qoi_image, чтобы она возвращала экземпляр MyImage.

Неинициализированная память

Есть ещё один приём. Наша текущая функция инициализирует структуру qoi_desc нулями (или любыми значениями, которые вы записали при создании экземпляра). Это расточительно, потому что внешняя функция всё равно перезапишет эти значения. Поскольку внешняя функция связывается на этапе линковки, компилятор, вероятно, не имеет достаточно информации, чтобы оптимизировать эту инициализацию.

Для небольшой структуры вроде qoi_desc это не слишком важно. Однако для больших структур или массивов это может заметно повлиять на производительность.

Если посмотреть на LLVM IR, промежуточное представление, которое генерируется и оптимизируется до превращения в ассемблер, видно, что оптимизатор не убрал инициализацию структуры значениями. Здесь memset обнуляет desc перед вызовом qoi_read:

call void @llvm.memset.p0.i64(ptr noundef nonnull align 4 dereferenceable(10) %desc.i, i8 0, i64 10, i1 false), !noalias !142
%pointer.i = call noundef ptr @qoi_read(ptr noundef nonnull %t.0.i.i, ptr noundef nonnull %desc.i, i32 noundef 4) #17, !noalias !142

(LLVM IR можно сгенерировать командой cargo rustc --bin qoi-bindgen --release -- --emit=llvm-ir.)

Решение — использовать std::mem::MaybeUninit:

#![allow(unused)]
fn main() {
let mut desc = MaybeUninit::uninit();
let pointer = unsafe { qoi_read(filename.as_ptr(), desc.as_mut_ptr(), 4) };
let desc = unsafe { desc.assume_init() };
}

Тип MaybeUninit — это абстракция над неинициализированной памятью. Метод .uninit() выделяет кусок неинициализированной памяти, достаточный для хранения значения нужного типа (в нашем случае тип qoi_desc будет выведен автоматически).

Итоги

В этом упражнении мы увидели, как с помощью bindgen создать привязки к C-библиотеке. Сгенерированные привязки довольно неудобны в работе: они небезопасны (unsafe) и опираются на C-типы. Мы обсудили, как создать удобные обёртки вокруг сгенерированных привязок, которые берут на себя работу с C-типами и делают код безопаснее.

Модуль 7.1 — Rust для программистов на Python

Слайды

Упражнение 7.1.1: Проверка окружения

Установите uv и Rust.

Проверьте, что всё работает:

cd rust-dna
uv run maturin develop --uv
cd ..
uv run pytest

Вывод должен выглядеть примерно так:

❯ uv run pytest
Installed 5 packages in 9ms
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.3.5, pluggy-1.5.0
rootdir: /home/tamme/dev/rust-training/exercises/7-rust-for-data-science/1-rust-from-python
configfile: pyproject.toml
collected 10 items

test_decoding.py ....                                                    [ 40%]
test_kmers.py ....                                                       [ 80%]
test_setup.py .                                                          [ 90%]
test_validation.py .                                                     [100%]

============================== 10 passed in 0.06s ==============================

Упражнение 7.1.2: Перевод Python-кода на Rust

Откройте exercises/7-rust-for-data-science/1-rust-from-python/ в редакторе и следуйте шагам в main.py, чтобы перевести логику на Rust в rust-dna/lib.rs.

Модуль 7.2 — Async Rust и Python

Слайды

Упражнение 7.2.1: Проверка окружения

7.2.1.A: Установка инструментов ⭐

Следуйте инструкциям по установке инструмента сборки Maturin: https://pyo3.rs/v0.21.2/getting-started. Рекомендуем использовать pyenv: https://github.com/pyenv/pyenv, но pyenv работает только в UNIX и WSL. Конечно, можно использовать и любой другой менеджер виртуальных окружений.

Перейдите в терминале в папку exercises/7-rust-for-data-science/1-rust-from-python/1-hello-world/. Создайте и активируйте виртуальное окружение и установите всё необходимое. Если вы используете pyenv, выполните следующие команды:

$ pyenv activate pyo3
$ pip install maturin
$ pip install asyncio
$ pip install aiofiles

7.2.1.B: Пробный запуск ⭐

Если всё прошло успешно, вы сможете собрать этот проект в расширение для Python:

$ maturin develop
$ python test.py

Скрипт test.py заканчивается вызовом функции, которая безусловно выбрасывает исключение, поэтому ожидается, что скрипт завершится необработанным исключением.

Теперь попробуйте запустить и test_async.py и понаблюдайте за его поведением.

7.2.1.C: Параллельный запуск футур ⭐⭐

В Python-скрипте для выполнения нескольких awaitable-объектов можно использовать asyncio.gather. Создайте несколько футур print_sleep с разной длительностью сна, запустите их через asyncio.gather и понаблюдайте за поведением.

Упражнение 7.2.2: Потоковый JSON

Вот полезный крейт: Struson. Он позволяет (де)сериализовать JSON-объекты потоково, то есть не держать все данные в памяти. Это контрастирует с отличной библиотекой serde_json, которая очень удобна, но не позволяет легко (де)сериализовать данные потоком. Мы создадим обёртку над Struson, которой можно будет пользоваться из Python.

Представим, что у нас есть поток JSON. Он может приходить от кого-то из интернета или от другого процесса на этой машине. Или, например, из файла. Поток состоит из данных такого вида:

[
    {
        "lhs": {
            "d":[
                1, 2, 3, 4,
                5, 6, 7, 8,
                9, 10, 11, 12
            ],
            "n": 3
        },

        "op": [
            {
                "code": "dot",
                "rhs": {
                    "d": [
                        13, 14, 15, 16,
                        17, 18, 19, 20,
                        21, 22, 23, 24
                    ],
                    "n": 3
                }
            }
        ]
    }
]

Этот JSON — массив объектов, каждый из которых описывает следующее:

  • lhs: матрица с m строками и n столбцами. m можно вычислить по n и длине d. В данном случае m = len / n = 12 / 3 = 4.
  • op: последовательность операций, которые нужно выполнить над lhs и, если операция принимает два операнда, над полем rhs. Поле code задаёт операцию, которая должна более или менее соответствовать методам типа Matrix из nalgebra. В данном случае нужно выполнить метод Matrix::dot, который, и не без оснований, описан как «скалярное произведение двух векторов или матриц (рассматриваемых как векторы)».

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

Упражнение 7.2.2.A: Попробуйте ⭐

Заготовка в exercises/7-rust-for-data-science/1-rust-from-python/2-strompy/ реализует задуманное на Rust и также содержит синхронную реализацию. Посмотрите код и попробуйте в нём разобраться. Если хотите узнать больше о зависимостях, помогут docs.rs. Обратите внимание, что для struson мы импортируем форк, который поддерживает асинхронную десериализацию.

Чтобы собрать и выполнить тесты из src/lib.rs, запустите cargo test.

Упражнение 7.2.2.B: Синхронный путь ⭐⭐

В ходе следующих упражнений полезно иметь под рукой руководство PyO3 и документацию API PyO3.

Это упражнение посвящено тому, чтобы заставить работать strompy_test.py. Для этого исправьте todo!() в функции exec в src/lib.rs. Не забудьте добавить функцию в экспортируемый модуль!

Если всё сделано правильно, запустите:

$ maturin develop
$ python strompy_test.py

7.2.2.B: Асинхронный путь ⭐⭐⭐

На этот раз заставим работать strompy_test_async.py. Реализуйте feed_bytes и добавьте в тип StrompyJsonReader асинхронный метод, который возвращает PyResult<Option<Vec<Vec<f64>>>> и экспортируется под именем next.

Если всё сделано правильно, запустите:

$ maturin develop
$ python strompy_test_async.py

7.2.2.C: Дополнительные возможности (бонус) ⭐⭐⭐

Сможете ли вы добавить в strompy поддержку других операций из nalgebra?

7.2.2.D: Наведение порядка (бонус) ⭐⭐⭐⭐

Представление векторов как Vec<Vec<f64>> работает, но не слишком хорошо. Сделайте так, чтобы StrompyJsonReader возвращал PyResult<Option<Py<PyList>>>, то есть списки PyList вместо векторов.

Это довольно трудное упражнение, поскольку оно связано с умными указателями PyO3: Py<T>, Bound<'py, T> и токенами GIL: Python.

Модуль 8.1 — Экосистема встраиваемого Rust

Слайды

Упражнение 8.1.1: ID LSM303AGR

Используйте новые знания, чтобы запустить первое приложение и прочитать ID акселерометра LSM303AGR. Связаться с LSM303AGR можно по I2C, который есть на плате micro:bit. Обратите внимание, что nRF52833 поддерживает I2C через периферийное устройство TWIM (Two-Wire Interface Master).

Для начала настроим периферию I2C на нашей отладочной плате и прочитаем регистр ID акселерометра LSM303AGR. Отправная точка находится в файле exercises/8-embedded/1-embedded-ecosystem/src/main.rs в репозитории.

Попробуйте запустить существующий проект, а затем реализуйте функциональность в соответствии с комментариями в коде.

Чтобы работать с проектом, выполните из его папки в терминале:

  • cargo build: собирает проект;
  • cargo run: собирает проект, прошивает его на устройство и выводит логи в терминал (использует инструмент probe-rs run).

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

Несколько подсказок, которые помогут начать
  • Документацию по HAL можно открыть командой cargo doc --open. Она собирается для точного чипа и версии, которые мы используем. Обычно документацию ищут на docs.rs, но там может открыться HAL для другого варианта nRF.
  • Как настроить I2C для nRF52833: демонстрационный пример TWIM из embassy-nrf.
  • Даташит LSM303AGR: https://www.st.com/resource/en/datasheet/lsm303agr.pdf. ID акселерометра хранится в регистре WHO_AM_I_A по адресу 0x0F. Для обращения к самому акселерометру нужен адрес 0x19.
  • Используйте метод Twim::blocking_write_read: сначала он записывает адрес устройства, затем адрес регистра, а затем читает содержимое в буфер. (Подсказка: поискать метод можно в сгенерированной документации.)

Модуль 8.2 — Переносимые драйверы на Rust

Слайды

Упражнение 8.2.1: Драйвер LSM303AGR

Когда вы действительно хотите использовать устройство, нужен драйвер. Напишем настоящий переносимый драйвер для нашего акселерометра.

Перейдите к заданию в exercises/8-embedded/2-portable-drivers/1-lsm303agr-driver и реализуйте модуль lsm303agr. Цель — использовать embedded-hal для описания аппаратных интерфейсов, поэтому постарайтесь не использовать в этом модуле типы, специфичные для nRF.

Используйте драйвер, чтобы считывать данные с датчика в src/main.rs.

В качестве бонусного упражнения добавьте поддержку чтения магнитометра.

Модуль 8.3 — Async на встраиваемых системах

Слайды

Упражнение 8.3.1: Компас

В этом упражнении мы используем драйвер lsm303agr. Хотя в документации это не показано, драйвер поддерживает async, если включить его фичу async.

Посмотрите примеры в репозитории lsm303agr-rs, чтобы понять, как использовать драйвер.

С помощью драйвера lsm303agr реализуйте компас. Для указания направления на северный полюс можно использовать модуль dial. В коде есть несколько мест с todo!() и инструкциями.

Компиляция стартового кода выдаёт множество предупреждений. Они исчезнут, когда вы закончите.

Упражнение 8.3.2: Компас с мигающим индикатором

В примере отправителя и получателя каналов из репозитория embassy показано, как запускать отдельные задачи и использовать каналы для обмена сообщениями между ними. Используя эти знания, заставьте светодиод-индикатор в модуле dial мигать в то время, как снимаются показания магнитометра.

Поскольку в этом упражнении мы не используем defmt, замените вызовы макроса unwrap!(<expr>) на вызовы <expr>.unwrap().

Модуль 8.4 — Фреймворк Embassy

Слайды

Упражнение 8.4.1: Проект на Embassy

Это упражнение немного больше, чем вы привыкли: мы сделаем собственный проект на Embassy и micro:bit V2. В этом упражнении работайте командами по 2–3 человека.

Для вас подготовлен крейт с простым приложением blinky. Вы можете менять в нём что угодно: это лишь отправная точка. Крейт находится в exercises/8-embedded/4-embassy-framework/1-embassy-project. Попробуйте его запустить, чтобы убедиться, что начальная настройка работает.

Выберите один из проектов:

  • Создайте игру «змейка», которая использует светодиодную матрицу как игровой экран. Для ввода можно использовать кнопки, акселерометр или магнитометр, либо их комбинацию.
  • Создайте световой баннер, который читает текст по UART через виртуальный COM-порт, предоставляемый MCU интерфейса, и выводит его на светодиодную матрицу.
  • Создайте аудиорекордер, который считывает сэмплы со встроенного микрофона и воспроизводит их через встроенный динамик. Дополнительно можно показывать на дисплее график FFT или индикатор громкости.
  • Напишите драйвер для сенсорного датчика ёмкости (capacitive touch sensor).
  • Создайте многопользовательскую игру «кто быстрее жмёт» (button masher). Кто быстрее всех нажимает кнопку? Работайте либо с двумя встроенными кнопками, либо соедините несколько плат между собой. Счёт выводите на светодиодную матрицу.

Итоги

Слайды

Форма обратной связи

Спасибо, что нашли время помочь нам сделать тренинг лучше!

Оценить курс можно по этой ссылке