Привет, Хабр!
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 расскажут в рамках бесплатного вебинара.