Разработчик Rust (Профессиональный уровень)
Курс предназначен для опытных разработчиков, стремящихся овладеть профессиональными навыками системного программирования на языке 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 и тестирование.
-
Что делает Rust уникальным среди языков системного программирования?
Rust сочетает безопасность памяти без использования сборщика мусора, низкоуровневый контроль и высокую производительность. Его система владения (ownership) и проверки заимствования (borrowing) гарантирует отсутствие ошибок вроде dangling pointers и data races на этапе компиляции. -
Как устроена система владения (ownership) в Rust?
Ownership — это набор правил управления памятью, определяющих, какой участок кода «владеет» данными. Когда владелец выходит из области видимости, данные уничтожаются. Это позволяет избежать утечек памяти и неявных освобождений. -
Что такое заимствование (borrowing) и почему оно важно?
Заимствование позволяет ссылаться на данные без их владения. Это снижает накладные расходы копирования и позволяет нескольким частям кода работать с одними данными, соблюдая правила borrow checker. -
Как работает borrow checker и для чего он нужен?
Borrow checker анализирует ссылки на этапе компиляции и гарантирует, что они не выходят за пределы времени жизни данных. Он предотвращает такие ошибки, как использование освобождённой памяти или изменение неизменяемой ссылки. -
В чём разница между &T и &mut T?
&T — неизменяемая ссылка, позволяет читать данные, но не изменять их. &mut T — изменяемая ссылка, позволяет модифицировать данные, но может существовать только одна такая ссылка в данной области видимости. -
Что такое lifetimes и как они обозначаются в коде?
Lifetimes — это аннотации, указывающие, сколько должна существовать ссылка. Они обозначаются с помощью апострофа: 'a. Например: fn foo<'a>(x: &'a str). -
Почему в Rust нет сборщика мусора (GC)?
Отказ от GC позволяет достичь предсказуемой работы с ресурсами и минимальных пауз в выполнении. Управление памятью осуществляется через систему ownership и lifetimes, что делает Rust подходящим для embedded-систем и high-load приложений. -
Что такое unsafe Rust и когда его стоит использовать?
Unsafe Rust — это подмножество языка, позволяющее обходить ограничения borrow checker. Используется для FFI, прямого доступа к памяти, реализации низкоуровневых абстракций. Требует особой осторожности. -
Как в Rust реализуется параллелизм?
Rust обеспечивает потокобезопасность через Send и Sync трейты. Send разрешает передачу данных между потоками, Sync — безопасное совместное использование. Это позволяет писать конкурентный код без data races. -
Что такое async/await в Rust и как его использовать?
Async/await — механизм асинхронного программирования, позволяющий писать асинхронный код как синхронный. Реализован через futures и runtime, например Tokio или async-std. -
Какие основные отличия между структурами и перечислениями в Rust?
Структуры (struct) группируют поля данных, перечисления (enum) представляют значение из множества возможных. Enum могут содержать данные в каждом варианте и часто используются с pattern matching. -
Что такое match и как он используется в Rust?
Match — мощный механизм сопоставления с образцом. Он позволяет проверять значения и выполнять разные блоки кода в зависимости от случая. Обязательно покрывает все возможные варианты. -
Как работают итераторы в Rust?
Итераторы предоставляют ленивый способ обработки последовательностей. Они эффективны и безопасны благодаря типизации и гарантиям lifetime. Методы вроде map, filter и collect применяются цепочками. -
Что такое макросы в Rust и чем они отличаются от функций?
Макросы — это метапрограммирование, позволяющее генерировать код на этапе компиляции. В отличие от функций, они работают с синтаксическими деревьями и могут принимать переменное число аргументов. -
Как создать свой декларативный макрос в Rust?
Создание макроса начинается с #[macro_export] и macro_rules!. Макрос определяет шаблоны и соответствующие им действия, например: macro_rules! vec { ... }. -
Что такое процедурные макросы и где они применяются?
Процедурные макросы — это функции, принимающие код в виде токенов и возвращающие новый код. Применяются для автоматической генерации кода, например, derive-атрибутов. -
Как организовать проект на Rust с использованием Cargo?
Cargo — это стандартный инструмент управления зависимостями и сборки. Проект создаётся командой cargo new. Зависимости добавляются в Cargo.toml, модули — через файлы и pub ключевые слова. -
Что такое крейты и как они используются в экосистеме Rust?
Крейт — это единица повторного использования кода в Rust. Бывают библиотечными и исполняемыми. Публикуются на crates.io и подключаются через Cargo. -
Как происходит тестирование в Rust?
Тестирование проводится с помощью атрибута #[test]. Cargo test запускает unit- и integration-тесты. Можно использовать assert!, assert_eq! и другие макросы для проверок. -
Как работает документирование в Rust и как его генерировать?
Документирование пишется с помощью /// или /** */. Cargo doc генерирует HTML-документацию. Хорошая практика — писать примеры в комментариях, которые проверяются при тестировании. -
Что такое FFI в Rust и как с ним работать?
FFI (Foreign Function Interface) позволяет вызывать функции на других языках, например C. Для этого используется extern "C" блок и unsafe код. -
Как интегрировать Rust с C/C++?
Интеграция выполняется через FFI. Rust может вызывать C-функции и экспортировать свои функции для использования в C/C++. Также можно использовать rust-bindgen для автоматической генерации связок. -
Какие инструменты помогают в отладке Rust-кода?
GDB, LLDB, rust-gdb, rust-lldb, а также плагины для IDE (VS Code, CLion). Также полезны cargo clippy для статического анализа и cargo fmt для форматирования. -
Что такое Send и Sync в контексте многопоточности?
Send — маркер, показывающий, что тип можно безопасно перемещать между потоками. Sync — что ссылку на тип можно безопасно использовать из нескольких потоков. -
Какие лучшие практики рекомендуются при разработке на Rust?
Писать безопасный код по умолчанию, минимизировать использование unsafe, документировать всё, тестировать регулярно, использовать Cargo best practices, применять Clippy и форматтер, следить за обновлениями языка и экосистемы.
-
Как в Rust обрабатываются ошибки и какие типы для этого используются?
Rust предоставляет два основных типа для обработки ошибок: Result и Option. Result<T, E> используется для возврата либо успешного значения T, либо ошибки E. Option<T> применяется, когда значение может отсутствовать (Some(T) или None). Для удобства есть операторы ?, unwrap(), expect() и макрос ensure!. -
Что такое Deref- coercion и как оно работает?
Deref-coercion — это механизм автоматического приведения ссылок при вызове методов через *. Например, &String может быть автоматически преобразован в &str. Это упрощает работу с умными указателями и абстракциями. -
В чём разница между String и &str в Rust?
String — это динамически выделяемая строка, владеющая данными. &str — неизменяемая ссылка на строку, часто используется для чтения данных. String можно изменять, а &str — нет. -
Как работают трейты (traits) и зачем они нужны?
Трейты определяют общее поведение для типов. Они похожи на интерфейсы в других языках. Используются для реализации полиморфизма, ограничений в generic-кодах и реализации стандартных методов (например, Debug, Clone). -
Что такое impl Trait и где он применяется?
impl Trait позволяет указывать тип без явного написания его имени. Применяется в сигнатурах функций, чтобы возвращать сложные типы, например, итераторы, обеспечивая простоту и гибкость. -
Как организовать модульность в крупном проекте на Rust?
Модули создаются через ключевое слово mod. Публичные элементы помечаются pub. Можно использовать файловую структуру: mod.rs и подкаталоги. Хорошая практика — группировка логически связанных компонентов. -
Что такое Copy и Clone и в чём их отличие?
Copy — автоматическое копирование значений при присвоении. Применяется только к малым данным (например, числа). Clone требует явного вызова .clone() и может быть дорогим. Не все типы могут быть Copy. -
Как работает Drop в Rust и когда он вызывается?
Drop — это трейт, позволяющий определить поведение при освобождении ресурсов. Вызывается автоматически, когда переменная выходит из области видимости. Полезен для освобождения внешних ресурсов (сетевые соединения, файлы). -
Что такое PhantomData и когда он используется?
PhantomData — маркерный тип, используемый для передачи информации компилятору о зависимости, которая не представлена в данных. Часто применяется в unsafe коде для соблюдения гарантий lifetime. -
Как реализовать свой итератор в Rust?
Для этого нужно реализовать трейт Iterator, определив тип Item и метод next(). Это позволяет создавать пользовательские последовательности и использовать их с цепочками map/filter/take и другими. -
Что такое Into и From и как они связаны?
From и Into — трейты для преобразования типов. From позволяет создавать один тип из другого. Into — обратное, но требует явного приведения. Взаимосвязаны: если реализован From, Into реализуется автоматически. -
Как работает система сборки Cargo и что она поддерживает?
Cargo управляет зависимостями, тестами, документацией и сборкой. Поддерживает профили (dev, release), фичи, workspaces, custom build scripts и cross-compilation через target параметры. -
Что такое workspaces в Cargo и зачем они нужны?
Workspaces позволяют объединить несколько пакетов в один проект. Все крейты внутри workspace разделяют одну директорию Cargo.lock и могут ссылаться друг на друга. Удобно для микросервисов или библиотек в одном репозитории. -
Как работают фичи (features) в Cargo и как их активировать?
Фичи — это условные зависимости, которые включаются по запросу. Определяются в Cargo.toml. Активируются через команду --features "feature_name" или в других крейтах как зависимости. -
Что такое pinning и когда оно необходимо?
Pinning используется в асинхронном программировании, чтобы гарантировать, что объект не будет перемещён в памяти. Необходимо при работе с self-referential структурами и futures. -
Какие существуют способы управления состоянием в async-приложениях?
Состояние можно хранить в Arc<Mutex<T>>, использовать tokio::sync::Mutex, OnceCell, или передавать в handler'ы через middleware. Важно обеспечивать Send + Sync для совместимости с runtime. -
Что такое Tokio и какой его роль в экосистеме Rust?
Tokio — популярный асинхронный runtime для выполнения async/await кода. Предоставляет executor, I/O, timer и мультипоточный диспетчер. Используется для сетевых приложений, серверов, клиентов. -
Как работает сериализация и десериализация в Rust?
С помощью трейтов Serialize и Deserialize из библиотеки serde. Поддерживает JSON, TOML, YAML и другие форматы. Реализуется через derive-атрибуты или вручную для сложных случаев. -
Какие инструменты используются для тестирования производительности в Rust?
Для benchmarking используется #[bench] (устарел), criterion.rs или divan. Также можно использовать perf из Linux, flamegraph и другие низкоуровневые средства анализа. -
Что такое const generics и как они используются?
Const generics позволяют параметризовать типы числами. Например, [T; N] — массив фиксированной длины. Используются для безопасной работы с размерами буферов, матриц и других структур. -
Как происходит работа с файлами в Rust?
Через модуль std::fs. Чтение и запись выполняются с использованием File, BufReader, BufWriter. Можно использовать read_to_string(), write_all() и другие методы. Для асинхронного доступа — tokio::fs. -
Что такое NonZero и зачем он нужен?
NonZero — типы вроде NonZeroU32, гарантирующие, что значение не равно нулю. Используются для оптимизации памяти и предотвращения недопустимых состояний, особенно в Option-like структурах. -
Как использовать атрибуты #[derive] и какие из них наиболее важны?
Атрибут #[derive] автоматически реализует стандартные трейты: Debug, Clone, Copy, PartialEq, Eq, Hash, Default. Полезны для упрощения кода и обеспечения базового поведения. -
Какие существуют подходы к логированию в Rust?
Базовое логирование осуществляется через log crate и реализацию env_logger, slog, tracing. Для production рекомендуется tracing с поддержкой span’ов и уровней детализации. -
Какие лучшие практики оформления кода в Rust?
Использовать rustfmt для форматирования, clippy для статического анализа, соблюдать стиль PSR (Rust API Guidelines), комментировать public API, писать unit- и integration-тесты, применять documentation tests.
-
Какой из следующих типов гарантирует, что значение не может быть null?
A) *const T
B) &T
C) Option<T>
D) String
Правильный ответ: B) &T -
Что делает ключевое слово 'mut' в объявлении переменной?
A) Позволяет изменять значение переменной
B) Делает переменную константной
C) Указывает на использование unsafe-кода
D) Включает автоматическое клонирование значения
Правильный ответ: A) Позволяет изменять значение переменной -
Какой трейт используется для сравнения двух значений на равенство?
A) Eq
B) PartialEq
C) Ord
D) Clone
Правильный ответ: B) PartialEq -
Что такое borrow checker в Rust?
A) Средство форматирования кода
B) Компонент компилятора, управляющий временем жизни ссылок
C) Инструмент для отладки памяти
D) Библиотека для работы с итераторами
Правильный ответ: B) Компонент компилятора, управляющий временем жизни ссылок -
Какой тип ссылки позволяет изменять данные?
A) &T
B) *const T
C) &mut T
D) Box<T>
Правильный ответ: C) &mut T -
Какая команда создаёт новый проект с помощью Cargo?
A) cargo new
B) cargo create
C) cargo init
D) cargo project
Правильный ответ: A) cargo new -
Какой атрибут используется для создания unit-теста?
A) #[test]
B) #[unit_test]
C) #[testing]
D) #[assert]
Правильный ответ: A) #[test] -
Какой тип данных используется для хранения строки с возможностью изменения?
A) str
B) &str
C) String
D) CString
Правильный ответ: C) String -
Что означает реализация трейта Drop для структуры?
A) Автоматическое копирование значения
B) Освобождение ресурсов при выходе из области видимости
C) Возможность итерации по полям структуры
D) Поддержку сериализации
Правильный ответ: B) Освобождение ресурсов при выходе из области видимости -
Что делает оператор '?' в функции, возвращающей Result?
A) Принудительно завершает программу
B) Возвращает ошибку, если она возникает
C) Игнорирует результат выполнения
D) Выполняет разыменование указателя
Правильный ответ: B) Возвращает ошибку, если она возникает -
Какой тип указателя требует использования unsafe-блока для разыменования?
A) &T
B) &mut T
C) *const T
D) Box<T>
Правильный ответ: C) *const T -
Какой из следующих макросов используется для генерации сообщения panic?
A) assert!
B) panic!
C) todo!
D) unimplemented!
Правильный ответ: B) panic! -
Какой тип позволяет делить владение данными между потоками?
A) Rc<T>
B) Arc<T>
C) Mutex<T>
D) RefCell<T>
Правильный ответ: B) Arc<T> -
Какой из перечисленных типов реализует Send?
A) *const T
B) *mut T
C) Rc<T>
D) Box<dyn Send + Sync>
Правильный ответ: D) Box<dyn Send + Sync> -
Какой из следующих фрагментов кода корректно объявляет 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) -
Что такое FFI в контексте Rust?
A) Формат сериализации
B) Интерфейс взаимодействия с другими языками
C) Асинхронная библиотека
D) Система тестирования
Правильный ответ: B) Интерфейс взаимодействия с другими языками -
Какой инструмент используется для форматирования Rust-кода?
A) rustfmt
B) clippy
C) cargo fmt
D) format-rust
Правильный ответ: C) cargo fmt -
Какой из следующих макросов используется для генерации документации?
A) doc!
B) ///
C) #[doc]
D) comment!
Правильный ответ: B) /// -
Что делает атрибут #[derive(Debug)]?
A) Включает проверки времени выполнения
B) Разрешает вывод lifetime
C) Автоматически реализует трейт Debug
D) Включает сборку с отладочной информацией
Правильный ответ: C) Автоматически реализует трейт Debug -
Какой из следующих типов не поддерживает копирование?
A) u32
B) bool
C) Vec<T>
D) f64
Правильный ответ: C) Vec<T> -
Какой атрибут используется для предотвращения инлайна функции?
A) #[inline]
B) #[no_inline]
C) #[cold]
D) #[inline(never)]
Правильный ответ: D) #[inline(never)] -
Какой из следующих типов реализует interior mutability?
A) &mut T
B) Cell<T>
C) *mut T
D) Box<T>
Правильный ответ: B) Cell<T> -
Что делает ключевое слово 'static' при объявлении переменной?
A) Указывает на время жизни всей программы
B) Обозначает локальную переменную
C) Создаёт константу времени компиляции
D) Объявляет глобальную ссылку
Правильный ответ: A) Указывает на время жизни всей программы -
Какой из следующих типов не является частью стандартной библиотеки?
A) Vec<T>
B) HashMap<K, V>
C) BTreeMap<K, V>
D) LinkedList<T>
Правильный ответ: Ни один из перечисленных (все являются частью std). -
Что делает Cargo.lock?
A) Хранит информацию о зависимостях с конкретными версиями
B) Задаёт параметры сборки
C) Хранит пользовательские настройки
D) Содержит пути к исходникам
Правильный ответ: A) Хранит информацию о зависимостях с конкретными версиями
Экзаменационный билет №1
Теоретическая часть
- Объясните, что такое ownership и как он влияет на управление памятью в Rust.
- Что означает ключевое слово unsafe и в каких случаях его можно использовать?
Практическая часть
- Напишите функцию, которая принимает строку и возвращает Option<char>, представляющий первый заглавный символ строки. Если такового нет — вернуть None.
Ответы:
-
Ownership — это система управления памятью в Rust, основанная на праве владения данными. У каждого значения есть один владелец. Когда владелец выходит из области видимости, значение уничтожается. Это исключает утечки памяти и позволяет обойтись без сборщика мусора.
-
Ключевое слово unsafe разрешает выполнение операций, которые не проверяются borrow checker'ом: разыменование сырого указателя, вызов unsafe-функций, доступ к статической изменяемой переменной и реализация unsafe-трейтов. Используется редко и требует осторожности.
-
fn first_uppercase(s: &str) -> Option<char> { s.chars().find(|c| c.is_uppercase()) }
Экзаменационный билет №2
Теоретическая часть
- В чём разница между &T и &mut T? Приведите примеры, когда используется каждый тип ссылки.
- Что такое lifetimes и зачем они нужны в Rust?
Практическая часть
- Реализуйте итератор, который возвращает только чётные числа из заданного вектора.
Ответы:
-
&T — неизменяемая ссылка, позволяющая читать данные, но не изменять их. &mut T — изменяемая ссылка, позволяющая модифицировать данные. Однако может быть только одна &mut T ссылка в одной области видимости. Пример: &String для чтения, &mut String для изменения содержимого.
-
Lifetimes — это аннотации времени жизни ссылок. Они гарантируют, что ссылки не живут дольше данных, на которые ссылаются. Помогают компилятору предотвращать использование освобождённой памяти.
-
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
Теоретическая часть
- Опишите, как работает borrow checker и какие ошибки он предотвращает.
- Что такое макросы в Rust и чем они отличаются от функций?
Практическая часть
- Напишите макрос vec_of!, принимающий два параметра: тип и количество элементов, и создающий вектор заполненный нулевыми значениями этого типа.
Ответы:
-
Borrow checker — это компонент компилятора, анализирующий ссылки на этапе компиляции. Он гарантирует, что ссылки не выходят за пределы времени жизни данных и не возникает гонок данных. Предотвращает такие ошибки, как использование освобождённой памяти или изменение неизменяемой ссылки.
-
Макросы — это метапрограммирование, позволяющее генерировать код на этапе компиляции. В отличие от функций, работают с синтаксическими деревьями и могут принимать переменное число аргументов. Часто используются для автоматизации повторяющегося кода.
-
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
Теоретическая часть
- Что такое параллелизм и как он реализован в Rust?
- Что такое Send и Sync и как они связаны с многопоточностью?
Практическая часть
- Напишите программу, которая запускает два потока, каждый из которых считает сумму части массива, а затем объединяет результаты.
Ответы:
-
Параллелизм в Rust реализуется через потоки (std::thread). Каждый поток может запускаться с помощью spawn. Для передачи данных между потоками используется move, а также типы, реализующие Send.
-
Send — маркер, показывающий, что тип можно безопасно перемещать между потоками. Sync — что ссылку на тип можно безопасно использовать из нескольких потоков. Эти трейты обеспечивают потокобезопасность на уровне типов.
-
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
Теоретическая часть
- Как работает асинхронное программирование в Rust и что такое futures?
- Что такое процедурные макросы и где они применяются?
Практическая часть
- Напишите асинхронную функцию, которая отправляет HTTP-запрос и возвращает статус-код. Используйте библиотеку reqwest.
Ответы:
-
Асинхронное программирование в Rust основано на futures и async/await. Future представляет собой асинхронную операцию, которую можно опросить до завершения. Async/await — это синтаксический сахар над futures, упрощающий написание асинхронного кода.
-
Процедурные макросы — это функции, принимающие код в виде токенов и возвращающие новый код. Применяются для автоматической генерации кода, например, derive-атрибутов. Они мощнее декларативных макросов и дают больше контроля над AST.
-
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
Теоретическая часть
- Что такое Drop и как он используется в практике?
- В чём разница между String и &str?
Практическая часть
- Напишите функцию, которая принимает строку и возвращает true, если она является палиндромом (без учёта регистра и пробелов).
Ответы:
-
Drop — это трейт, позволяющий определить поведение при уничтожении значения. Метод drop вызывается автоматически при выходе переменной из области видимости. Часто используется для освобождения внешних ресурсов, таких как файлы или сетевые соединения.
- String — это динамически выделяемая строка, владеющая данными. &str — неизменяемая ссылка на строку, часто используется для чтения данных. String можно изменять, а &str — нет.
-
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
Теоретическая часть
- Опишите концепцию Result и Option в Rust.
- Что такое Deref coercion и как оно работает?
Практическая часть
- Реализуйте простую структуру Counter, реализующую трейт Iterator, которая возвращает числа от 1 до 10.
Ответы:
-
Result<T, E> используется для возврата либо успешного значения T, либо ошибки E. Option<T> применяется, когда значение может отсутствовать (Some(T) или None). Оба типа обеспечивают безопасную обработку возможных состояний без использования null.
-
Deref coercion — это механизм автоматического приведения ссылок при вызове методов через *. Например, &String может быть автоматически преобразован в &str. Это упрощает работу с умными указателями и абстракциями.
-
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
Теоретическая часть
- Что такое Arc и Mutex, и как они используются вместе?
- Какие существуют способы сериализации и десериализации JSON в Rust?
Практическая часть
- Напишите структуру Person, реализующую сериализацию/десериализацию, и сохраните её в JSON.
Ответы:
-
Arc<T> — это тип с поддержкой reference counting, позволяющий делить владение данными между потоками. Mutex<T> обеспечивает взаимное исключение при доступе к данным. Их совместное использование позволяет безопасно изменять общее состояние из нескольких потоков.
-
Основные средства — библиотека serde с флагом derive. Типы to_string, from_str, to_vec и другие из serde_json. Также есть специализированные решения вроде simd-json.
-
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
Теоретическая часть
- Что такое Pin и когда он нужен в асинхронном программировании?
- Как работают условные зависимости в Cargo?
Практическая часть
- Напишите функцию, которая принимает два Vec<i32> и возвращает новый вектор, содержащий только уникальные элементы из обоих.
Ответы:
-
Pin гарантирует, что объект не будет перемещён в памяти. Это необходимо для self-referential структур и некоторых futures. Используется вместе с Pin и Unpin.
-
Условные зависимости (features) определяются в Cargo.toml и активируются через --features. Они позволяют включать или исключать части кода в зависимости от платформы или других факторов.
-
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
Теоретическая часть
- Что такое const generics и где они могут применяться?
- Как происходит работа с файлами в Rust?
Практическая часть
- Напишите программу, которая считывает содержимое текстового файла и выводит количество слов в нём.
Ответы:
-
Const generics позволяют параметризовать типы числами. Например, [T; N] — массив фиксированной длины. Используются для безопасной работы с размерами буферов, матриц и других структур.
-
Работа с файлами осуществляется через модуль std::fs. Чтение и запись выполняются с использованием File, BufReader, BufWriter. Можно использовать read_to_string(), write_all() и другие методы. Для асинхронного доступа — tokio::fs.
-
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
Теоретическая часть
- Что такое Interior Mutability и какие типы её реализуют?
- В чём отличие между Cell<T> и RefCell<T>?
Практическая часть
- Напишите функцию, которая принимает строку и возвращает Vec<String>, содержащий все слова, начинающиеся с заглавной буквы.
Ответы:
-
Interior Mutability — это паттерн, позволяющий изменять данные внутри неизменяемой структуры. Реализуется через типы Cell<T>, RefCell<T>, Mutex<T>. Используется в случаях, когда нужно изменить состояние без явного использования mut.
-
Cell<T> работает только с копируемыми типами (Copy) и позволяет менять значение напрямую. RefCell<T> поддерживает заимствование во время выполнения и может содержать сложные типы, но требует вызова .borrow() или .borrow_mut().
-
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
Теоретическая часть
- Что такое Pin и как он связан с Future?
- Как работают атрибуты в макросах? Приведите пример.
Практическая часть
- Напишите процедурный макрос #[derive(HasLength)], который добавляет метод length(&self) -> usize к структуре с полем data: String.
Ответы:
-
Pin<P> гарантирует, что объект, на который указывает P, не будет перемещён в памяти. Это важно для self-referential структур и futures, где движок async должен быть уверен, что данные останутся на месте.
-
Атрибуты в макросах используются для добавления информации к элементам кода. Например, #[derive(Debug)] автоматически реализует трейт Debug. Также можно создавать собственные атрибуты в процедурных макросах.
-
// Предполагается, что вы создаёте отдельный 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
Теоретическая часть
- Что такое const fn и какие ограничения на него есть?
- Как происходит работа с сигналами в Rust?
Практическая часть
- Напишите программу, которая ждёт сигнала SIGINT и выводит сообщение перед выходом.
Ответы:
-
const fn — это функция, которую можно вызывать в константных выражениях. Она должна быть pure и не содержать сложных конструкций. С версии Rust 1.56 поддерживает больше возможностей, но всё ещё ограничена.
-
Работа с системными сигналами выполняется через библиотеки вроде nix или signal-hook. Они предоставляют API для регистрации обработчиков, например, на SIGINT или SIGTERM. В async-коде применяется tokio::signal для асинхронного ожидания.
-
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
Теоретическая часть
- Что такое PhantomData и когда он используется?
- Какие существуют способы работы с сетевыми сокетами в Rust?
Практическая часть
- Напишите TCP-сервер, который принимает соединения и отправляет клиенту строку "Hello from server".
Ответы:
-
PhantomData — маркерный тип, используемый для передачи информации компилятору о зависимости, которая не представлена в данных. Часто применяется в unsafe-коде для соблюдения гарантий lifetime.
-
Сетевой ввод-вывод реализуется через модуль std::net (TcpStream, TcpListener, UdpSocket). Для асинхронной работы — tokio::net и async/await. Также поддерживаются low-level socket’ы через libc или crate socket2.
-
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
Теоретическая часть
- Что такое NLL и как он влияет на код?
- Какие существуют инструменты для тестирования производительности в Rust?
Практическая часть
- Напишите бенчмарк, который сравнивает производительность Vec<u32>::with_capacity и Vec<u32>::new.
Ответы:
-
NLL (Non-Lexical Lifetimes) — улучшенный алгоритм анализа времени жизни ссылок. Он расширяет возможности borrow checker, позволяя более точно определять, когда ссылка больше не используется, что повышает гибкость кода.
-
Основные средства: #[bench] (устарел), criterion.rs, divan. Также можно использовать perf из Linux, flamegraph и другие низкоуровневые средства анализа.
-
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 (по желанию)
Сеттинг
Вы — команда инженеров в стартапе, занимающемся разработкой облачного сервиса для обработки пользовательских данных. Недавно ваш сервис начал падать под нагрузкой. Ваша задача — найти проблему, исправить её и оптимизировать работу системы.
Каждый участник получает роль в команде. У всех своя зона ответственности, но успех зависит от совместной работы.
Роли в команде
Примечание: при малом количестве участников роли можно комбинировать.
Этапы игры
Этап 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.
Возможные проблемы и вызовы во время игры
Материалы для игры
- Исходный код сервиса с преднамеренными багами
- Скрипт нагрузки (например, на 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-интерфейсу.
В ходе тестирования система начала выдавать неожиданные значения, иногда — крашиться. Ваша команда должна найти источник проблемы, переписать критические участки кода и обеспечить стабильную работу системы.
Роли в команде
Примечание: при малом количестве участников роли можно объединить.
Этапы игры
Этап 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 коде.
Возможные проблемы и вызовы во время игры
Материалы для игры
- Исходный код модуля с преднамеренными ошибками;
- Тестовый скрипт нагрузки;
- Шаблон технического отчета;
- Лист рефлексии и самооценки после игры.
Ролевая игра №3: "Ошибка в Процедурном Макросе"
Цель игры
Научить студентов:
- Понимать устройство и работу процедурных макросов;
- Анализировать и исправлять ошибки в коде, генерируемом макросами;
- Использовать библиотеки proc-macro2, quote, syn для создания собственных макросов;
- Применять лучшие практики тестирования и отладки макросов.
Формат
- Тип: Ситуационная ролевая игра (Role-playing simulation)
- Длительность: 2–3 академических часа
- Участники: 4–6 человек в группе
- Инструменты: IDE (VS Code/RustRover), Cargo, Rust Analyzer, Miri (по желанию)
Сеттинг
Вы — команда разработчиков, работающих над open-source фреймворком на Rust. Один из ключевых модулей — это набор процедурных макросов, которые автоматически реализуют поведение для структур данных.
Пользователи начали жаловаться, что макросы работают некорректно: не все поля обрабатываются, происходят паники при компиляции, а иногда и вовсе возникают внутренние ошибки компилятора (ICE).
Ваша задача — найти источник проблемы, переписать критические участки кода и восстановить надёжность макросов.
Роли в команде
Примечание: при малом количестве участников роли можно объединить.
Этапы игры
Этап 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 для дебага генерируемого кода.
Возможные проблемы и вызовы во время игры
Материалы для игры
- Исходный код макроса с преднамеренными ошибками;
- Шаблон тестового крейта для проверки;
- Инструкция по использованию cargo expand;
- Лист рефлексии и самооценки после игры.
Ролевая игра №4: "Ошибка в Системе Сертификации"
Цель игры
Научить студентов:
- Разрабатывать безопасные и надёжные системы аутентификации и авторизации;
- Работать с криптографическими примитивами в Rust (хэши, подписи, шифрование);
- Понимать особенности реализации JWT (JSON Web Tokens) на Rust;
- Обнаруживать и исправлять уязвимости безопасности в коде.
Формат
- Тип: Ситуационная ролевая игра (Role-playing simulation)
- Длительность: 2–3 академических часа
- Участники: 4–6 человек в группе
- Инструменты: IDE, Cargo, jsonwebtoken / ring, Git (по желанию)
Сеттинг
Вы — команда разработчиков, работающих над сервисом управления цифровыми сертификатами. Ваша задача — реализовать систему выдачи и проверки JWT для API-авторизации.
Однако после запуска система начала показывать подозрительное поведение: некоторые пользователи могут получить доступ ко всем ресурсам без прав, а другие получают ошибки при входе. Ваша задача — найти проблему, переписать критические участки и восстановить безопасность системы.
Роли в команде
Примечание: при малом количестве участников роли можно объединить.
Этапы игры
Этап 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-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 обеспечивают безопасность системного программирования без использования сборщика мусора.
Полезна для:
- Теоретического обоснования курса;
- Использования в курсах по языкам программирования;
- Исследовательской и научной работы.
- Курс: "Системное программирование на Rust"
Анонс: Изучение низкоуровневой разработки с применением безопасного кода, управления памятью и работы с аппаратными ресурсами без использования сборщика мусора.
- Курс: "Разработка высокопроизводительных сервисов на Rust"
Анонс: Создание производительных, масштабируемых backend-сервисов с использованием async/await, Tokio, HTTP API и оптимизации под нагрузку.
- Курс: "Rust для Embedded-разработки"
Анонс: Применение языка Rust в системах с ограниченными ресурсами: микроконтроллеры, драйверы, bare-metal разработка.
- Курс: "Метапрограммирование и макросы в Rust"
Анонс: Углублённое изучение процедурных макросов, генерации кода, derive-атрибутов и написания собственных расширений языка.
- Курс: "Безопасная параллельность и конкурентность в Rust"
Анонс: Разработка многопоточных приложений с использованием Send/Sync, атомарных типов, каналов и блокировок.
- Курс: "Unsafe Rust: контроль и ответственность"
Анонс: Работа с сырыми указателями, FFI, inline-ассемблером и управление рисками при использовании unsafe-кода.
- Курс: "Интеграция Rust с C/C++"
Анонс: Реализация взаимодействия через FFI, создание биндингов, работа с ABI и обеспечение безопасности при вызовах из других языков.
- Курс: "Компиляция и оптимизация Rust-кода"
Анонс: Исследование процесса компиляции, использование MIR, LLVM, инструментов анализа и оптимизации производительности.
- Курс: "Разработка крейтов и экосистема Rust"
Анонс: Публикация, документирование и тестирование библиотек, управление зависимостями, best practices сообщества.
- Курс: "Тестирование и верификация Rust-программ"
Анонс: Написание unit- и integration-тестов, фаззинг, property-based testing, использование Miri для проверки корректности unsafe-кода.
- Курс: "Rust в DevOps и CI/CD"
Анонс: Использование Rust в автоматизации, создание CLI-утилит, интеграция в pipeline’ы, написание плагинов и утилит.
- Курс: "Криптография и безопасность в Rust"
Анонс: Реализация шифрования, хэширования, цифровых подписей и JWT с акцентом на безопасную реализацию и защиту данных.
- Курс: "Проектирование и разработка библиотек на Rust"
Анонс: Проектирование API, обобщённое программирование, документирование, тестирование и поддержка обратной совместимости.
- Курс: "Продвинутые темы в async/await программировании"
Анонс: Глубокое погружение в futures, executor'ы, работу с tokio, pinning, Wakers и реализацию пользовательских runtime.
- Курс: "Реализация интерфейсов и абстракций в Rust"
Анонс: Техники создания высокоуровневых абстракций поверх low-level кода, реализация трейтов, generics и lifetime-aware API.
- Курс: "Ошибки и обработка исключений в Rust"
Анонс: Сравнение подходов к обработке ошибок, реализация пользовательских типов ошибок, использование anyhow и thiserror.
- Курс: "Жизнь вне стандартной библиотеки: no_std"
Анонс: Разработка кода без std, использование core/alloc, реализация базовых операций в средах с ограниченными ресурсами.
- Курс: "Rust для сетевых приложений"
Анонс: Разработка TCP/UDP серверов, сокетов, протоколов, работа с async I/O, реализация клиент-серверных архитектур.
- Курс: "Формальные методы и верификация в Rust"
Анонс: Использование формальных методов, статического анализа и инструментов вроде Prusti для доказательства корректности кода.
- Курс: "Управление состоянием в многопоточных приложениях"
Анонс: Мьютексы, RWLock, атомарные типы, crossbeam, scoped threads и другие способы безопасного доступа к общему состоянию.
- Курс: "Rust в блокчейн-разработке"
Анонс: Разработка смарт-контрактов, работа с WebAssembly, интеграция с Solana/Substrate, безопасность и производительность.
- Курс: "Программирование ОС на Rust"
Анонс: Создание загрузчика, управления памятью, прерываний и драйверов устройств на языке Rust без зависимости от C.
- Курс: "Rust в области машинного обучения и AI"
Анонс: Использование Rust в вычислениях с низкой задержкой, работа с ONNX, tch-rs, ndarray и polars для обработки данных.
- Курс: "Компиляторы и трансляторы на Rust"
Анонс: Разработка интерпретаторов, компиляторов, анализ AST, генерация кода и оптимизация на примере собственного языка.
- Курс: "Rust для Game Engine Development"
Анонс: Разработка игровых движков на Rust: работа с графикой, физикой, потоками, безопасное взаимодействие с GPU и SDL/Vulkan.
Нет элементов для просмотра