Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu Cargo features là gì — cơ chế toggle code và dependency tại compile-time, cho phép một crate phục vụ nhiều use case khác nhau với cùng một codebase.
- Viết được section
[features]trongCargo.toml: khai báo feature name,default, mối quan hệ feature gọi feature khác. - Tạo optional dependency bằng
optional = truevà bật nó qua feature flag. - Gate code bằng
#[cfg(feature = "x")]ở mức function, struct, module,usestatement. - Biết các flag CLI:
--features foo,bar,--no-default-features,--all-features. - Hiểu feature unification: trong dependency graph, feature được gộp union — bật ở một crate là bật cho tất cả crate dùng dep đó.
- Áp dụng pattern
serdeoptional — kinh điển trong ecosystem Rust. - Dùng namespaced features với prefix
dep:(Cargo 1.60+) để tránh đụng tên giữa feature và optional dep.
Features Là Gì
Feature trong Cargo là một flag tên (string identifier) gắn với crate. Khi feature được bật, Cargo sẽ: (1) compile thêm code được gate #[cfg(feature = "name")], (2) kéo thêm optional dependency mà feature đó kích hoạt, (3) bật các feature khác mà feature này phụ thuộc.
Mục đích: cho phép một crate phục vụ nhiều consumer khác nhau mà không nhồi tất cả code và dependency vào một build duy nhất. Ví dụ thực tế:
- Crate
tokiocó hàng chục feature (rt,rt-multi-thread,net,fs,macros,full...) — user chỉ bật feature cần dùng, binary nhẹ hơn nhiều so vớifull. - Crate
reqwestcódefault-tls,rustls-tls,native-tls— user chọn TLS backend, không phải kéo cả 3. - Crate library tự viết có thể có feature
serdeđể optional implSerialize/Deserializecho type nội bộ mà không bắt user kéoserdenếu không cần.
Quan trọng: feature là compile-time, không phải runtime. Một khi build xong, không thể bật/tắt feature qua biến môi trường hay flag runtime — phải rebuild với flag khác. Khác hẳn với feature flag SaaS kiểu LaunchDarkly/Unleash (runtime toggle qua HTTP).
Một nguyên tắc thiết kế cốt lõi của Cargo: feature phải additive — bật thêm feature chỉ thêm code/dep, không bao giờ thay đổi hoặc xoá. Vi phạm nguyên tắc này (vd feature no-std xoá std) sẽ phá feature unification ở bước sau.
Cú Pháp [features] Trong Cargo.toml
Khai báo feature trong section [features] của Cargo.toml. Mỗi key là tên feature, value là list các thứ feature đó kích hoạt (feature khác hoặc optional dep):
[package]
name = "my-crate"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", optional = true }
tokio = { version = "1", optional = true, features = ["rt"] }
log = "0.4"
[features]
# default = list feature bật khi user không chỉ định gì
default = ["std"]
# Feature đơn giản — chỉ gate code, không kéo dep
std = []
# Feature kéo optional dep "serde" và bật feature "derive" của nó
serialize = ["dep:serde", "serde/derive"]
# Feature gọi feature khác
async = ["dep:tokio"]
full = ["serialize", "async"]
Giải thích từng phần:
default = ["std"]— featurestdtự động bật khi user chỉ ghimy-crate = "0.1"không kèm option. User có thể tắt bằng--no-default-features.std = []— feature thuần, không phụ thuộc gì, chỉ tồn tại để gate code bằng#[cfg(feature = "std")]."dep:serde"— kích hoạt optional dependency tênserde(cú pháp namespaced, sẽ bàn ở bước 8). Không códep:thì Cargo tự tạo feature ngầm trùng tên dep — gây nhầm lẫn."serde/derive"— bật featurederivecủa crateserde(cú phápcrate/feature).full = ["serialize", "async"]— feature gom nhóm, bật cả hai cùng lúc. Idiom phổ biến cho crate lớn (vdtokio = { version = "1", features = ["full"] }).
Tên feature theo convention: lowercase, kebab-case (rt-multi-thread), tránh underscore. Không được trùng với tên dependency (trừ khi cố ý dùng cú pháp ngầm — không khuyến nghị).
Optional Dependency
Optional dependency là dep được khai báo trong [dependencies] nhưng không tự động kéo về khi build. Chỉ kéo khi user bật feature kích hoạt nó. Cú pháp:
[dependencies]
# Required — luôn được build
log = "0.4"
# Optional — chỉ build khi feature "serialize" bật
serde = { version = "1", optional = true, features = ["derive"] }
# Optional + có version + có feature riêng
chrono = { version = "0.4", optional = true, default-features = false, features = ["clock"] }
[features]
serialize = ["dep:serde"]
time = ["dep:chrono"]
Khi user thêm crate vào project mà không bật feature serialize, Cargo không tải về serde, không compile nó — build nhanh hơn, binary nhỏ hơn. Đây là lý do serde trong rất nhiều crate được đặt optional: user chỉ cần Serialize/Deserialize trong một số trường hợp cụ thể.
Lưu ý: optional dep vẫn xuất hiện trong Cargo.lock và resolver xét tới khi tính dependency graph (để chọn version compatible). Nó chỉ không bị compile/link khi feature tương ứng tắt.
Pitfall thường gặp: quên thêm optional = true nhưng vẫn list trong feature — Cargo sẽ build dep đó dù feature tắt, vì nó là dep required mặc định. Luôn cặp đôi optional = true + feature kích hoạt qua dep:.
cfg Gate Code Theo Feature
Trong source code Rust, dùng attribute #[cfg(feature = "name")] để compile có điều kiện. Compiler chỉ giữ block code khi feature được bật, ngược lại xoá hoàn toàn (như chưa từng tồn tại):
// Chỉ compile khi feature "serialize" bật
#[cfg(feature = "serialize")]
use serde::{Serialize, Deserialize};
#[cfg(feature = "serialize")]
#[derive(Serialize, Deserialize)]
pub struct User {
pub name: String,
pub age: u32,
}
#[cfg(not(feature = "serialize"))]
pub struct User {
pub name: String,
pub age: u32,
}
// Gate function
#[cfg(feature = "async")]
pub async fn fetch_user(id: u32) -> Result<User, Error> {
// ... gọi tokio
todo!()
}
// Gate cả module
#[cfg(feature = "experimental")]
pub mod experimental {
pub fn new_api() { /* ... */ }
}
// Kết hợp điều kiện
#[cfg(all(feature = "serialize", feature = "async"))]
pub async fn save_to_disk(u: &User) { /* cần cả 2 feature */ }
#[cfg(any(feature = "json", feature = "yaml"))]
pub fn format_output(u: &User) -> String { /* json hoặc yaml */ todo!() }
Variants quan trọng:
#[cfg(feature = "x")]— featurexbật.#[cfg(not(feature = "x"))]— featurextắt (cặp với cái trên để tạo 2 implementation alternative).#[cfg(all(feature = "a", feature = "b"))]— cả hai bật.#[cfg(any(feature = "a", feature = "b"))]— ít nhất một bật.cfg_attr(feature = "x", derive(Debug))]— apply derive macro có điều kiện.
Có thể gate use statement, function, struct, enum, impl block, mod, thậm chí một expression bằng macro cfg!(). Tip: nếu một type có 2 phiên bản (có serde và không), cân nhắc tách thành 2 file type_with_serde.rs / type_no_serde.rs và gate ở mức mod để code rõ ràng hơn.
Bật/Tắt Khi cargo build
Có hai chỗ điều khiển feature: (1) CLI khi build crate hiện tại, (2) trong Cargo.toml của crate khác khi depend.
CLI flag khi cargo build, cargo run, cargo test:
# Build với default features (std)
cargo build
# Bật thêm features serialize và async (cộng dồn với default)
cargo build --features "serialize async"
# hoặc dùng dấu phẩy
cargo build --features serialize,async
# Tắt default features, chỉ bật cái chỉ định
cargo build --no-default-features --features serialize
# Tắt tất cả default, build bare-bone
cargo build --no-default-features
# Bật TẤT CẢ features (CI test pattern)
cargo build --all-features
# Test với feature set khác nhau
cargo test --no-default-features
cargo test --features full
cargo test --all-features
Trong Cargo.toml của crate consumer:
[dependencies]
# Default features (kéo theo "std")
my-crate = "0.1"
# Chỉ bật feature cần dùng, tắt default
my-crate = { version = "0.1", default-features = false, features = ["serialize"] }
# Bật thêm feature ngoài default
my-crate = { version = "0.1", features = ["async", "experimental"] }
# Ví dụ thực tế: tokio chỉ với runtime + macros, không kéo "full"
tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] }
Trong CI khuyến nghị chạy ít nhất 3 build matrix: --no-default-features, default, --all-features để bắt sớm trường hợp gate sai (vd use serde::Serialize không gate #[cfg(feature = "serialize")] → build fail khi tắt feature).
Feature Unification
Đây là cơ chế quan trọng nhưng dễ gây bất ngờ. Trong một dependency graph, một crate có thể được nhiều crate khác cùng dùng. Mỗi crate có thể yêu cầu feature khác nhau. Cargo không build crate đó nhiều lần với các bộ feature khác nhau — thay vào đó nó gộp union tất cả feature yêu cầu và build một lần với union set.
Ví dụ: project app dùng cả crate-a và crate-b, cả hai đều phụ thuộc serde nhưng với feature khác nhau:
# crate-a/Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
# crate-b/Cargo.toml
[dependencies]
serde = { version = "1", features = ["rc"] }
# app/Cargo.toml
[dependencies]
crate-a = "0.1"
crate-b = "0.1"
Cargo resolve: serde được build một lần với feature set = {"derive", "rc"} (union). Cả crate-a và crate-b đều link tới đúng instance đó.
Hệ quả thực tế:
- Feature không thể "tắt": một khi bất kỳ crate nào trong dep graph bật feature, tất cả crate dùng dep đó đều thấy feature bật. Đây là lý do feature phải additive — nếu một feature thay đổi behavior, một dependency vô tình bật nó có thể phá code của bạn.
- Build minimal khó kiểm soát: bạn nghĩ tắt feature ở
Cargo.tomlcủa bạn là đủ, nhưng nếu một transitive dep bật nó, dep đó vẫn được build với feature đó. - Phải test với
--all-featurestrong CI để chắc chắn code không bị fail khi consumer bật feature khác.
Bug kinh điển: thiết kế feature no-std = tắt std. Nếu một dep nào trong graph cần std, union sẽ bao gồm cả std và no-std bật → code conflict. Đúng cách: tắt default-features và để feature std additive.
Resolver v2 (default từ edition 2021) cải thiện một chút: feature có thể tách giữa target khác nhau (build-dependencies và normal dependencies có thể có feature set khác). Nhưng trong cùng một target, unification vẫn áp dụng. Bật resolver v2 với resolver = "2" ở [workspace].
Pattern serde Optional & Namespaced dep:
Pattern phổ biến nhất ecosystem Rust: crate library cung cấp type của mình, optional impl Serialize/Deserialize sau feature serde để consumer không cần kéo serde nếu chỉ dùng type cho mục đích khác.
Ví dụ một lib crate geo-types giả lập:
[package]
name = "geo-types"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { version = "1", optional = true, features = ["derive"] }
[features]
default = []
serde = ["dep:serde"]
// src/lib.rs
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, PartialEq)]
pub struct Polygon {
pub points: Vec<Point>,
}
Consumer chỉ dùng Point trong code thuần Rust thì viết:
geo-types = "0.1" # không kéo serde
Consumer cần JSON serialization:
geo-types = { version = "0.1", features = ["serde"] }
serde_json = "1"
Về namespaced features (Cargo 1.60+): trước Cargo 1.60, mỗi optional dep tự động tạo một feature ngầm trùng tên — bật feature serde ≡ bật optional dep serde. Hệ quả: không thể có feature tên serde mà không trùng với dep name, gây xung đột khi feature muốn làm thêm việc khác.
Cú pháp dep: giải quyết bằng cách ẩn feature ngầm tự động — chỉ kích hoạt dep khi explicit dep:serde:
[dependencies]
serde = { version = "1", optional = true }
tokio = { version = "1", optional = true }
[features]
# Cách CŨ (pre-1.60): tên feature == tên dep, gây nhập nhằng
# serde = [] # sai — chỉ đổi tên không kéo dep
# serde = ["serde"] # sai cú pháp self-reference
# Cách MỚI (Cargo 1.60+): tách bạch feature name và dep activation
serde = ["dep:serde", "dep:serde_json"] # feature "serde" kéo cả 2 dep
async = ["dep:tokio"]
full = ["serde", "async"]
Sử dụng dep: còn cho phép một feature kéo nhiều optional dep, hoặc kéo dep với tên feature khác tên dep. Khuyến nghị: với crate mới, luôn dùng dep: prefix để code rõ ràng và tránh feature ngầm. Cargo cảnh báo deprecation cho cú pháp cũ trong tương lai.
Tổng Kết
- Cargo features = toggle compile-time cho code và dependency. Không phải runtime flag.
- Khai báo trong
[features]:default = ["x"],x = ["dep:foo", "crate/feature"]. Feature phải additive, không bao giờ thay đổi/xoá behavior. - Optional dependency qua
foo = { version = "1", optional = true }— không build cho tới khi feature kích hoạt quadep:foo. - Gate code bằng
#[cfg(feature = "x")],#[cfg(not(...))],#[cfg(all(...))],#[cfg(any(...))],#[cfg_attr(...)]. Apply được cho function, struct, mod, impl,use. - CLI:
--features a,b,--no-default-features,--all-features. Consumer trongCargo.toml:{ version, default-features = false, features = [...] }. - Feature unification: trong dep graph, feature được gộp union. Một crate yêu cầu feature → tất cả crate dùng dep đó đều thấy bật. Lý do feature phải additive.
- Pattern
serdeoptional kinh điển:#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]trên type, consumer chọn bật khi cần. - Namespaced features với prefix
dep:(Cargo 1.60+) — tách bạch giữa feature name và optional dep, tránh feature ngầm trùng tên. Luôn dùng cho crate mới. - CI nên build với 3 matrix tối thiểu:
--no-default-features, default,--all-featuresđể bắt sớm gate sai.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Phân biệt Cargo feature flag với feature flag runtime kiểu LaunchDarkly. Tại sao Cargo feature không thể đổi sau khi binary được build?
- Viết
Cargo.tomlcho lib cratemy-configcó 2 optional depserde_jsonvàserde_yaml, hai featurejsonvàyamlkích hoạt từng dep, một featurefullbật cả hai. Dùng cú pháp namespaceddep:. - Crate
foocó featurestdtrongdefault. Bạn muốn dùngfootrong môi trường no_std. Viết dòngCargo.tomltương ứng để tắt featurestdvà bật thêm featurealloc. - Một lib có code
#[cfg(feature = "x")] use serde::Serialize;nhưng dòngderive(Serialize)ở dưới không gatecfg. Build--no-default-featuressẽ fail vì sao? Cách fix? - Project
appdùngcrate-a(yêu cầuserde/derive) vàcrate-b(yêu cầuserde/rc).serdeđược build mấy lần và với feature set gì? Giải thích cơ chế. - Vì sao Cargo 1.60+ khuyến khích cú pháp
"dep:serde"thay vì chỉ"serde"trong feature list? Vấn đề gì xảy ra với cú pháp cũ khi muốn có feature tên trùng với optional dep nhưng làm thêm việc khác?
Đáp án
- Cargo feature = compile-time conditional compilation:
cfgattribute được xử lý lúc compile, code bị gate xoá khỏi binary final. LaunchDarkly = runtime flag: code luôn có trong binary, check biến HTTP/env lúc chạy. Cargo feature không đổi sau build vì binary đã không còn chứa code bị tắt; muốn bật feature khác phải recompile. Trade-off: Cargo feature cho binary nhỏ hơn, runtime flag linh hoạt hơn nhưng overhead. [dependencies] serde_json = { version = "1", optional = true } serde_yaml = { version = "0.9", optional = true } [features] default = [] json = ["dep:serde_json"] yaml = ["dep:serde_yaml"] full = ["json", "yaml"]. Note: featurefullgọi 2 feature khác chứ không list lạidep:— sạch hơn.foo = { version = "1", default-features = false, features = ["alloc"] }. Cốt lõi làdefault-features = falseđể tắt cảdefaultset (gồmstd), rồi explicit list các feature cần.- Vì
use serde::Serializeđã gate cẩn thận (chỉ import khi feature bật), nhưng#[derive(Serialize)]ở dưới không gate → khi build--no-default-features, derive macro tham chiếu tớiSerializetrait nhưng trait đó không được import (feature tắt) và crateserdekhông có trong build (optional dep không kéo) → compile error "cannot find derive macro Serialize". Fix: dùng#[cfg_attr(feature = "x", derive(Serialize, Deserialize))]để cả import và derive cùng được gate đồng bộ. serdeđược build một lần với feature set union ={"derive", "rc"}(cộng default features củaserdenữa nếu không tắt). Đây là feature unification: Cargo tránh build trùng lặp, gộp tất cả yêu cầu từ dep graph thành một bộ duy nhất. Cảcrate-avàcrate-bđều link tới cùng instance — không có chuyện hai versionserdekhác nhau cho hai crate.- Cú pháp cũ tạo feature ngầm tự động trùng tên optional dep — nên feature name luôn khớp 1-1 với dep name. Không thể có feature
serdemà không bật depserde, hoặc featureserdekéo thêmserde_json. Cú pháp"dep:serde"ẩn feature ngầm này → cho phép tách bạch: feature có thể tên gì cũng được, list explicit dep cần kích hoạt quadep:. Còn cho phép một feature kéo nhiều dep, hoặc kéo dep với tên feature hoàn toàn khác.
Bài Tiếp Theo
Bài 263: dev-dependencies & build-dependencies — giới thiệu hai loại dependency đặc biệt: [dev-dependencies] chỉ compile khi chạy cargo test/cargo bench/cargo run --example (vd tempfile, pretty_assertions); [build-dependencies] chỉ dùng trong build.rs để generate code hoặc link C library (vd cc, bindgen). Bạn sẽ thấy chúng không có trong dep graph chính của user — khác với feature optional dep bạn vừa học ở bài này.
