Определяем debug/release сборки в Rust через cfg: полный гайд

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

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

Начать курс

Как отличить debug от release в Rust? Разбираем cfg на реальных примерах

Вы когда-нибудь писали код на Rust и думали: «Вот бы в debug-режиме добавить подробное логирование, а в release — убрать всё лишнее»? Или, может быть, хотели включить ассерты только во время разработки, но выключить их в финальной сборке? Если да — вы попали по адресу. Сегодня мы разберём, как с помощью магии cfg управлять поведением кода в зависимости от типа сборки. Это не просто сухая теория — мы пройдём путь от простого флага до production-решения.

Тема взята не с потолка: на Stack Overflow вопрос «How to check release / debug builds using cfg in Rust?» собрал тысячи просмотров. Значит, это нужно многим. И мы это разжуём.

Почему это важно? Два мира — два подхода

В Rust есть два основных профиля сборки: debug (с флагом --debug или просто cargo build) и release (cargo build --release). Они отличаются не только оптимизациями, но и поведением некоторых встроенных проверок.

Например:

  • debug: медленный, но с проверками границ массивов, отладочными символами и ассертами.
  • release: быстрый, но без лишних проверок — всё ради производительности.

Что если вам нужно, чтобы в debug-версии программа писала в лог каждое действие, а в release — только критические ошибки? Или чтобы тестовый эндпоинт был доступен только локально? Для этого и существует cfg.

Основные инструменты: cfg!() и #[cfg()]

В Rust есть два способа проверить тип сборки:

  • Макрос cfg!() — выполняется во время компиляции, возвращает bool. Используется внутри функций.
  • Атрибут #[cfg()] — полностью включает или исключает блок кода из компиляции.

Оба используют одни и те же ключи. Самый популярный для нашей задачи — debug_assertions. Он активен только в debug-сборке. Есть и обратный — not(debug_assertions) для release.

Пример 1: Простейшая проверка внутри функции

Начнём с макроса cfg!(). Допустим, у нас есть функция расчёта чего-то сложного. В debug мы хотим проверять входные данные:

fn calculate_something(x: i32) -> i32 {
    // Проверка только в debug-сборке
    if cfg!(debug_assertions) {
        println!("[DEBUG] calculate_something вызвана с x = {}", x);
        assert!(x > 0, "x должен быть положительным!");
    }

    // Основная логика
    x * 2
}

fn main() {
    let result = calculate_something(10);
    println!("Результат: {}", result);
}

Что произойдёт?

  • При cargo build (debug) — вы увидите лог и, если x ≤ 0, программа упадёт с ассертом.
  • При cargo build --release — строка с cfg!(debug_assertions) превратится в false, и компилятор выкинет этот блок как мёртвый код. Никакого оверхеда!

Пример 2: Полное исключение кода с #[cfg()]

Иногда нужно не просто пропустить выполнение, а полностью убрать функцию или модуль из бинарника. Для этого используем атрибут:

// Этот модуль будет скомпилирован ТОЛЬКО в debug-сборке
#[cfg(debug_assertions)]
mod debug_helpers {
    pub fn print_state(state: &str) {
        println!("[DEBUG] Текущее состояние: {}", state);
    }

    pub fn validate_data(data: &[u8]) -> bool {
        // Дорогая проверка, не нужна в релизе
        data.len() < 1000
    }
}

// Этот модуль — только для release
#[cfg(not(debug_assertions))]
mod release_helpers {
    pub fn print_state(_state: &str) {
        // В релизе ничего не делаем — экономим CPU
    }

    pub fn validate_data(_data: &[u8]) -> bool {
        true // Всегда true — доверяем данным
    }
}

fn main() {
    // В зависимости от сборки будет вызвана нужная версия
    debug_helpers::print_state("старт");
    release_helpers::print_state("старт"); // Эта строка не скомпилируется в debug!
}

Важный нюанс: если вы используете #[cfg()] на уровне модуля, то в другом модуле не должно быть прямого вызова его функций без условной компиляции. Иначе получите ошибку «function not found». Лучше вынести общий интерфейс, как мы сделали выше.

Пример 3: Реальный сценарий — разное поведение HTTP-сервера

Представьте, что вы пишете веб-сервер на Actix или Axum. В debug вам нужно показывать панель отладки и принимать запросы только с localhost. В release — открыть доступ для всех, но убрать лишние эндпоинты.

Вот как это может выглядеть с cfg:

use axum::{Router, routing::get};

#[cfg(debug_assertions)]
fn add_debug_routes(app: Router) -> Router {
    app.route("/debug/state", get(|| async { "Секретная отладочная информация" }))
     .route("/debug/heap", get(|| async { "Статистика памяти" }))
}

#[cfg(not(debug_assertions))]
fn add_debug_routes(app: Router) -> Router {
    // В релизе ничего не добавляем
    app
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        // Добавляем отладочные маршруты только если это debug
        .and_then(add_debug_routes);

    let addr = if cfg!(debug_assertions) {
        "127.0.0.1:3000" // Только локально
    } else {
        "0.0.0.0:3000"   // Для всех
    };

    println!("Сервер запущен на {}", addr);
    axum::Server::bind(&addr.parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Обратите внимание: мы используем #[cfg()] для целых функций и cfg!() для выбора адреса. Это пример грамотного разделения ответственности.

Что ещё можно проверять через cfg?

debug_assertions — не единственный ключ. Вот список самых полезных для повседневной работы:

  • debug_assertions — true в debug, false в release.
  • target_os = "windows" — проверка ОС.
  • target_arch = "x86_64" — архитектура.
  • feature = "my_feature" — кастомные фичи из Cargo.toml.
  • <

Блоги

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