
Привет, Хабр!
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/
Добавить комментарий