Feature Flags в Rust: Гибкая настройка без перекомпиляции

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

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

Начать курс

Feature Flags в Rust: секретное оружие для гибкой разработки

Представьте, что вы пишете библиотеку для работы с изображениями. Одни пользователи хотят только базовое изменение размера, другие — продвинутую цветокоррекцию, а третьи — интеграцию с нейросетями. Как сделать всех счастливыми, не раздувая код и не плодя десятки версий? Ответ прост — feature flags (или feature toggles).

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

В этой статье мы разберём, как работают feature flags в Rust, на реальных примерах покажем их преимущества и дадим практические советы. Поехали!

Что такое feature flags и зачем они нужны?

Feature flags (или условная компиляция) — это механизм, позволяющий включать или отключать определённые части кода на этапе сборки. В Rust это реализуется через систему features в Cargo.toml и директивы #[cfg(feature = "...")].

Зачем это нужно? Вот три главных сценария:

  • Модульность библиотек — пользователь сам решает, какие функции ему нужны.
  • Экспериментальные фичи — можно тестировать новую функциональность без риска для продакшена.
  • Платформозависимость — разный код для Windows, Linux и macOS.

В отличие от runtime-флагов (которые проверяются во время выполнения), Cargo features работают на этапе компиляции. Это значит, что неиспользуемый код даже не попадает в бинарник — вы получаете максимальную производительность и минимальный размер.

Как работают feature flags в Rust: базовый синтаксис

Давайте сразу к коду. Предположим, мы пишем библиотеку image_tools с опциональной поддержкой WebP.

Сначала определяем feature в Cargo.toml:

[package]
name = "image_tools"
version = "0.1.0"

[features]
default = ["png"]
png = []
webp = []

Теперь в коде используем атрибут #[cfg]:

// src/lib.rs

#[cfg(feature = "png")]
pub mod png_processor;

#[cfg(feature = "webp")]
pub mod webp_processor;

pub mod common;

// Внутри модуля:
#[cfg(feature = "webp")]
pub fn encode_webp(data: &[u8]) -> Vec<u8> {
    // реализация для WebP
    println!("Encoding WebP...");
    data.to_vec()
}

#[cfg(not(feature = "webp"))]
pub fn encode_webp(data: &[u8]) -> Vec<u8> {
    panic!("WebP support not enabled. Enable 'webp' feature.");
}

Обратите внимание на хитрый трюк: мы предоставляем заглушку для случая, когда фича отключена. Это позволяет сохранить API единым, но при попытке использовать неподдерживаемую функцию — получить понятную ошибку.

Продвинутые техники: комбинации и зависимости

Feature flags в Rust позволяют создавать сложные иерархии. Например, вы можете сделать feature "full", которая включает сразу несколько:

[features]
default = ["basic"]
basic = ["resize", "crop"]
advanced = ["color_correction", "filters"]
full = ["basic", "advanced", "webp"]

А ещё можно указать зависимости от внешних крейтов:

[features]
webp = ["dep:libwebp-sys"]  # включает системную зависимость

[dependencies]
libwebp-sys = { version = "0.1", optional = true }

Здесь optional = true говорит Cargo, что крейт подключается только при активации соответствующей фичи. Это экономит время сборки и место на диске.

Feature flags в реальном проекте: пример HTTP-сервера

Давайте представим, что мы пишем мини-фреймворк для HTTP-серверов. Он должен поддерживать:

  • Базовый роутинг (обязательно)
  • WebSocket (опционально)
  • SSL/TLS (опционально)
  • Логирование запросов (опционально)

Вот как это выглядит в Cargo.toml:

[features]
default = ["logging"]
websocket = ["dep:tokio-tungstenite"]
ssl = ["dep:native-tls"]
logging = []

[dependencies]
tokio = "1.0"
tokio-tungstenite = { version = "0.18", optional = true }
native-tls = { version = "0.2", optional = true }

А теперь код сервера с условной компиляцией:

// src/server.rs

use tokio::net::TcpListener;

pub struct ServerConfig {
    pub host: String,
    pub port: u16,
    #[cfg(feature = "ssl")]
    pub ssl_cert_path: Option<String>,
}

impl ServerConfig {
    pub fn new(host: &str, port: u16) -> Self {
        Self {
            host: host.to_string(),
            port,
            #[cfg(feature = "ssl")]
            ssl_cert_path: None,
        }
    }

    #[cfg(feature = "ssl")]
    pub fn with_ssl(mut self, cert_path: &str) -> Self {
        self.ssl_cert_path = Some(cert_path.to_string());
        self
    }
}

pub async fn start_server(config: ServerConfig) {
    let addr = format!("{}:{}", config.host, config.port);
    let listener = TcpListener::bind(&addr).await.unwrap();

    println!("Server listening on {}", addr);

    loop {
        let (socket, _) = listener.accept().await.unwrap();

        #[cfg(feature = "logging")]
        println!("New connection from {:?}", socket.peer_addr());

        // Обработка запроса
        handle_connection(socket, &config).await;
    }
}

#[cfg(feature = "ssl")]
async fn handle_connection(socket: tokio::net::TcpStream, config: &ServerConfig) {
    if let Some(cert_path) = &config.ssl_cert_path {
        // SSL-рукопожатие
        let tls_acceptor = native_tls::TlsAcceptor::new(
            native_tls::Identity::from_pkcs12(&std::fs::read(cert_path).unwrap(), "password").unwrap()
        ).unwrap();
        let tls_stream = tokio_native_tls::TlsAcceptor::from(tls_acceptor);
        let _ = tls_stream.accept(socket).await;
    }
}

#[cfg(not(feature = "ssl"))]
async fn handle_connection(socket: tokio::net::TcpStream, _config: &ServerConfig) {
    // Обычная обработка без шифрования
}

Теперь пользователь может собрать сервер с нужным набором фич:

# Только базовый сервер
cargo build --no-default-features

# С WebSocket и логами
cargo build --features "websocket,logging"

# Полная версия
cargo build --features "websocket,ssl,logging"

Тестирование с feature flags

Одна из частых проблем — как тестировать код, который зависит от feature flags? В Rust есть элегантное решение: вы можете указать разные наборы фич

Блоги

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