Feature của Cargo và biên dịch có điều kiện (#[cfg]) trong Rust
Hãy nghĩ tới việc gọi một chiếc pizza. Ai cũng có đế bánh và sốt cà chua. Sau đó bạn chọn thêm: nấm, thêm phô mai, ô liu. Nhà bếp chỉ làm đúng những gì bạn gọi, nên bạn không phải trả tiền (hay chờ đợi) cho những thứ bạn không muốn.
Code Rust cũng hoạt động như vậy. Với biên dịch có điều kiện (conditional compilation, #[cfg]), bạn nói với trình biên dịch “chỉ build phần này trên Windows” hoặc “chỉ khi chạy test”. Với feature của Cargo (Cargo feature), những người dùng crate của bạn chọn các phần tuỳ chọn họ cần, như hỗ trợ JSON. Code không được chọn sẽ hoàn toàn không được biên dịch. Nó không làm chậm quá trình build, và không có mặt trong chương trình cuối cùng.
Trang này xây tiếp trên Cargo và crate (Cargo.toml, cargo add) và Công cụ và cài đặt Rust. Nếu những từ đó còn lạ, hãy xem qua hai trang đó trước.
#[cfg]: giữ lại hoặc bỏ đi code
Phần tiêu đề “#[cfg]: giữ lại hoặc bỏ đi code”#[cfg(...)] là một thuộc tính (attribute) bạn đặt lên một hàm, một struct, một dòng use, một module hay thậm chí một câu lệnh. Nếu điều kiện đúng, code được giữ lại. Nếu sai, trình biên dịch xoá nó đi hoàn toàn, như thể bạn chưa từng viết.
#[cfg(target_os = "linux")]fn os_name() -> &'static str { "Linux"}
#[cfg(target_os = "windows")]fn os_name() -> &'static str { "Windows"}
#[cfg(target_os = "macos")]fn os_name() -> &'static str { "macOS"}
fn main() { println!("Xin chào từ {}!", os_name());}Kết quả:
Xin chào từ Linux!Rust Playground chạy trên Linux, nên nó in ra Xin chào từ Linux!. Trên laptop Windows của bạn, cùng đoạn code đó in ra Xin chào từ Windows!. Ba hàm trùng tên nhau, nhưng không sao: trên bất kỳ máy nào, chỉ một trong số chúng còn lại.
Có một cái bẫy nhỏ ở đây. Trên FreeBSD, không điều kiện nào trong ba điều kiện đúng, nên os_name không tồn tại và quá trình build dừng lại với lỗi E0425: “cannot find function os_name in this scope” (không tìm thấy hàm os_name trong phạm vi này). Các phần tiếp theo chỉ bạn cách thêm phương án dự phòng.
cfg!(): một giá trị true hoặc false đơn giản
Phần tiêu đề “cfg!(): một giá trị true hoặc false đơn giản”Đôi khi bạn không muốn xoá code. Bạn chỉ muốn đặt một câu hỏi. Macro cfg!() nhận cùng các điều kiện đó, nhưng trả về một bool bình thường:
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Đây là chương trình {bits}-bit"); println!("usize lớn nhất là {}", usize::MAX);
if cfg!(target_os = "windows") { println!("Đường dẫn trông như C:\\Users\\ana"); } else { println!("Đường dẫn trông như /home/ana"); }}Kết quả:
Đây là chương trình 64-bitusize lớn nhất là 18446744073709551615Đường dẫn trông như /home/anatarget_pointer_width là kích thước của một địa chỉ bộ nhớ: "64" trên gần như mọi máy tính hiện đại, "32" trên một số thiết bị nhỏ. Nó cũng là kích thước của usize, đó là lý do usize::MAX lớn như vậy ở đây.
Vậy khi nào dùng cái nào?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| Nó làm gì | Xoá code khi điều kiện sai | Trở thành true hoặc false |
| Code ở nhánh “sai” có còn được biên dịch không? | Không, nó biến mất | Có, cả hai nhánh phải biên dịch được ở mọi nơi |
| Dùng cho | Code không thể biên dịch ở nơi khác (API chỉ có trên một hệ điều hành, crate tuỳ chọn) | Những khác biệt nhỏ về hành vi |
Dòng ở giữa rất quan trọng. Những thứ chỉ có trên Unix như std::os::unix::fs::PermissionsExt không tồn tại trên Windows, nên code dùng chúng phải nằm sau #[cfg(unix)]. Một if cfg!(unix) là không đủ, vì trình biên dịch trên Windows vẫn sẽ cố biên dịch nhánh đó.
Kết hợp điều kiện: not, any, all
Phần tiêu đề “Kết hợp điều kiện: not, any, all”Bạn có thể dựng điều kiện lớn từ những điều kiện nhỏ:
not(a): đúng khiasaiany(a, b, ...): đúng khi ít nhất một điều kiện đúng (như “hoặc”)all(a, b, ...): đúng khi tất cả đều đúng (như “và”)
Đây lại là ví dụ hệ điều hành, lần này có phương án dự phòng để biên dịch được ở mọi nơi:
#[cfg(any(target_os = "linux", target_os = "macos"))]fn shell() -> &'static str { "sh"}
#[cfg(target_os = "windows")]fn shell() -> &'static str { "cmd"}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]fn shell() -> &'static str { "không rõ"}
fn main() { println!("Shell mặc định: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Một hệ thống Unix 64-bit"); }}Kết quả:
Shell mặc định: shMột hệ thống Unix 64-bitunix và windows là những tên ngắn tiện dụng. unix đúng trên Linux, macOS, các hệ BSD và những hệ thống tương tự. Đây là các điều kiện bạn sẽ gặp nhiều nhất:
| Điều kiện | Đúng khi… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Bạn build cho hệ điều hành đó |
unix / windows |
Bạn build cho bất kỳ hệ giống Unix nào / cho Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Bạn build cho loại vi xử lý đó (WebAssembly là wasm32) |
target_pointer_width = "64" |
Địa chỉ (và usize) có 64 bit |
debug_assertions |
Đây là bản debug (cargo build), không phải --release |
test |
Bạn đang chạy cargo test |
feature = "json" |
Feature json của Cargo đang bật (xem bên dưới) |
Muốn xem danh sách đầy đủ cho máy của bạn? Hãy chạy rustc --print cfg. Trên một máy Linux 64-bit, nó in ra những dòng như target_os="linux", target_family="unix" và debug_assertions. Bạn sẽ thấy #[cfg(windows)] và #[cfg(unix)] rất nhiều trong code FFI, vì thư viện C thường khác nhau giữa các hệ thống.
#[cfg(test)] và #[cfg_attr]
Phần tiêu đề “#[cfg(test)] và #[cfg_attr]”Bạn đã dùng một điều kiện mà không để ý. Mọi module test ở trang Kiểm thử đều bắt đầu bằng #[cfg(test)]:
pub fn add_points(total: u32, points: u32) -> u32 { total + points}
#[cfg(test)]mod tests { use super::*;
#[test] fn adds_points() { assert_eq!(add_points(10, 5), 15); }}cargo test bật test, nên module được build và các test của nó chạy. Một lệnh cargo build bình thường bỏ qua toàn bộ module, nên chương trình thật của bạn không mang theo chút code test nào. #[cfg] dùng được trên mọi module như vậy (xem Module), ví dụ #[cfg(windows)] mod windows_paths;.
#[cfg_attr(condition, attribute)] có nghĩa là: “nếu điều kiện đúng, thêm thuộc tính này vào”. Một cách dùng phổ biến là một derive mà bạn chỉ muốn có trong một số trường hợp:
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// chỉ có trong bản debug; in ra stderr, không phải stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} có {} điểm", player.name, player.score);}Kết quả:
Ana có 120 điểmTrong bản debug (mặc định của Playground), bạn còn thấy debug: Player { name: "Ana", score: 120 } ở phần “Standard Error” của kết quả. Chuyển Playground sang “Release” thì dòng đó biến mất, cùng với code Debug của Player. Hãy để ý rằng eprintln! cần #[cfg(debug_assertions)] của riêng nó. Thiếu nó, bản release sẽ build thất bại, vì ở đó Player không cài đặt Debug. Bạn có thể đọc thêm về bản debug và bản release ở trang Cargo và crate.
Feature của Cargo trong crate của riêng bạn
Phần tiêu đề “Feature của Cargo trong crate của riêng bạn”Giờ đến thực đơn pizza. Một feature là một công tắc có tên mà bạn định nghĩa trong Cargo.toml, trong bảng [features]. Hãy dựng một chương trình nhỏ, pizza_order, với hai feature:
extra_cheese: một phần thêm miễn phí, được bật mặc định.json: in đơn hàng ra dạng JSON. Nó cần các crateserdevàserde_json, nên chúng chỉ được tải về và biên dịch khi có người yêu cầujson.
[package]name = "pizza_order"version = "0.1.0"edition = "2024"
[dependencies]serde = { version = "1", features = ["derive"], optional = true }serde_json = { version = "1", optional = true }
[features]default = ["extra_cheese"]extra_cheese = []json = ["dep:serde", "dep:serde_json"]Từng dòng một:
optional = truenghĩa là “đừng dùng crate này trừ khi có feature yêu cầu”.extra_cheese = []tạo một feature không bật thêm thứ gì khác. Nó chỉ là một công tắc để code của bạn kiểm tra.json = ["dep:serde", "dep:serde_json"]nói rằng: khijsonbật, hãy bật luôn hai dependency tuỳ chọn này.dep:nghĩa là “đây là một dependency, không phải tên feature”.default = [...]liệt kê các feature được bật khi không ai nói gì khác.
Trong code, bạn kiểm tra một feature bằng feature = "name":
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomato".to_string(), "basil".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("extra cheese".to_string()); }
let order = Order { size: "large".to_string(), toppings }; println!("Order: {} pizza with {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("As JSON: {}", serde_json::to_string(&order).unwrap());}Code của dự án mẫu này và kết quả bên dưới được giữ nguyên tiếng Anh, vì đó là kết quả thật khi chạy đúng đoạn code này. Dòng JSON dùng #[cfg], không phải cfg!. Không có feature json, serde_json thậm chí không phải là dependency, nên dòng đó phải biến mất hoàn toàn. Phép kiểm tra phô mai thì biên dịch được trong cả hai trường hợp, nên cfg! là đủ.
Giờ hãy gọi pizza từ dòng lệnh. (-q chỉ để ẩn các dòng “Compiling…” của Cargo.) Lần lượt: chỉ feature mặc định; mặc định cộng json; không có gì thêm, kể cả mặc định; và mọi feature:
cargo run -q # default features onlycargo run -q --features json # defaults + jsoncargo run -q --no-default-features # nothing extra, not even the defaultscargo run -q --all-features # every featureĐây là kết quả thật:
$ cargo run -qOrder: large pizza with tomato, basil, extra cheese
$ cargo run -q --features jsonOrder: large pizza with tomato, basil, extra cheeseAs JSON: {"size":"large","toppings":["tomato","basil","extra cheese"]}
$ cargo run -q --no-default-featuresOrder: large pizza with tomato, basil
$ cargo run -q --all-featuresOrder: large pizza with tomato, basil, extra cheeseAs JSON: {"size":"large","toppings":["tomato","basil","extra cheese"]}Bạn có thể kết hợp các cờ. --no-default-features --features json cho bạn JSON mà không có phô mai. Với nhiều feature, hãy ngăn cách bằng dấu phẩy: --features json,extra_cheese. Các cờ này cũng dùng được với cargo build, cargo check và cargo test.
Bật feature của các dependency
Phần tiêu đề “Bật feature của các dependency”Phần lớn thời gian, bạn đứng ở phía bên kia quầy: bạn dùng các crate có feature. Bạn đã thấy điều này ở trang Cargo và crate:
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- Feature
derivecủaserdecho bạn#[derive(Serialize, Deserialize)]. tokiokhông có feature mặc định nào. Tự nó gần như không cho bạn gì cả.fullbật mọi thứ: runtime,#[tokio::main], bộ hẹn giờ, mạng, file và nhiều nữa. Lúc học thì vậy là tiện. Trong dự án thật, bạn có thể yêu cầu ít hơn, ví dụfeatures = ["rt-multi-thread", "macros"].- Feature
preserve_ordercủaserde_jsongiữ các khoá của object theo đúng thứ tự bạn chèn vào.
cargo add viết những dòng này giúp bạn: cargo add tokio --features full.
Vì sao các crate lại chia nhỏ như vậy? Vì mỗi phần bạn không dùng là phần code bạn không phải tải về, biên dịch hay phát hành. Một crate lớn như tokio sẽ làm mọi dự án nhỏ build chậm nếu lúc nào nó cũng biên dịch mọi thứ. Feature giữ cho thời gian biên dịch và kích thước file chạy nhỏ gọn.
Bạn cũng có thể tắt các feature mặc định của một crate và chỉ chọn thứ mình cần, bằng default-features = false:
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Để xem những feature nào đang bật trong dự án, hãy hỏi Cargo:
cargo tree -e features --features json --depth 1Với pizza_order, lệnh này in ra:
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Không có --depth 1, bạn sẽ thấy cả cây, kể cả những feature mà các dependency của bạn bật trong dependency của chúng. Danh sách dài, nhưng đó là công cụ tốt nhất khi bạn thắc mắc “vì sao crate này lại được biên dịch?”.
Quy tắc nhanh
Phần tiêu đề “Quy tắc nhanh”Feature chỉ nên thêm vào. Bật một feature không bao giờ được xoá một hàm hay thay đổi việc mà một hàm có sẵn đang làm. Lý do là: nếu hai crate trong dự án của bạn cùng dùng serde_json, một crate yêu cầu preserve_order còn crate kia thì không, Cargo sẽ biên dịch serde_json một lần, với feature của cả hai được bật. Vì vậy mọi crate phải vẫn chạy đúng khi người khác bật thêm feature.
Tránh những feature không thể bật cùng nhau. Một thiết kế kiểu “feature postgres HOẶC feature sqlite, nhưng không bao giờ cả hai” sẽ hỏng ngay khi hai crate chọn hai cái khác nhau, và --all-features sẽ không còn chạy được. Hãy ưu tiên những feature có thể bật cùng lúc.
Ghi tài liệu cho các feature. Viết một danh sách ngắn trong README hoặc tài liệu của crate: tên feature, nó thêm gì, và nó có được bật mặc định không. Trên docs.rs, mỗi crate có một trang “Feature flags” (ví dụ của tokio, tiếng Anh), và nhiều crate đánh dấu các mục bằng dòng “Available on crate feature … only”.
Kiểm tra mọi tổ hợp trong CI. Code nằm sau một feature chỉ được biên dịch khi feature đó bật. Hãy thêm những lệnh sau vào CI để không có gì ẩn nấp ở đó:
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresVới các crate lớn hơn, công cụ cargo-hack có thể kiểm tra riêng từng feature (cargo hack check --each-feature).
Lỗi thường gặp
Phần tiêu đề “Lỗi thường gặp”Gõ sai tên feature. Nếu bạn viết #[cfg(feature = "jsno")], code vẫn biên dịch được. Dòng JSON chỉ đơn giản là không bao giờ được in, kể cả với --features json. May mắn là Rust hiện nay sẽ cảnh báo bạn:
warning: unexpected `cfg` condition value: `jsno` --> src/main.rs:17:11 |17 | #[cfg(feature = "jsno")] | ^^^^^^^^^^------ | | | help: there is a expected value with a similar name: `"json"` | = note: expected values for `feature` are: `default`, `extra_cheese`, and `json` = help: consider adding `jsno` as a feature in `Cargo.toml` = note: `#[warn(unexpected_cfgs)]` on by defaultRustc báo giá trị jsno không nằm trong danh sách feature mong đợi, và gợi ý tên gần giống là "json". Đừng bỏ qua cảnh báo này. Trên dòng lệnh, Cargo còn khắt khe hơn: cargo run --features jsno dừng lại với error: the package 'pizza_order' does not contain this feature: jsno và gợi ý json.
Kiểm tra tên dependency thay vì tên feature. Vì có dep:serde, trong pizza_order không có feature nào tên là serde. Nếu bạn viết #[cfg_attr(feature = "serde", ...)], bạn nhận được cùng cảnh báo “unexpected cfg condition value”, lần này cho serde. Rồi cargo run --features json thất bại với E0277: “the trait bound Order: serde::Serialize is not satisfied” (Order không cài đặt serde::Serialize), vì derive chưa bao giờ được thêm vào. Hãy kiểm tra tên trong [features]: feature = "json".
Quên dep:. Một khi có feature nào viết dep:serde, bạn phải dùng dep: cho serde ở mọi chỗ. Thêm một feature mới yaml = ["serde"] và Cargo sẽ từ chối đọc file:
error: failed to parse manifest at `/home/ana/pizza_order/Cargo.toml`
Caused by: feature `yaml` includes `serde`, but `serde` is an optional dependency without an implicit feature Use `dep:serde` to enable the dependency.Cargo báo feature yaml nhắc tới serde, nhưng serde là dependency tuỳ chọn không có feature ngầm định, và yêu cầu bạn dùng dep:serde. Cách sửa là yaml = ["dep:serde"]. Còn nếu bạn bỏ dep: ở mọi chỗ, Cargo lặng lẽ tạo một feature công khai cho mỗi dependency tuỳ chọn (ở đây --features serde sẽ chạy được). Đó là phong cách cũ. Dùng dep: giúp danh sách feature gọn gàng và có chủ đích.
Code không bao giờ được biên dịch. Giả sử bạn gõ sai serde_json::to_strng bên trong dòng #[cfg(feature = "json")]. cargo check báo mọi thứ đều ổn, vì dòng đó mặc định không được biên dịch. Chỉ cargo check --all-features mới tìm ra:
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:41 |18 | println!("As JSON: {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("As JSON: {}", serde_json::to_string(&order).unwrap()); | +Rustc báo không tìm thấy hàm to_strng trong crate serde_json, và gợi ý to_string. Đây chính là lý do --all-features phải có trong CI.
Luyện tập
Phần tiêu đề “Luyện tập”1. Thư mục cấu hình cho mọi hệ điều hành
Phần tiêu đề “1. Thư mục cấu hình cho mọi hệ điều hành”Hãy viết hàm config_dir() trả về nơi một ứng dụng ghi chú lưu thiết lập: "~/.config/notes" trên Linux, "~/Library/Application Support/notes" trên macOS, r"%APPDATA%\notes" trên Windows, và "./notes" trên mọi hệ thống khác. In kết quả trong main. Gợi ý: mỗi trường hợp một hàm với #[cfg(target_os = "...")], và not(any(...)) cho phương án dự phòng.
Xem lời giải
#[cfg(target_os = "linux")]fn config_dir() -> &'static str { "~/.config/notes"}
#[cfg(target_os = "macos")]fn config_dir() -> &'static str { "~/Library/Application Support/notes"}
#[cfg(target_os = "windows")]fn config_dir() -> &'static str { r"%APPDATA%\notes"}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]fn config_dir() -> &'static str { "./notes"}
fn main() { println!("Thiết lập được lưu trong {}", config_dir());}Kết quả:
Thiết lập được lưu trong ~/.config/notesTrên bất kỳ máy nào cũng chỉ đúng một trong bốn hàm còn lại, nên main luôn tìm thấy một config_dir. Playground chạy Linux, nên bạn thấy đường dẫn của Linux. Một ứng dụng thật sẽ dùng crate như dirs để tìm những thư mục này cho chuẩn.
2. Xuất JSON tuỳ chọn
Phần tiêu đề “2. Xuất JSON tuỳ chọn”Bạn có một chương trình nhỏ leaderboard in ra bảng điểm. Hãy thêm feature export (tắt theo mặc định) kéo theo serde_json và thêm hàm export_json. Với --features export, chương trình phải in thêm bảng điểm dưới dạng JSON. Gợi ý: cho serde_json thành tuỳ chọn, dùng dep:serde_json trong feature, và đặt #[cfg(feature = "export")] lên cả hàm lẫn dòng gọi nó. Playground không đặt được feature của Cargo, nên hãy thử bài này bằng cargo new leaderboard trên máy của bạn.
Xem lời giải
[package]name = "leaderboard"version = "0.1.0"edition = "2024"
[dependencies]serde_json = { version = "1", optional = true }
[features]export = ["dep:serde_json"]struct Score { name: &'static str, points: u32,}
#[cfg(feature = "export")]fn export_json(scores: &[Score]) -> String { let list: Vec<serde_json::Value> = scores .iter() .map(|s| serde_json::json!({ "name": s.name, "points": s.points })) .collect(); serde_json::Value::Array(list).to_string()}
fn main() { let scores = [ Score { name: "Ana", points: 120 }, Score { name: "Bo", points: 95 }, ];
for s in &scores { println!("{}: {} points", s.name, s.points); }
#[cfg(feature = "export")] println!("Export: {}", export_json(&scores));}$ cargo run -qAna: 120 pointsBo: 95 points
$ cargo run -q --features exportAna: 120 pointsBo: 95 pointsExport: [{"name":"Ana","points":120},{"name":"Bo","points":95}]Không có dòng default = [...], nên export tắt trừ khi bạn yêu cầu. Không có nó, serde_json thậm chí không được tải về. Cả hàm lẫn lời gọi đều cần #[cfg], vì mỗi chỗ đều nhắc tới serde_json.
Tóm tắt
Phần tiêu đề “Tóm tắt”#[cfg(condition)]chỉ giữ code khi điều kiện đúng. Nếu không, code bị xoá trước khi biên dịch.cfg!(condition)chỉ là mộtbool. Cả hai nhánh vẫn phải biên dịch được trên mọi nền tảng.- Kết hợp điều kiện bằng
not(..),any(..)vàall(..). Những điều kiện thường gặp làtarget_os,unix/windows,debug_assertionsvàtest. #[cfg_attr(condition, attr)]chỉ thêm một thuộc tính khi điều kiện đúng.- Feature của Cargo nằm trong
[features]. Dùngoptional = truecùngdep:namecho crate tuỳ chọn, vàdefault = [...]cho những gì bật mặc định. - Feature chỉ nên thêm vào. Chạy
cargo check --all-featurestrong CI, và để ý các cảnh báounexpected_cfgs.
Kiểm tra nhanh
Phần tiêu đề “Kiểm tra nhanh”Kiểm tra nhanh
1. Chuyện gì xảy ra với một hàm được đánh dấu #[cfg(target_os = "windows")] khi bạn build trên Linux?
2. Vì sao if cfg!(unix) { ... } là không đủ cho code dùng API chỉ có trên Unix?
3. Trong Cargo.toml, json = ["dep:serde_json"] làm gì?
4. Bạn lỡ viết #[cfg(feature = "jsno")]. Chuyện gì xảy ra?
Điểm: 0 / 4
Đọc thêm
Phần tiêu đề “Đọc thêm”- Features — The Cargo Book (tiếng Anh): hướng dẫn đầy đủ về
[features], dependency tuỳ chọn,dep:, và cách các feature được hợp nhất. - Conditional compilation — The Rust Reference (tiếng Anh): mọi tuỳ chọn
cfg, cùng các quy tắc chính xác cho#[cfg],#[cfg_attr]vàcfg!. cfg!macro — Rust standard library (tiếng Anh):cfg!trả về gì và nó khác thuộc tính#[cfg]thế nào.- Specifying Dependencies — The Cargo Book (tiếng Anh): cách viết
features,default-features = falsevàoptionalcho từng dependency. - cfg — Rust by Example (tiếng Anh): ví dụ ngắn về
#[cfg]vàcfg!đặt cạnh nhau.