Software real raramente é monolítico. Uma biblioteca de criptografia precisa de implementações diferentes para ARM e x86. Um servidor precisa de backends diferentes para SQLite em desenvolvimento e PostgreSQL em produção. Uma crate pública precisa de funcionalidades opcionais que não forçam dependências pesadas em quem não as usa.
Rust resolve esses problemas com um sistema elegante de compilação condicional — feature flags, atributos cfg, e o mecanismo de features do Cargo. A diferença em relação a #ifdef do C é fundamental: em Rust, código excluído por flags é verificado sintaticamente mas não compilado, e o sistema de tipos ainda protege contra combinações inválidas.
cfg — compilação condicional básica
O atributo #[cfg(...)] e a macro cfg!(...) controlam quais partes do código são compiladas:
// Atributo: exclui o item inteiro se a condição não for satisfeita
#[cfg(target_os = "linux")]
fn obter_info_linux() -> String {
std::fs::read_to_string("/etc/os-release")
.unwrap_or_else(|_| "Não disponível".to_string())
}
#[cfg(target_os = "macos")]
fn obter_info_macos() -> String {
"macOS".to_string()
}
#[cfg(windows)]
fn obter_info_windows() -> String {
"Windows".to_string()
}
// cfg! macro: condição avaliada em tempo de compilação, retorna bool
fn nome_plataforma() -> &'static str {
if cfg!(target_os = "linux") {
"Linux"
} else if cfg!(target_os = "macos") {
"macOS"
} else if cfg!(windows) {
"Windows"
} else {
"Plataforma desconhecida"
}
}
// cfg em blocos de código
fn configurar_sistema() {
#[cfg(debug_assertions)]
println!("Modo debug — validações extras ativas");
#[cfg(not(debug_assertions))]
println!("Modo release — performance máxima");
// Código específico de arquitetura
#[cfg(target_arch = "x86_64")]
println!("Arquitetura: x86_64");
#[cfg(target_arch = "aarch64")]
println!("Arquitetura: ARM64 (Apple Silicon, Raspberry Pi 4)");
}
// cfg em structs — campos condicionais
pub struct Metricas {
pub requisicoes: u64,
pub erros: u64,
// Campo só existe em builds de desenvolvimento
#[cfg(debug_assertions)]
pub tempo_total_ms: u64,
#[cfg(debug_assertions)]
pub chamadas_db: u64,
}
impl Metricas {
pub fn nova() -> Self {
Metricas {
requisicoes: 0,
erros: 0,
#[cfg(debug_assertions)]
tempo_total_ms: 0,
#[cfg(debug_assertions)]
chamadas_db: 0,
}
}
pub fn registrar_requisicao(&mut self, _tempo_ms: u64) {
self.requisicoes += 1;
#[cfg(debug_assertions)]
{
self.tempo_total_ms += _tempo_ms;
}
}
}
fn main() {
println!("Plataforma: {}", nome_plataforma());
configurar_sistema();
let mut metricas = Metricas::nova();
metricas.registrar_requisicao(42);
println!("Requisições: {}", metricas.requisicoes);
#[cfg(debug_assertions)]
println!("Tempo total (debug): {}ms", metricas.tempo_total_ms);
}
Predicados cfg disponíveis
// Sistema operacional
#[cfg(target_os = "linux")]
#[cfg(target_os = "macos")]
#[cfg(target_os = "windows")]
#[cfg(target_os = "android")]
#[cfg(target_os = "ios")]
#[cfg(unix)] // Linux, macOS, iOS, Android, ...
#[cfg(windows)] // Windows (todas versões)
// Arquitetura do processador
#[cfg(target_arch = "x86_64")]
#[cfg(target_arch = "x86")]
#[cfg(target_arch = "aarch64")]
#[cfg(target_arch = "arm")]
#[cfg(target_arch = "wasm32")]
#[cfg(target_arch = "riscv64")]
// Família de CPU
#[cfg(target_family = "unix")]
#[cfg(target_family = "windows")]
#[cfg(target_family = "wasm")]
// Ponteiro (tamanho em bits)
#[cfg(target_pointer_width = "64")]
#[cfg(target_pointer_width = "32")]
// Endianness
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]
// Features de CPU (ativadas via RUSTFLAGS ou target-cpu)
#[cfg(target_feature = "avx2")]
#[cfg(target_feature = "neon")] // ARM SIMD
// Modo debug vs release
#[cfg(debug_assertions)] // true em debug
#[cfg(not(debug_assertions))] // true em release
// Presença de features Cargo (veremos a seguir)
#[cfg(feature = "minha_feature")]
// Combinações com operadores lógicos
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
#[cfg(all(unix, not(target_os = "macos")))]
Feature flags no Cargo.toml
Features permitem funcionalidades opcionais em crates. Usuários da crate escolhem quais ativar:
Cargo.toml de uma biblioteca:
[package]
name = "minha_lib"
version = "1.0.0"
edition = "2021"
[features]
# Feature padrão — ativada automaticamente se não especificado
default = ["json", "logging"]
# Features individuais
json = ["dep:serde", "dep:serde_json"]
yaml = ["dep:serde", "dep:serde_yaml"]
logging = ["dep:tracing"]
async = ["dep:tokio"]
tls = ["dep:rustls", "dep:tokio-rustls", "async"]
full = ["json", "yaml", "logging", "async", "tls"]
# Feature de desenvolvimento
dev-utils = []
[dependencies]
# Dependências opcionais — só compiladas se a feature for ativa
serde = { version = "1", optional = true, features = ["derive"] }
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
tracing = { version = "0.1", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }
rustls = { version = "0.22", optional = true }
tokio-rustls = { version = "0.25", optional = true }
src/lib.rs:
//! # Minha Lib
//!
//! ## Features
//!
//! - `json` — Serialização JSON (padrão)
//! - `yaml` — Serialização YAML
//! - `async` — Runtime assíncrono com Tokio
//! - `tls` — Suporte TLS (requer `async`)
//! - `full` — Todas as features
#[cfg(feature = "json")]
pub mod serializacao_json {
use serde::{Deserialize, Serialize};
pub fn para_json<T: Serialize>(valor: &T) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(valor)
}
pub fn de_json<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, serde_json::Error> {
serde_json::from_str(s)
}
}
#[cfg(feature = "yaml")]
pub mod serializacao_yaml {
use serde::{Deserialize, Serialize};
pub fn para_yaml<T: Serialize>(valor: &T) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(valor)
}
pub fn de_yaml<T: for<'de> Deserialize<'de>>(s: &str) -> Result<T, serde_yaml::Error> {
serde_yaml::from_str(s)
}
}
#[cfg(feature = "logging")]
pub fn inicializar_logging() {
tracing_subscriber::fmt::init();
tracing::info!("Logging inicializado");
}
#[cfg(not(feature = "logging"))]
pub fn inicializar_logging() {
// Sem logging — função vazia
}
// API unificada que funciona com qualquer feature ativa
pub struct Cliente {
base_url: String,
}
impl Cliente {
pub fn novo(base_url: &str) -> Self {
#[cfg(feature = "logging")]
tracing::debug!("Criando cliente para {base_url}");
Cliente { base_url: base_url.to_string() }
}
#[cfg(feature = "async")]
pub async fn get(&self, caminho: &str) -> Result<String, reqwest::Error> {
let url = format!("{}{}", self.base_url, caminho);
#[cfg(feature = "logging")]
tracing::info!("GET {url}");
reqwest::get(&url).await?.text().await
}
#[cfg(not(feature = "async"))]
pub fn get_sync(&self, caminho: &str) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}{}", self.base_url, caminho);
Ok(ureq::get(&url).call()?.into_string()?)
}
}
// Feature de desenvolvimento — utilitários de teste
#[cfg(feature = "dev-utils")]
pub mod dev {
pub fn gerar_dados_teste(n: usize) -> Vec<String> {
(0..n).map(|i| format!("item_{i}")).collect()
}
pub fn simular_latencia(ms: u64) {
std::thread::sleep(std::time::Duration::from_millis(ms));
}
}
Usando features como consumidor
# Cargo.toml do projeto que usa minha_lib
[dependencies]
# Sem especificar features — usa as defaults
minha_lib = "1"
# Desabilita defaults, ativa features específicas
minha_lib = { version = "1", default-features = false, features = ["json"] }
# Adiciona features às defaults
minha_lib = { version = "1", features = ["yaml", "async"] }
# Feature completa
minha_lib = { version = "1", features = ["full"] }
# Na linha de comando
cargo build --features "yaml async"
cargo build --no-default-features --features "json"
cargo build --all-features
# Testar com features específicas
cargo test --features "dev-utils"
cargo test --all-features
Padrão: detecção de feature em tempo de compilação
Às vezes você precisa de comportamento diferente dependendo de quais features estão ativas, mas quer uma API unificada:
// Trait que abstrai a serialização
pub trait Serializador: Send + Sync {
fn serializar<T: serde::Serialize>(&self, valor: &T)
-> Result<Vec<u8>, Box<dyn std::error::Error>>;
fn desserializar<T: for<'de> serde::Deserialize<'de>>(
&self,
dados: &[u8],
) -> Result<T, Box<dyn std::error::Error>>;
fn nome(&self) -> &str;
}
#[cfg(feature = "json")]
pub struct SerializadorJson;
#[cfg(feature = "json")]
impl Serializador for SerializadorJson {
fn serializar<T: serde::Serialize>(&self, valor: &T)
-> Result<Vec<u8>, Box<dyn std::error::Error>>
{
Ok(serde_json::to_vec(valor)?)
}
fn desserializar<T: for<'de> serde::Deserialize<'de>>(
&self,
dados: &[u8],
) -> Result<T, Box<dyn std::error::Error>>
{
Ok(serde_json::from_slice(dados)?)
}
fn nome(&self) -> &str { "JSON" }
}
#[cfg(feature = "yaml")]
pub struct SerializadorYaml;
#[cfg(feature = "yaml")]
impl Serializador for SerializadorYaml {
fn serializar<T: serde::Serialize>(&self, valor: &T)
-> Result<Vec<u8>, Box<dyn std::error::Error>>
{
Ok(serde_yaml::to_string(valor)?.into_bytes())
}
fn desserializar<T: for<'de> serde::Deserialize<'de>>(
&self,
dados: &[u8],
) -> Result<T, Box<dyn std::error::Error>>
{
Ok(serde_yaml::from_slice(dados)?)
}
fn nome(&self) -> &str { "YAML" }
}
// Seleciona o melhor serializador disponível em tempo de compilação
pub fn serializador_padrao() -> Box<dyn Serializador> {
#[cfg(feature = "json")]
return Box::new(SerializadorJson);
#[cfg(all(feature = "yaml", not(feature = "json")))]
return Box::new(SerializadorYaml);
#[cfg(not(any(feature = "json", feature = "yaml")))]
compile_error!(
"Pelo menos uma feature de serialização deve estar ativa: 'json' ou 'yaml'"
);
}
cfg_if! — condicionais mais legíveis
[dependencies]
cfg-if = "1"
use cfg_if::cfg_if;
cfg_if! {
if #[cfg(target_os = "linux")] {
use std::os::unix::fs::PermissionsExt;
pub fn permissoes_arquivo(caminho: &str) -> String {
let meta = std::fs::metadata(caminho).unwrap();
format!("{:o}", meta.permissions().mode() & 0o777)
}
pub fn e_executavel(caminho: &str) -> bool {
let meta = std::fs::metadata(caminho).unwrap();
meta.permissions().mode() & 0o111 != 0
}
} else if #[cfg(target_os = "macos")] {
pub fn permissoes_arquivo(caminho: &str) -> String {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(caminho).unwrap();
format!("{:o}", meta.permissions().mode() & 0o777)
}
pub fn e_executavel(caminho: &str) -> bool {
use std::os::unix::fs::PermissionsExt;
let meta = std::fs::metadata(caminho).unwrap();
meta.permissions().mode() & 0o111 != 0
}
} else if #[cfg(windows)] {
pub fn permissoes_arquivo(_caminho: &str) -> String {
// Windows não usa o modelo Unix de permissões
"N/A (Windows)".to_string()
}
pub fn e_executavel(caminho: &str) -> bool {
// No Windows, extensão determina executabilidade
caminho.ends_with(".exe") ||
caminho.ends_with(".bat") ||
caminho.ends_with(".cmd")
}
} else {
pub fn permissoes_arquivo(_caminho: &str) -> String {
"Plataforma não suportada".to_string()
}
pub fn e_executavel(_caminho: &str) -> bool {
false
}
}
}
fn main() {
println!("Plataforma: {}", std::env::consts::OS);
println!("Arch: {}", std::env::consts::ARCH);
}
Flags customizadas via build.rs
O script de build pode definir flags cfg baseadas em detecção do ambiente:
build.rs:
fn main() {
// Detecta capacidades do compilador
detectar_versao_rust();
// Detecta recursos do sistema
detectar_recursos_sistema();
// Detecta bibliotecas externas
detectar_biblioteca_externa();
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=DATABASE_URL");
}
fn detectar_versao_rust() {
// Detecta versão do compilador
let versao = rustc_version::version().unwrap();
if versao.major >= 1 && versao.minor >= 70 {
println!("cargo:rustc-cfg=rust_1_70");
}
if versao.major >= 1 && versao.minor >= 75 {
println!("cargo:rustc-cfg=rust_1_75");
}
}
fn detectar_recursos_sistema() {
// Verifica se estamos em CI
if std::env::var("CI").is_ok() {
println!("cargo:rustc-cfg=ambiente_ci");
}
// Verifica DATABASE_URL para backend de banco
if let Ok(url) = std::env::var("DATABASE_URL") {
if url.starts_with("postgres") {
println!("cargo:rustc-cfg=backend_postgres");
} else if url.starts_with("sqlite") {
println!("cargo:rustc-cfg=backend_sqlite");
}
}
// Detecta número de CPUs para otimizações
let num_cpus = num_cpus::get();
if num_cpus >= 8 {
println!("cargo:rustc-cfg=muitos_cpus");
}
}
fn detectar_biblioteca_externa() {
// Verifica se libssl está disponível
if pkg_config::probe_library("openssl").is_ok() {
println!("cargo:rustc-cfg=tem_openssl");
}
}
Adicione ao Cargo.toml:
[build-dependencies]
rustc-version = "0.4"
num_cpus = "1"
pkg-config = "0.3"
Uso das flags customizadas:
#[cfg(rust_1_75)]
fn usar_feature_nova() {
// Código que usa API introduzida no Rust 1.75
}
#[cfg(backend_postgres)]
mod db {
pub use sqlx::PgPool as Pool;
pub async fn conectar(url: &str) -> Pool {
sqlx::PgPool::connect(url).await.unwrap()
}
}
#[cfg(backend_sqlite)]
mod db {
pub use sqlx::SqlitePool as Pool;
pub async fn conectar(url: &str) -> Pool {
sqlx::SqlitePool::connect(url).await.unwrap()
}
}
#[cfg(ambiente_ci)]
fn configurar_para_ci() {
println!("Rodando em CI — ajustando configurações");
}
Um projeto completo: biblioteca multi-backend
Vamos construir uma biblioteca de cache que suporta múltiplos backends selecionados por features:
Cargo.toml:
[package]
name = "cache_lib"
version = "0.1.0"
edition = "2021"
[features]
default = ["memoria"]
memoria = []
redis = ["dep:redis", "dep:tokio"]
disco = []
full = ["memoria", "redis", "disco"]
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", optional = true, features = ["full"] }
redis = { version = "0.24", optional = true, features = ["tokio-comp"] }
src/lib.rs:
use std::time::Duration;
use serde::{Serialize, de::DeserializeOwned};
#[derive(Debug)]
pub enum ErroCacheLib {
SerializacaoFalhou(String),
BackendIndisponivel(String),
ChaveNaoEncontrada,
ExpiracaoInvalida,
}
impl std::fmt::Display for ErroCacheLib {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ErroCacheLib::SerializacaoFalhou(e) =>
write!(f, "Falha na serialização: {e}"),
ErroCacheLib::BackendIndisponivel(e) =>
write!(f, "Backend indisponível: {e}"),
ErroCacheLib::ChaveNaoEncontrada =>
write!(f, "Chave não encontrada"),
ErroCacheLib::ExpiracaoInvalida =>
write!(f, "TTL inválido"),
}
}
}
pub trait CacheBackend: Send + Sync {
fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib>;
fn set(&self, chave: &str, valor: &str, ttl: Option<Duration>)
-> Result<(), ErroCacheLib>;
fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib>;
fn clear(&self) -> Result<(), ErroCacheLib>;
fn nome_backend(&self) -> &str;
}
// ── Backend em Memória ───────────────────────────────
#[cfg(feature = "memoria")]
mod backend_memoria {
use super::*;
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::Instant;
struct Entrada {
valor: String,
expira_em: Option<Instant>,
}
pub struct CacheMemoria {
dados: Mutex<HashMap<String, Entrada>>,
}
impl CacheMemoria {
pub fn novo() -> Self {
CacheMemoria {
dados: Mutex::new(HashMap::new()),
}
}
fn limpar_expirados(mapa: &mut HashMap<String, Entrada>) {
let agora = Instant::now();
mapa.retain(|_, v| {
v.expira_em.map(|e| e > agora).unwrap_or(true)
});
}
}
impl CacheBackend for CacheMemoria {
fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
let mut mapa = self.dados.lock()
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
Self::limpar_expirados(&mut mapa);
Ok(mapa.get(chave).map(|e| e.valor.clone()))
}
fn set(
&self,
chave: &str,
valor: &str,
ttl: Option<Duration>,
) -> Result<(), ErroCacheLib> {
let mut mapa = self.dados.lock()
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
mapa.insert(chave.to_string(), Entrada {
valor: valor.to_string(),
expira_em: ttl.map(|d| Instant::now() + d),
});
Ok(())
}
fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
let mut mapa = self.dados.lock()
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
Ok(mapa.remove(chave).is_some())
}
fn clear(&self) -> Result<(), ErroCacheLib> {
let mut mapa = self.dados.lock()
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
mapa.clear();
Ok(())
}
fn nome_backend(&self) -> &str { "Memória" }
}
}
// ── Backend em Disco ─────────────────────────────────
#[cfg(feature = "disco")]
mod backend_disco {
use super::*;
use std::path::{Path, PathBuf};
pub struct CacheDisco {
diretorio: PathBuf,
}
impl CacheDisco {
pub fn novo(dir: &str) -> Result<Self, ErroCacheLib> {
let path = PathBuf::from(dir);
std::fs::create_dir_all(&path)
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
Ok(CacheDisco { diretorio: path })
}
fn caminho_chave(&self, chave: &str) -> PathBuf {
// Sanitiza a chave para uso como nome de arquivo
let nome_seguro = chave
.chars()
.map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' })
.collect::<String>();
self.diretorio.join(format!("{nome_seguro}.cache"))
}
}
impl CacheBackend for CacheDisco {
fn get(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
let path = self.caminho_chave(chave);
match std::fs::read_to_string(&path) {
Ok(conteudo) => Ok(Some(conteudo)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(ErroCacheLib::BackendIndisponivel(e.to_string())),
}
}
fn set(
&self,
chave: &str,
valor: &str,
_ttl: Option<Duration>,
) -> Result<(), ErroCacheLib> {
// TTL não implementado para disco (simplificação)
let path = self.caminho_chave(chave);
std::fs::write(path, valor)
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))
}
fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
let path = self.caminho_chave(chave);
match std::fs::remove_file(&path) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(ErroCacheLib::BackendIndisponivel(e.to_string())),
}
}
fn clear(&self) -> Result<(), ErroCacheLib> {
for entrada in std::fs::read_dir(&self.diretorio)
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?
{
let entrada = entrada
.map_err(|e| ErroCacheLib::BackendIndisponivel(e.to_string()))?;
if entrada.path().extension().and_then(|e| e.to_str()) == Some("cache") {
let _ = std::fs::remove_file(entrada.path());
}
}
Ok(())
}
fn nome_backend(&self) -> &str { "Disco" }
}
}
// ── Interface Pública ─────────────────────────────────
use std::sync::Arc;
pub struct Cache {
backend: Arc<dyn CacheBackend>,
}
impl Cache {
pub fn com_backend(backend: impl CacheBackend + 'static) -> Self {
Cache { backend: Arc::new(backend) }
}
#[cfg(feature = "memoria")]
pub fn em_memoria() -> Self {
Cache::com_backend(backend_memoria::CacheMemoria::novo())
}
#[cfg(feature = "disco")]
pub fn em_disco(dir: &str) -> Result<Self, ErroCacheLib> {
Ok(Cache::com_backend(backend_disco::CacheDisco::novo(dir)?))
}
pub fn get_str(&self, chave: &str) -> Result<Option<String>, ErroCacheLib> {
self.backend.get(chave)
}
pub fn set_str(
&self,
chave: &str,
valor: &str,
ttl: Option<Duration>,
) -> Result<(), ErroCacheLib> {
self.backend.set(chave, valor, ttl)
}
pub fn get<T: DeserializeOwned>(
&self,
chave: &str,
) -> Result<Option<T>, ErroCacheLib> {
match self.backend.get(chave)? {
None => Ok(None),
Some(s) => {
let valor = serde_json::from_str(&s)
.map_err(|e| ErroCacheLib::SerializacaoFalhou(e.to_string()))?;
Ok(Some(valor))
}
}
}
pub fn set<T: Serialize>(
&self,
chave: &str,
valor: &T,
ttl: Option<Duration>,
) -> Result<(), ErroCacheLib> {
let s = serde_json::to_string(valor)
.map_err(|e| ErroCacheLib::SerializacaoFalhou(e.to_string()))?;
self.backend.set(chave, &s, ttl)
}
pub fn delete(&self, chave: &str) -> Result<bool, ErroCacheLib> {
self.backend.delete(chave)
}
pub fn clear(&self) -> Result<(), ErroCacheLib> {
self.backend.clear()
}
pub fn info(&self) -> String {
format!("Cache [backend: {}]", self.backend.nome_backend())
}
}
src/main.rs:
use cache_lib::Cache;
use std::time::Duration;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct UsuarioCache {
id: u32,
nome: String,
email: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Em memória (sempre disponível via feature "memoria")
#[cfg(feature = "memoria")]
{
let cache = Cache::em_memoria();
println!("{}", cache.info());
// Strings simples
cache.set_str("chave1", "valor1", None)?;
cache.set_str("chave2", "temporario", Some(Duration::from_secs(1)))?;
println!("chave1: {:?}", cache.get_str("chave1")?);
println!("chave2: {:?}", cache.get_str("chave2")?);
// Structs serializadas
let usuario = UsuarioCache {
id: 1,
nome: "Ana".to_string(),
email: "ana@ex.com".to_string(),
};
cache.set("usuario:1", &usuario, Some(Duration::from_secs(300)))?;
let recuperado: Option<UsuarioCache> = cache.get("usuario:1")?;
assert_eq!(recuperado.as_ref(), Some(&usuario));
println!("Usuário recuperado: {:?}", recuperado);
// Aguarda expiração
std::thread::sleep(Duration::from_secs(2));
println!("Após expiração: {:?}", cache.get_str("chave2")?);
}
// Disco (só com feature "disco")
#[cfg(feature = "disco")]
{
let cache = Cache::em_disco("/tmp/meu_cache")?;
println!("
{}", cache.info());
cache.set_str("persistente", "valor no disco", None)?;
println!("Lido do disco: {:?}", cache.get_str("persistente")?);
cache.clear()?;
}
Ok(())
}
# Apenas memória (default)
cargo run
# Apenas disco
cargo run --no-default-features --features "disco"
# Ambos
cargo run --features "disco"
# Tudo
cargo run --all-features
Documentando features corretamente
//! # Minha Crate
//!
//! ## Feature Flags
//!
//! Esta crate é configurável via feature flags:
//!
//! | Feature | Padrão | Descrição |
//! |-----------|--------|----------------------------------|
//! | `memoria` | ✓ | Backend em memória |
//! | `disco` | | Backend em disco |
//! | `redis` | | Backend Redis (requer servidor) |
//! | `full` | | Todos os backends |
//!
//! ### Exemplo mínimo
//! ```toml
//! cache_lib = { version = "1", default-features = false, features = ["memoria"] }
//! ```
/// Cria um cache em memória.
///
/// # Panics
/// Nunca.
///
/// # Feature
/// Requer feature `memoria` (padrão).
///
/// # Exemplo
/// ```rust
/// # #[cfg(feature = "memoria")]
/// # {
/// use cache_lib::Cache;
/// let cache = Cache::em_memoria();
/// # }
/// ```
#[cfg(feature = "memoria")]
pub fn em_memoria() -> Cache {
todo!()
}
Exercícios
Exercício 1
Explique a diferença entre #[cfg(...)] e #[cfg_attr(...)].
Ver resposta
✓ Resposta: #[cfg(...)] decide se o item existe: se a condição for falsa, a função, campo ou módulo simplesmente não é compilado. #[cfg_attr(condição, atributo)] decide se um atributo é aplicado — o item existe sempre, mas ganha o atributo só quando a condição vale. O uso mais comum é aplicar derive condicionalmente, como #[cfg_attr(feature = "serde", derive(Serialize))], que só deriva a serialização quando o usuário ativou a feature.
Exercício 2
Por que uma feature deve ser aditiva, e o que dá errado quando não é?
Ver resposta
✓ Resposta: Porque o Cargo unifica as features: se duas dependências do mesmo projeto usam sua crate e uma delas ativa uma feature, ela fica ativa para todas. Uma feature que apenas acrescenta não causa problema nesse cenário. Uma que altera comportamento — muda o algoritmo padrão, troca a precisão, desliga uma validação — quebra o outro consumidor sem que ele tenha pedido nada, e o sintoma aparece longe da causa. Daí a regra: feature acrescenta, nunca substitui nem remove.
Exercício 3
Escreva uma função que tenha implementações diferentes por sistema operacional.
Ver resposta
✓ Resposta:
#[cfg(unix)]
fn diretorio_config() -> PathBuf {
PathBuf::from(std::env::var("HOME").unwrap()).join(".config")
}
#[cfg(windows)]
fn diretorio_config() -> PathBuf {
PathBuf::from(std::env::var("APPDATA").unwrap())
}
Só uma das duas é compilada, e as duas precisam ter a mesma assinatura. O risco a vigiar é a variante que ninguém compila no dia a dia envelhecer sem que se perceba — por isso convém que a integração contínua compile em todas as plataformas suportadas.
Exercício 4
Como declarar uma dependência opcional ligada a uma feature?
Ver resposta
✓ Resposta:
[dependencies]
serde = { version = "1", optional = true }
[features]
default = []
json = ["dep:serde"]
Com optional = true, a crate só é baixada e compilada se a feature for ativada. A sintaxe dep:serde deixa explícito que se trata da dependência — sem ela, o nome da dependência viraria implicitamente uma feature de mesmo nome, o que confunde a interface pública. Manter default enxuto é o que permite a quem usa pagar só pelo que precisa.
Exercício 5
Como garantir que a documentação publicada mostre o código que depende de features?
Ver resposta
✓ Resposta: Declarando no Cargo.toml o que o docs.rs deve ativar:
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
E marcando os itens com #[cfg_attr(docsrs, doc(cfg(feature = "json")))], o que faz a documentação exibir uma etiqueta indicando qual feature é necessária. Sem isso, quem lê a documentação não encontra a função e conclui que ela não existe — quando na verdade ela só não estava ativada na geração.
Fontes e leituras recomendadas
Documentação oficial
- The Cargo Book — Features — A referência completa de feature flags: declaração, features padrão, dependências opcionais e a regra de unificação que faz uma feature ativada por um consumidor valer para todos.
- The Rust Reference — Conditional compilation — Especificação do
cfg: todos os predicados disponíveis, a diferença entrecfgecfg_attr, e como as condições se combinam. - The Cargo Book — Build Scripts — Como o
build.rsfunciona e como emitir flags personalizadas decfgpara detectar ambiente em tempo de compilação.
Ferramentas
- cfg-if — docs.rs — A crate que substitui cadeias de
#[cfg]aninhadas por uma estrutura legível de se-senão. Pequena e sem dependências. - docs.rs — Metadados de build — Como declarar no
Cargo.tomlquais features o docs.rs deve ativar ao gerar a documentação — sem isso, o código condicional não aparece publicado.
Leitura complementar
- Rust API Guidelines — As convenções da comunidade para APIs públicas, incluindo o cuidado com features que alteram comportamento em vez de apenas acrescentar.