Разработчик Rust (Профессиональный уровень)

Курс «Разработчик Rust (Профессиональный уровень)» — это углублённое погружение в системное программирование на Rust. Вы освоите безопасность памяти без GC, владение данными, работу с unsafe-кодом, макросами и крейтами, а также разработку высокопроизводительных и конкурентных приложений. Научитесь писать надёжный, эффективный код для системного уровня и интеграции с другими языками.

Курс предназначен для опытных разработчиков, стремящихся овладеть профессиональными навыками системного программирования на языке Rust. Программа включает углублённое изучение безопасной работы с памятью, владения данными (ownership), времени жизни (lifetimes), unsafe-кода, макросов, крейтов и создания высокопроизводительных приложений. Обучение построено на реальных задачах и практико-ориентированных проектах.


Что предстоит пройти на курсе:

  • Введение в Rust: синтаксис, типы, ошибки, pattern matching.
  • Системное программирование: работа с указателями, FFI, raw-память.
  • Безопасность памяти: borrow checker, lifetimes, ownership.
  • Unsafe Rust: низкоуровневые операции, работа с сырыми указателями.
  • Макросы: создание декларативных и процедурных макросов.
  • Параллелизм и асинхронность: Send/Sync, async/await, futures.
  • Разработка библиотек: проектирование API, документирование, тестирование.
  • Cargo и крейты: управление зависимостями, публикация, best practices.
  • Интеграция с C/C++ и другими языками.
  • Проектная работа: реализация системного приложения или библиотеки.

Ожидаемые результаты после прохождения:
Должен знать:

  • Механизмы безопасности памяти в Rust.
  • Принципы работы borrow checker и времени жизни.
  • Структуру крейтов и модульную архитектуру.
  • Основы параллельного и асинхронного программирования.
  • Методы интеграции Rust с другими языками.

Должен уметь:

  • Писать эффективный и безопасный код без использования сборщика мусора.
  • Проектировать и реализовывать сложные системные приложения.
  • Создавать собственные крейты и использовать существующие с максимальной отдачей.
  • Отлаживать и оптимизировать производительность кода.
  • Писать unsafe-код ответственно и контролируемо.

Формат обучения:
Онлайн-лекции, практические задания, проектная работа, code review и тестирование.

  1. Что делает Rust уникальным среди языков системного программирования?
    Rust сочетает безопасность памяти без использования сборщика мусора, низкоуровневый контроль и высокую производительность. Его система владения (ownership) и проверки заимствования (borrowing) гарантирует отсутствие ошибок вроде dangling pointers и data races на этапе компиляции.

  2. Как устроена система владения (ownership) в Rust?
    Ownership — это набор правил управления памятью, определяющих, какой участок кода «владеет» данными. Когда владелец выходит из области видимости, данные уничтожаются. Это позволяет избежать утечек памяти и неявных освобождений.

  3. Что такое заимствование (borrowing) и почему оно важно?
    Заимствование позволяет ссылаться на данные без их владения. Это снижает накладные расходы копирования и позволяет нескольким частям кода работать с одними данными, соблюдая правила borrow checker.

  4. Как работает borrow checker и для чего он нужен?
    Borrow checker анализирует ссылки на этапе компиляции и гарантирует, что они не выходят за пределы времени жизни данных. Он предотвращает такие ошибки, как использование освобождённой памяти или изменение неизменяемой ссылки.

  5. В чём разница между &T и &mut T?
    &T — неизменяемая ссылка, позволяет читать данные, но не изменять их. &mut T — изменяемая ссылка, позволяет модифицировать данные, но может существовать только одна такая ссылка в данной области видимости.

  6. Что такое lifetimes и как они обозначаются в коде?
    Lifetimes — это аннотации, указывающие, сколько должна существовать ссылка. Они обозначаются с помощью апострофа: 'a. Например: fn foo<'a>(x: &'a str).

  7. Почему в Rust нет сборщика мусора (GC)?
    Отказ от GC позволяет достичь предсказуемой работы с ресурсами и минимальных пауз в выполнении. Управление памятью осуществляется через систему ownership и lifetimes, что делает Rust подходящим для embedded-систем и high-load приложений.

  8. Что такое unsafe Rust и когда его стоит использовать?
    Unsafe Rust — это подмножество языка, позволяющее обходить ограничения borrow checker. Используется для FFI, прямого доступа к памяти, реализации низкоуровневых абстракций. Требует особой осторожности.

  9. Как в Rust реализуется параллелизм?
    Rust обеспечивает потокобезопасность через Send и Sync трейты. Send разрешает передачу данных между потоками, Sync — безопасное совместное использование. Это позволяет писать конкурентный код без data races.

  10. Что такое async/await в Rust и как его использовать?
    Async/await — механизм асинхронного программирования, позволяющий писать асинхронный код как синхронный. Реализован через futures и runtime, например Tokio или async-std.

  11. Какие основные отличия между структурами и перечислениями в Rust?
    Структуры (struct) группируют поля данных, перечисления (enum) представляют значение из множества возможных. Enum могут содержать данные в каждом варианте и часто используются с pattern matching.

  12. Что такое match и как он используется в Rust?
    Match — мощный механизм сопоставления с образцом. Он позволяет проверять значения и выполнять разные блоки кода в зависимости от случая. Обязательно покрывает все возможные варианты.

  13. Как работают итераторы в Rust?
    Итераторы предоставляют ленивый способ обработки последовательностей. Они эффективны и безопасны благодаря типизации и гарантиям lifetime. Методы вроде map, filter и collect применяются цепочками.

  14. Что такое макросы в Rust и чем они отличаются от функций?
    Макросы — это метапрограммирование, позволяющее генерировать код на этапе компиляции. В отличие от функций, они работают с синтаксическими деревьями и могут принимать переменное число аргументов.

  15. Как создать свой декларативный макрос в Rust?
    Создание макроса начинается с #[macro_export] и macro_rules!. Макрос определяет шаблоны и соответствующие им действия, например: macro_rules! vec { ... }.

  16. Что такое процедурные макросы и где они применяются?
    Процедурные макросы — это функции, принимающие код в виде токенов и возвращающие новый код. Применяются для автоматической генерации кода, например, derive-атрибутов.

  17. Как организовать проект на Rust с использованием Cargo?
    Cargo — это стандартный инструмент управления зависимостями и сборки. Проект создаётся командой cargo new. Зависимости добавляются в Cargo.toml, модули — через файлы и pub ключевые слова.

  18. Что такое крейты и как они используются в экосистеме Rust?
    Крейт — это единица повторного использования кода в Rust. Бывают библиотечными и исполняемыми. Публикуются на crates.io и подключаются через Cargo.

  19. Как происходит тестирование в Rust?
    Тестирование проводится с помощью атрибута #[test]. Cargo test запускает unit- и integration-тесты. Можно использовать assert!, assert_eq! и другие макросы для проверок.

  20. Как работает документирование в Rust и как его генерировать?
    Документирование пишется с помощью /// или /** */. Cargo doc генерирует HTML-документацию. Хорошая практика — писать примеры в комментариях, которые проверяются при тестировании.

  21. Что такое FFI в Rust и как с ним работать?
    FFI (Foreign Function Interface) позволяет вызывать функции на других языках, например C. Для этого используется extern "C" блок и unsafe код.

  22. Как интегрировать Rust с C/C++?
    Интеграция выполняется через FFI. Rust может вызывать C-функции и экспортировать свои функции для использования в C/C++. Также можно использовать rust-bindgen для автоматической генерации связок.

  23. Какие инструменты помогают в отладке Rust-кода?
    GDB, LLDB, rust-gdb, rust-lldb, а также плагины для IDE (VS Code, CLion). Также полезны cargo clippy для статического анализа и cargo fmt для форматирования.

  24. Что такое Send и Sync в контексте многопоточности?
    Send — маркер, показывающий, что тип можно безопасно перемещать между потоками. Sync — что ссылку на тип можно безопасно использовать из нескольких потоков.

  25. Какие лучшие практики рекомендуются при разработке на Rust?
    Писать безопасный код по умолчанию, минимизировать использование unsafe, документировать всё, тестировать регулярно, использовать Cargo best practices, применять Clippy и форматтер, следить за обновлениями языка и экосистемы.

  1. Как в Rust обрабатываются ошибки и какие типы для этого используются?
    Rust предоставляет два основных типа для обработки ошибок: Result и Option. Result<T, E> используется для возврата либо успешного значения T, либо ошибки E. Option<T> применяется, когда значение может отсутствовать (Some(T) или None). Для удобства есть операторы ?, unwrap(), expect() и макрос ensure!.

  2. Что такое Deref- coercion и как оно работает?
    Deref-coercion — это механизм автоматического приведения ссылок при вызове методов через *. Например, &String может быть автоматически преобразован в &str. Это упрощает работу с умными указателями и абстракциями.

  3. В чём разница между String и &str в Rust?
    String — это динамически выделяемая строка, владеющая данными. &str — неизменяемая ссылка на строку, часто используется для чтения данных. String можно изменять, а &str — нет.

  4. Как работают трейты (traits) и зачем они нужны?
    Трейты определяют общее поведение для типов. Они похожи на интерфейсы в других языках. Используются для реализации полиморфизма, ограничений в generic-кодах и реализации стандартных методов (например, Debug, Clone).

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

  6. Как организовать модульность в крупном проекте на Rust?
    Модули создаются через ключевое слово mod. Публичные элементы помечаются pub. Можно использовать файловую структуру: mod.rs и подкаталоги. Хорошая практика — группировка логически связанных компонентов.

  7. Что такое Copy и Clone и в чём их отличие?
    Copy — автоматическое копирование значений при присвоении. Применяется только к малым данным (например, числа). Clone требует явного вызова .clone() и может быть дорогим. Не все типы могут быть Copy.

  8. Как работает Drop в Rust и когда он вызывается?
    Drop — это трейт, позволяющий определить поведение при освобождении ресурсов. Вызывается автоматически, когда переменная выходит из области видимости. Полезен для освобождения внешних ресурсов (сетевые соединения, файлы).

  9. Что такое PhantomData и когда он используется?
    PhantomData — маркерный тип, используемый для передачи информации компилятору о зависимости, которая не представлена в данных. Часто применяется в unsafe коде для соблюдения гарантий lifetime.

  10. Как реализовать свой итератор в Rust?
    Для этого нужно реализовать трейт Iterator, определив тип Item и метод next(). Это позволяет создавать пользовательские последовательности и использовать их с цепочками map/filter/take и другими.

  11. Что такое Into и From и как они связаны?
    From и Into — трейты для преобразования типов. From позволяет создавать один тип из другого. Into — обратное, но требует явного приведения. Взаимосвязаны: если реализован From, Into реализуется автоматически.

  12. Как работает система сборки Cargo и что она поддерживает?
    Cargo управляет зависимостями, тестами, документацией и сборкой. Поддерживает профили (dev, release), фичи, workspaces, custom build scripts и cross-compilation через target параметры.

  13. Что такое workspaces в Cargo и зачем они нужны?
    Workspaces позволяют объединить несколько пакетов в один проект. Все крейты внутри workspace разделяют одну директорию Cargo.lock и могут ссылаться друг на друга. Удобно для микросервисов или библиотек в одном репозитории.

  14. Как работают фичи (features) в Cargo и как их активировать?
    Фичи — это условные зависимости, которые включаются по запросу. Определяются в Cargo.toml. Активируются через команду --features "feature_name" или в других крейтах как зависимости.

  15. Что такое pinning и когда оно необходимо?
    Pinning используется в асинхронном программировании, чтобы гарантировать, что объект не будет перемещён в памяти. Необходимо при работе с self-referential структурами и futures.

  16. Какие существуют способы управления состоянием в async-приложениях?
    Состояние можно хранить в Arc<Mutex<T>>, использовать tokio::sync::Mutex, OnceCell, или передавать в handler'ы через middleware. Важно обеспечивать Send + Sync для совместимости с runtime.

  17. Что такое Tokio и какой его роль в экосистеме Rust?
    Tokio — популярный асинхронный runtime для выполнения async/await кода. Предоставляет executor, I/O, timer и мультипоточный диспетчер. Используется для сетевых приложений, серверов, клиентов.

  18. Как работает сериализация и десериализация в Rust?
    С помощью трейтов Serialize и Deserialize из библиотеки serde. Поддерживает JSON, TOML, YAML и другие форматы. Реализуется через derive-атрибуты или вручную для сложных случаев.

  19. Какие инструменты используются для тестирования производительности в Rust?
    Для benchmarking используется #[bench] (устарел), criterion.rs или divan. Также можно использовать perf из Linux, flamegraph и другие низкоуровневые средства анализа.

  20. Что такое const generics и как они используются?
    Const generics позволяют параметризовать типы числами. Например, [T; N] — массив фиксированной длины. Используются для безопасной работы с размерами буферов, матриц и других структур.

  21. Как происходит работа с файлами в Rust?
    Через модуль std::fs. Чтение и запись выполняются с использованием File, BufReader, BufWriter. Можно использовать read_to_string(), write_all() и другие методы. Для асинхронного доступа — tokio::fs.

  22. Что такое NonZero и зачем он нужен?
    NonZero — типы вроде NonZeroU32, гарантирующие, что значение не равно нулю. Используются для оптимизации памяти и предотвращения недопустимых состояний, особенно в Option-like структурах.

  23. Как использовать атрибуты #[derive] и какие из них наиболее важны?
    Атрибут #[derive] автоматически реализует стандартные трейты: Debug, Clone, Copy, PartialEq, Eq, Hash, Default. Полезны для упрощения кода и обеспечения базового поведения.

  24. Какие существуют подходы к логированию в Rust?
    Базовое логирование осуществляется через log crate и реализацию env_logger, slog, tracing. Для production рекомендуется tracing с поддержкой span’ов и уровней детализации.

  25. Какие лучшие практики оформления кода в Rust?
    Использовать rustfmt для форматирования, clippy для статического анализа, соблюдать стиль PSR (Rust API Guidelines), комментировать public API, писать unit- и integration-тесты, применять documentation tests.

  1. Какой из следующих типов гарантирует, что значение не может быть null?
    A) *const T
    B) &T
    C) Option<T>
    D) String
    Правильный ответ: B) &T

  2. Что делает ключевое слово 'mut' в объявлении переменной?
    A) Позволяет изменять значение переменной
    B) Делает переменную константной
    C) Указывает на использование unsafe-кода
    D) Включает автоматическое клонирование значения
    Правильный ответ: A) Позволяет изменять значение переменной

  3. Какой трейт используется для сравнения двух значений на равенство?
    A) Eq
    B) PartialEq
    C) Ord
    D) Clone
    Правильный ответ: B) PartialEq

  4. Что такое borrow checker в Rust?
    A) Средство форматирования кода
    B) Компонент компилятора, управляющий временем жизни ссылок
    C) Инструмент для отладки памяти
    D) Библиотека для работы с итераторами
    Правильный ответ: B) Компонент компилятора, управляющий временем жизни ссылок

  5. Какой тип ссылки позволяет изменять данные?
    A) &T
    B) *const T
    C) &mut T
    D) Box<T>
    Правильный ответ: C) &mut T

  6. Какая команда создаёт новый проект с помощью Cargo?
    A) cargo new
    B) cargo create
    C) cargo init
    D) cargo project
    Правильный ответ: A) cargo new

  7. Какой атрибут используется для создания unit-теста?
    A) #[test]
    B) #[unit_test]
    C) #[testing]
    D) #[assert]
    Правильный ответ: A) #[test]

  8. Какой тип данных используется для хранения строки с возможностью изменения?
    A) str
    B) &str
    C) String
    D) CString
    Правильный ответ: C) String

  9. Что означает реализация трейта Drop для структуры?
    A) Автоматическое копирование значения
    B) Освобождение ресурсов при выходе из области видимости
    C) Возможность итерации по полям структуры
    D) Поддержку сериализации
    Правильный ответ: B) Освобождение ресурсов при выходе из области видимости

  10. Что делает оператор '?' в функции, возвращающей Result?
    A) Принудительно завершает программу
    B) Возвращает ошибку, если она возникает
    C) Игнорирует результат выполнения
    D) Выполняет разыменование указателя
    Правильный ответ: B) Возвращает ошибку, если она возникает

  11. Какой тип указателя требует использования unsafe-блока для разыменования?
    A) &T
    B) &mut T
    C) *const T
    D) Box<T>
    Правильный ответ: C) *const T

  12. Какой из следующих макросов используется для генерации сообщения panic?
    A) assert!
    B) panic!
    C) todo!
    D) unimplemented!
    Правильный ответ: B) panic!

  13. Какой тип позволяет делить владение данными между потоками?
    A) Rc<T>
    B) Arc<T>
    C) Mutex<T>
    D) RefCell<T>
    Правильный ответ: B) Arc<T>

  14. Какой из перечисленных типов реализует Send?
    A) *const T
    B) *mut T
    C) Rc<T>
    D) Box<dyn Send + Sync>
    Правильный ответ: D) Box<dyn Send + Sync>

  15. Какой из следующих фрагментов кода корректно объявляет lifetime для ссылки?
    A) fn foo(x: &str)
    B) fn foo<'a>(x: &'a str)
    C) fn foo(x: str<'a>)
    D) fn foo(x: &'a mut str)
    Правильный ответ: B) fn foo<'a>(x: &'a str)

  16. Что такое FFI в контексте Rust?
    A) Формат сериализации
    B) Интерфейс взаимодействия с другими языками
    C) Асинхронная библиотека
    D) Система тестирования
    Правильный ответ: B) Интерфейс взаимодействия с другими языками

  17. Какой инструмент используется для форматирования Rust-кода?
    A) rustfmt
    B) clippy
    C) cargo fmt
    D) format-rust
    Правильный ответ: C) cargo fmt

  18. Какой из следующих макросов используется для генерации документации?
    A) doc!
    B) ///
    C) #[doc]
    D) comment!
    Правильный ответ: B) ///

  19. Что делает атрибут #[derive(Debug)]?
    A) Включает проверки времени выполнения
    B) Разрешает вывод lifetime
    C) Автоматически реализует трейт Debug
    D) Включает сборку с отладочной информацией
    Правильный ответ: C) Автоматически реализует трейт Debug

  20. Какой из следующих типов не поддерживает копирование?
    A) u32
    B) bool
    C) Vec<T>
    D) f64
    Правильный ответ: C) Vec<T>

  21. Какой атрибут используется для предотвращения инлайна функции?
    A) #[inline]
    B) #[no_inline]
    C) #[cold]
    D) #[inline(never)]
    Правильный ответ: D) #[inline(never)]

  22. Какой из следующих типов реализует interior mutability?
    A) &mut T
    B) Cell<T>
    C) *mut T
    D) Box<T>
    Правильный ответ: B) Cell<T>

  23. Что делает ключевое слово 'static' при объявлении переменной?
    A) Указывает на время жизни всей программы
    B) Обозначает локальную переменную
    C) Создаёт константу времени компиляции
    D) Объявляет глобальную ссылку
    Правильный ответ: A) Указывает на время жизни всей программы

  24. Какой из следующих типов не является частью стандартной библиотеки?
    A) Vec<T>
    B) HashMap<K, V>
    C) BTreeMap<K, V>
    D) LinkedList<T>
    Правильный ответ: Ни один из перечисленных (все являются частью std).

  25. Что делает Cargo.lock?
    A) Хранит информацию о зависимостях с конкретными версиями
    B) Задаёт параметры сборки
    C) Хранит пользовательские настройки
    D) Содержит пути к исходникам
    Правильный ответ: A) Хранит информацию о зависимостях с конкретными версиями

Экзаменационный билет №1

Теоретическая часть

  1. Объясните, что такое ownership и как он влияет на управление памятью в Rust.
  2. Что означает ключевое слово unsafe и в каких случаях его можно использовать?

Практическая часть

  1. Напишите функцию, которая принимает строку и возвращает Option<char>, представляющий первый заглавный символ строки. Если такового нет — вернуть None.

Ответы:

  1. Ownership — это система управления памятью в Rust, основанная на праве владения данными. У каждого значения есть один владелец. Когда владелец выходит из области видимости, значение уничтожается. Это исключает утечки памяти и позволяет обойтись без сборщика мусора.

  2. Ключевое слово unsafe разрешает выполнение операций, которые не проверяются borrow checker'ом: разыменование сырого указателя, вызов unsafe-функций, доступ к статической изменяемой переменной и реализация unsafe-трейтов. Используется редко и требует осторожности.

  3. fn first_uppercase(s: &str) -> Option<char> {
        s.chars().find(|c| c.is_uppercase())
    }

Экзаменационный билет №2

Теоретическая часть

  1. В чём разница между &T и &mut T? Приведите примеры, когда используется каждый тип ссылки.
  2. Что такое lifetimes и зачем они нужны в Rust?

Практическая часть

  1. Реализуйте итератор, который возвращает только чётные числа из заданного вектора.

Ответы:

  1. &T — неизменяемая ссылка, позволяющая читать данные, но не изменять их. &mut T — изменяемая ссылка, позволяющая модифицировать данные. Однако может быть только одна &mut T ссылка в одной области видимости. Пример: &String для чтения, &mut String для изменения содержимого.

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

  3. struct EvenIterator {
        index: usize,
        data: Vec<i32>,
    }
    
    impl Iterator for EvenIterator {
        type Item = i32;
    
        fn next(&mut self) -> Option<Self::Item> {
            while let Some(&value) = self.data.get(self.index) {
                self.index += 1;
                if value % 2 == 0 {
                    return Some(value);
                }
            }
            None
        }
    }
 

Экзаменационный билет №3

Теоретическая часть

  1. Опишите, как работает borrow checker и какие ошибки он предотвращает.
  2. Что такое макросы в Rust и чем они отличаются от функций?

Практическая часть

  1. Напишите макрос vec_of!, принимающий два параметра: тип и количество элементов, и создающий вектор заполненный нулевыми значениями этого типа.

Ответы:

  1. Borrow checker — это компонент компилятора, анализирующий ссылки на этапе компиляции. Он гарантирует, что ссылки не выходят за пределы времени жизни данных и не возникает гонок данных. Предотвращает такие ошибки, как использование освобождённой памяти или изменение неизменяемой ссылки.

  2. Макросы — это метапрограммирование, позволяющее генерировать код на этапе компиляции. В отличие от функций, работают с синтаксическими деревьями и могут принимать переменное число аргументов. Часто используются для автоматизации повторяющегося кода.

  3. macro_rules! vec_of {
        ($t:ty, $n:expr) => {{
            vec![0 as $t; $n]
        }};
    }
    
    // Пример использования:
    let v = vec_of!(u32, 5); // [0, 0, 0, 0, 0]
     

Экзаменационный билет №4

Теоретическая часть

  1. Что такое параллелизм и как он реализован в Rust?
  2. Что такое Send и Sync и как они связаны с многопоточностью?

Практическая часть

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

Ответы:

  1. Параллелизм в Rust реализуется через потоки (std::thread). Каждый поток может запускаться с помощью spawn. Для передачи данных между потоками используется move, а также типы, реализующие Send.

  2. Send — маркер, показывающий, что тип можно безопасно перемещать между потоками. Sync — что ссылку на тип можно безопасно использовать из нескольких потоков. Эти трейты обеспечивают потокобезопасность на уровне типов.

  3. use std::thread;
    
    fn main() {
        let data = vec![1, 2, 3, 4, 5, 6];
    
        let handle1 = thread::spawn(move || {
            data[..3].iter().sum::<i32>()
        });
    
        let handle2 = thread::spawn(move || {
            data[3..].iter().sum::<i32>()
        });
    
        let sum1 = handle1.join().unwrap();
        let sum2 = handle2.join().unwrap();
    
        println!("Total sum: {}", sum1 + sum2);
    }
     

Экзаменационный билет №5

Теоретическая часть

  1. Как работает асинхронное программирование в Rust и что такое futures?
  2. Что такое процедурные макросы и где они применяются?

Практическая часть

  1. Напишите асинхронную функцию, которая отправляет HTTP-запрос и возвращает статус-код. Используйте библиотеку reqwest.

Ответы:

  1. Асинхронное программирование в Rust основано на futures и async/await. Future представляет собой асинхронную операцию, которую можно опросить до завершения. Async/await — это синтаксический сахар над futures, упрощающий написание асинхронного кода.

  2. Процедурные макросы — это функции, принимающие код в виде токенов и возвращающие новый код. Применяются для автоматической генерации кода, например, derive-атрибутов. Они мощнее декларативных макросов и дают больше контроля над AST.

  3. use reqwest::Error;
    
    #[tokio::main]
    async fn main() -> Result<(), Error> {
        let response = reqwest::get("https://example.com ").await?;
        println!("Status code: {}", response.status());
        Ok(())
    }
     

Экзаменационный билет №6

Теоретическая часть

  1. Что такое Drop и как он используется в практике?
  2. В чём разница между String и &str?

Практическая часть

  1. Напишите функцию, которая принимает строку и возвращает true, если она является палиндромом (без учёта регистра и пробелов).

Ответы:

  1. Drop — это трейт, позволяющий определить поведение при уничтожении значения. Метод drop вызывается автоматически при выходе переменной из области видимости. Часто используется для освобождения внешних ресурсов, таких как файлы или сетевые соединения.

  2. String — это динамически выделяемая строка, владеющая данными. &str — неизменяемая ссылка на строку, часто используется для чтения данных. String можно изменять, а &str — нет.
  3. fn is_palindrome(s: &str) -> bool {
        let filtered: String = s.chars()
            .filter(|c| c.is_alphanumeric())
            .map(|c| c.to_ascii_lowercase())
            .collect();
        filtered == filtered.chars().rev().collect::<String>()
    }
    

Экзаменационный билет №7

Теоретическая часть

  1. Опишите концепцию Result и Option в Rust.
  2. Что такое Deref coercion и как оно работает?

Практическая часть

  1. Реализуйте простую структуру Counter, реализующую трейт Iterator, которая возвращает числа от 1 до 10.

Ответы:

  1. Result<T, E> используется для возврата либо успешного значения T, либо ошибки E. Option<T> применяется, когда значение может отсутствовать (Some(T) или None). Оба типа обеспечивают безопасную обработку возможных состояний без использования null.

  2. Deref coercion — это механизм автоматического приведения ссылок при вызове методов через *. Например, &String может быть автоматически преобразован в &str. Это упрощает работу с умными указателями и абстракциями.

  3. struct Counter {
        count: u32,
    }
    
    impl Counter {
        fn new() -> Self {
            Counter { count: 1 }
        }
    }
    
    impl Iterator for Counter {
        type Item = u32;
    
        fn next(&mut self) -> Option<Self::Item> {
            if self.count <= 10 {
                let current = self.count;
                self.count += 1;
                Some(current)
            } else {
                None
            }
        }
    }

Экзаменационный билет №8

Теоретическая часть

  1. Что такое Arc и Mutex, и как они используются вместе?
  2. Какие существуют способы сериализации и десериализации JSON в Rust?

Практическая часть

  1. Напишите структуру Person, реализующую сериализацию/десериализацию, и сохраните её в JSON.

Ответы:

  1. Arc<T> — это тип с поддержкой reference counting, позволяющий делить владение данными между потоками. Mutex<T> обеспечивает взаимное исключение при доступе к данным. Их совместное использование позволяет безопасно изменять общее состояние из нескольких потоков.

  2. Основные средства — библиотека serde с флагом derive. Типы to_string, from_str, to_vec и другие из serde_json. Также есть специализированные решения вроде simd-json.

  3. use serde::{Serialize, Deserialize};
    use serde_json;
    
    #[derive(Serialize, Deserialize, Debug)]
    struct Person {
        name: String,
        age: u32,
    }
    
    fn main() {
        let person = Person {
            name: "Alice".to_string(),
            age: 30,
        };
    
        let json = serde_json::to_string(&person).unwrap();
        println!("Serialized: {}", json);
    
        let deserialized: Person = serde_json::from_str(&json).unwrap();
        println!("Deserialized: {:?}", deserialized);
    }

Экзаменационный билет №9

Теоретическая часть

  1. Что такое Pin и когда он нужен в асинхронном программировании?
  2. Как работают условные зависимости в Cargo?

Практическая часть

  1. Напишите функцию, которая принимает два Vec<i32> и возвращает новый вектор, содержащий только уникальные элементы из обоих.

Ответы:

  1. Pin гарантирует, что объект не будет перемещён в памяти. Это необходимо для self-referential структур и некоторых futures. Используется вместе с Pin и Unpin.

  2. Условные зависимости (features) определяются в Cargo.toml и активируются через --features. Они позволяют включать или исключать части кода в зависимости от платформы или других факторов.

  3. fn unique_union(a: Vec<i32>, b: Vec<i32>) -> Vec<i32> {
        let mut set: std::collections::HashSet<_> = a.into_iter().collect();
        set.extend(b.into_iter());
        set.into_iter().collect()
    }

Экзаменационный билет №10

Теоретическая часть

  1. Что такое const generics и где они могут применяться?
  2. Как происходит работа с файлами в Rust?

Практическая часть

  1. Напишите программу, которая считывает содержимое текстового файла и выводит количество слов в нём.

Ответы:

  1. Const generics позволяют параметризовать типы числами. Например, [T; N] — массив фиксированной длины. Используются для безопасной работы с размерами буферов, матриц и других структур.

  2. Работа с файлами осуществляется через модуль std::fs. Чтение и запись выполняются с использованием File, BufReader, BufWriter. Можно использовать read_to_string(), write_all() и другие методы. Для асинхронного доступа — tokio::fs.

  3. use std::fs::File;
    use std::io::{BufRead, BufReader};
    
    fn count_words<P: AsRef<std::path::Path>>(path: P) -> std::io::Result<usize> {
        let file = File::open(path)?;
        let reader = BufReader::new(file);
    
        let count = reader
            .lines()
            .filter_map(Result::ok)
            .flat_map(|line| line.split_whitespace().map(String::from))
            .count();
    
        Ok(count)
    }

Экзаменационный билет №11

Теоретическая часть

  1. Что такое Interior Mutability и какие типы её реализуют?
  2. В чём отличие между Cell<T> и RefCell<T>?

Практическая часть

  1. Напишите функцию, которая принимает строку и возвращает Vec<String>, содержащий все слова, начинающиеся с заглавной буквы.

Ответы:

  1. Interior Mutability — это паттерн, позволяющий изменять данные внутри неизменяемой структуры. Реализуется через типы Cell<T>, RefCell<T>, Mutex<T>. Используется в случаях, когда нужно изменить состояние без явного использования mut.

  2. Cell<T> работает только с копируемыми типами (Copy) и позволяет менять значение напрямую. RefCell<T> поддерживает заимствование во время выполнения и может содержать сложные типы, но требует вызова .borrow() или .borrow_mut().

  3. fn uppercase_words(s: &str) -> Vec<String> {
        s.split_whitespace()
            .filter(|word| word.chars().next().map_or(false, |c| c.is_uppercase()))
            .map(String::from)
            .collect()
    }

Экзаменационный билет №12

Теоретическая часть

  1. Что такое Pin и как он связан с Future?
  2. Как работают атрибуты в макросах? Приведите пример.

Практическая часть

  1. Напишите процедурный макрос #[derive(HasLength)], который добавляет метод length(&self) -> usize к структуре с полем data: String.

Ответы:

  1. Pin<P> гарантирует, что объект, на который указывает P, не будет перемещён в памяти. Это важно для self-referential структур и futures, где движок async должен быть уверен, что данные останутся на месте.

  2. Атрибуты в макросах используются для добавления информации к элементам кода. Например, #[derive(Debug)] автоматически реализует трейт Debug. Также можно создавать собственные атрибуты в процедурных макросах.

  3. // Предполагается, что вы создаёте отдельный crate типа proc-macro
    
    extern crate proc_macro;
    
    use proc_macro::TokenStream;
    use quote::quote;
    use syn::{parse_macro_input, Data, DeriveInput, Fields};
    
    #[proc_macro_derive(HasLength)]
    pub fn derive_has_length(input: TokenStream) -> TokenStream {
        let input = parse_macro_input!(input as DeriveInput);
        let name = &input.ident;
    
        let expanded = match input.data {
            Data::Struct(ref data) => {
                if let Fields::Named(ref fields) = data.fields {
                    let field_names: Vec<_> = fields.named.iter()
                        .filter(|f| f.ident.as_ref().map_or(false, |i| i == "data"))
                        .map(|f| &f.ident)
                        .collect();
    
                    if field_names.len() == 1 {
                        quote! {
                            impl #name {
                                pub fn length(&self) -> usize {
                                    self.#field_names.len()
                                }
                            }
                        }
                    } else {
                        quote! {}
                    }
                } else {
                    quote! {}
                }
            },
            _ => quote! {},
        };
    
        TokenStream::from(expanded)
    }

Экзаменационный билет №13

Теоретическая часть

  1. Что такое const fn и какие ограничения на него есть?
  2. Как происходит работа с сигналами в Rust?

Практическая часть

  1. Напишите программу, которая ждёт сигнала SIGINT и выводит сообщение перед выходом.

Ответы:

  1. const fn — это функция, которую можно вызывать в константных выражениях. Она должна быть pure и не содержать сложных конструкций. С версии Rust 1.56 поддерживает больше возможностей, но всё ещё ограничена.

  2. Работа с системными сигналами выполняется через библиотеки вроде nix или signal-hook. Они предоставляют API для регистрации обработчиков, например, на SIGINT или SIGTERM. В async-коде применяется tokio::signal для асинхронного ожидания.

  3. use nix::sys::signal;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    
    fn main() {
        let running = Arc::new(AtomicBool::new(true));
        let r = running.clone();
    
        unsafe {
            signal::signal(signal::Signal::SIGINT, move |_| {
                r.store(false, Ordering::Relaxed);
            }).expect("Error setting signal handler");
        }
    
        println!("Waiting for SIGINT...");
        while running.load(Ordering::Relaxed) {
            std::thread::sleep(std::time::Duration::from_millis(100));
        }
    
        println!("Got SIGINT, exiting.");
    }

Экзаменационный билет №14

Теоретическая часть

  1. Что такое PhantomData и когда он используется?
  2. Какие существуют способы работы с сетевыми сокетами в Rust?

Практическая часть

  1. Напишите TCP-сервер, который принимает соединения и отправляет клиенту строку "Hello from server".

Ответы:

  1. PhantomData — маркерный тип, используемый для передачи информации компилятору о зависимости, которая не представлена в данных. Часто применяется в unsafe-коде для соблюдения гарантий lifetime.

  2. Сетевой ввод-вывод реализуется через модуль std::net (TcpStream, TcpListener, UdpSocket). Для асинхронной работы — tokio::net и async/await. Также поддерживаются low-level socket’ы через libc или crate socket2.

  3. use std::io::Write;
    use std::net::{TcpListener, TcpStream};
    
    fn handle_client(mut stream: TcpStream) {
        let response = "Hello from server\n";
        stream.write_all(response.as_bytes()).unwrap();
    }
    
    fn main() {
        let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
        println!("Server started on port 8080");
    
        for stream in listener.incoming() {
            match stream {
                Ok(stream) => {
                    std::thread::spawn(|| handle_client(stream));
                }
                Err(e) => eprintln!("Connection failed: {}", e),
            }
        }
    }

Экзаменационный билет №15

Теоретическая часть

  1. Что такое NLL и как он влияет на код?
  2. Какие существуют инструменты для тестирования производительности в Rust?

Практическая часть

  1. Напишите бенчмарк, который сравнивает производительность Vec<u32>::with_capacity и Vec<u32>::new.

Ответы:

  1. NLL (Non-Lexical Lifetimes) — улучшенный алгоритм анализа времени жизни ссылок. Он расширяет возможности borrow checker, позволяя более точно определять, когда ссылка больше не используется, что повышает гибкость кода.

  2. Основные средства: #[bench] (устарел), criterion.rs, divan. Также можно использовать perf из Linux, flamegraph и другие низкоуровневые средства анализа.

  3. use criterion::{black_box, Criterion, criterion_group, criterion_main};
    
    fn bench_with_capacity(c: &mut Criterion) {
        c.bench_function("vec with capacity", |b| {
            b.iter(|| {
                let mut v = Vec::with_capacity(1000);
                for i in 0..1000 {
                    v.push(i);
                }
                black_box(v)
            })
        });
    }
    
    fn bench_new(c: &mut Criterion) {
        c.bench_function("vec new", |b| {
            b.iter(|| {
                let mut v = Vec::new();
                for i in 0..1000 {
                    v.push(i);
                }
                black_box(v)
            })
        });
    }
    
    criterion_group!(benches, bench_with_capacity, bench_new);
    criterion_main!(benches);

Кейс №1: "Ошибка в многопоточном кэшировании"


Описание ситуации

Вы работаете над высоконагруженным веб-сервисом на Rust, который обрабатывает запросы к внешнему API. Чтобы уменьшить количество повторных обращений, вы решили реализовать простой LRU-кэш (Least Recently Used), который хранит последние N результатов.

Для параллелизма вы используете Arc<Mutex<LruCache>>, чтобы несколько потоков могли безопасно читать и записывать данные в кэш. Однако после запуска в staging-среде вы начинаете получать странные ошибки: иногда данные возвращаются некорректными или вообще отсутствуют, несмотря на то, что они должны быть в кэше.


Техническая реализация

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;

struct LruCache {
    map: HashMap<String, String>,
    order: Vec<String>,
    capacity: usize,
}

impl LruCache {
    fn new(capacity: usize) -> Self {
        LruCache {
            map: HashMap::new(),
            order: Vec::new(),
            capacity,
        }
    }

    fn get(&mut self, key: &str) -> Option<&str> {
        let key_str = key.to_string();
        if let Some(value) = self.map.get(&key_str) {
            // Обновляем порядок использования
            if let Some(pos) = self.order.iter().position(|k| k == &key_str) {
                self.order.remove(pos);
            }
            self.order.insert(0, key_str);
            Some(value.as_str())
        } else {
            None
        }
    }

    fn put(&mut self, key: String, value: String) {
        if self.map.len() >= self.capacity {
            if let Some(key_to_remove) = self.order.pop() {
                self.map.remove(&key_to_remove);
            }
        }
        self.map.insert(key.clone(), value);
        self.order.insert(0, key);
    }
}

fn main() {
    let cache = Arc::new(Mutex::new(LruCache::new(3)));

    for i in 0..5 {
        let cache_clone = Arc::clone(&cache);
        thread::spawn(move || {
            let mut cache = cache_clone.lock().unwrap();
            let key = format!("key{}", i % 2);
            let value = format!("value{}", i);
            cache.put(key, value);
            println!("Put {} into cache", key);
        });
    }

    thread::sleep(std::time::Duration::from_secs(1));
}

Проблемы, которые нужно найти и исправить

Проблема 1: Некорректное обновление порядка использования ключей

В методе get() происходит удаление ключа из вектора order с последующим добавлением его в начало. Однако при одновременном вызове get() и put() из разных потоков может возникнуть гонка данных, приводящая к несогласованности состояния.

Проблема 2: Использование Vec для очереди

Vec — плохой выбор для хранения порядка использования. Операции вроде insert(0, ...) и remove(...) выполняются за O(n), что делает реализацию медленной при большом количестве элементов.

Проблема 3: Отсутствие защиты от deadlock'ов

Хотя в данном примере явного deadlock’а нет, использование .lock().unwrap() без таймера или обработки паники может привести к зависанию потока при панике владельца блокировки.

Проблема 4: Утечка ресурсов

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


Решения и рекомендации

Используйте готовую реализацию LRU-кэша , например lru или dashmap вместо самописной структуры.

Замените Vec на LinkedList или очередь с двусторонним доступом (VecDeque) для более эффективного управления порядком .

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

match cache_clone.lock() {
    Ok(mut cache) => { /* работа с кэшем */ },
    Err(e) => eprintln!("Mutex poisoned: {}", e),
}

Используйте OnceCell или Lazy для инициализации кэша один раз :

use once_cell::sync::Lazy;

static CACHE: Lazy<Mutex<LruCache>> = Lazy::new(|| Mutex::new(LruCache::new(3)));

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


Итоговая цель обучения

Этот кейс демонстрирует типичные подводные камни при создании потокобезопасного кэша вручную , особенно:

  • Неправильное управление временем жизни данных;
  • Неверный выбор структур данных;
  • Проблемы с конкурентностью и блокировками;
  • Сложность тестирования и отладки в многопоточной среде.

Кейс №2: "Падение сервиса из-за неправильного async/await"


Описание ситуации

Вы работаете над микросервисом на Rust, который получает HTTP-запросы и асинхронно обрабатывает их с помощью фреймворка axum и рантайма tokio. Сервис должен:

  • Принимать запросы от клиентов;
  • Делать запрос к внешнему API;
  • Кэшировать результат;
  • Возвращать ответ.

Все операции реализованы через async/await, но при увеличении нагрузки сервис начинает падать , выдавая ошибку runtime executor is already shutdown.


Техническая реализация

use axum::{routing::get, Router};
use reqwest::Client;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::runtime::Builder;

type Cache = Arc<Mutex<HashMap<String, String>>>;

#[tokio::main]
async fn main() {
    let cache: Cache = Arc::new(Mutex::new(HashMap::new()));
    let client = Arc::new(Client::new());

    let app = Router::new()
        .route("/fetch", get(move || fetch_data(Arc::clone(&client), Arc::clone(&cache))));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn fetch_data(client: Arc<reqwest::Client>, cache: Cache) -> String {
    let key = "example_key";
    {
        let cache_lock = cache.lock().unwrap();
        if let Some(value) = cache_lock.get(key) {
            return value.clone();
        }
    }

    // Создание нового runtime внутри async-функции — ошибка!
    let rt = Builder::new_current_thread().build().unwrap();
    let response = rt.block_on(async move {
        let res = client.get("https://example.com ").send().await.unwrap();
        res.text().await.unwrap()
    });

    {
        let mut cache_lock = cache.lock().unwrap();
        cache_lock.insert(key.to_string(), response.clone());
    }

    response
}

Проблемы, которые нужно найти и исправить

Проблема 1: Создание нового Tokio-runtime внутри async-функции

Метод block_on() вызывается внутри fetch_data, где уже запущен tokio::main. Это приводит к вложенному запуску рантайма, что недопустимо в Tokio и может вызвать панику или deadlock.

Проблема 2: Использование .unwrap() без обработки ошибок

Если внешний API недоступен, unwrap() вызовет панику, что завершит поток и может повлиять на работу всего сервера.

Проблема 3: Неправильная работа с мьютексом

Блокировка мьютекса выполняется дважды (на чтение и запись), но не используется RAII-паттерн для автоматического освобождения. Хотя Rust гарантирует освобождение при выходе из области видимости, явное управление может усложнить логику.

Проблема 4: Отсутствие таймера ожидания запроса

Запрос к внешнему API может зависнуть. Нет ограничения по времени выполнения, что делает сервис уязвимым к DoS-атакам или просто медленным API.


Решения и рекомендации

Удалите вложенное создание Tokio-runtime — используйте существующий, запущенный через #[tokio::main]. Все асинхронные вызовы должны быть частью одного рантайма.

Добавьте таймаут для внешнего запроса :

use tokio::time::{timeout, Duration};

let timeout_duration = Duration::from_secs(5);
match timeout(timeout_duration, client.get("https://example.com ").send()).await {
    Ok(Ok(res)) => res.text().await.unwrap_or_default(),
    _ => "Timeout or error".to_string(),
}

Используйте tokio::sync::Mutex вместо std::sync::Mutex в async-коде , чтобы избежать блокировок потока:

use tokio::sync::Mutex;
type Cache = Arc<Mutex<HashMap<String, String>>>;

Объедините блокировки чтения и записи в один вызов , чтобы минимизировать количество захватов мьютекса:

let mut cache_lock = cache.lock().await;
if let Some(value) = cache_lock.get(key) {
    return value.clone();
}

let response = client.get(...).send().await?.text().await?;
cache_lock.insert(key.to_string(), response.clone());

 Используйте типичную структуру шины данных с State , чтобы передавать зависимости через middleware:

struct AppState {
    client: Arc<Client>,
    cache: Cache,
}

async fn fetch_data(
    State(state): State<Arc<AppState>>,
) -> Result<String, (StatusCode, String)> {
    // ...
}

 


Итоговая цель обучения

Этот кейс демонстрирует распространённые ошибки при работе с асинхронным программированием в Rust:

  • Вложенные рантаймы;
  • Отсутствие обработки ошибок;
  • Неправильная работа с мьютексами в async-среде;
  • Отсутствие таймаутов и защиты от долгих операций.

Ролевая игра №1: "Атака на Серверный Кластер"


Цель игры

Научить студентов применять навыки разработки на Rust в реальных условиях:

  • Работать с асинхронностью и многопоточностью;
  • Писать безопасный и производительный код;
  • Анализировать и исправлять баги в существующем коде;
  • Взаимодействовать в команде, распределяя роли и задачи.

Формат

  • Тип: Ситуационная ролевая игра (Role-playing simulation)
  • Длительность: 2–3 академических часа
  • Участники: 4–6 человек в группе
  • Инструменты: IDE (VS Code/RustRover), терминал, Cargo, Git (по желанию)

Сеттинг

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

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


Роли в команде

Роль
Описание
Team Lead
Координирует работу, распределяет задачи, следит за соблюдением сроков
Systems Engineer
Отвечает за запуск, мониторинг и диагностику сервиса
Performance Optimizer
Занимается анализом производительности, поиском узких мест
Security & Safety Expert
Проверяет код на наличие потенциальных уязвимостей и несоответствий безопасности
Backend Developer
Исправляет ошибки в логике, пишет новые функции, работает с async/await
QA / Tester
Пишет тесты, проверяет корректность изменений

Примечание: при малом количестве участников роли можно комбинировать.


Этапы игры

Этап 1: Диагностика проблемы
  • Команде выдается исходный код сервиса (упрощённая версия HTTP-сервиса на axum + tokio)
  • Запускается сценарий нагрузки (wrk, ab, или простой скрипт)
  • Сервис начинает "падать", появляются ошибки: runtime shutdown, deadlock, data race
Этап 2: Поиск багов
  • Команда анализирует логи, проводит дебаг
  • Находит несколько критических ошибок:
    • Неправильное использование block_on() внутри async-функций
    • Race condition при работе с кэшем
    • Memory leak через Arc<Mutex<T>>
    • Отсутствие таймаутов на внешние запросы
Этап 3: Рефакторинг и исправление
  • Переписывают проблемные места
  • Добавляют обработку ошибок
  • Используют tokio::sync::Mutex, OnceCell, DashMap вместо std::sync::Mutex
  • Добавляют таймеры, логирование, обработку сигналов
Этап 4: Тестирование и повторный запуск
  • QA пишет unit- и integration-тесты
  • Performance Optimizer запускает нагрузочное тестирование
  • Systems Engineer перезапускает сервис
  • Team Lead оценивает готовность

Обучающие эффекты

После игры участники смогут:

  • Применять лучшие практики написания асинхронного кода на Rust;
  • Использовать правильные примитивы синхронизации;
  • Обнаруживать и исправлять распространённые ошибки в production-коде;
  • Эффективно взаимодействовать в команде при разработке сложных систем;
  • Понимать, как устроены рантаймы, futures и executor'ы в Rust.

Возможные проблемы и вызовы во время игры

Проблема
Возможное решение
Участники не понимают, где искать баги
Учитель может давать подсказки по одному элементу за раз
Кто-то доминирует в обсуждении
Team Lead должен равномерно распределить участие
Код слишком сложный
Предоставить документацию или примеры
Не хватает времени
Можно заранее разделить задачи между ролями
Конфликт решений
Провести голосование или технический раунд обсуждения

Материалы для игры

  • Исходный код сервиса с преднамеренными багами
  • Скрипт нагрузки (например, на Python или bash)
  • Шаблон отчета по найденным проблемам и решениям
  • Лист самооценки и рефлексии после игры

Ролевая игра №2: "Ошибка в Системе Управления Памятью"


Цель игры

Научить студентов:

  • Разбираться в тонкостях управления памятью в Rust без сборщика мусора;
  • Работать с unsafe кодом безопасно и осознанно;
  • Анализировать и исправлять ошибки, связанные с сырыми указателями, lifetimes и заимствованием;
  • Применять принципы memory safety на практике.

Формат

  • Тип: Ситуационная ролевая игра (Role-playing simulation)
  • Длительность: 2–3 академических часа
  • Участники: 4–6 человек в группе
  • Инструменты: IDE (VS Code/RustRover), терминал, Cargo, GDB/LLDB (по желанию)

Сеттинг

Вы — часть команды разработчиков embedded-системы, написанной на Rust. Ваша задача — внедрить низкоуровневый модуль для работы с памятью, который будет управлять буферами данных для передачи по UART-интерфейсу.

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


Роли в команде

Роль
Описание
Team Lead
Организует работу, следит за соблюдением дедлайнов, управляет коммуникацией
Embedded Developer
Работает с low-level кодом, понимает устройство памяти и регистров
Memory Safety Expert
Ищет потенциальные ошибки в работе с памятью, проверяет lifetimes
Unsafe Code Auditor
Изучает unsafe-блоки, проверяет их корректность и безопасность
QA Engineer
Пишет unit- и integration-тесты, проверяет работоспособность изменений
Debugger / Reverse Engineer
Запускает отладку, анализирует поведение программы

Примечание: при малом количестве участников роли можно объединить.


Этапы игры

Этап 1: Получение задания

Команда получает исходный код модуля, реализующего низкоуровневое управление памятью:

pub struct Buffer {
    ptr: *mut u8,
    capacity: usize,
}

impl Buffer {
    pub fn new(size: usize) -> Self {
        let ptr = unsafe { std::alloc::alloc(std::alloc::Layout::from_size_align_unchecked(size, 1)) };
        Buffer { ptr, capacity: size }
    }

    pub fn write(&mut self, offset: usize, value: u8) {
        unsafe {
            *self.ptr.offset(offset as isize) = value;
        }
    }

    pub fn read(&self, offset: usize) -> u8 {
        unsafe {
            *self.ptr.offset(offset as isize)
        }
    }
}
Этап 2: Диагностика проблем

При запуске тестового скрипта обнаруживаются следующие ошибки:

  • Вылетает SIGSEGV при обращении к памяти;
  • Иногда данные читаются некорректно;
  • Некоторые вызовы write() перезаписывают соседние области памяти.
Этап 3: Анализ кода

Команда:

  • Проверяет работу с сырыми указателями;
  • Находит возможные причины UB (undefined behavior);
  • Обнаруживает ошибки:
    • Отсутствие проверок границ (offset);
    • Некорректное использование from_size_align_unchecked;
    • Отсутствие реализации Drop, что приводит к утечкам памяти;
    • Отсутствие синхронизации lifetimes между ссылками и указателем.
Этап 4: Рефакторинг и безопасность

Команда:

  • Добавляет проверки границ;
  • Переписывает new() с использованием Layout::from_size_align() + .unwrap();
  • Реализует Drop для освобождения памяти;
  • Вводит lifetime аннотации для методов;
  • Создаёт safe-обёртки над unsafe-функциями.
Этап 5: Тестирование

QA пишет тесты:

#[test]
fn test_buffer_write_read() {
    let mut buffer = Buffer::new(10);
    buffer.write(5, 0xA5);
    assert_eq!(buffer.read(5), 0xA5);
}

Команда проводит повторный запуск нагрузочного скрипта — всё работает стабильно.


Обучающие эффекты

После игры участники смогут:

  • Безопасно использовать unsafe блоки в Rust;
  • Корректно работать с сырыми указателями;
  • Понимать, как устроена работа с памятью без GC;
  • Объяснять различия между &T, *const T, Box<T> и Vec<u8>;
  • Применять трейты Drop, Clone, Copy на практике;
  • Использовать lifetime аннотации в low-level коде.

Возможные проблемы и вызовы во время игры

Проблема
Возможное решение
Не понимают, как работают сырые указатели
Демонстрация примеров с
Box::into_raw()
и
Vec::as_mut_ptr()
Не знают, где искать UB
Учитель даёт подсказки или демонстрирует UB через Miri
Конфликт подходов к безопасности
Обсуждение в формате code review
Кто-то не участвует активно
Team Lead должен распределить задачи четко
Код слишком сложный
Предоставить документацию или чеклист по lifetimes

Материалы для игры

  • Исходный код модуля с преднамеренными ошибками;
  • Тестовый скрипт нагрузки;
  • Шаблон технического отчета;
  • Лист рефлексии и самооценки после игры.

Ролевая игра №3: "Ошибка в Процедурном Макросе"


Цель игры

Научить студентов:

  • Понимать устройство и работу процедурных макросов;
  • Анализировать и исправлять ошибки в коде, генерируемом макросами;
  • Использовать библиотеки proc-macro2, quote, syn для создания собственных макросов;
  • Применять лучшие практики тестирования и отладки макросов.

Формат

  • Тип: Ситуационная ролевая игра (Role-playing simulation)
  • Длительность: 2–3 академических часа
  • Участники: 4–6 человек в группе
  • Инструменты: IDE (VS Code/RustRover), Cargo, Rust Analyzer, Miri (по желанию)

Сеттинг

Вы — команда разработчиков, работающих над open-source фреймворком на Rust. Один из ключевых модулей — это набор процедурных макросов, которые автоматически реализуют поведение для структур данных.

Пользователи начали жаловаться, что макросы работают некорректно: не все поля обрабатываются, происходят паники при компиляции, а иногда и вовсе возникают внутренние ошибки компилятора (ICE).

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


Роли в команде

Роль
Описание
Team Lead
Координирует команду, распределяет задачи, следит за качеством решений
Macro Developer
Знаком с процедурными макросами, работает с AST, TokenStream
Parser Expert
Разбирается в парсинге входного кода через
syn
, понимает структуру AST
Code Generator
Отвечает за генерацию кода через
quote
, знает, как строить правильный вывод
QA Engineer
Пишет unit- и integration-тесты для проверки макросов
Debugger / Compiler Explorer
Использует
cargo expand
, Miri, или редактор для просмотра генерируемого кода

Примечание: при малом количестве участников роли можно объединить.


Этапы игры

Этап 1: Получение задания

Команда получает исходный код макроса #[derive(FromEnv)], который должен автоматически создавать метод from_env(), заполняющий поля структуры значениями из переменных окружения:

#[derive(FromEnv)]
struct Config {
    host: String,
    port: u16,
}
Этап 2: Диагностика проблем

При использовании макроса:

  • Не все типы поддерживаются;
  • Поле может быть опущено в структуре — но макрос не обрабатывает это корректно;
  • При отсутствии переменной окружения происходит паника вместо возврата Result;
  • В некоторых случаях генерируется невалидный код, вызывающий ошибку компиляции.
Этап 3: Анализ кода

Команда изучает реализацию макроса:

#[proc_macro_derive(FromEnv)]
pub fn derive_from_env(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;

    let fields = if let Data::Struct(data) = &ast.data {
        &data.fields
    } else {
        panic!("Only structs are supported");
    };

    let expanded = quote! {
        impl #name {
            pub fn from_env() -> Self {
                Self {
                    #(
                        #fields: std::env::var(stringify!(#fields)).unwrap().parse().unwrap(),
                    )*
                }
            }
        }
    };

    TokenStream::from(expanded)
}

Обнаруживаются следующие ошибки:

  • Нет обработки ошибок (всё .unwrap());
  • Нет поддержки типов кроме String и u32;
  • Неверное обращение с именами полей;
  • Нет обработки Option<T> для необязательных полей.
Этап 4: Рефакторинг и безопасность

Команда:

  • Добавляет обработку ошибок через Result;
  • Поддерживает больше типов (u16, bool, i32 и т.д.);
  • Обрабатывает необязательные поля через Option<T>;
  • Улучшает вывод сообщений об ошибках;
  • Генерирует более читаемый и безопасный код.
Этап 5: Тестирование

QA пишет тесты:

#[test]
fn test_optional_field() {
    let config = MyConfig::from_env();
    assert_eq!(config.port, 8080);
}

Команда проверяет корректность работы через cargo expand и повторные сборки.


Обучающие эффекты

После игры участники смогут:

  • Понимать структуру AST и как её анализировать через syn;
  • Генерировать код через quote;
  • Безопасно использовать TokenStream и обрабатывать ошибки;
  • Создавать собственные процедурные макросы;
  • Применять best practices по тестированию макросов;
  • Использовать cargo expand для дебага генерируемого кода.

Возможные проблемы и вызовы во время игры

Проблема
Возможное решение
Не понимают, как работает AST
Учитель даёт примеры использования
syn
Не знают, как тестировать макросы
Предоставляется шаблон тестового крейта
Кто-то не участвует активно
Team Lead распределяет задачи четко
Конфликт подходов к генерации кода
Обсуждение в формате code review
Код слишком сложный
Предоставить документацию или чеклист по
quote
и
syn

Материалы для игры

  • Исходный код макроса с преднамеренными ошибками;
  • Шаблон тестового крейта для проверки;
  • Инструкция по использованию cargo expand;
  • Лист рефлексии и самооценки после игры.

Ролевая игра №4: "Ошибка в Системе Сертификации"


Цель игры

Научить студентов:

  • Разрабатывать безопасные и надёжные системы аутентификации и авторизации;
  • Работать с криптографическими примитивами в Rust (хэши, подписи, шифрование);
  • Понимать особенности реализации JWT (JSON Web Tokens) на Rust;
  • Обнаруживать и исправлять уязвимости безопасности в коде.

Формат

  • Тип: Ситуационная ролевая игра (Role-playing simulation)
  • Длительность: 2–3 академических часа
  • Участники: 4–6 человек в группе
  • Инструменты: IDE, Cargo, jsonwebtoken / ring, Git (по желанию)

Сеттинг

Вы — команда разработчиков, работающих над сервисом управления цифровыми сертификатами. Ваша задача — реализовать систему выдачи и проверки JWT для API-авторизации.

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


Роли в команде

Роль
Описание
Team Lead
Координирует работу, распределяет задачи, следит за качеством решений
Security Expert
Ищет уязвимости, проверяет корректность криптографии
JWT Developer
Знаком с протоколом JWT, работает с библиотеками
jsonwebtoken
,
serde
Crypto Engineer
Отвечает за генерацию ключей, хэширование, шифрование
QA Engineer
Пишет unit- и integration-тесты для проверки токенов
Debugger / Reverse Engineer
Анализирует пакеты, декодирует токены, проверяет payload

Примечание: при малом количестве участников роли можно объединить.


Этапы игры

Этап 1: Получение задания

Команда получает фрагмент сервиса, реализующего выдачу и проверку JWT:

use jsonwebtoken::{encode, decode, Header, Validation};
use serde::{Serialize, Deserialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
    role: String,
}

fn generate_token(sub: &str, role: &str, secret: &[u8]) -> String {
    let claims = Claims {
        sub: sub.to_string(),
        exp: 10000000000,
        role: role.to_string(),
    };
    encode(&Header::default(), &claims, &secret.into()).unwrap()
}

fn verify_token(token: &str, secret: &[u8]) -> Option<Claims> {
    decode::<Claims>(token, &DecodingKey::from_secret(secret), &Validation::default())
        .map(|t| t.claims)
        .ok()
}
Этап 2: Диагностика проблем

Пользователи сообщают о следующих проблемах:

  • Некоторые токены принимаются даже с неверным секретом;
  • Администраторские права получаются без явного указания;
  • Возможна манипуляция полями через replay-атаки или подделку заголовков.
Этап 3: Анализ кода

Команда обнаруживает несколько критических ошибок:

  • Использование Validation::default() вместо строгой проверки алгоритма;
  • Хранение секрета как обычной строки или в коде;
  • Поле role не проверяется при авторизации;
  • Отсутствует валидация exp;
  • Используется небезопасный алгоритм (HS256 без проверки).
Этап 4: Рефакторинг и безопасность

Команда:

  • Устанавливает строгую проверку алгоритма;
  • Добавляет валидацию exp, iat, nbf;
  • Вводит enum UserRole и проверку прав;
  • Реализует использование RS256 и асимметричное шифрование;
  • Переносит секреты в безопасное хранилище (например, .env);
Этап 5: Тестирование

QA пишет тесты:

#[test]
fn test_invalid_role() {
    let token = generate_token("user", "admin", b"secret");
    let claims = verify_token(&token, b"secret").unwrap();
    assert_eq!(claims.role, "admin");
}

Команда проверяет корректность работы через jwt-cli или онлайн-декодеры.


Обучающие эффекты

После игры участники смогут:

  • Понимать структуру JWT и как её правильно использовать;
  • Применять криптографические библиотеки в Rust (jsonwebtoken, ring, openssl);
  • Создавать и проверять токены с безопасными алгоритмами;
  • Обнаруживать и исправлять распространённые уязвимости (например, none-algorithm, weak signing);
  • Использовать best practices по управлению секретами и жизненным циклом токенов.

Возможные проблемы и вызовы во время игры

Проблема
Возможное решение
Не понимают, как устроен JWT
Учитель даёт лекцию или чеклист
Не знают, как тестировать токены
Предоставляется пример с
curl
или
jwt-cli
Кто-то не участвует активно
Team Lead распределяет задачи четко
Конфликт подходов к безопасности
Обсуждение в формате code review
Код слишком сложный
Предоставить документацию или чеклист по
jsonwebtoken

Материалы для игры

  • Исходный код модуля с преднамеренными ошибками;
  • Шаблон тестового крейта;
  • Инструкция по использованию jwt-cli;
  • Лист рефлексии и самооценки после игры.

Интеллект-карта №1: Основы языка Rust

Центральная тема:
Основы языка Rust

Подтемы:

1. Синтаксис и типы

  • Переменные (let, mut)
  • Типы данных (i32, f64, bool, char, str, String)
  • Кортежи, массивы, слайсы
  • Управление потоком: if, loop, while, for, match

2. Владение (Ownership)

  • Что такое владение
  • Правила владения
  • Передача владения
  • Клонирование значений

3. Заимствование и ссылки

  • Неизменяемые и изменяемые ссылки
  • Правила borrow checker
  • Дерево заимствования

4. Lifetimes

  • Аннотации lifetimes
  • Статические ссылки 'static
  • Вывод lifetime

5. Ошибки и обработка

  • Enum Result и Option
  • Макросы unwrap(), expect(), оператор ?
  • Пользовательские типы ошибок

Интеллект-карта №2: Безопасность памяти и управление ресурсами

Центральная тема:
Безопасность памяти и управление ресурсами

Подтемы:

1. Механизмы безопасности

  • Borrow checker
  • Ownership model
  • Drop trait

2. Unsafe Rust

  • Когда используется
  • Опасные операции: сырой указатель, FFI, статическая переменная
  • Как писать безопасно

3. Работа с памятью

  • Выделение и освобождение
  • Использование Box<T>, Vec<T>, String
  • Утечки памяти и как их избежать

4. Сложные типы и абстракции

  • Cell и RefCell
  • Interior mutability
  • Atomic и Sync types

5. RAII (Resource Acquisition Is Initialization)

  • Автоматическое управление ресурсами
  • Drop trait и его реализация
  • Примеры: файлы, сокеты, мьютексы

Интеллект-карта №3: Асинхронное программирование

Центральная тема:
Асинхронное программирование

Подтемы:

1. Futures и async/await

  • Что такое Future
  • Как работает async/await
  • Реализация асинхронных функций

2. Tokio Runtime

  • Executor и задачи
  • Работа с I/O
  • Мультипоточный runtime

3. Параллелизм и конкурентность

  • Send и Sync трейты
  • Обмен данными между потоками
  • Мьютексы, каналы, atomic

4. Асинхронные библиотеки

  • reqwest, hyper, sqlx
  • Работа с сетью и БД
  • Async-читатели и писатели

5. Отладка и тестирование

  • Инструменты: tracing, log
  • Нагрузочное тестирование
  • Ловушки и анти-паттерны

1. Книга: "The Rust Programming Language" (2024, официальная документация)

Авторы: Steve Klabnik, Carol Nichols
Формат: Учебник + онлайн-ресурс
Описание:
Официальное руководство по языку Rust от сообщества Rust. Охватывает все аспекты языка, включая безопасность памяти, владение, lifetimes, unsafe код, макросы и многопоточность. Написано понятно, с примерами.
Полезен для:

  • Базового и углубленного изучения;
  • Как справочное пособие;
  • Для самостоятельной работы студентов.

🔗 https://doc.rust-lang.org/book/


2. Книга: "Programming Rust: Fast, Safe Systems Development"

Авторы: Jim Blandy, Jason Orendorff
Издательство: O’Reilly Media
Год издания: 2021
Описание:
Подробное пособие по системному программированию на Rust. Особое внимание уделено низкоуровневым абстракциям, работе с памятью, FFI, async/await и разработке библиотек.
Полезен для:

  • Студентов, уже знакомых с основами;
  • Разработчиков, желающих перейти на Rust;
  • Преподавателей, как источник практических примеров.

3. Методическое пособие: "Rust for Embedded Systems"

Авторы: Various contributors (Rust embedded working group)
Формат: Онлайн-документация и гайды
Описание:
Сборник методических материалов по использованию Rust в embedded-разработке. Содержит практические рекомендации по созданию безосных приложений, работе с периферией, оптимизации памяти.
Полезен для:

  • Обучения low-level программированию;
  • Подготовки лабораторных работ;
  • Реализации проектов в области IoT и микроконтроллеров.

🔗 https://docs.rust-embedded.org/book/


4. Задачник: "Rustlings" (обучающий набор упражнений)

Автор: Rust Core Team
Формат: Практический курс с заданиями
Описание:
Набор коротких заданий, охватывающих ключевые темы Rust: типы, заимствование, lifetimes, ошибки, макросы, тестирование и многое другое. Отличный инструмент для закрепления теории через практику.
Полезен для:

  • Самостоятельной подготовки;
  • Включения в учебный план как дидактическое пособие;
  • Проверки знаний перед экзаменами или собеседованиями.

🔗 https://github.com/rust-lang/rustlings


5. Научно-практическая статья: "Safe Systems Programming in Rust"

Авторы: Aaron Turon et al.
Журнал: Proceedings of the ACM on Programming Languages (PACMPL), 2021
Описание:
Академическая статья, рассматривающая, как система владения и borrow checking в Rust обеспечивают безопасность системного программирования без использования сборщика мусора.
Полезна для:

  • Теоретического обоснования курса;
  • Использования в курсах по языкам программирования;
  • Исследовательской и научной работы.

🔗 DOI:10.1145/3434304

  1. Курс: "Системное программирование на Rust"
    Анонс: Изучение низкоуровневой разработки с применением безопасного кода, управления памятью и работы с аппаратными ресурсами без использования сборщика мусора.

  1. Курс: "Разработка высокопроизводительных сервисов на Rust"
    Анонс: Создание производительных, масштабируемых backend-сервисов с использованием async/await, Tokio, HTTP API и оптимизации под нагрузку.

  1. Курс: "Rust для Embedded-разработки"
    Анонс: Применение языка Rust в системах с ограниченными ресурсами: микроконтроллеры, драйверы, bare-metal разработка.

  1. Курс: "Метапрограммирование и макросы в Rust"
    Анонс: Углублённое изучение процедурных макросов, генерации кода, derive-атрибутов и написания собственных расширений языка.

  1. Курс: "Безопасная параллельность и конкурентность в Rust"
    Анонс: Разработка многопоточных приложений с использованием Send/Sync, атомарных типов, каналов и блокировок.

  1. Курс: "Unsafe Rust: контроль и ответственность"
    Анонс: Работа с сырыми указателями, FFI, inline-ассемблером и управление рисками при использовании unsafe-кода.

  1. Курс: "Интеграция Rust с C/C++"
    Анонс: Реализация взаимодействия через FFI, создание биндингов, работа с ABI и обеспечение безопасности при вызовах из других языков.

  1. Курс: "Компиляция и оптимизация Rust-кода"
    Анонс: Исследование процесса компиляции, использование MIR, LLVM, инструментов анализа и оптимизации производительности.

  1. Курс: "Разработка крейтов и экосистема Rust"
    Анонс: Публикация, документирование и тестирование библиотек, управление зависимостями, best practices сообщества.

  1. Курс: "Тестирование и верификация Rust-программ"
    Анонс: Написание unit- и integration-тестов, фаззинг, property-based testing, использование Miri для проверки корректности unsafe-кода.

  1. Курс: "Rust в DevOps и CI/CD"
    Анонс: Использование Rust в автоматизации, создание CLI-утилит, интеграция в pipeline’ы, написание плагинов и утилит.

  1. Курс: "Криптография и безопасность в Rust"
    Анонс: Реализация шифрования, хэширования, цифровых подписей и JWT с акцентом на безопасную реализацию и защиту данных.

  1. Курс: "Проектирование и разработка библиотек на Rust"
    Анонс: Проектирование API, обобщённое программирование, документирование, тестирование и поддержка обратной совместимости.

  1. Курс: "Продвинутые темы в async/await программировании"
    Анонс: Глубокое погружение в futures, executor'ы, работу с tokio, pinning, Wakers и реализацию пользовательских runtime.

  1. Курс: "Реализация интерфейсов и абстракций в Rust"
    Анонс: Техники создания высокоуровневых абстракций поверх low-level кода, реализация трейтов, generics и lifetime-aware API.

  1. Курс: "Ошибки и обработка исключений в Rust"
    Анонс: Сравнение подходов к обработке ошибок, реализация пользовательских типов ошибок, использование anyhow и thiserror.

  1. Курс: "Жизнь вне стандартной библиотеки: no_std"
    Анонс: Разработка кода без std, использование core/alloc, реализация базовых операций в средах с ограниченными ресурсами.

  1. Курс: "Rust для сетевых приложений"
    Анонс: Разработка TCP/UDP серверов, сокетов, протоколов, работа с async I/O, реализация клиент-серверных архитектур.

  1. Курс: "Формальные методы и верификация в Rust"
    Анонс: Использование формальных методов, статического анализа и инструментов вроде Prusti для доказательства корректности кода.

  1. Курс: "Управление состоянием в многопоточных приложениях"
    Анонс: Мьютексы, RWLock, атомарные типы, crossbeam, scoped threads и другие способы безопасного доступа к общему состоянию.

  1. Курс: "Rust в блокчейн-разработке"
    Анонс: Разработка смарт-контрактов, работа с WebAssembly, интеграция с Solana/Substrate, безопасность и производительность.

  1. Курс: "Программирование ОС на Rust"
    Анонс: Создание загрузчика, управления памятью, прерываний и драйверов устройств на языке Rust без зависимости от C.

  1. Курс: "Rust в области машинного обучения и AI"
    Анонс: Использование Rust в вычислениях с низкой задержкой, работа с ONNX, tch-rs, ndarray и polars для обработки данных.

  1. Курс: "Компиляторы и трансляторы на Rust"
    Анонс: Разработка интерпретаторов, компиляторов, анализ AST, генерация кода и оптимизация на примере собственного языка.

  1. Курс: "Rust для Game Engine Development"
    Анонс: Разработка игровых движков на Rust: работа с графикой, физикой, потоками, безопасное взаимодействие с GPU и SDL/Vulkan.
Заявка ученика, студента, слушателя
Заявка преподавателя, репетитора админу сети.
15:39
16
Посещая этот сайт, вы соглашаетесь с тем, что мы используем файлы cookie.