9 полезных крейтов в Rust

от автора

Привет, Хабр!

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

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

В этой статье рассмотрим 9 полезных крейтов в Rust.

Для начала о том, как устанавливать крейты

Нужен естественно Rust и Cargo. Rust поддерживает Windows, Linux, macOS, FreeBSD и NetBSD. Установка Rust на Unix-системах происходит через терминал с помощью команды curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh, а на Windows — скачиванием и запуском файла rustup-init.exe с официального сайта.

Для создания проекта юзаем команду cargo new <название_проекта>, которая сгенерирует базовую структуру проекта, включая файл Cargo.toml для конфигурации и зависимостей.

Чтобы добавить крейт как зависимость, нужно отредактировать файл Cargo.toml, добавив строку в секцию [dependencies], например: serde = "1.0". Это можно сделать вручную или с помощью команды cargo add serde, если установлен cargo-edit.

cargo install <имя_крейта> позволяет установить бинарные крейты, то есть программы или инструменты, которые можно запускать из командной строки. Можно уточнить версию с помощью флага --version, выбрать конкретный бинарный файл с --bin или установить примеры с --example.

Чтобы обновить зависимости проекта до последних версий, юзаем cargo update. Для удаления бинарного крейта, установленного через cargo install, юзаемcargo uninstall <имя_крейта>.

Для компиляции проекта используем cargo build, а для запуска — cargo run. Эти команды автоматически скачивают и устанавливают необходимые зависимости, компилируют проект и, в случае cargo run, запускают выполнение программы.

Сериализация данных с serde

Serde — это фреймворк для сериализации и десериализации структур данных Rust. В отличие от многих ЯПов, которые полагаются на рефлексию во время выполнения для сериализации данных, Serde основан на мощной системе трейтов Rust. Структуры данных, которые знают, как сериализоваться и десериализоваться, реализуют трейты Serialize и Deserialize Serde или используют атрибуты derive для автоматической генерации реализаций на этапе компиляции.

Для сериализации и десериализации в JSON используется крейт serde_json. Например, для структуры Person:

use serde::{Serialize, Deserialize};  #[derive(Serialize, Deserialize)] struct Person {     name: String,     age: u8,     is_active: bool, }

Для сериализации объекта Person в строку JSON и обратно:

let person = Person {     name: "Alex".to_owned(),     age: 28,     is_active: true, };  let serialized = serde_json::to_string(&person).unwrap(); println!("serialized = {}", serialized);  let deserialized: Person = serde_json::from_str(&serialized).unwrap(); println!("deserialized = {:?}", deserialized);

Serde поддерживает множество форматов, таких как JSON, YAML, TOML и др. Например, для работы с YAML используется крейт serde_yaml, а для TOML — крейт toml.

Для более сложных случаев, таких как условное включение полей или изменение структуры сериализуемого объекта, Serde предоставляет различные атрибуты. Например, можно использовать атрибут #[serde(flatten)], чтобы «сплющить» структуру при сериализации, избегая лишнего уровня вложенности:

#[derive(Serialize, Deserialize)] struct Request {     calculation: Calculation,     #[serde(flatten)]     shape: Shape, }

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

Асинхронное программирование с tokio

Tokio — это асинхронный runtime для Раста, предназначенный для создания сетевых приложений и поддержки асинхронных операций ввода-вывода, масштабируемых и надёжных.

Для создания нового проекта на Tokio, нужно установить зависимости вCargo.toml. Для большинства проектов достаточно использовать флаг features = ["full"] для включения всех доступных функций:

tokio = { version = "1", features = ["full"] }

Простейший пример использования Tokio — это асинхронная функция main, аннотированная с #[tokio::main], что позволяет использовать async/await синтаксис:

#[tokio::main] async fn main() {     println!("Hello, Tokio!"); }

А вот так может выглядеть асинхронная задача задержки с использованием Tokio:

use tokio::time::{sleep, Duration};  #[tokio::main] async fn main() {     println!("Start delay");     sleep(Duration::from_secs(5)).await;     println!("Delay complete"); }

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

Также имеются инструменты для работы с асинхронным вводом-выводом, включая поддержку TCP, UDP, таймеров и др. Вот пример асинхронного TCP эхо-сервера на Tokio:

use tokio::net::TcpListener; use tokio::io::{AsyncReadExt, AsyncWriteExt};  #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> {     let listener = TcpListener::bind("127.0.0.1:8080").await?;     loop {         let (mut socket, _) = listener.accept().await?;         tokio::spawn(async move {             let mut buf = [0; 1024];             loop {                 let n = match socket.read(&mut buf).await {                     Ok(n) if n == 0 => return,                     Ok(n) => n,                     Err(e) => {                         eprintln!("failed to read from socket; err = {:?}", e);                         return;                     },                 };                 if let Err(e) = socket.write_all(&buf[0..n]).await {                     eprintln!("failed to write to socket; err = {:?}", e);                     return;                 }             }         });     } }

Веб-разработка с warp

Warp является высокопроизводительным веб-фреймворком для Rust, позволяющим строить асинхронные веб-приложения. Он использует систему фильтров для обработки запросов, делая создание веб-серверов удобным и гибким.

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

#[tokio::main] async fn main() {     let route = warp::path::end().map(|| warp::reply::html("Hello, Habr!"));     warp::serve(route).run(([127, 0, 0, 1], 3030)).await; }

Для работы с JSON в Warp используются макросы Serialize и Deserialize из крейта serde. Пример создания эндпоинта, принимающего JSON, выглядит так:

#[derive(Deserialize, Serialize, Clone)] struct Item {     name: String,     quantity: i32, }  fn json_body() -> impl Filter<Extract = (Item,), Error = warp::Rejection> + Clone {     warp::body::content_length_limit(1024 * 16).and(warp::body::json()) }  #[tokio::main] async fn main() {     let add_items = warp::post()         .and(warp::path("item"))         .and(json_body())         .map(|item: Item| {             warp::reply::json(&item)         });     warp::serve(add_items).run(([127, 0, 0, 1], 3030)).await; }

Создание полноценного CRUD API требует организации работы с БД и обработки различных HTTP методов. В Warp это можно организовать с помощью фильтров и асинхронных функций. Например, для обработки GET и POST запросов можно использовать следующий код:

let get_route = warp::get()     .and(warp::path("items"))     .and(with_db(pool.clone()))     .and_then(handlers::get_items);  let post_route = warp::post()     .and(warp::path("items"))     .and(json_body())     .and(with_db(pool.clone()))     .and_then(handlers::add_item);  let routes = get_route.or(post_route); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;

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

Подробнее с warp можно ознакомиться здесь.

Работа с БД с diesel

Diesel представляет собой ORM и конструктор запросов, который поддерживает работу с PostgreSQL, MySQL и SQLite. Он предназначен для упрощения взаимодействия с БД, минимизации шаблонного кода и предотвращения ошибок времени выполнения, не жертвуя при этом производительностью. Diesel полностью интегрируется с системой типов Rust!

Для начала работы с Diesel необходимо добавить его в Cargo.toml файл, указав нужные функциональности, в зависимости от того, с какой БД идет работа.

[dependencies] diesel = { version = "1.0", features = ["postgres", "sqlite", "mysql"] }

После добавления зависимости идентификация схемы БД и генерация соответствующего кода осуществляется через использование макросов Diesel и команды Diesel CLI для миграций.

Миграции позволяют контролировать версии схемы БД аналогично системам контроля версий кода. В Diesel CLI это делается так:

diesel migration generate create_students

Это создаст структуру каталогов для миграций, включая файлы up.sql и down.sql для каждой миграции, в которые добавляется SQL код для изменений схемы и отката изменений соответственно.

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

#[derive(Queryable)] pub struct Post {     pub id: i32,     pub title: String,     pub body: String,     pub published: bool, }

А для вставки новых записей используется Insertable:

#[derive(Insertable)] #[table_name="posts"] pub struct NewPost<'a> {     pub title: &'a str,     pub body: &'a str, }

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

pub fn create_post<'a>(conn: &PgConnection, title: &'a str, body: &'a str) -> Post {     let new_post = NewPost {         title: title,         body: body,     };      diesel::insert_into(posts::table)         .values(&new_post)         .get_result(conn)         .expect("Error saving new post") }

Этот код вставляет новую запись в таблицу и возвращает её как структуру Post.

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

let results = posts.filter(published.eq(true))     .limit(5)     .load::<Post>(&connection)     .expect("Error loading posts");

Diesel поддерживает сложные запросы и операции, такие как соединения, фильтрация, сортировка и пагинаци.

Подробнее с Diesel можно ознакомиться здесь.

Многопоточность и параллелизм с rayon

Rayon очень мощная вещь, которая позволяет достигать параллелизма данных в Rust. Она позволяет легко выполнять операции в параллельном режиме.

Для начала работы с Rayon, добавьте зависимость в ваш Cargo.toml:

[dependencies] rayon = "1.5.1"

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

use rayon::prelude::*;

Параллельные итераторы являются самым простым и часто наиболее используемым способом использования Rayon. Они позволяют автоматически преобразовать последовательные вычисления в параллельные, обеспечивая при этом защиту от гонок данных. Параллельные итераторы поддерживают множество методов, аналогичных обычным итераторам в Rust, включая map, for_each, filter, fold и многие другие.

Пример использования параллельного итератора для изменения элементов массива:

use rayon::prelude::*;  fn main() {     let mut arr = [0, 7, 9, 11];     arr.par_iter_mut().for_each(|p| *p -= 1);     println!("{:?}", arr); }

Код параллельно уменьшит каждый элемент массива на единицу.

Для более тонкой настройки параллельных вычислений Rayon предлагает использовать методы join и scope, которые позволяют делить работу на параллельные задачи. Метод join используется для одновременного выполнения двух замыканий, а scope создает область видимости, в которой можно создать произвольное количество параллельных задач.

Пример использования метода join для параллельного выполнения двух задач:

rayon::join(|| do_something(), || do_something_else());

Метод хорош для задач, привязанных к CPU, но его не следует использовать для блокирующих операций, таких как ввод-вывод.

Подробнее про крейт — здесь.

GUI разработка с iced

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

Основные возможности Iced:

  • Простой и удобный API «всё включено».

  • Реактивная модель программирования на основе типов.

  • Поддержка кросс-платформенности: Windows, macOS, Linux и веб.

  • Адаптивная компоновка.

  • Встроенные виджеты (текстовые поля, прокрутка и многое другое).

  • Поддержка пользовательских виджетов.

  • Отладочная панель с метриками производительности.

  • Встроенная поддержка асинхронных действий через futures.

  • Модульная экосистема с возможностью интеграции в существующие системы【.

Рассмотрим пример кода для создания простого GUI приложения с счётчиком. Реализуем основные концепции Iced, такие как моделирование состояния, обработка сообщений от пользователя, логика отображения и обновления состояния:

use iced::{button, executor, Application, Button, Column, Command, Element, Settings, Text};  pub fn main() -> iced::Result {     Counter::run(Settings::default()) }  struct Counter {     value: i32,     increment_button: button::State,     decrement_button: button::State, }  #[derive(Debug, Clone, Copy)] enum Message {     IncrementPressed,     DecrementPressed, }  impl Application for Counter {     type Executor = executor::Default;     type Message = Message;     type Flags = ();      fn new(_flags: ()) -> (Counter, Command<Self::Message>) {         (             Counter {                 value: 0,                 increment_button: button::State::new(),                 decrement_button: button::State::new(),             },             Command::none(),         )     }      fn title(&self) -> String {         String::from("A simple counter")     }      fn update(&mut self, message: Self::Message) -> Command<Self::Message> {         match message {             Message::IncrementPressed => {                 self.value += 1;             }             Message::DecrementPressed => {                 self.value -= 1;             }         }          Command::none()     }      fn view(&self) -> Element<Self::Message> {         Column::new()             .push(                 Button::new(&mut self.increment_button, Text::new("Increment"))                     .on_press(Message::IncrementPressed),             )             .push(Text::new(self.value.to_string()).size(50))             .push(                 Button::new(&mut self.decrement_button, Text::new("Decrement"))                     .on_press(Message::DecrementPressed),             )             .into()     } }

Здесь будет простое приложение с счётчиком, который можно увеличивать и уменьшать с помощью двух кнопок. Состояние счётчика хранится в поле value структуры Counter. Для каждой кнопки создается свой экземпляр button::State, который используется для отслеживания состояния кнопки. Взаимодействия юзера с кнопками генерируют сообщения Message, которые обрабатываются в методе update, изменяя состояние счётчика. Виджеты для отображения составляются в методе view, который возвращает layout с кнопками и текстом, отображающим текущее значение счётчика.

Парсинг и анализ кода с syn и quote

Процедурные макросы в Rust позволяют манипулировать синтаксическими деревьями кода на этапе компиляции. Два крейта, syn и quoteпозволяют создавать такие макросы. syn используется для парсинга кода Rust в структуры данных, которые можно исследовать и манипулировать, а quote позволяет генерировать код Rust из этих структур.

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

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

#[proc_macro_attribute] pub fn trace_vars(metadata: TokenStream, input: TokenStream) -> TokenStream {     let input_fn = parse_macro_input!(input as ItemFn);     // парсинг аргументов макроса     let args = parse_macro_input!(metadata as Args);     // генерация нового кода     TokenStream::from(quote!{fn dummy(){}}) }

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

Простой пример макроса с использованием quote, который генерирует функцию:

#[proc_macro] pub fn minimal(input: TokenStream) -> TokenStream {     let Combinations { name, n } = parse_macro_input!(input as Combinations);     (quote!{         fn #name() -> i32 {             #n         }     }).into() }

Рассмотрим пример, в котором создадим процедурный макрос на Rust, использующий syn и quote для анализа структуры и генерации функции, которая считает сумму значений её числовых полей.

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

Сначала определим структуру Point и напишем макрос derive_summation, который будет генерировать функцию sum:

// в файле lib.rs крейта с процедурными макросами  use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput};  #[proc_macro_derive(SumFields)] pub fn derive_summation(input: TokenStream) -> TokenStream {     let input = parse_macro_input!(input as DeriveInput);          // получение имени структуры     let name = &input.ident;          // генерация кода функции sum     let gen = quote! {         impl #name {             pub fn sum(&self) -> i32 {                 self.x + self.y             }         }     };          gen.into() }

Теперь в основном проекте или другом крейте, юзаем макрос SumFields для автоматической генерации метода sum для структуры Point.

use my_macro_crate::SumFields; // Замените my_macro_crate на имя вашего крейта с макросами  #[derive(SumFields)] struct Point {     x: i32,     y: i32, }  fn main() {     let point = Point { x: 1, y: 2 };     println!("Sum of fields: {}", point.sum()); }

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


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

А про главные особенности разработки приложения на Rust мои коллеги из OTUS расскажут в рамках бесплатного вебинара.


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


Комментарии

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

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