Перейти к содержимому

Cargo и крейты в Rust

На странице Первые шаги ты уже пользовался cargo new и cargo run. Это было только начало. Cargo — это и система сборки Rust, и его менеджер пакетов. Он компилирует код, запускает тесты, форматирует файлы и скачивает библиотеки, которые написали другие люди.

Такие библиотеки называются крейтами (crates). Представь их как приложения в магазине приложений: кто-то уже решил задачу (чтение JSON, веб-запросы, разбор дат), и ты можешь добавить его решение в свой проект одной командой. На этой странице ты увидишь, как это работает и как выбрать хороший крейт.

Два слова встречаются постоянно:

  • Крейт (crate) — это единица кода на Rust, которую собирает компилятор. Он бывает бинарным (программа, которую можно запустить, начинается с src/main.rs) или библиотечным (код для других программ, начинается с src/lib.rs).
  • Пакет (package) — это то, что создаёт cargo new: папка с файлом Cargo.toml и одним или несколькими крейтами внутри.

В обычной речи «крейтом» называют и то и другое. Когда говорят «возьми крейт serde», имеют в виду «добавь пакет serde с crates.io в свой проект».

У каждого пакета есть файл Cargo.toml. Он написан в формате TOML (на английском) — простом формате настроек. Вот что даёт cargo new hello_rust:

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
  • [package] описывает твой проект. name и version увидят другие люди, если ты его опубликуешь.
  • edition выбирает редакцию языка Rust. Редакции (2018, 2021, 2024) позволяют Rust развиваться, не ломая старый код. Просто оставь то, что написал cargo new.
  • [dependencies] перечисляет крейты, которыми пользуется твой код. Сначала этот раздел пустой.

Когда проект растёт, появляются и другие разделы:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
# нужны только тестам и примерам, а не самой программе
pretty_assertions = "1.4"
[profile.release]
# настройки для `cargo build --release`
opt-level = 3

Раздел [dependencies] можно править вручную, но проще всего — cargo add. Команда находит последнюю версию на crates.io и сама пишет нужную строку:

Terminal window
cargo add serde_json

У некоторых крейтов есть необязательные части — фичи (features). По умолчанию они выключены, чтобы сборка оставалась маленькой. Включаются они через --features:

Terminal window
cargo add serde --features derive

После этих двух команд в Cargo.toml появится примерно такое:

[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"

Теперь используй крейт в коде. Следующий cargo run скачает его, один раз скомпилирует и подключит к твоей программе:

use serde_json::json;
fn main() {
let crab = json!({ "legs": 10, "name": "Феррис" });
println!("{}", crab);
println!("Имя: {}", crab["name"]);
}

Вывод:

{"legs":10,"name":"Феррис"}
Имя: "Феррис"
▶ Попробуй сам

Кавычки вокруг "Феррис" появляются потому, что crab["name"] — всё ещё значение JSON, а не обычная строка Rust.

В Rust Playground самые популярные крейты уже есть, поэтому там это работает без cargo add. На своём компьютере сначала нужно выполнить cargo add serde_json. Гораздо больше об этом крейте — на странице JSON и Serde.

Чтобы убрать ненужный крейт, выполни cargo remove serde_json.

Когда ты пишешь serde_json = "1.0", ты не просишь ровно версию 1.0. Ты говоришь: «любая версия, совместимая с 1.0». Cargo следует семантическому версионированию (SemVer): версия выглядит как MAJOR.MINOR.PATCH, и ломать твой код может только изменение старшего (major) числа.

Ты пишешь Cargo может взять Почему
"1.0" (то же, что "^1.0") >=1.0.0 и <2.0.0 все версии 1.x должны быть совместимы
"1.4.2" >=1.4.2 и <2.0.0 не ниже 1.4.2, но всё ещё 1.x
"0.4" >=0.4.0 и <0.5.0 до 1.0 несовместимым считается изменение минорного числа
"~1.4" >=1.4.0 и <1.5.0 только патч-обновления
"=1.4.2" ровно 1.4.2 нужно редко

Кто же тогда выбирает точную версию? Это работа Cargo.lock. При первой сборке Cargo выбирает самые новые версии, которые подходят под твои требования, и записывает их в Cargo.lock. Все следующие сборки используют те же самые версии — и на твоём компьютере, и у всех остальных.

  • Не правь Cargo.lock вручную. Им управляет Cargo.
  • Добавляй его в git, особенно для программ. Тогда коллеги и CI соберут ровно то же, что и ты.
  • cargo update переходит на самые новые версии, которые всё ещё подходят под Cargo.toml, и переписывает Cargo.lock. На новую старшую версию он никогда не перепрыгивает — для этого ты сам меняешь Cargo.toml.

Есть три сайта, к которым ты будешь возвращаться снова и снова:

  • crates.io — официальный реестр. Здесь живёт каждый публичный крейт. Поищи слово вроде «csv» или «date».
  • docs.rs хранит документацию каждого крейта, собранную автоматически. Например, docs.rs/serde_json.
  • lib.rs — неофициальный и более быстрый каталог, который группирует крейты по категориям. Его удобно просматривать.

Одну и ту же задачу решают многие крейты, так как же выбрать? Прежде чем добавить крейт, потрать минуту и проверь:

  1. Загрузки. У популярных крейтов миллионы загрузок. Значит, многие люди уже нашли и исправили ошибки до тебя.
  2. Свежие обновления. Посмотри дату последнего релиза. Крейт, который не менялся годами, может быть вполне в порядке (некоторые просто «готовы»), но проверь, что его репозиторий не завален вопросами без ответа.
  3. Документация. Открой его страницу на docs.rs. У хороших крейтов понятная главная страница с примером, который можно скопировать.
  4. Версия 1.0 или выше. Не обязательно, но так автор обещает стабильный API.
  5. Мало зависимостей. Каждый добавленный крейт тянет свои зависимости. Чем меньше, тем легче доверять.

Если сомневаешься, поищи «rust» плюс свою задачу и посмотри, какой крейт сообщество советует снова и снова.

Заучивать их не нужно. Держи эту таблицу под рукой, и ты выучишь их на практике:

Команда Что делает
cargo new my_app Создаёт новый бинарный проект (добавь --lib для библиотеки)
cargo build Компилирует в target/debug/
cargo run Компилирует и запускает (аргументы — после --: cargo run -- input.txt)
cargo check Проверяет ошибки, не собирая программу. Гораздо быстрее, чем build
cargo test Запускает тесты (см. Тестирование)
cargo fmt Форматирует весь код в стандартном стиле
cargo clippy Запускает линтер. Он предлагает более простой и идиоматичный код
cargo doc --open Собирает HTML-документацию для проекта и всех зависимостей и открывает её
cargo update Обновляет Cargo.lock до самых новых совместимых версий
cargo build --release Компилирует оптимизированную программу в target/release/

Хорошая привычка: почаще запускай cargo check, пока пишешь код, а перед коммитом — cargo fmt и cargo clippy. И cargo doc --open — настоящая скрытая жемчужина: он показывает документацию ровно для тех версий крейтов, которыми ты пользуешься, и работает без интернета.

По умолчанию Cargo делает отладочную сборку (debug). Она быстро компилируется и сохраняет дополнительные проверки безопасности, например панику при переполнении целого числа. Пока ты разрабатываешь, это идеально.

Release-сборка (cargo build --release или cargo run --release) компилируется дольше, но работает гораздо быстрее — часто в 10 раз и больше. Используй её, когда выпускаешь программу или измеряешь её скорость.

Код даже может узнать, какая у него сборка:

fn main() {
if cfg!(debug_assertions) {
println!("Отладочная сборка: дополнительные проверки включены");
} else {
println!("Release-сборка: оптимизирована для скорости");
}
}

Вывод:

Отладочная сборка: дополнительные проверки включены
▶ Попробуй сам

В Playground можно переключить меню с «Debug» на «Release» и запустить ещё раз — тогда ты увидишь другую строку.

Пакет может одновременно содержать библиотеку и программу. Это очень распространённая схема: настоящая логика живёт в src/lib.rs, а src/main.rs остаётся маленьким и просто вызывает её:

greeter/
├── Cargo.toml
└── src/
├── lib.rs # библиотечный крейт: переиспользуемая логика
└── main.rs # бинарный крейт: программа, которую запускаешь
src/lib.rs
pub fn greet(name: &str) -> String {
format!("Привет, {name}!")
}
src/main.rs
fn main() {
// библиотека называется так же, как пакет
println!("{}", greeter::greet("Феррис"));
}

Зачем так делить? Код в lib.rs легко тестировать, и его могут переиспользовать другие программы (или второй бинарный крейт). В Playground только один файл, но то же самое можно почувствовать через pub mod, как на странице Модули:

mod greeter {
pub fn greet(name: &str) -> String {
format!("Привет, {name}!")
}
}
fn main() {
println!("{}", greeter::greet("Феррис"));
}

Вывод:

Привет, Феррис!
▶ Попробуй сам

Когда проект разрастается в несколько связанных пакетов (например, консольное приложение, веб-сервер и общая библиотека), их можно сложить в одно рабочее пространство (workspace). У них общий Cargo.lock и общая папка target/, поэтому всё собирается вместе и использует одни и те же версии зависимостей.

В корневом Cargo.toml перечислены участники:

[workspace]
members = ["app", "core"]
resolver = "3"
my_project/
├── Cargo.toml # файл рабочего пространства (выше)
├── Cargo.lock # общий для всех участников
├── app/ # бинарный пакет
└── core/ # библиотечный пакет

Тогда cargo build в корне собирает всё, а cargo run -p app запускает одного участника. Для первых проектов это не понадобится, но полезно узнавать такую структуру, когда читаешь чужой код.

Используешь крейт, не добавив его. Если написать use serde_json::json;, но забыть cargo add serde_json, компилятор его не найдёт:

error[E0432]: unresolved import `serde_json`
--> src/main.rs:1:5
|
1 | use serde_json::json;
| ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
|
= help: you might be missing a crate named `serde_json`

Компилятор говорит, что не может разрешить импорт serde_json, и подсказывает, что тебе, возможно, не хватает крейта с таким именем. Решение — выполнить cargo add serde_json. И проверь написание: в именах крейтов бывают - или _, а в коде - всегда превращается в _.

Забыл фичу. Если #[derive(Serialize)] сообщает, что не может найти derive-макрос, скорее всего, ты добавил serde без --features derive. Выполни cargo add serde --features derive ещё раз — команда обновит существующую строку.

Меряешь скорость на отладочной сборке. Если программа кажется медленной, попробуй cargo run --release, прежде чем менять код.

С помощью макроса json! из serde_json опиши крейт с тремя ключами: downloads (число), name и version. Выведи всё значение целиком, а потом только версию.

Подсказка: повтори форму примера с crab из раздела Добавляем зависимость через cargo add. На своём компьютере сначала выполни cargo add serde_json; в Playground он уже есть.

Показать решение
use serde_json::json;
fn main() {
let krate = json!({
"downloads": 500,
"name": "serde_json",
"version": "1.0"
});
println!("{}", krate);
println!("Версия: {}", krate["version"]);
}

Вывод:

{"downloads":500,"name":"serde_json","version":"1.0"}
Версия: "1.0"
▶ Попробуй сам

Переменная называется krate, потому что crate в Rust — зарезервированное слово. Такое написание — популярная шутка в коде на Rust. Как и раньше, версия выводится в кавычках, потому что это всё ещё значение JSON; как достать из него обычный текст, показано на странице JSON и Serde.

Напиши функцию debug_log(msg: &str), которая выводит [debug] и сообщение, но только в отладочной сборке. Используй её в main вокруг небольшого вычисления, а результат выведи как обычно.

Подсказка: оберни println! в if cfg!(debug_assertions) { ... }, как в разделе Отладочная и release-сборка.

Показать решение
fn debug_log(msg: &str) {
if cfg!(debug_assertions) {
println!("[debug] {msg}");
}
}
fn main() {
debug_log("начинаем вычисление");
let mut total = 0;
for n in 1..=10 {
total += n;
}
debug_log("вычисление закончено");
println!("Сумма чисел от 1 до 10 равна {total}");
}

Вывод:

[debug] начинаем вычисление
[debug] вычисление закончено
Сумма чисел от 1 до 10 равна 55
▶ Попробуй сам

Переключи Playground на «Release» (или выполни cargo run --release у себя) и запусти снова: выведется только последняя строка. Для настоящих проектов на странице Логирование показаны крейты, которые делают это как следует, с уровнями вроде debug и info.

  • Cargo собирает, запускает, тестирует и форматирует код, а ещё скачивает крейты с crates.io.
  • Cargo.toml — это то, что ты просишь; Cargo.lock записывает точные версии, которые ты получил. Добавляй в git оба файла.
  • cargo add name --features x добавляет зависимость. "1.0" значит «любая совместимая версия 1.x».
  • Оценивай крейт по загрузкам, свежим обновлениям и хорошей документации на docs.rs.
  • Пока пишешь — cargo check, перед коммитом — cargo fmt и cargo clippy, для скорости — --release.
  • Переиспользуемую логику клади в src/lib.rs, а src/main.rs держи маленьким.

Проверь себя

1. Что значит serde_json = "1.0" в Cargo.toml?

2. Какой файл записывает точные версии всех зависимостей, с которыми прошла сборка?

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

4. Как добавить serde с включённой фичей derive?

Результат: 0 / 4