Практическое руководство по Rust. 4/4

от автора

Hello world!

Представляю вашему вниманию четвертую и последнюю часть практического руководства по Rust.

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

Руководство основано на Comprehensive Rust — руководстве по Rust от команды Android в Google и рассчитано на людей, которые уверенно владеют любым современным языком программирования. Еще раз: это руководство не рассчитано на тех, кто только начинает кодить ?

В этой части мы рассмотрим следующие темы:

  • итераторы (iterators): глубокое погружение в трейт Iterator
  • модули и видимость
  • тестирование
  • обработка ошибок: паника, Result и оператор ?
  • небезопасный Rust: случаи, когда безопасного Rust оказывается недостаточно

Материалы для более глубокого изучения названных тем:

Также см. Большую шпаргалку по Rust.

Итераторы

Iterator

Трейт Iterator позволяет перебирать значения коллекции. Он требует реализации метода next и предоставляет большое количество полезных методов. Многие типы стандартной библиотеки реализуют Iterator, и мы также можем его реализовывать на собственных типах:

struct Fibonacci {     curr: u32,     next: u32, }  impl Iterator for Fibonacci {     type Item = u32;      fn next(&mut self) -> Option<Self::Item> {         let new_next = self.curr + self.next;         self.curr = self.next;         self.next = new_next;         Some(self.curr)     } }  fn main() {     let fib = Fibonacci { curr: 0, next: 1 };     for (i, n) in fib.enumerate().take(5) {         println!("fib({i}): {n}");     } }

Ремарки:

  • трейт Iterator реализует много популярных операций функционального программирования над коллекциями (map, filter, reduce и т.д.). В Rust эти функции должны создавать код, столь же эффективный, как и эквивалентные императивные реализации
  • IntoIterator — это трейт, обеспечивающий работу цикла for. Он реализуется типами коллекций, такими как Vec<T>, и ссылками на них, такими как &Vec<T> и &[T]. Диапазоны (ranges) также реализуют этот трейт. Вот почему мы можем перебирать элементы вектора с помощью for i in some_vec { .. }, но some_vec.next() отсутствует

IntoIterator

Трейт Iterator сообщает, как выполнять итерацию после создания итератора. Трейт IntoIterator определяет, как создать итератор для типа. Он автоматически используется циклом for.

struct Grid {     x_coords: Vec<u32>,     y_coords: Vec<u32>, }  impl IntoIterator for Grid {     type Item = (u32, u32);     type IntoIter = GridIter;      fn into_iter(self) -> GridIter {         GridIter { grid: self, i: 0, j: 0 }     } }  struct GridIter {     grid: Grid,     i: usize,     j: usize, }  impl Iterator for GridIter {     type Item = (u32, u32);      fn next(&mut self) -> Option<(u32, u32)> {         if self.i >= self.grid.x_coords.len() {             self.i = 0;             self.j += 1;             if self.j >= self.grid.y_coords.len() {                 return None;             }         }         let res = Some((self.grid.x_coords[self.i], self.grid.y_coords[self.j]));         self.i += 1;         res     } }  fn main() {     let grid = Grid { x_coords: vec![3, 5, 7, 9], y_coords: vec![10, 20, 30, 40] };     for (x, y) in grid {         println!("point = {x}, {y}");     } }

Каждая реализация IntoIterator должна определять 2 типа:

  • Item — перебираемый тип, такой как i8
  • IntoIter — тип Iterator, возвращаемый методом into_iter

Обратите внимание, что IntoIter и Iter связаны: итератор должен иметь такой же тип Item. Это означает, что он должен возвращать Option<Type>.

В примере перебираются все комбинации координат x и y.

Обратите внимание, что IntoIterator::into_iter принимает владение (ownership) над self. Попробуйте дважды перебрать grid в функции main.

Решите эту проблему путем реализации IntoIterator для &Grid и сохранения ссылки на Grid в GridIter.

Аналогичная проблема может возникнуть при использовании стандартных типов: for e in some_vec принимает владение над some_vec и перебирает собственные элементы вектора. Для перебора ссылок на элементы вектора следует использовать for e in &some_vec.

FromIterator

Трейт FromIterator позволяет создавать коллекции из Iterator:

fn main() {     let primes = vec![2, 3, 5, 7];     let prime_squares = primes.into_iter().map(|p| p * p).collect::<Vec<_>>();     println!("prime_squares: {prime_squares:?}"); }

Iterator реализует

fn collect<B>(self) -> B where     B: FromIterator<Self::Item>,     Self: Sized

Существует 2 способа определить B для этого метода:

  • с помощью turbofish: some_iterator.collect::<COLLECTION_TYPE>(), как показано в примере. Сокращение _ позволяет Rust вывести тип элементов вектора самостоятельно
  • с помощью вывода типов: let prime_squares: Vec<_> = some_iterator.collect()

Базовые реализации IntoIterator существуют для Vec, HashMap и некоторых других типов. Существуют также более специализированные реализации, позволяющие делать клевые вещи, вроде преобразования Iterator<Item = Result<V, E>> в Result<Vec<V>, E>

Упражнение: цепочка методов итератора

В этом упражнении вам нужно найти и использовать некоторые методы трейта Iterator для реализации сложных вычислений.

Используйте выражение итератора и соберите (collect) результат для построения возвращаемого значения.

// Функция для вычисления разницы между элементами `values`, смещенными на `offset`. // `values` перебираются по кругу. // // Элемент `n` результата - `values[(n+offset)%len] - values[n]`. fn offset_differences<N>(offset: usize, values: Vec<N>) -> Vec<N> where     N: Copy + std::ops::Sub<Output = N>, {     todo!("реализуй меня") }  fn main() {     let res = offset_differences(1, vec![1, 3, 5, 7]);     println!("{:?}", res); }  #[cfg(test)] mod tests {     use super::*;      #[test]     fn test_offset_one() {         assert_eq!(offset_differences(1, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);         assert_eq!(offset_differences(1, vec![1, 3, 5]), vec![2, 2, -4]);         assert_eq!(offset_differences(1, vec![1, 3]), vec![2, -2]);     }      #[test]     fn test_larger_offsets() {         assert_eq!(offset_differences(2, vec![1, 3, 5, 7]), vec![4, 4, -4, -4]);         assert_eq!(offset_differences(3, vec![1, 3, 5, 7]), vec![6, -2, -2, -2]);         assert_eq!(offset_differences(4, vec![1, 3, 5, 7]), vec![0, 0, 0, 0]);         assert_eq!(offset_differences(5, vec![1, 3, 5, 7]), vec![2, 2, 2, -6]);     }      #[test]     fn test_custom_type() {         assert_eq!(             offset_differences(1, vec![1.0, 11.0, 5.0, 0.0]),             vec![10.0, -6.0, -5.0, 1.0]         );     }      #[test]     fn test_degenerate_cases() {         assert_eq!(offset_differences(1, vec![0]), vec![0]);         assert_eq!(offset_differences(1, vec![1]), vec![0]);         let empty: Vec<i32> = vec![];         assert_eq!(offset_differences(1, empty), vec![]);     } }

Решение

fn offset_differences<N>(offset: usize, values: Vec<N>) -> Vec<N> where     N: Copy + std::ops::Sub<Output = N>, {     let a = (&values).into_iter();     let b = (&values).into_iter().cycle().skip(offset);     a.zip(b).map(|(a, b)| *b - *a).take(values.len()).collect() }

Модули

Модули

Мы видели, как блоки impl позволяют нам использовать функции пространства имен (namespace) для типа.

Аналогично, mod позволяет нам использовать типы и функции пространства имен:

mod foo {     pub fn do_something() {         println!("в модуле foo");     } }  mod bar {     pub fn do_something() {         println!("в модуле bar");     } }  fn main() {     foo::do_something();     bar::do_something(); }

Ремарки:

  • пакеты (packages) предоставляют функционал и включают файл Cargo.toml, описывающий сборку из нескольких крейтов
  • крейты (crates) — это дерево модулей, где бинарный крейт является исполняемым файлом, а библиотечный крейт компилируется в библиотеку
  • модули определяют организацию и область видимости кода

Иерархия файловой системы

Если опустить содержимое модуля, Rust будет искать его в другом файле:

mod garden;

Это сообщает Rust, что содержимое модуля Garden находится по адресу src/garden.rs. Аналогично, модуль Garden::vegetables следует искать по адресу src/garden/vegetables.rs.

Корневой crate находится в:

  • src/lib.rs (для библиотечного крейта)
  • src/main.rs (для бинарного крейта)

Модули, определенные в файлах, можно документировать с помощью «внутренних комментариев документа». Они документируют элемент, который их содержит — в данном случае модуль.

//! This module implements the garden, including a highly performant germination //! implementation.  // Re-export types from this module. pub use garden::Garden; pub use seeds::SeedPacket;  /// Sow the given seed packets. pub fn sow(seeds: Vec<SeedPacket>) {     todo!() }  /// Harvest the produce in the garden that is ready. pub fn harvest(garden: &mut Garden) {     todo!() }

Ремарки:

  • до Rust 2018 модули должны были находиться в module/mod.rs вместо module.rs, и это по-прежнему работает
  • основная причина представления filename.rs в качестве альтернативы filename/mod.rs заключается в том, что при большом количестве файлов mod.rs становится сложно в них разбираться
  • при более глубокой вложенности можно использовать директории, даже если основной модуль является файлом:

src/ ├── main.rs ├── top_module.rs └── top_module/     └── sub_module.rs

  • место поиска модулей может быть изменено с помощью директивы компилятора:

#[path = "some/path.rs"] mod some_module;

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

Видимость

Модули являются приватными/закрытыми:

  • элементы модулей являются приватными по умолчанию (скрывают детали своей реализации)
  • родители и сиблинги всегда являются видимыми (для элементов модулей)
  • если элемент видим в модуле foo, он видим всем потомкам foo

mod outer {     fn private() {         println!("outer::private");     }      pub fn public() {         println!("outer::public");     }      mod inner {         fn private() {             println!("outer::inner::private");         }          pub fn public() {             println!("outer::inner::public");             super::private();         }     } }  fn main() {     outer::public(); }

Ремарки:

  • для того, чтобы сделать модуль публичным/открытым, используется ключевое слово pub
  • существуют продвинутые спецификаторы pub, позволяющие ограничивать область публичной видимости

use, super, self

Модуль может импортировать элементы другого модуля в свою область видимости с помощью ключевого слова use. В начале каждого модуля можно увидеть что-то вроде этого:

use std::collections::HashSet; use std::process::abort;

Пути

Путь (path) разрешается следующим образом:

  1. Как относительный путь:
    • foo или self::foo ссылается на foo в текущем модуле
    • super::foo ссылается на foo в родительском модуле
  2. Как абсолютный путь:
    • crate:foo ссылается на foo в корне текущего крейта
    • bar::foo ссылается на foo в крейте bar

Ремарки:

  • распространенной практикой является повторный экспорт элементов модулей. Например, корневой файл lib.rs может содержать:

mod storage;  pub use storage::disk::DiskStorage; pub use storage::network::NetworkStorage;

Это сделает DiskStorage и NetworkStorage доступными другим крейтам по короткому пути.

  • в основном, необходимо use (использовать) только элементы, которые используются в модуле. Однако для того, чтобы вызывать методы трейта, он должен находиться в области видимости, даже если тип, реализующий этот трейт, уже находится в ней. Например, чтобы использовать метод read_to_string для типа, реализующего трейт Read, необходимо use std::io::Read
  • в операторе use может использоваться подстановочный знак: use std::io::*. Делать так не рекомендуется, поскольку неясно, какие элементы импортируются, и эти элементы могут измениться со временем

Упражнение: модули для библиотеки пользовательского интерфейса

В этом упражнении вы реорганизуете код «Библиотеки графического интерфейса» из раздела «Методы и трейты» в набор модулей. Обычно каждый тип или набор тесно связанных типов помещают в отдельный модуль, поэтому каждый тип виджета должен иметь свой собственный модуль.

Код:

pub trait Widget {     fn width(&self) -> usize;      fn draw_into(&self, buffer: &mut dyn std::fmt::Write);      fn draw(&self) {         let mut buffer = String::new();         self.draw_into(&mut buffer);         println!("{buffer}");     } }  pub struct Label {     label: String, }  impl Label {     fn new(label: &str) -> Label {         Label { label: label.to_owned() }     } }  pub struct Button {     label: Label, }  impl Button {     fn new(label: &str) -> Button {         Button { label: Label::new(label) }     } }  pub struct Window {     title: String,     widgets: Vec<Box<dyn Widget>>, }  impl Window {     fn new(title: &str) -> Window {         Window { title: title.to_owned(), widgets: Vec::new() }     }      fn add_widget(&mut self, widget: Box<dyn Widget>) {         self.widgets.push(widget);     }      fn inner_width(&self) -> usize {         std::cmp::max(             self.title.chars().count(),             self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),         )     } }  impl Widget for Window {     fn width(&self) -> usize {         self.inner_width() + 4     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         let mut inner = String::new();         for widget in &self.widgets {             widget.draw_into(&mut inner);         }          let inner_width = self.inner_width();          writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();         writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();         writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();         for line in inner.lines() {             writeln!(buffer, "| {:inner_width$} |", line).unwrap();         }         writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();     } }  impl Widget for Button {     fn width(&self) -> usize {         self.label.width() + 4     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         let width = self.width();         let mut label = String::new();         self.label.draw_into(&mut label);          writeln!(buffer, "+{:-<width$}+", "").unwrap();         for line in label.lines() {             writeln!(buffer, "|{:^width$}|", &line).unwrap();         }         writeln!(buffer, "+{:-<width$}+", "").unwrap();     } }  impl Widget for Label {     fn width(&self) -> usize {         self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         writeln!(buffer, "{}", &self.label).unwrap();     } }  fn main() {     let mut window = Window::new("Rust GUI Demo 1.23");     window.add_widget(Box::new(Label::new("This is a small text GUI demo.")));     window.add_widget(Box::new(Button::new("Click me!")));     window.draw(); }

Упражнение можно начать с выполнения следующих команд:

cargo init gui-modules cd gui-modules cargo run

Отредактируйте файл src/main.rs, добавив в него инструкции mod, и создайте необходимые файлы в директории src.

Решение

src ├── main.rs ├── widgets │   ├── button.rs │   ├── label.rs │   └── window.rs └── widgets.rs

// src/widgets.rs mod button; mod label; mod window;  pub trait Widget {     fn width(&self) -> usize;      fn draw_into(&self, buffer: &mut dyn std::fmt::Write);      fn draw(&self) {         let mut buffer = String::new();         self.draw_into(&mut buffer);         println!("{buffer}");     } }  pub use button::Button; pub use label::Label; pub use window::Window;

// src/widgets/label.rs use super::Widget;  pub struct Label {     label: String, }  impl Label {     pub fn new(label: &str) -> Label {         Label { label: label.to_owned() }     } }  impl Widget for Label {     fn width(&self) -> usize {         self.label.lines().map(|line| line.chars().count()).max().unwrap_or(0)     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         writeln!(buffer, "{}", &self.label).unwrap();     } }

// src/widgets/button.rs use super::{Label, Widget};  pub struct Button {     label: Label, }  impl Button {     pub fn new(label: &str) -> Button {         Button { label: Label::new(label) }     } }  impl Widget for Button {     fn width(&self) -> usize {         self.label.width() + 4     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         let width = self.width();         let mut label = String::new();         self.label.draw_into(&mut label);          writeln!(buffer, "+{:-<width$}+", "").unwrap();         for line in label.lines() {             writeln!(buffer, "|{:^width$}|", &line).unwrap();         }         writeln!(buffer, "+{:-<width$}+", "").unwrap();     } }

// src/widgets/window.rs use super::Widget;  pub struct Window {     title: String,     widgets: Vec<Box<dyn Widget>>, }  impl Window {     pub fn new(title: &str) -> Window {         Window { title: title.to_owned(), widgets: Vec::new() }     }      pub fn add_widget(&mut self, widget: Box<dyn Widget>) {         self.widgets.push(widget);     }      fn inner_width(&self) -> usize {         std::cmp::max(             self.title.chars().count(),             self.widgets.iter().map(|w| w.width()).max().unwrap_or(0),         )     } }  impl Widget for Window {     fn width(&self) -> usize {         self.inner_width() + 4     }      fn draw_into(&self, buffer: &mut dyn std::fmt::Write) {         let mut inner = String::new();         for widget in &self.widgets {             widget.draw_into(&mut inner);         }          let inner_width = self.inner_width();          writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();         writeln!(buffer, "| {:^inner_width$} |", &self.title).unwrap();         writeln!(buffer, "+={:=<inner_width$}=+", "").unwrap();         for line in inner.lines() {             writeln!(buffer, "| {:inner_width$} |", line).unwrap();         }         writeln!(buffer, "+-{:-<inner_width$}-+", "").unwrap();     } }

// src/main.rs mod widgets;  use widgets::Widget;  fn main() {     let mut window = widgets::Window::new("Rust GUI Demo 1.23");     window         .add_widget(Box::new(widgets::Label::new("This is a small text GUI demo.")));     window.add_widget(Box::new(widgets::Button::new("Click me!")));     window.draw(); }

Тестирование

Модульные/юнит-тесты

Rust и Cargo поставляются с фреймворком для тестирования:

  • модульные (unit) тесты поддерживаются прямо в коде, который мы пишем
  • интеграционные (integration) тесты поддерживаются через директорию tests

Тесты помечаются с помощью директивы #[test]. Юнит-тесты часто помещаются во вложенный модуль tests. Директива #[cfg(test)] сообщает компилятору, что содержащийся далее код следует компилировать только при запуске тестов:

fn first_word(text: &str) -> &str {     match text.find(' ') {         Some(idx) => &text[..idx],         None => &text,     } }  #[cfg(test)] mod test {     use super::*;      #[test]     fn test_empty() {         assert_eq!(first_word(""), "");     }      #[test]     fn test_single_word() {         assert_eq!(first_word("Hello"), "Hello");     }      #[test]     fn test_multiple_words() {         assert_eq!(first_word("Hello World"), "Hello");     } }

  • Модульные тесты позволяют тестировать приватный функционал
  • атрибут #[cfg(test)] активируется только при выполнении cargo test

Другие типы тестов

Интеграционные тесты

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

Создаем файл .rs в директории tests/:

// tests/my_library.rs use my_library::init;  #[test] fn test_init() {     assert!(init().is_ok()); }

Эти тесты имеют доступ только к публичному API тестируемого крейта.

Документационные тесты

Rust имеет встроенную поддержку документационных тестов:

/// Сокращает строку до указанной длины. /// /// ``` /// # use playground::shorten_string; /// assert_eq!(shorten_string("Hello World", 5), "Hello"); /// assert_eq!(shorten_string("Hello World", 20), "Hello World"); /// ``` pub fn shorten_string(s: &str, length: usize) -> &str {     &s[..std::cmp::min(length, s.len())] } 

  • Блоки кода в комментариях /// считаются валидным кодом Rust (разумеется, если код компилируется)
  • код будет скомпилирован и выполнен как часть cargo test
  • добавление # в код скроет его из документации, но он по-прежнему будет компилироваться/выполняться

Полезные крейты

Rust предоставляет лишь базовую поддержку тестов.

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

  • googletest — комплексная библиотека тестирования в лучших традициях GoogleTest для C++
  • proptest — библиотека тестирования на основе свойств (properties)
  • rstest — библиотека тестирования, поддерживающая фикстуры (fixtures) и параметризованные тесты

GoogleTest

Крейт googletest позволяет создавать гибкие тесты с использованием сопоставителей (matchers):

use googletest::prelude::*;  #[googletest::test] fn test_elements_are() {     let value = vec!["foo", "bar", "baz"];     expect_that!(value, elements_are!(eq("foo"), lt("xyz"), starts_with("b"))); }

Если мы изменим b на ! в последнем элементе, тест провалится с выдачей структурированного сообщения об ошибке:

---- test_elements_are stdout ---- Value of: value Expected: has elements:   0. is equal to "foo"   1. is less than "xyz"   2. starts with prefix "!" Actual: ["foo", "bar", "baz"],   where element #2 is "baz", which does not start with "!"   at src/testing/googletest.rs:6:5 Error: See failure output above

Ремарки:

  • выполните cargo add googletest для установки googletest
  • use googletest::prelude::*; импортирует некоторые часто используемые макросы и типы
  • googletest предоставляет большое количество сопоставителей
  • приятной особенностью googletest является то, что несоответствия в многострочных строках отображаются в виде разницы:

#[test] fn test_multiline_string_diff() {     let haiku = "Memory safety found,\n\                  Rust's strong typing guides the way,\n\                  Secure code you'll write.";     assert_that!(         haiku,         eq("Memory safety found,\n\             Rust's silly humor guides the way,\n\             Secure code you'll write.")     ); }

Вывод будет цветным:

    Value of: haiku Expected: is equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure code you'll write." Actual: "Memory safety found,\nRust's strong typing guides the way,\nSecure code you'll write.",   which isn't equal to "Memory safety found,\nRust's silly humor guides the way,\nSecure code you'll write." Difference(-actual / +expected):  Memory safety found, -Rust's strong typing guides the way, +Rust's silly humor guides the way,  Secure code you'll write.   at src/testing/googletest.rs:17:5=

Мокинг

Для мокинга (mocking — создание макета) широко используется библиотека mockall:

use std::time::Duration;  #[mockall::automock] pub trait Pet {     fn is_hungry(&self, since_last_meal: Duration) -> bool; }  #[test] fn test_robot_dog() {     let mut mock_dog = MockPet::new();     mock_dog.expect_is_hungry().return_const(true);     assert_eq!(mock_dog.is_hungry(Duration::from_secs(10)), true); }

Ремарки:

  • для установки mockall выполните команду cargo add mockall
  • на crates.io доступны и другие библиотеки для мокинга, в частности, для мокинга HTTP-сервисов. Другие библиотеки работают аналогично Mockall: они позволяют легко получить макет реализации определенного трейта
  • обратите внимание, что использование макетов несколько противоречиво: макеты позволяют полностью изолировать тест от его зависимостей. Непосредственным результатом является более быстрое и стабильное выполнение тестов. С другой стороны, макеты могут быть настроены неправильно и возвращать данные, отличные от того, что делали бы реальные зависимости. По-возможности следует использовать реальные зависимости. Например, многие базы данных позволяют настроить серверную часть в памяти (in-memory backend). Это означает, что мы получаем правильное поведение тестов, плюс они работают быстро и автоматически очищаются. Многие веб-фреймворки позволяют запускать внутрипроцессный сервер, который привязывается к произвольному порту на локальном хосте. Этот подход является более предпочтительным, чем мокинг, поскольку позволяет тестировать код в реальной среде
  • Mockall предоставляет много полезных функций. В частности, мы можем настроить ожидания (expectations), которые зависят от переданных аргументов. Здесь мы используем это, чтобы создать макет кошки, которая проголодалась через 3 часа после того, как ее в последний раз покормили:

#[test] fn test_robot_cat() {     let mut mock_cat = MockPet::new();     mock_cat         .expect_is_hungry()         .with(mockall::predicate::gt(Duration::from_secs(3 * 3600)))         .return_const(true);     mock_cat.expect_is_hungry().return_const(false);     assert_eq!(mock_cat.is_hungry(Duration::from_secs(1 * 3600)), false);     assert_eq!(mock_cat.is_hungry(Duration::from_secs(5 * 3600)), true); }

  • мы можем использовать .times(n), чтобы ограничить количество вызовов фиктивного метода до n — при превышении этого лимита программа запаникует

Линтер и Clippy

Компилятор Rust выдает фантастические сообщения об ошибках, а также полезные подсказки (lints). Clippy предоставляет еще больше подсказок, организованных в группы, которые можно включать/выключать для каждого проекта.

#[deny(clippy::cast_possible_truncation)] fn main() {     let x = 3;     while (x < 70000) {         x *= 2;     }     println!("X помещается в u16, верно? {}", x as u16); }

Упражнение: алгоритм Луна

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

  • игнорируем все пробелы
  • отклоняем номера, содержащие менее двух цифр
  • двигаясь справа налево, удваиваем каждую вторую цифру: для числа 1234 удваиваем 3 и 1, для числа 98765 удваиваем 6 и 8
  • после удвоения цифры суммируем цифры, если результат больше 9. Таким образом, удвоение 7 дает 14, что дает 1 + 4 = 5
  • суммируем все неудвоенные и удвоенные цифры
  • номер кредитной карты действителен, если сумма заканчивается на 0

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

pub fn luhn(cc_number: &str) -> bool {     let mut sum = 0;     let mut double = false;      for c in cc_number.chars().rev() {         if let Some(digit) = c.to_digit(10) {             if double {                 let double_digit = digit * 2;                 sum +=                     if double_digit > 9 { double_digit - 9 } else { double_digit };             } else {                 sum += digit;             }             double = !double;         } else {             continue;         }     }      sum % 10 == 0 }  fn main() {     let cc_number = "1234 5678 1234 5670";     println!(         "{cc_number} является действительным номером кредитной карты? {}",         if luhn(cc_number) { "Да" } else { "Нет" }     ); }  #[cfg(test)] mod test {     use super::*;      #[test]     fn test_valid_cc_number() {         assert!(luhn("4263 9826 4026 9299"));         assert!(luhn("4539 3195 0343 6467"));         assert!(luhn("7992 7398 713"));     }      #[test]     fn test_invalid_cc_number() {         assert!(!luhn("4223 9826 4026 9299"));         assert!(!luhn("4539 3195 0343 6476"));         assert!(!luhn("8273 1232 7352 0569"));     }      #[test]     fn test_non_digit_cc_number() {         assert!(!luhn("foo"));         assert!(!luhn("foo 0 0"));     }      #[test]     fn test_empty_cc_number() {         assert!(!luhn(""));         assert!(!luhn(" "));         assert!(!luhn("  "));         assert!(!luhn("    "));     }      #[test]     fn test_single_digit_cc_number() {         assert!(!luhn("0"));     }      #[test]     fn test_two_digit_cc_number() {         assert!(luhn(" 0 0 "));     } }

Решение

pub fn luhn(cc_number: &str) -> bool {     // Итоговая сумма цифр     let mut sum = 0;     // Индикатор необходимости удвоения цифры     let mut double = false;     // Количество цифр     let mut digits = 0;      // Перебираем цифры справа налево     for c in cc_number.chars().rev() {         // Если символ является валидным десятичным числом         if let Some(digit) = c.to_digit(10) {             // Увеличиваем количество цифр             digits += 1;             // Если цифру нужно удвоить             if double {                 let double_digit = digit * 2;                 // Если удвоенная цифра больше 9, вычитаем из нее 9:                 // если получили 14, то 1 + 4 = 5, что эквивалентно 14 - 9 = 5                 sum +=                     if double_digit > 9 { double_digit - 9 } else { double_digit };             // Иначе просто добавляем цифру к сумме             } else {                 sum += digit;             }             // Удваиваем каждую вторую цифру             double = !double;         // Игнорируем пробелы         } else if c.is_whitespace() {             continue;         // Если строка содержит символ, отличающийся от цифры и пробела         } else {             return false;         }     }      // Цифр должно быть больше двух и сумма должна заканчиваться на 0     digits >= 2 && sum % 10 == 0 }

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

Паника

Фатальные ошибки обрабатываются в Rust с помощью «паники» (panic).

Паника происходит при возникновении фатальной ошибки во время выполнения:

fn main() {     let v = vec![10, 20, 30];     println!("v[100]: {}", v[100]); }

Ремарки:

  • паника связана с неисправимыми и неожиданными ошибками:
    • паника — это симптомы ошибок в программе
    • сбои во время выполнения, такие как неудачная проверка границ (boundaries), могут вызвать панику
    • утверждения (например, assert!) паникуют при неудаче
    • для целенаправленной паники можно использовать макрос panic!
  • паника «разматывает» (unwind) стек, сбрасывая значения так же, как если бы функции вернули значения
  • в примере для безопасного доступа к элементу вектора по индексу можно использовать Vec::get

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

use std::panic;  fn main() {     let result = panic::catch_unwind(|| "No problem here!");     println!("{result:?}");      let result = panic::catch_unwind(|| {         panic!("Oh no!");     });     println!("{result:?}"); }

  • Не пытайтесь реализовать исключения с помощью catch_unwind
  • это может быть полезно на серверах, которые должны продолжать работать даже в случае сбоя одного запроса
  • это не работает при установке panic = 'abort' в Cargo.toml

Оператор ?

Ошибки времени выполнения, такие как отказ в соединении или отсутствие файла, обрабатываются с помощью типа Result, но сопоставление (matching) этого типа при каждом вызове может быть утомительным и излишним. Оператор ? используется для возврата ошибок вызывающему (caller). Он позволяет заменить

match some_expression {     Ok(value) => value,     Err(err) => return Err(err), }

на

some_expression?

Попробуйте упростить обработку ошибок в следующем коде:

use std::io::Read; use std::{fs, io};  fn read_username(path: &str) -> Result<String, io::Error> {     let username_file_result = fs::File::open(path);     let mut username_file = match username_file_result {         Ok(file) => file,         Err(err) => return Err(err),     };      let mut username = String::new();     match username_file.read_to_string(&mut username) {         Ok(_) => Ok(username),         Err(err) => Err(err),     } }  fn main() {     // fs::write("config.dat", "alice").unwrap();     let username = read_username("config.dat");     println!("username or error: {username:?}"); }

Подсказки:

  • переменная username может быть либо Ok(String), либо Err(error)
  • используйте fs::write для тестирования разных случаев: отсутствие файла, пустой файл, файл с именем пользователя
  • обратите внимание, что main может возвращать Result<(), E> до тех пор, пока реализует std::process:Termination. На практике это означает, что E реализует Debug. Исполняемый файл напечатает вариант Err и вернет ненулевой статус выхода в случае ошибки

Преобразования Try

Оператор ? работает немного сложнее, чем можно подумать.

Это:

expression?

Эквивалентно этому:

match expression {     Ok(value) => value,     Err(err)  => return Err(From::from(err)), }

Вызов From::from здесь означает, что мы пытаемся преобразовать тип ошибки в тип, возвращаемый функцией. Это позволяет легко преобразовать локальные ошибки в ошибки более высокого уровня.

Пример

use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::fs::File; use std::io::{self, Read};  #[derive(Debug)] enum ReadUsernameError {     IoError(io::Error),     EmptyUsername(String), }  impl Error for ReadUsernameError {}  impl Display for ReadUsernameError {     fn fmt(&self, f: &mut Formatter) -> fmt::Result {         match self {             Self::IoError(e) => write!(f, "Ошибка ввода/вывода: {e}"),             Self::EmptyUsername(path) => write!(f, "Имя пользователя отсутствует в {path}"),         }     } }  impl From<io::Error> for ReadUsernameError {     fn from(err: io::Error) -> Self {         Self::IoError(err)     } }  fn read_username(path: &str) -> Result<String, ReadUsernameError> {     let mut username = String::with_capacity(100);     File::open(path)?.read_to_string(&mut username)?;     if username.is_empty() {         return Err(ReadUsernameError::EmptyUsername(String::from(path)));     }     Ok(username) }  fn main() {     // fs::write("config.dat", "").unwrap();     let username = read_username("config.dat");     println!("Имя пользователя или ошибка: {username:?}"); }

Оператор ? должен возвращать значение, совместимое с типом значения, возвращаемого функцией. Для Result это означает, что типы ошибок должны быть совместимыми. Функция, возвращающая Result<T, ErrorOuter>, может использовать ? только для значения типа Result<U, ErrorInner>, если ErrorOuter и ErrorInner имеют один и тот же тип, или если ErrorOuter реализует From<ErrorInner>.

Распространенной альтернативой реализации From является Result::map_err, особенно когда преобразование происходит только в одном месте.

Для Option нет требований совместимости. Функция, возвращающая Option<T>, может использовать оператор ? на Option<U> для произвольных типов T и U.

Функция, возвращающая Result, не может использовать ? на Option, и наоборот. Однако, Option::ok_or преобразует Option в Result, а Result::okResult в Option.

Динамические типы ошибок

Иногда мы хотим возвращать любой тип ошибки без создания перечисления, охватывающего все варианты. Трейт std::error::Error позволяет легко создать трейт-объект, который может содержать любую ошибку:

use std::error::Error; use std::fs; use std::io::Read;  fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {     let mut count_str = String::new();     fs::File::open(path)?.read_to_string(&mut count_str)?;     let count: i32 = count_str.parse()?;     Ok(count) }  fn main() {     fs::write("count.dat", "1i3").unwrap();     match read_count("count.dat") {         Ok(count) => println!("Содержимое: {count}"),         Err(err) => println!("Ошибка: {err}"),     } }

Функция read_count может возвращать std::io::Error (из операций с файлом) или std::num::ParseIntError (из String::parse).

Использование динамических (boxing) ошибок сокращает количество кода, но лишает возможности по-разному обрабатывать разные ошибки. Поэтому использовать Box<dyn Error> в общедоступном API библиотеки не рекомендуется, но это может быть хорошим вариантом, когда мы просто хотим где-то отображать сообщение об ошибке.

При создании кастомных типов ошибок убедитесь, что они реализуют std::error::Error, чтобы их можно было оборачивать в Box.

thiserror и anyhow

Крейты thiserror и anyhow широко используются для упрощения обработки ошибок. thiserror помогает создавать кастомные типы ошибок, реализующие From<T>. anyhow помогает с обработкой ошибок в функциях, включая добавление контекстуальной информации в ошибки.

use anyhow::{bail, Context, Result}; use std::fs; use std::io::Read; use thiserror::Error;  #[derive(Clone, Debug, Eq, Error, PartialEq)] #[error("Имя пользователя отсутствует в {0}")] struct EmptyUsernameError(String);  fn read_username(path: &str) -> Result<String> {     let mut username = String::with_capacity(100);     fs::File::open(path)         .with_context(|| format!("Ошибка при открытии {path}"))?         .read_to_string(&mut username)         .context("Ошибка при чтении")?;     if username.is_empty() {         bail!(EmptyUsernameError(path.to_string()));     }     Ok(username) }  fn main() {     // fs::write("config.dat", "").unwrap();     match read_username("config.dat") {         Ok(username) => println!("Имя пользователя: {username}"),         Err(err) => println!("Ошибка: {err:?}"),     } }

thiserror:

  • макрос error предоставляется thiserror и содержит большое количество атрибутов для лаконичного определения типов ошибок
  • трейт std::error::Error реализуется автоматически
  • сообщение из #[error] используется для автоматической реализации трейта Display

anyhow:

  • anyhow::Error — это обертка над Box<dyn Error>. Опять же это не лучший выбор для общедоступного API библиотеки, но он широко используется в приложениях
  • anyhow::Result<V> — это синоним типа Result<V, anyhow::Error>
  • при необходимости фактический тип ошибки внутри него можно извлечь для проверки
  • anyhow::Context — это трейт, реализованный для стандартных типов Result и Option. use anyhow::Context необходим для включения .context() и .with_context() на этих типах

Упражнение: без паники

Следующий код реализует очень простой синтаксический анализатор языка выражений. Однако он обрабатывает ошибки путем паники. Перепишите его, чтобы вместо этого использовать идиоматическую обработку ошибок и распространять ошибки на возврат из main. Не стесняйтесь использовать thiserror и anyhow.

Подсказка: начните исправлять ошибки в функции parse. После того, как она заработает, обновите Tokenizer для реализации Iterator<Item=Result<Token, TokenizerError>> и обработайте его в парсере.

use std::iter::Peekable; use std::str::Chars;  // Арифметический оператор #[derive(Debug, PartialEq, Clone, Copy)] enum Op {     Add,     Sub, }  // Токен языка #[derive(Debug, PartialEq)] enum Token {     Number(String),     Identifier(String),     Operator(Op), }  // Выражение языка #[derive(Debug, PartialEq)] enum Expression {     // Ссылка на переменную     Var(String),     // Литеральное число     Number(u32),     // Бинарная операция     Operation(Box<Expression>, Op, Box<Expression>), }  fn tokenize(input: &str) -> Tokenizer {     return Tokenizer(input.chars().peekable()); }  struct Tokenizer<'a>(Peekable<Chars<'a>>);  impl<'a> Iterator for Tokenizer<'a> {     type Item = Token;      fn next(&mut self) -> Option<Token> {         let c = self.0.next()?;         match c {             '0'..='9' => {                 let mut num = String::from(c);                 while let Some(c @ '0'..='9') = self.0.peek() {                     num.push(*c);                     self.0.next();                 }                 Some(Token::Number(num))             }             'a'..='z' => {                 let mut ident = String::from(c);                 while let Some(c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() {                     ident.push(*c);                     self.0.next();                 }                 Some(Token::Identifier(ident))             }             '+' => Some(Token::Operator(Op::Add)),             '-' => Some(Token::Operator(Op::Sub)),             _ => panic!("Неожиданный символ {c}"),         }     } }  fn parse(input: &str) -> Expression {     let mut tokens = tokenize(input);      fn parse_expr<'a>(tokens: &mut Tokenizer<'a>) -> Expression {         let Some(tok) = tokens.next() else {             panic!("Неожиданный конец ввода");         };         let expr = match tok {             Token::Number(num) => {                 let v = num.parse().expect("Невалидное 32-битное целое число");                 Expression::Number(v)             }             Token::Identifier(ident) => Expression::Var(ident),             Token::Operator(_) => panic!("Неожиданный токен {tok:?}"),         };         // Проверяем наличие бинарной операции         match tokens.next() {             None => expr,             Some(Token::Operator(op)) => Expression::Operation(                 Box::new(expr),                 op,                 Box::new(parse_expr(tokens)),             ),             Some(tok) => panic!("Неожиданный токен {tok:?}"),         }     }      parse_expr(&mut tokens) }  fn main() {     let expr = parse("10+foo+20-30");     println!("{expr:?}"); }

Решение

use thiserror::Error; use std::iter::Peekable; use std::str::Chars;  #[derive(Debug, PartialEq, Clone, Copy)] enum Op {     Add,     Sub, }  #[derive(Debug, PartialEq)] enum Token {     Number(String),     Identifier(String),     Operator(Op), }  #[derive(Debug, PartialEq)] enum Expression {     Var(String),     Number(u32),     Operation(Box<Expression>, Op, Box<Expression>), }  fn tokenize(input: &str) -> Tokenizer {     return Tokenizer(input.chars().peekable()); }  #[derive(Debug, Error)] enum TokenizerError {     #[error("Неожиданный символ {0}")]     UnexpectedCharacter(char), }  struct Tokenizer<'a>(Peekable<Chars<'a>>);  impl<'a> Iterator for Tokenizer<'a> {     type Item = Result<Token, TokenizerError>;      fn next(&mut self) -> Option<Result<Token, TokenizerError>> {         let c = self.0.next()?;         match c {             '0'..='9' => {                 let mut num = String::from(c);                 while let Some(c @ '0'..='9') = self.0.peek() {                     num.push(*c);                     self.0.next();                 }                 Some(Ok(Token::Number(num)))             }             'a'..='z' => {                 let mut ident = String::from(c);                 while let Some(c @ ('a'..='z' | '_' | '0'..='9')) = self.0.peek() {                     ident.push(*c);                     self.0.next();                 }                 Some(Ok(Token::Identifier(ident)))             }             '+' => Some(Ok(Token::Operator(Op::Add))),             '-' => Some(Ok(Token::Operator(Op::Sub))),             _ => Some(Err(TokenizerError::UnexpectedCharacter(c))),         }     } }  #[derive(Debug, Error)] enum ParserError {     #[error("Ошибка токенизатора: {0}")]     TokenizerError(#[from] TokenizerError),     #[error("Неожиданный конец ввода")]     UnexpectedEOF,     #[error("Неожиданный токен {0:?}")]     UnexpectedToken(Token),     #[error("Невалидное число")]     InvalidNumber(#[from] std::num::ParseIntError), }  fn parse(input: &str) -> Result<Expression, ParserError> {     let mut tokens = tokenize(input);      fn parse_expr<'a>(         tokens: &mut Tokenizer<'a>,     ) -> Result<Expression, ParserError> {         let tok = tokens.next().ok_or(ParserError::UnexpectedEOF)??;         let expr = match tok {             Token::Number(num) => {                 let v = num.parse()?;                 Expression::Number(v)             }             Token::Identifier(ident) => Expression::Var(ident),             Token::Operator(_) => return Err(ParserError::UnexpectedToken(tok)),         };          Ok(match tokens.next() {             None => expr,             Some(Ok(Token::Operator(op))) => Expression::Operation(                 Box::new(expr),                 op,                 Box::new(parse_expr(tokens)?),             ),             Some(Err(e)) => return Err(e.into()),             Some(Ok(tok)) => return Err(ParserError::UnexpectedToken(tok)),         })     }      parse_expr(&mut tokens) }  fn main() -> anyhow::Result<()> {     let expr = parse("10+foo+20-30")?;     println!("{expr:?}");     Ok(()) }

Небезопасный Rust

Небезопасный Rust

Rust состоит из двух частей:

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

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

Небезопасный код обычно небольшой и изолированный, и его корректность должна быть тщательно документирована. Обычно он оборачивается в безопасный уровень абстракции (safe abstraction layer).

Небезопасный Rust предоставляет доступ к 5 новым возможностям:

  • разыменование сырых указателей (raw pointers)
  • доступ и модификация мутабельных статичных переменных
  • доступ к полям union
  • вызов unsafe функций, включая extern (внешние) функции
  • реализация unsafe трейтов

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

Разыменование сырых указателей

Создание указателей является безопасным, но их разыменование требует unsafe:

fn main() {     let mut s = String::from("careful!");      let r1 = &mut s as *mut String;     let r2 = r1 as *const String;      // Безопасно, поскольку r1 и r2 были получены из ссылок и поэтому     // гарантированно не равны нулю и правильно выровнены (properly aligned), объекты, лежащие в основе ссылок,     // из которых они были получены, активны на протяжении всего небезопасного блока,     // и к ним нельзя получить доступ ни через ссылки, ни (конкурентно) через другие указатели     unsafe {         println!("r1 is: {}", *r1);         *r1 = String::from("uhoh");         println!("r2 is: {}", *r2);     }      // Небезопасно. Не делайте так     /*     let r3: &String = unsafe { &*r1 };     drop(s);     println!("r3 is: {}", *r3);     */ }

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

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

  • указатель не должен равняться нулю
  • указатель должен быть разыменовываемым (в пределах одного выделенного объекта)
  • объект не должен быть освобожден
  • не должно быть одновременного доступа к одной и той же локации памяти
  • если указатель был получен путем приведения ссылки (reference coercion), базовый объект должен быть активным и никакая ссылка не может использоваться для доступа к памяти

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

В разделе «Небезопасно» приведен пример распространенной ошибки неопределенного поведения: *r1 имеет 'static время жизни, поэтому r3 имеет тип &'static String и, таким образом, переживает s. Создание ссылки из указателя требует большой осторожности.

Модификация статичных переменных

Чтение иммутабельной статичной переменной является безопасным:

static HELLO_WORLD: &str = "Hello, world!";  fn main() {     println!("HELLO_WORLD: {HELLO_WORLD}"); }

Однако, учитывая риск возникновения гонки данных (data race), чтение и модификация мутабельных статичных переменных являются небезопасными:

static mut COUNTER: u32 = 0;  fn add_to_counter(inc: u32) {     unsafe {         COUNTER += inc;     } }  fn main() {     add_to_counter(42);      unsafe {         println!("COUNTER: {COUNTER}");     } }

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

Использование изменяемой статики, как правило, является плохой идеей, но в некоторых случаях это может иметь смысл в низкоуровневом коде no_std, например, при реализации распределителя кучи (heap allocator) или работе с некоторыми API языка C.

Объединения

Объединения (unions) похожи на перечисления, но активное поле нужно отслеживать самостоятельно:

#[repr(C)] union MyUnion {     i: u8,     b: bool, }  fn main() {     let u = MyUnion { i: 42 };     println!("int: {}", unsafe { u.i });     println!("bool: {}", unsafe { u.b }); // неопределенное поведение }

В Rust объединения нужны очень редко, поскольку обычно можно использовать перечисления. Иногда они необходимы для взаимодействия с API библиотек языка C.

Если мы просто хотим интерпретировать байты как другой тип, нам, вероятно, понадобится std::mem::transmute или безопасная оболочка, такая как крейт zerocopy.

Небезопасные функции

Вызов небезопасных функций

Функция или метод могут быть помечены как unsafe, если у них есть дополнительные условия, которые должны быть соблюдены во избежание неопределенного поведения:

extern "C" {     fn abs(input: i32) -> i32; }  fn main() {     let emojis = "?∈?";      // Безопасно, потому что индексы находятся в правильном порядке, в пределах     // фрагмента строки (string slice) и последовательности UTF-8     unsafe {         println!("эмодзи: {}", emojis.get_unchecked(0..4));         println!("эмодзи: {}", emojis.get_unchecked(4..7));         println!("эмодзи: {}", emojis.get_unchecked(7..11));     }      println!("количество символов: {}", count_chars(unsafe { emojis.get_unchecked(0..7) }));      unsafe {         // Потенциально неопределенное поведение         println!("абсолютное значение -3 согласно C: {}", abs(-3));     }      // Несоблюдение требований кодировки UTF-8 нарушает безопасность памяти     // println!("эмодзи: {}", unsafe { emojis.get_unchecked(0..3) });     // println!("количество символов: {}", count_chars(unsafe {     // emojis.get_unchecked(0..3) })); }  fn count_chars(s: &str) -> usize {     s.chars().count() }

Создание небезопасных функций

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

/// Меняет значения, на которые указывают указатели /// /// # Безопасность /// /// Указатели должны быть валидными и правильно выровненными unsafe fn swap(a: *mut u8, b: *mut u8) {     let temp = *a;     *a = *b;     *b = temp; }  fn main() {     let mut a = 42;     let mut b = 66;      // Безопасно, поскольку...     unsafe {         swap(&mut a, &mut b);     }      println!("a = {}, b = {}", a, b); }

Вызов небезопасных функций

get_unchecked, как и большинство функций _unchecked, небезопасна, поскольку может привести к неопределенному поведению, если диапазон неверен. abs небезопасна по другой причине: это внешняя функция (FFI). Вызов внешних функций обычно является проблемой только тогда, когда эти функции совершают действия с указателями, которые могут нарушить модель памяти Rust, но в целом любая функция C может иметь неопределенное поведение при определенных обстоятельствах.

Создание небезопасных функций

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

Обратите внимание, что небезопасный код разрешен внутри небезопасной функции без блока unsafe. Мы можем запретить это с помощью #[deny(unsafe_op_in_unsafe_fn)]. Попробуйте добавить его и посмотрите, что произойдет. Вероятно, это изменится в будущей версии Rust.

Небезопасные трейты

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

Например, крейт zerocopy имеет небезопасный трейт, который выглядит примерно так:

use std::mem::size_of_val; use std::slice;  /// ... /// # Безопасность /// Тип должен иметь определенное представление и не иметь отступов (padding) pub unsafe trait AsBytes {     fn as_bytes(&self) -> &[u8] {         unsafe {             slice::from_raw_parts(                 self as *const Self as *const u8,                 size_of_val(self),             )         }     } }  // Безопасно, поскольку `u32` имеет определенное представление и не имеет отступов unsafe impl AsBytes for u32 {}

В Rustdoc должен быть раздел # Safety (безопасность) с требованиями к безопасной реализации трейта.

Реальный раздел безопасности для AsBytes гораздо длиннее и сложнее.

Встроенные трейты Send и Sync являются небезопасными.

Упражнение: безопасная обертка FFI

Обратите внимание: это упражнение является сложным и опциональным.

В Rust имеется отличная поддержка вызова функций через интерфейс внешних функций (foreign function interface, FFI). Мы будем использовать это для создания безопасной оболочки для функций libc, которые используются в C для чтения имен файлов в директории.

Полезно изучить следующие страницы руководства:

Также полезно изучить документацию модуля std::ffi. Там вы найдете несколько типов строк, которые вам понадобятся для упражнения:

Типы Кодировка Назначение
str и String UTF-8 Обработка текста в Rust
CStr и CString NUL-завершенная Взаимодействие с функциями C
OsStr и OsString Зависит от ОС Взаимодействие с ОС

Вы будете выполнять следующие преобразования типов:

  • &str в CString — необходимо выделение пространства для завершающего символа \0
  • CString в *const i8 — для вызова функций C нужен указатель
  • *const i8 в &CStr — требуется средство обнаружения завершающего символа \0
  • &CStr в &[u8] — срез байтов — это универсальный интерфейс для «некоторых неизвестных данных»
  • &[u8] в &OsStr&OsStr — это шаг на пути к OsString, используйте OsStrExt для ее создания
  • &OsStr в OsString — данные в &OsStr нужно клонировать для того, чтобы иметь возможность их вернуть и повторно вызвать readdir

В Nomicon имеется отличный раздел о FFI.

mod ffi {     use std::os::raw::{c_char, c_int};     #[cfg(not(target_os = "macos"))]     use std::os::raw::{c_long, c_uchar, c_ulong, c_ushort};      // Непрозрачный тип. См. https://doc.rust-lang.org/nomicon/ffi.html.     #[repr(C)]     pub struct DIR {         _data: [u8; 0],         _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,     }      // Макет в соответствии со страницей руководства Linux для `readdir(3)`, где `ino_t` и     // `off_t` разрешаются согласно определениям в     // /usr/include/x86_64-linux-gnu/{sys/types.h, bits/typesizes.h}.     #[cfg(not(target_os = "macos"))]     #[repr(C)]     pub struct dirent {         pub d_ino: c_ulong,         pub d_off: c_long,         pub d_reclen: c_ushort,         pub d_type: c_uchar,         pub d_name: [c_char; 256],     }      // Макет в соответствии со страницей руководства `macOS` для `dir(5)`.     #[cfg(all(target_os = "macos"))]     #[repr(C)]     pub struct dirent {         pub d_fileno: u64,         pub d_seekoff: u64,         pub d_reclen: u16,         pub d_namlen: u16,         pub d_type: u8,         pub d_name: [c_char; 1024],     }      extern "C" {         pub fn opendir(s: *const c_char) -> *mut DIR;          #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))]         pub fn readdir(s: *mut DIR) -> *const dirent;          // См. https://github.com/rust-lang/libc/issues/414 и раздел         // _DARWIN_FEATURE_64_BIT_INODE на странице руководства `macOS` для `stat(2)`.         //         // "Платформы, существовавшие до того, как эти обновления стали доступны"         // (platforms that existed before these updates were available) относятся к         // macOS (но не к iOS, wearOS и т.д.) на Intel и PowerPC.         #[cfg(all(target_os = "macos", target_arch = "x86_64"))]         #[link_name = "readdir$INODE64"]         pub fn readdir(s: *mut DIR) -> *const dirent;          pub fn closedir(s: *mut DIR) -> c_int;     } }  use std::ffi::{CStr, CString, OsStr, OsString}; use std::os::unix::ffi::OsStrExt;  #[derive(Debug)] struct DirectoryIterator {     path: CString,     dir: *mut ffi::DIR, }  impl DirectoryIterator {     fn new(path: &str) -> Result<DirectoryIterator, String> {         // Вызываем `opendir` и возвращаем значение `Ok` при успехе         // и `Err` с сообщением при неудаче         unimplemented!()     } }  impl Iterator for DirectoryIterator {     type Item = OsString;     fn next(&mut self) -> Option<OsString> {         // Продолжаем вызывать `readdir` до тех пор, пока не вернется указатель на значение NULL         unimplemented!()     } }  impl Drop for DirectoryIterator {     fn drop(&mut self) {         // Вызывваем `closedir` по необходимости         unimplemented!()     } }  fn main() -> Result<(), String> {     let iter = DirectoryIterator::new(".")?;     println!("файлы: {:#?}", iter.collect::<Vec<_>>());     Ok(()) }  #[cfg(test)] mod tests {     use super::*;     use std::error::Error;      #[test]     fn test_nonexisting_directory() {         let iter = DirectoryIterator::new("no-such-directory");         assert!(iter.is_err());     }      #[test]     fn test_empty_directory() -> Result<(), Box<dyn Error>> {         let tmp = tempfile::TempDir::new()?;         let iter = DirectoryIterator::new(             tmp.path().to_str().ok_or("Non UTF-8 character in path")?,         )?;         let mut entries = iter.collect::<Vec<_>>();         entries.sort();         assert_eq!(entries, &[".", ".."]);         Ok(())     }      #[test]     fn test_nonempty_directory() -> Result<(), Box<dyn Error>> {         let tmp = tempfile::TempDir::new()?;         std::fs::write(tmp.path().join("foo.txt"), "The Foo Diaries\n")?;         std::fs::write(tmp.path().join("bar.png"), "<PNG>\n")?;         std::fs::write(tmp.path().join("crab.rs"), "//! Crab\n")?;         let iter = DirectoryIterator::new(             tmp.path().to_str().ok_or("Non UTF-8 character in path")?,         )?;         let mut entries = iter.collect::<Vec<_>>();         entries.sort();         assert_eq!(entries, &[".", "..", "bar.png", "crab.rs", "foo.txt"]);         Ok(())     } }

Решение

impl DirectoryIterator {     fn new(path: &str) -> Result<DirectoryIterator, String> {         // Вызываем `opendir` и возвращаем значение `Ok` при успехе         // и `Err` с сообщением при неудаче         let path =             CString::new(path).map_err(|err| format!("Invalid path: {err}"))?;         // Безопасность: `path.as_ptr()` не может возвращать NULL         let dir = unsafe { ffi::opendir(path.as_ptr()) };         if dir.is_null() {             Err(format!("Could not open {:?}", path))         } else {             Ok(DirectoryIterator { path, dir })         }     } }  impl Iterator for DirectoryIterator {     type Item = OsString;     fn next(&mut self) -> Option<OsString> {         // Продолжаем вызывать `readdir` до тех пор, пока не вернется указатель на значение NULL         // Безопасность: `self.dir` никогда не должно иметь значение NULL         let dirent = unsafe { ffi::readdir(self.dir) };         if dirent.is_null() {             // Мы достигли конца директории             return None;         }         // Безопасность: `dirent` не должно иметь значение NULL и `dirent.d_name` должно завершаться NUL         let d_name = unsafe { CStr::from_ptr((*dirent).d_name.as_ptr()) };         let os_str = OsStr::from_bytes(d_name.to_bytes());         Some(os_str.to_owned())     } }  impl Drop for DirectoryIterator {     fn drop(&mut self) {         // Вызываем `closedir` по необходимости         if !self.dir.is_null() {             // Безопасноть: `self.dir` не должно иметь значение NULL             if unsafe { ffi::closedir(self.dir) } != 0 {                 panic!("Could not close {:?}", self.path);             }         }     } }

Это конец четвертой части руководства и руководства, в целом.

Материалы для более глубокого изучения рассмотренных тем:

Happy coding!



ссылка на оригинал статьи https://habr.com/ru/articles/801201/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *