Fitur Cargo dan Kompilasi Kondisional (#[cfg]) di Rust
Bayangkan kamu memesan pizza. Semua pizza dapat adonan dasar dan saus tomat. Setelah itu kamu memilih tambahan: jamur, ekstra keju, zaitun. Dapur hanya membuat apa yang kamu pesan, jadi kamu tidak membayar (atau menunggu) topping yang tidak kamu mau.
Kode Rust bekerja dengan cara yang sama. Dengan conditional compilation (kompilasi kondisional, #[cfg]), kamu bilang ke compiler “build bagian ini hanya di Windows” atau “hanya saat test”. Dengan fitur Cargo (Cargo features), orang yang memakai crate-mu memilih sendiri tambahan opsional yang mereka butuhkan, misalnya dukungan JSON. Kode yang tidak dipilih sama sekali tidak di-compile. Build tidak jadi lebih lambat karenanya, dan kode itu tidak ikut masuk ke program akhir.
Halaman ini melanjutkan Cargo dan Crate (Cargo.toml, cargo add) dan Tools dan setup Rust. Kalau istilah-istilah itu masih baru, mampir dulu sebentar ke sana.
#[cfg]: mempertahankan atau membuang kode
Section titled “#[cfg]: mempertahankan atau membuang kode”#[cfg(...)] adalah sebuah attribute (atribut) yang bisa kamu pasang di fungsi, struct, baris use, modul, bahkan satu statement saja. Kalau kondisinya benar, kodenya tetap ada. Kalau salah, compiler membuangnya sepenuhnya, seolah-olah kamu tidak pernah menulisnya.
#[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!("Halo dari {}!", os_name());}Output:
Halo dari Linux!Rust Playground berjalan di Linux, jadi yang tercetak adalah Halo dari Linux!. Di laptop Windows-mu sendiri, kode yang sama mencetak Halo dari Windows!. Ada tiga fungsi dengan nama yang sama, tapi itu tidak masalah: di satu komputer, hanya satu yang tersisa.
Ada satu jebakan kecil di sini. Di FreeBSD, tidak ada satu pun dari tiga kondisi itu yang benar, jadi os_name tidak ada dan build berhenti dengan E0425: “cannot find function os_name in this scope” (fungsi os_name tidak ditemukan). Bagian-bagian berikutnya menunjukkan cara menambahkan fallback (cadangan).
cfg!(): cukup true atau false
Section titled “cfg!(): cukup true atau false”Kadang kamu tidak ingin membuang kode. Kamu cuma ingin bertanya. Makro cfg!() menerima kondisi yang sama, tapi hasilnya berupa bool biasa:
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Ini program {bits}-bit"); println!("usize terbesar adalah {}", usize::MAX);
if cfg!(target_os = "windows") { println!("Bentuk path: C:\\Users\\ana"); } else { println!("Bentuk path: /home/ana"); }}Output:
Ini program 64-bitusize terbesar adalah 18446744073709551615Bentuk path: /home/anatarget_pointer_width adalah ukuran sebuah alamat memori: "64" di hampir semua komputer modern, "32" di beberapa perangkat kecil. Itu juga ukuran usize, karena itulah usize::MAX di sini besar sekali.
Jadi, kapan pakai yang mana?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| Apa yang dilakukannya | Membuang kode kalau kondisinya salah | Menjadi true atau false |
| Apakah kode untuk kondisi “salah” tetap di-compile? | Tidak, kodenya hilang | Ya, kedua cabang harus bisa di-compile di mana saja |
| Pakai untuk | Kode yang tidak bisa di-compile di tempat lain (API khusus OS, crate opsional) | Perbedaan kecil dalam perilaku |
Baris tengah itu penting. Hal-hal khusus Unix seperti std::os::unix::fs::PermissionsExt tidak ada di Windows, jadi kode yang memakainya harus berada di balik #[cfg(unix)]. if cfg!(unix) saja tidak cukup, karena compiler di Windows tetap akan mencoba meng-compile cabang itu.
Menggabungkan kondisi: not, any, all
Section titled “Menggabungkan kondisi: not, any, all”Kamu bisa menyusun kondisi besar dari kondisi-kondisi kecil:
not(a): benar kalauasalahany(a, b, ...): benar kalau minimal satu benar (seperti “atau”)all(a, b, ...): benar kalau semuanya benar (seperti “dan”)
Ini contoh OS tadi lagi, kali ini dengan fallback supaya bisa di-compile di mana saja:
#[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 { "unknown"}
fn main() { println!("Shell bawaan: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Sistem Unix 64-bit"); }}Output:
Shell bawaan: shSistem Unix 64-bitunix dan windows adalah nama pendek yang praktis. unix bernilai benar di Linux, macOS, keluarga BSD, dan sistem sejenis. Inilah kondisi yang paling sering akan kamu temui:
| Kondisi | Benar kalau… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Kamu build untuk sistem operasi itu |
unix / windows |
Kamu build untuk sistem mirip Unix apa pun / untuk Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Kamu build untuk jenis prosesor itu (WebAssembly adalah wasm32) |
target_pointer_width = "64" |
Alamat (dan usize) berukuran 64 bit |
debug_assertions |
Ini build debug (cargo build), bukan --release |
test |
Kamu sedang menjalankan cargo test |
feature = "json" |
Fitur Cargo json sedang aktif (dibahas di bawah) |
Mau lihat daftar lengkap untuk komputermu? Jalankan rustc --print cfg. Di PC Linux 64-bit, perintah itu mencetak baris seperti target_os="linux", target_family="unix" dan debug_assertions. Kamu akan sering melihat #[cfg(windows)] dan #[cfg(unix)] di kode FFI, karena library C sering berbeda antara satu sistem dengan yang lain.
#[cfg(test)] dan #[cfg_attr]
Section titled “#[cfg(test)] dan #[cfg_attr]”Sebenarnya kamu sudah pernah memakai satu kondisi tanpa sadar. Setiap modul test di halaman Testing diawali dengan #[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 menyalakan test, jadi modul itu ikut di-build dan test-nya dijalankan. cargo build biasa membuang seluruh modul itu, jadi program aslimu tidak membawa kode test sama sekali. #[cfg] bisa dipakai di modul mana pun seperti ini (lihat Modul), misalnya #[cfg(windows)] mod windows_paths;.
#[cfg_attr(kondisi, attribute)] artinya: “kalau kondisinya benar, tambahkan attribute ini”. Contoh pemakaian yang umum adalah derive yang hanya kamu inginkan sesekali:
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// hanya di build debug; masuk ke stderr, bukan stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} punya {} poin", player.name, player.score);}Output:
Ana punya 120 poinDi build debug (default di Playground), kamu juga melihat debug: Player { name: "Ana", score: 120 } di bagian “Standard Error” pada output. Ubah Playground ke “Release”, dan baris itu hilang, bersama dengan kode Debug untuk Player. Perhatikan bahwa eprintln! butuh #[cfg(debug_assertions)] sendiri. Tanpa itu, build release gagal, karena di sana Player tidak mengimplementasikan Debug. Penjelasan lebih lanjut tentang build debug dan release ada di Cargo dan Crate.
Fitur Cargo di crate milikmu sendiri
Section titled “Fitur Cargo di crate milikmu sendiri”Sekarang saatnya menu pizza. Sebuah fitur (feature) adalah saklar bernama yang kamu definisikan di Cargo.toml, di dalam tabel [features]. Ayo buat program kecil, pizza_order, dengan dua fitur:
extra_cheese: tambahan gratis, aktif secara default.json: mencetak pesanan dalam format JSON. Fitur ini butuh crateserdedanserde_json, jadi keduanya hanya diunduh dan di-compile kalau ada yang memintajson.
[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"]Baris demi baris:
optional = trueartinya “jangan pakai crate ini kecuali ada fitur yang memintanya”.extra_cheese = []membuat fitur yang tidak menyalakan apa-apa lagi. Isinya cuma saklar yang bisa dicek oleh kodemu.json = ["dep:serde", "dep:serde_json"]berarti: kalaujsonaktif, nyalakan juga dua dependency opsional ini.dep:artinya “ini nama dependency, bukan nama fitur”.default = [...]berisi daftar fitur yang aktif kalau tidak ada yang bilang lain.
Di dalam kode, kamu mengecek sebuah fitur dengan feature = "nama":
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomat".to_string(), "kemangi".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("ekstra keju".to_string()); }
let order = Order { size: "besar".to_string(), toppings }; println!("Pesanan: pizza {} dengan {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("Dalam JSON: {}", serde_json::to_string(&order).unwrap());}Baris JSON memakai #[cfg], bukan cfg!. Tanpa fitur json, serde_json sama sekali bukan dependency, jadi baris itu harus benar-benar hilang. Pengecekan keju tetap bisa di-compile dalam kondisi apa pun, jadi cfg! sudah cukup.
Sekarang pesan pizzamu lewat command line. (-q hanya menyembunyikan baris “Compiling…” dari Cargo.)
cargo run -q # hanya fitur defaultcargo run -q --features json # default + jsoncargo run -q --no-default-features # tanpa tambahan, bahkan tanpa defaultcargo run -q --all-features # semua fiturIni output aslinya:
$ cargo run -qPesanan: pizza besar dengan tomat, kemangi, ekstra keju
$ cargo run -q --features jsonPesanan: pizza besar dengan tomat, kemangi, ekstra kejuDalam JSON: {"size":"besar","toppings":["tomat","kemangi","ekstra keju"]}
$ cargo run -q --no-default-featuresPesanan: pizza besar dengan tomat, kemangi
$ cargo run -q --all-featuresPesanan: pizza besar dengan tomat, kemangi, ekstra kejuDalam JSON: {"size":"besar","toppings":["tomat","kemangi","ekstra keju"]}Flag-flag ini bisa digabung. --no-default-features --features json memberimu JSON tanpa keju. Untuk beberapa fitur sekaligus, pisahkan dengan koma: --features json,extra_cheese. Flag yang sama juga berlaku untuk cargo build, cargo check dan cargo test.
Mengaktifkan fitur dari dependency
Section titled “Mengaktifkan fitur dari dependency”Sering kali kamu berada di sisi lain meja kasir: kamu memakai crate yang punya fitur. Kamu sudah pernah melihat ini di Cargo dan Crate:
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- Fitur
derivemilikserdememberimu#[derive(Serialize, Deserialize)]. tokiotidak punya fitur default. Kalau dipasang begitu saja, hampir tidak ada yang bisa kamu pakai.fullmenyalakan semuanya: runtime,#[tokio::main], timer, networking, file, dan lainnya. Itu memudahkan selama kamu belajar. Di proyek sungguhan, kamu bisa meminta lebih sedikit, misalnyafeatures = ["rt-multi-thread", "macros"].preserve_ordermilikserde_jsonmenjaga key objek tetap dalam urutan saat kamu memasukkannya.
cargo add menuliskan baris-baris ini untukmu: cargo add tokio --features full.
Kenapa crate memecah dirinya seperti ini? Karena setiap bagian yang tidak kamu pakai adalah kode yang tidak perlu kamu unduh, compile, atau kirim. Crate besar seperti tokio akan membuat setiap proyek kecil lambat di-build kalau selalu meng-compile semuanya. Fitur menjaga waktu compile dan ukuran binary tetap kecil.
Kamu juga bisa mematikan fitur default sebuah crate dan memilih hanya yang kamu butuhkan, dengan default-features = false:
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Untuk melihat fitur mana saja yang aktif di proyekmu, tanyakan ke Cargo:
cargo tree -e features --features json --depth 1Untuk pizza_order, perintah ini mencetak:
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Tanpa --depth 1, kamu melihat seluruh pohonnya, termasuk fitur yang dinyalakan dependency-mu di dalam dependency mereka. Hasilnya panjang, tapi ini tool terbaik saat kamu bertanya-tanya “kenapa crate ini ikut di-compile?”.
Aturan praktis
Section titled “Aturan praktis”Fitur sebaiknya hanya menambahkan sesuatu. Menyalakan sebuah fitur tidak boleh menghapus fungsi atau mengubah apa yang dilakukan fungsi yang sudah ada. Alasannya begini. Kalau dua crate di proyekmu sama-sama memakai serde_json, dan satu meminta preserve_order sementara yang lain tidak, Cargo meng-compile serde_json satu kali saja, dengan fitur dari keduanya dinyalakan. Jadi setiap crate harus tetap berfungsi ketika pihak lain menyalakan fitur tambahan.
Hindari fitur yang tidak bisa aktif bersamaan. Desain seperti “fitur postgres ATAU fitur sqlite, tapi jangan dua-duanya” langsung rusak begitu dua crate memilih yang berbeda, dan --all-features jadi tidak berfungsi. Lebih baik buat fitur yang bisa aktif semuanya sekaligus.
Dokumentasikan fiturmu. Tulis daftar singkat di README atau dokumentasi crate-mu: nama fitur, apa yang ditambahkannya, dan apakah fitur itu aktif secara default. Di docs.rs, setiap crate punya halaman “Feature flags” (contohnya milik tokio), dan banyak crate menandai item dengan “Available on crate feature … only” (hanya tersedia dengan fitur tertentu).
Cek setiap kombinasi di CI. Kode di balik sebuah fitur hanya di-compile kalau fitur itu aktif. Tambahkan perintah-perintah ini ke CI-mu supaya tidak ada error yang bersembunyi di sana:
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresUntuk crate yang lebih besar, tool cargo-hack bisa mengecek setiap fitur satu per satu (cargo hack check --each-feature).
Kesalahan umum
Section titled “Kesalahan umum”Salah ketik nama fitur. Kalau kamu menulis #[cfg(feature = "jsno")], kodenya tetap bisa di-compile. Baris JSON-nya saja yang tidak pernah tercetak, bahkan dengan --features json. Untungnya, Rust versi sekarang memperingatkanmu:
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 defaultWarning ini bilang nilai jsno tidak dikenal, ada nilai dengan nama mirip ("json"), dan fitur apa saja yang memang ada. Jangan abaikan warning ini. Di command line, Cargo lebih tegas: cargo run --features jsno berhenti dengan error: the package 'pizza_order' does not contain this feature: jsno (package ini tidak punya fitur tersebut) dan menyarankan json.
Mengecek nama dependency, bukan nama fitur. Karena ada dep:serde, tidak ada fitur bernama serde di pizza_order. Kalau kamu menulis #[cfg_attr(feature = "serde", ...)], kamu mendapat warning “unexpected cfg condition value” yang sama, kali ini untuk serde. Lalu cargo run --features json gagal dengan E0277: “the trait bound Order: serde::Serialize is not satisfied” (Order tidak mengimplementasikan serde::Serialize), karena derive-nya tidak pernah ditambahkan. Cek nama yang ada di [features]: feature = "json".
Lupa menulis dep:. Begitu ada satu fitur yang menulis dep:serde, kamu harus memakai dep: untuk serde di mana-mana. Tambahkan fitur baru yaml = ["serde"], dan Cargo menolak membaca file-nya:
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 menjelaskan bahwa serde adalah dependency opsional tanpa fitur implisit, lalu memintamu memakai dep:serde. Perbaikannya adalah yaml = ["dep:serde"]. Dan kalau kamu tidak menulis dep: sama sekali di mana pun, Cargo diam-diam membuat fitur publik untuk setiap dependency opsional (di sini --features serde akan berfungsi). Itu gaya lama. Dengan dep:, daftar fiturmu tetap rapi dan disengaja.
Kode yang tidak pernah di-compile. Misalnya kamu salah ketik serde_json::to_strng di baris yang memakai #[cfg(feature = "json")]. cargo check bilang semuanya baik-baik saja, karena baris itu tidak di-compile secara default. Hanya cargo check --all-features yang menemukannya:
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:44 |18 | println!("Dalam JSON: {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("Dalam JSON: {}", serde_json::to_string(&order).unwrap()); | +Compiler tidak menemukan to_strng di serde_json dan menyarankan to_string. Inilah alasan --all-features wajib ada di CI.
Latihan
Section titled “Latihan”1. Folder konfigurasi untuk setiap OS
Section titled “1. Folder konfigurasi untuk setiap OS”Tulis fungsi config_dir() yang mengembalikan lokasi penyimpanan pengaturan sebuah aplikasi catatan: "~/.config/notes" di Linux, "~/Library/Application Support/notes" di macOS, r"%APPDATA%\notes" di Windows, dan "./notes" di sistem lainnya. Cetak hasilnya di main. Petunjuk: satu fungsi untuk setiap kasus dengan #[cfg(target_os = "...")], dan not(any(...)) untuk fallback-nya.
Lihat solusi
#[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!("Pengaturan disimpan di {}", config_dir());}Output:
Pengaturan disimpan di ~/.config/notesDi komputer mana pun, tepat satu dari empat fungsi itu yang tersisa, jadi main selalu menemukan sebuah config_dir. Playground berjalan di Linux, jadi kamu melihat path Linux. Aplikasi sungguhan akan memakai crate seperti dirs untuk menemukan folder-folder ini dengan benar.
2. Ekspor JSON yang opsional
Section titled “2. Ekspor JSON yang opsional”Kamu punya program kecil leaderboard yang mencetak skor. Tambahkan fitur export (mati secara default) yang menarik serde_json dan menambahkan fungsi export_json. Dengan --features export, program itu juga harus mencetak skor dalam format JSON. Petunjuk: jadikan serde_json opsional, pakai dep:serde_json di fiturnya, dan pasang #[cfg(feature = "export")] di fungsinya sekaligus di baris yang memanggilnya. Playground tidak bisa mengatur fitur Cargo, jadi coba latihan ini dengan cargo new leaderboard di komputermu.
Lihat solusi
[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!("{}: {} poin", s.name, s.points); }
#[cfg(feature = "export")] println!("Ekspor: {}", export_json(&scores));}$ cargo run -qAna: 120 poinBo: 95 poin
$ cargo run -q --features exportAna: 120 poinBo: 95 poinEkspor: [{"name":"Ana","points":120},{"name":"Bo","points":95}]Tidak ada baris default = [...], jadi export mati kecuali kamu memintanya. Tanpa fitur itu, serde_json bahkan tidak diunduh. Fungsi dan pemanggilannya sama-sama butuh #[cfg], karena keduanya menyebut serde_json.
Ringkasan
Section titled “Ringkasan”#[cfg(kondisi)]mempertahankan kode hanya kalau kondisinya benar. Kalau tidak, kode itu dibuang sebelum di-compile.cfg!(kondisi)hanyalah sebuahbool. Kedua cabangnya tetap harus bisa di-compile di setiap platform.- Gabungkan kondisi dengan
not(..),any(..)danall(..). Yang paling umum adalahtarget_os,unix/windows,debug_assertionsdantest. #[cfg_attr(kondisi, attr)]menambahkan sebuah attribute hanya kalau kondisinya benar.- Fitur Cargo ditulis di
[features]. Pakaioptional = trueditambahdep:namauntuk crate opsional, dandefault = [...]untuk fitur yang aktif secara default. - Fitur sebaiknya hanya menambahkan sesuatu. Jalankan
cargo check --all-featuresdi CI, dan perhatikan warningunexpected_cfgs.
Kuis singkat
Section titled “Kuis singkat”Kuis singkat
1. Apa yang terjadi pada fungsi bertanda #[cfg(target_os = "windows")] kalau kamu build di Linux?
2. Kenapa if cfg!(unix) { ... } tidak cukup untuk kode yang memakai API khusus Unix?
3. Di Cargo.toml, apa fungsi json = ["dep:serde_json"]?
4. Kamu tidak sengaja menulis #[cfg(feature = "jsno")]. Apa yang terjadi?
Skor: 0 / 4
Bacaan lebih lanjut
Section titled “Bacaan lebih lanjut”- Features — The Cargo Book (bahasa Inggris): panduan lengkap tentang
[features], dependency opsional,dep:, dan cara fitur-fitur digabungkan (unification). - Conditional compilation — The Rust Reference (bahasa Inggris): semua opsi
cfg, ditambah aturan persis untuk#[cfg],#[cfg_attr]dancfg!. - Makro
cfg!— Rust standard library (bahasa Inggris): apa yang dikembalikancfg!dan bedanya dengan attribute#[cfg]. - Specifying Dependencies — The Cargo Book (bahasa Inggris): cara menulis
features,default-features = falsedanoptionaluntuk setiap dependency. - cfg — Rust by Example (bahasa Inggris): contoh-contoh singkat
#[cfg]dancfg!berdampingan.