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 есть элегантное решение: вы можете указать разные наборы фич