Библиотека и бинарник в Rust: как упаковать и не потерять производительность

Онлайн-тренажер Python для начинающих

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

Начать курс

Один пакет — две сущности: миф или реальность?

Представьте: вы написали на Rust потрясающую библиотеку для работы с большими данными. Она молниеносна, безопасна и элегантна. Но вам нужен не только API для других разработчиков, но и готовая CLI-утилита, чтобы тестировать библиотеку или использовать её в пайплайнах. Возникает классический вопрос, который собрал тысячи просмотров на Stack Overflow: «Package with both a library and a binary?» (пакет, содержащий и библиотеку, и исполняемый файл).

Для новичка в Rust это звучит как головоломка. Для опытного разработчика — это задача на оптимизацию структуры проекта и производительности. В этой статье мы не просто ответим на этот вопрос, а сделаем это с фокусом на оптимизацию и производительность — две вещи, за которые Rust любят больше всего.

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

Структура пакета: Cargo.toml решает всё

В Rust один Cargo-пакет может содержать как библиотеку (lib), так и несколько бинарников (bin). Это не магия, а простое соглашение о структуре папок. Давайте посмотрим на минимальный пример.

Создадим проект:

cargo new my_tool --lib

Теперь изменим файл Cargo.toml, чтобы явно указать, что у нас есть и библиотека, и бинарник:

[package]
name = "my_tool"
version = "0.1.0"
edition = "2021"

[lib]
name = "my_tool_lib"
path = "src/lib.rs"

[[bin]]
name = "my_tool"
path = "src/main.rs"

[dependencies]

Структура папок будет выглядеть так:

my_tool/
├── Cargo.toml
├── src/
│   ├── lib.rs      // Библиотека
│   └── main.rs     // Бинарник

Всё просто, правда? Но дьявол кроется в деталях. Если вы просто скопируете код из библиотеки в main.rs, вы получите дублирование и проблемы с производительностью. Нужно действовать умнее.

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

Бинарник должен импортировать библиотеку как внешний крейт. Даже если они находятся в одном пакете. Это не только уменьшает дублирование, но и позволяет компилятору Rust лучше оптимизировать код через кросс-крейтовую оптимизацию (LTO).

Пример src/lib.rs:

pub fn calculate(data: &[u64]) -> u64 {
    data.iter().sum()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_calculate() {
        assert_eq!(calculate(&[1, 2, 3]), 6);
    }
}

Пример src/main.rs:

use my_tool_lib::calculate;

fn main() {
    let numbers = vec![10, 20, 30];
    println!("Сумма: {}", calculate(&numbers));
}

Заметьте: мы используем my_tool_lib — это имя, которое мы задали для библиотеки в Cargo.toml. Компилятор автоматически свяжет их в один бинарник, но при этом сохранит модульность.

Оптимизация производительности: LTO, inline и бенчмарки

Теперь, когда структура готова, поговорим о главном — как выжать максимум из вашего пакета. Ведь если вы создаёте инструмент для обработки данных, каждая микросекунда на счету.

1. Включаем Link-Time Optimization (LTO)

LTO позволяет компилятору оптимизировать код через границы крейтов. Без LTO функции из библиотеки и бинарника оптимизируются отдельно. С LTO — как единое целое.

Добавьте в Cargo.toml:

[profile.release]
lto = true
codegen-units = 1

Это увеличит время компиляции, но сделает бинарник заметно быстрее. Для образовательного сайта pythonlib.ru важно подчеркнуть: LTO — это стандарт для production-сборок на Rust.

2. Используем inline-подсказки

Если в библиотеке есть критически важные функции, которые вызываются из бинарника, добавьте атрибут #[inline]. Это подскажет компилятору встроить код функции прямо в место вызова, избегая накладных расходов на вызов.

#[inline(always)]
pub fn fast_calculate(data: &[u64]) -> u64 {
    data.iter().sum()
}

Но будьте осторожны: чрезмерное использование #[inline(always)] может раздуть бинарник. Используйте его только для маленьких, часто вызываемых функций.

3. Бенчмаркинг: измеряем производительность

Чтобы убедиться, что ваши оптимизации работают, нужно тестировать производительность. Rust предлагает встроенные бенчмарки (ночной канал) или крейт criterion.

Установите criterion как зависимость для разработки:

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }

[[bench]]
name = "my_bench"
harness = false

Создайте файл benches/my_bench.rs:

use criterion::{black_box, criterion_group, criterion_main, Criterion};
use my_tool_lib::calculate;

fn bench_calculate(c: &mut Criterion) {
    let data: Vec<u64> = (0..1000).collect();
    c.bench_function("calculate 1000", |b| {
        b.iter(|| calculate(black_box(&data)))
    });
}

criterion_group!(benches, bench_calculate);
criterion_main!(benches);

Запустите бенчмарки:

cargo bench

Вы получите красивый HTML-отчёт с графиками. Это лучший способ увидеть, как ваши изменения влияют на скорость.

Условная компиляция: один код — разные сценарии

Иногда вам нужно, чтобы библиотека вела себя по-разному в зависимости от того, используется ли она внутри бинарника или как внешний крейт. Для этого в Rust есть conditional compilation и фичи Cargo.

Добавим в Cargo.toml:

[features]
default = []
cli = []

Теперь в lib.rs мы можем написать:

#[cfg(feature = "cli")]
pub fn print_help() {
    println!("Использование: my_tool [OPTIONS]");
}

#[cfg(not(feature = "cli"))]
pub fn print_help() {
    // Ничего не делаем, если это не CLI-сборка
}

В main.rs подключаем фичу:

#[cfg(feature = "cli")]
use my_tool_lib::print_help;

fn main() {
    print_help();
}


                
                

Блоги

Рекомендаций по книгам