Rust: main.rs и lib.rs — как не запутаться в модулях и ускорить код

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

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

Начать курс

Rust modules confusion: как main.rs и lib.rs сводят с ума (и как это исправить)

Вы когда-нибудь писали на Rust, создавали новый проект, добавляли main.rs и lib.rs — и вдруг компилятор начинал выдавать ошибки, которые, казалось, не имеют смысла? Знакомо? Если вы гуглили "Rust modules confusion when there is main.rs and lib.rs", то вы не одиноки. Этот вопрос на Stack Overflow набрал тысячи просмотров, и не зря. Путаница с модулями — одна из самых частых проблем у новичков (и даже у опытных разработчиков, переходящих с других языков).

В этой статье мы не просто разберем, как работает модульная система Rust. Мы покажем, как её правильная организация напрямую влияет на оптимизацию и производительность вашего кода. Вы узнаете, почему lib.rs — это не просто "еще один файл", а мощный инструмент для ускорения компиляции и создания чистого API. Поехали!

Проблема: "Почему компилятор не видит мои модули?"

Представьте: вы создали проект my_project с помощью cargo new my_project. По умолчанию Cargo создает src/main.rs. Вы добавляете второй файл — src/lib.rs — и пишете в нем функцию. Затем пытаетесь её вызвать из main.rs — и получаете ошибку:

error[E0432]: unresolved import `my_project::some_function`
 --> src/main.rs:1:5
  |
1 | use my_project::some_function;
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^ no `some_function` in the root

Знакомая боль? В чём дело? Всё просто: Rust считает main.rs и lib.rs двумя разными корнями крейта. Если в Cargo.toml не указано иное, Cargo автоматически создает два крейта: бинарный (из main.rs) и библиотечный (из lib.rs). Они не "видят" друг друга напрямую, пока вы явно не укажете зависимости.

Это и есть та самая Rust modules confusion, о которой все говорят. Но вместо того, чтобы злиться, давайте превратим эту "особенность" в преимущество.

Решение: как правильно организовать модули

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

Вариант 1: Всё в одном крейте (простой, но не масштабируемый)

Если ваш проект небольшой, вы можете объявить модули внутри main.rs с помощью mod:

// src/main.rs
mod utils; // ищет src/utils.rs или src/utils/mod.rs

fn main() {
    utils::do_something();
}

Это работает, но если вы добавите lib.rs, начнется путаница. lib.rs станет отдельным крейтом, и модули из main.rs не будут в нём доступны. Для маленьких скриптов — ок, для серьезных проектов — нет.

Вариант 2: Библиотека + бинарник (правильный путь для оптимизации)

Самый грамотный способ — сделать lib.rs основным хранилищем логики, а main.rs — тонкой обёрткой для запуска. Так вы получаете:

  • Модульность: вся бизнес-логика в библиотеке.
  • Тестируемость: библиотеку можно тестировать отдельно.
  • Производительность: компилятор лучше оптимизирует библиотечный код, если он используется многократно.

Как это сделать? Просто:

// src/lib.rs
pub mod utils;

pub fn run() {
    utils::do_something();
}

// src/main.rs
fn main() {
    my_project::run(); // my_project — имя крейта из Cargo.toml
}

Обратите внимание: main.rs теперь использует my_project::run(). Это работает, потому что Cargo видит lib.rs как библиотеку с именем my_project. Вуаля — путаница исчезла!

Оптимизация и производительность: почему lib.rs — ваш друг

Теперь самое интересное. Как Rust modules confusion связана с производительностью? Напрямую. Когда вы используете lib.rs как библиотеку, компилятор может применять более агрессивные оптимизации, такие как:

  • Inlining: функции из библиотеки могут быть встроены в бинарник, если они помечены #[inline].
  • LTO (Link-Time Optimization): если библиотека и бинарник компилируются отдельно, LTO позволяет оптимизировать их как единое целое.
  • Уменьшение времени компиляции: библиотека компилируется один раз, бинарник — отдельно. При изменении только main.rs перекомпилируется только он, а не вся библиотека.

Пример: представьте, что у вас есть тяжёлая функция обработки данных:

// src/lib.rs
pub fn heavy_computation(data: &[u8]) -> Vec {
    // ... сложные алгоритмы ...
    data.iter().map(|&x| x.wrapping_add(42)).collect()
}

Если вы поместите её в lib.rs и будете вызывать из main.rs, компилятор сможет заинлайнить её при необходимости. Если же она будет в main.rs как часть бинарника — оптимизация может быть менее эффективной из-за ограничений кросс-крейтовой видимости.

Практический пример: от хаоса к порядку

Давайте соберём всё вместе. Допустим, у нас есть проект, который парсит данные и выводит результат. Изначально — полный хаос с Rust modules confusion:

// src/main.rs (попытка использовать lib.rs, но неправильно)
mod parser; // ошибка! parser не объявлен в lib.rs

fn main() {
    let data = "42";
    let result = parser::parse(data);
    println!("{}", result);
}

Исправляем:

// src/lib.rs
pub mod parser;

pub fn process_data(input: &str) -> i32 {
    parser::parse(input)
}

// src/parser.rs
pub fn parse(input: &str) -> i32 {
    input.parse().unwrap_or(0)
}

// src/main.rs
fn main() {
    let result = my_project::process_data("42");
    println!("{}", result);
}

Теперь всё работает. Более того, вы можете написать тесты для lib.rs:

// tests/integration_test.rs
use my_project;

#[test]
fn test_process_data() {
    assert_eq!(my_project::process_data("10"), 10);
}

Производительность? Компилятор видит, что parser::parse вызывается только из process_data, и может заинлайнить её. А если вы включите LTO в Cargo.toml:

<

Блоги

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