RustのCargoフィーチャーと条件付きコンパイル(#[cfg])
ピザを注文するところを想像してみてください。生地とトマトソースはみんな同じです。そこからトッピングを選びます。マッシュルーム、チーズ増量、オリーブ。お店は頼まれたものだけを作るので、いらないトッピングのためにお金を払ったり、待たされたりすることはありません。
Rust のコードも同じです。条件付きコンパイル(conditional compilation、#[cfg])を使うと、「この部分は Windows のときだけビルドして」「テストのときだけにして」とコンパイラに伝えられます。Cargo のフィーチャー(features)を使うと、あなたのクレートを使う人が、JSON 対応のような追加機能を必要な分だけ選べます。選ばれなかったコードはまったくコンパイルされません。ビルドが遅くなることもなく、完成したプログラムにも入りません。
このページは Cargoとクレート(Cargo.toml、cargo add)と Rustのツールと環境構築 の続きです。これらの言葉が初めてなら、先にざっと目を通しておくと安心です。
#[cfg]:コードを残すか、消すか
Section titled “#[cfg]:コードを残すか、消すか”#[cfg(...)] は、関数、構造体、use の行、モジュール、さらにはひとつの文にも付けられる属性です。条件が真ならコードはそのまま残ります。偽なら、コンパイラはそのコードを完全に取り除きます。まるで最初から書かなかったかのように、です。
#[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!("{} からこんにちは!", os_name());}実行結果:
Linux からこんにちは!Rust Playground は Linux で動いているので、Linux からこんにちは! と表示されます。あなたの Windows のノートパソコンで同じコードを動かすと、Windows からこんにちは! になります。同じ名前の関数が3つありますが、問題ありません。どのコンピュータでも、生き残るのはそのうちのひとつだけだからです。
ただし、ここには小さな落とし穴があります。FreeBSD では3つの条件のどれも真にならないので、os_name が存在しなくなり、E0425 でビルドが止まります。「cannot find function os_name in this scope」(このスコープに関数 os_name が見つからない)というエラーです。代わりの関数(フォールバック)を用意する方法は、このあと説明します。
cfg!():シンプルな真か偽か
Section titled “cfg!():シンプルな真か偽か”コードを消したいわけではなく、ただ質問したいだけのこともあります。cfg!() マクロは同じ条件を受け取りますが、結果をふつうの bool として返します。
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("これは {bits} ビットのプログラムです"); println!("usize の最大値は {}", usize::MAX);
if cfg!(target_os = "windows") { println!("パスはこんな形です: C:\\Users\\ana"); } else { println!("パスはこんな形です: /home/ana"); }}実行結果:
これは 64 ビットのプログラムですusize の最大値は 18446744073709551615パスはこんな形です: /home/anatarget_pointer_width はメモリアドレスの大きさです。最近のコンピュータならほぼすべて "64" で、一部の小さなデバイスでは "32" になります。これは usize の大きさでもあるので、ここでは usize::MAX がこんなに大きな数になっています。
では、どちらをいつ使えばいいのでしょうか。
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| 何をするか | 偽のときにコードを取り除く | true か false になる |
| 「偽」の側のコードもコンパイルされる? | いいえ、消えてなくなる | はい、両方の分岐がどの環境でもコンパイルできる必要がある |
| 使いどころ | ほかの環境ではコンパイルできないコード(特定の OS だけの API、オプションのクレート) | ちょっとした動作の違い |
大事なのは真ん中の行です。std::os::unix::fs::PermissionsExt のような Unix 専用のものは Windows には存在しないので、それを使うコードは #[cfg(unix)] の内側に置く必要があります。if cfg!(unix) では足りません。Windows のコンパイラも、その分岐をコンパイルしようとしてしまうからです。
条件を組み合わせる:not、any、all
Section titled “条件を組み合わせる:not、any、all”小さな条件を組み合わせて、大きな条件を作れます。
not(a):aが偽のときに真any(a, b, ...):少なくともひとつが真のときに真(「または」のようなもの)all(a, b, ...):すべてが真のときに真(「かつ」のようなもの)
もう一度 OS の例です。今度はフォールバックを用意して、どこでもコンパイルできるようにしました。
#[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 { "不明"}
fn main() { println!("デフォルトのシェル: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("64ビットの Unix システムです"); }}実行結果:
デフォルトのシェル: sh64ビットの Unix システムですunix と windows は便利な短い名前です。unix は Linux、macOS、BSD 系などのシステムで真になります。よく見かける条件をまとめておきます。
| 条件 | 真になるのは… |
|---|---|
target_os = "linux" / "windows" / "macos" |
その OS 向けにビルドするとき |
unix / windows |
Unix 系のシステム向け / Windows 向けにビルドするとき |
target_arch = "x86_64" / "aarch64" / "wasm32" |
その種類のプロセッサ向けにビルドするとき(WebAssembly は wasm32) |
target_pointer_width = "64" |
アドレス(と usize)が64ビットのとき |
debug_assertions |
--release ではなく、デバッグビルド(cargo build)のとき |
test |
cargo test を実行しているとき |
feature = "json" |
Cargo のフィーチャー json がオンのとき(詳しくは後述) |
自分のマシンでの一覧をすべて見たいときは、rustc --print cfg を実行してみてください。64ビットの Linux PC なら、target_os="linux"、target_family="unix"、debug_assertions のような行が表示されます。C のライブラリは OS ごとに違うことが多いので、FFI のコードでは #[cfg(windows)] や #[cfg(unix)] をよく見かけます。
#[cfg(test)] と #[cfg_attr]
Section titled “#[cfg(test)] と #[cfg_attr]”実は、あなたはもう条件をひとつ、意識せずに使っています。テストのページに出てくるテストモジュールは、どれも #[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 は test をオンにするので、このモジュールがビルドされ、テストが実行されます。ふつうの cargo build ではモジュールごと外されるので、本番のプログラムにはテスト用のコードが一切入りません。#[cfg] はこのように、どんなモジュールにも使えます(モジュールを参照)。たとえば #[cfg(windows)] mod windows_paths; のように書けます。
#[cfg_attr(条件, 属性)] は、「条件が真なら、この属性を付ける」という意味です。よくある使い方は、ときどきだけ付けたい derive です。
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// デバッグビルドのときだけ。stdout ではなく stderr に出力される #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} のスコアは {} 点", player.name, player.score);}実行結果:
Ana のスコアは 120 点デバッグビルド(Playground のデフォルト)では、出力の「Standard Error」の部分に debug: Player { name: "Ana", score: 120 } も表示されます。Playground を「Release」に切り替えると、その行は消えます。Player の Debug のコードも一緒に消えます。eprintln! にも専用の #[cfg(debug_assertions)] が必要なことに注目してください。これがないと、リリースビルドが失敗します。リリースビルドでは Player が Debug を実装していないからです。デバッグビルドとリリースビルドについては、Cargoとクレートで詳しく説明しています。
自分のクレートで Cargo のフィーチャーを使う
Section titled “自分のクレートで Cargo のフィーチャーを使う”いよいよピザのメニューです。フィーチャーとは、Cargo.toml の [features] テーブルに定義する、名前付きのスイッチです。2つのフィーチャーを持つ小さなプログラム pizza_order を作ってみましょう。
extra_cheese:無料のトッピング。デフォルトでオンです。json:注文を JSON で表示します。serdeとserde_jsonのクレートが必要ですが、誰かがjsonを頼んだときだけダウンロードされ、コンパイルされます。
[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"]1行ずつ見ていきましょう。
optional = trueは、「フィーチャーに頼まれない限り、このクレートは使わない」という意味です。extra_cheese = []は、ほかに何もオンにしないフィーチャーを作ります。コードから確認できる、ただのスイッチです。json = ["dep:serde", "dep:serde_json"]は、「jsonがオンなら、この2つのオプションの依存クレートもオンにする」という意味です。dep:は「これはフィーチャー名ではなく、依存クレートです」という印です。default = [...]には、誰も何も指定しなかったときにオンになるフィーチャーを並べます。
コードの中では、feature = "名前" でフィーチャーを確認します。
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["トマト".to_string(), "バジル".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("チーズ増量".to_string()); }
let order = Order { size: "Lサイズ".to_string(), toppings }; println!("注文: {}のピザ({})", order.size, order.toppings.join("、"));
#[cfg(feature = "json")] println!("JSON形式: {}", serde_json::to_string(&order).unwrap());}JSON の行には cfg! ではなく #[cfg] を使っています。json フィーチャーがないと serde_json はそもそも依存クレートに入らないので、この行は完全に消えなければならないからです。チーズの確認はどちらの場合でも問題なくコンパイルできるので、cfg! で十分です。
では、コマンドラインからピザを注文してみましょう(-q は Cargo の「Compiling…」の行を隠すだけのオプションです)。
cargo run -q # デフォルトのフィーチャーだけcargo run -q --features json # デフォルト + jsoncargo run -q --no-default-features # 追加なし。デフォルトのフィーチャーもなしcargo run -q --all-features # すべてのフィーチャー実際の出力はこうなります。
$ cargo run -q注文: Lサイズのピザ(トマト、バジル、チーズ増量)
$ cargo run -q --features json注文: Lサイズのピザ(トマト、バジル、チーズ増量)JSON形式: {"size":"Lサイズ","toppings":["トマト","バジル","チーズ増量"]}
$ cargo run -q --no-default-features注文: Lサイズのピザ(トマト、バジル)
$ cargo run -q --all-features注文: Lサイズのピザ(トマト、バジル、チーズ増量)JSON形式: {"size":"Lサイズ","toppings":["トマト","バジル","チーズ増量"]}これらのオプションは組み合わせられます。--no-default-features --features json なら、チーズなしで JSON が出ます。複数のフィーチャーを指定するときは、--features json,extra_cheese のようにカンマで区切ります。同じオプションは cargo build、cargo check、cargo test でも使えます。
依存クレートのフィーチャーをオンにする
Section titled “依存クレートのフィーチャーをオンにする”ふだんは、カウンターの反対側にいることのほうが多いはずです。つまり、フィーチャーを持つクレートを使う側です。Cargoとクレートでも、すでにこんな書き方を見ました。
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }serdeのderiveフィーチャーで、#[derive(Serialize, Deserialize)]が使えるようになります。tokioにはデフォルトのフィーチャーがありません。それだけでは、ほとんど何もできません。fullはすべてをオンにします。ランタイム、#[tokio::main]、タイマー、ネットワーク、ファイルなどです。学習中はこれが楽です。実際のプロジェクトでは、features = ["rt-multi-thread", "macros"]のように、必要なものだけを指定することもできます。serde_jsonのpreserve_orderは、オブジェクトのキーを追加した順番のまま保ちます。
これらの行は cargo add が書いてくれます。たとえば cargo add tokio --features full です。
なぜクレートはこんなふうに機能を分けているのでしょうか。使わない部分は、ダウンロードもコンパイルも配布もしなくて済むからです。tokio のような大きなクレートが毎回すべてをコンパイルしていたら、小さなプロジェクトでもビルドが遅くなってしまいます。フィーチャーのおかげで、コンパイル時間とバイナリのサイズを小さく抑えられるのです。
default-features = false を使えば、クレートのデフォルトのフィーチャーをオフにして、必要なものだけを選ぶこともできます。
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }プロジェクトでどのフィーチャーがオンになっているかは、Cargo に聞けばわかります。
cargo tree -e features --features json --depth 1pizza_order では、こう表示されます。
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"--depth 1 を付けないと、依存クレートがさらにその依存クレートでオンにしているフィーチャーまで含めた、ツリー全体が表示されます。長くなりますが、「どうしてこのクレートがコンパイルされているんだろう?」と思ったときには、いちばん頼りになる道具です。
覚えておきたいルール
Section titled “覚えておきたいルール”フィーチャーは機能を追加するだけにする。 フィーチャーをオンにしたことで、関数がなくなったり、既存の関数の動きが変わったりしてはいけません。理由はこうです。プロジェクトの中の2つのクレートがどちらも serde_json を使っていて、片方は preserve_order を頼み、もう片方は頼まなかったとします。このとき Cargo は、両方のフィーチャーをオンにした serde_json を1回だけコンパイルします。だから、どのクレートも、ほかの誰かが追加のフィーチャーをオンにしても動き続けなければならないのです。
同時にオンにできないフィーチャーは避ける。 「postgres フィーチャーか sqlite フィーチャーのどちらか一方。両方は不可」という設計は、2つのクレートが別々のほうを選んだ瞬間に壊れますし、--all-features も使えなくなります。すべて同時にオンにできるフィーチャーにしましょう。
フィーチャーをドキュメントに書く。 README やクレートのドキュメントに、フィーチャーの名前、何が追加されるか、デフォルトでオンかどうかを短くまとめておきましょう。docs.rs(英語)では、どのクレートにも「Feature flags」のページがあり(たとえば tokio のページ(英語))、多くのクレートでは要素に「Available on crate feature … only」(クレートのフィーチャー … がオンのときだけ使えます)という印が付いています。
CI ですべての組み合わせを確認する。 フィーチャーの内側にあるコードは、そのフィーチャーがオンのときしかコンパイルされません。そこに問題が隠れないように、次のコマンドを CI に追加しましょう。
cargo check --all-featurescargo check --no-default-featurescargo test --all-features大きなクレートなら、cargo-hack というツールでフィーチャーをひとつずつ確認できます(cargo hack check --each-feature)。
よくある間違い
Section titled “よくある間違い”フィーチャー名のタイプミス。 #[cfg(feature = "jsno")] と書いても、コードはコンパイルできてしまいます。--features json を付けても、JSON の行が表示されないだけです。幸い、最近の Rust は警告を出してくれます。
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 defaultこの警告は、「jsno という値は想定外です。似た名前の "json" があります」と教えてくれています。存在するフィーチャーの一覧も表示されます。この警告は無視しないでください。コマンドラインでは、Cargo はもっと厳しくなります。cargo run --features jsno は error: the package 'pizza_order' does not contain this feature: jsno(パッケージにこのフィーチャーはありません)で止まり、json を提案してくれます。
フィーチャー名ではなく、依存クレートの名前を確認してしまう。 dep:serde と書いたので、pizza_order には serde という名前のフィーチャーはありません。#[cfg_attr(feature = "serde", ...)] と書くと、同じ「unexpected cfg condition value」の警告が、今度は serde について出ます。そのうえ cargo run --features json は E0277 で失敗します。「the trait bound Order: serde::Serialize is not satisfied」(Order が serde::Serialize を実装していない)というエラーです。derive が一度も付かなかったからです。確認するのは [features] に書いた名前、つまり feature = "json" です。
dep: を付け忘れる。 どこかのフィーチャーで dep:serde と書いたら、serde にはどこでも dep: を付ける必要があります。新しいフィーチャー yaml = ["serde"] を追加すると、Cargo はファイルの読み込みを拒否します。
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 は、「serde は暗黙のフィーチャーを持たないオプションの依存クレートなので、dep:serde を使ってください」と言っています。直し方は yaml = ["dep:serde"] です。ちなみに、どこにも dep: を書かなければ、Cargo はオプションの依存クレートごとに公開のフィーチャーを黙って作ります(この例なら --features serde が使えてしまいます)。これは古い書き方です。dep: を使えば、フィーチャーの一覧がすっきりして、意図したものだけになります。
一度もコンパイルされないコード。 #[cfg(feature = "json")] の行で、うっかり serde_json::to_strng と打ち間違えたとします。その行はデフォルトではコンパイルされないので、cargo check は何も問題ないと言います。見つけられるのは cargo check --all-features だけです。
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:40 |18 | println!("JSON形式: {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("JSON形式: {}", serde_json::to_string(&order).unwrap()); | +コンパイラは「serde_json に to_strng という関数は見つからない」と言い、似た名前の to_string を提案しています。これこそ、--all-features を CI に入れるべき理由です。
1. OS ごとの設定フォルダ
Section titled “1. OS ごとの設定フォルダ”メモアプリが設定を保存する場所を返す関数 config_dir() を書いてください。Linux では "~/.config/notes"、macOS では "~/Library/Application Support/notes"、Windows では r"%APPDATA%\notes"、それ以外のシステムでは "./notes" を返します。結果は main で表示しましょう。ヒント: ケースごとに #[cfg(target_os = "...")] を付けた関数をひとつずつ作り、フォールバックには not(any(...)) を使います。
解答を見る
#[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!("設定の保存先: {}", config_dir());}実行結果:
設定の保存先: ~/.config/notesどのコンピュータでも、4つの関数のうち生き残るのはちょうどひとつだけなので、main は必ず config_dir を見つけられます。Playground は Linux で動いているので、Linux のパスが表示されます。本物のアプリなら、dirs のようなクレートを使って、これらのフォルダを正しく探します。
2. オプションの JSON エクスポート
Section titled “2. オプションの JSON エクスポート”スコアを表示する小さなプログラム leaderboard があります。serde_json を取り込んで export_json 関数を追加するフィーチャー export(デフォルトではオフ)を作ってください。--features export を付けたときは、スコアを JSON でも表示するようにします。ヒント: serde_json をオプションにして、フィーチャーでは dep:serde_json を使い、関数とそれを呼ぶ行の両方に #[cfg(feature = "export")] を付けます。Playground では Cargo のフィーチャーを指定できないので、この問題は自分のマシンで cargo new leaderboard から試してください。
解答を見る
[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!("{}: {}点", s.name, s.points); }
#[cfg(feature = "export")] println!("エクスポート: {}", export_json(&scores));}$ cargo run -qAna: 120点Bo: 95点
$ cargo run -q --features exportAna: 120点Bo: 95点エクスポート: [{"name":"Ana","points":120},{"name":"Bo","points":95}]default = [...] の行がないので、export は頼まない限りオフです。オフのときは、serde_json はダウンロードすらされません。関数と呼び出しの両方に #[cfg] が必要なのは、どちらも serde_json を使っているからです。
#[cfg(条件)]は、条件が真のときだけコードを残します。偽なら、コンパイルの前に取り除かれます。cfg!(条件)はただのboolです。両方の分岐が、どのプラットフォームでもコンパイルできる必要があります。- 条件は
not(..)、any(..)、all(..)で組み合わせられます。よく使うのはtarget_os、unix/windows、debug_assertions、testです。 #[cfg_attr(条件, 属性)]は、条件が真のときだけ属性を付けます。- Cargo のフィーチャーは
[features]に書きます。オプションのクレートにはoptional = trueとdep:名前を、デフォルトでオンにするものにはdefault = [...]を使います。 - フィーチャーは機能を追加するだけにしましょう。CI で
cargo check --all-featuresを実行し、unexpected_cfgsの警告に気をつけましょう。
理解度チェック
Section titled “理解度チェック”理解度チェック
1. #[cfg(target_os = "windows")] が付いた関数は、Linux でビルドするとどうなりますか?
2. Unix 専用の API を使うコードに、if cfg!(unix) { ... } では足りないのはなぜですか?
3. Cargo.toml の json = ["dep:serde_json"] は何をしますか?
4. うっかり #[cfg(feature = "jsno")] と書いてしまいました。どうなりますか?
スコア: 0 / 4
さらに詳しく
Section titled “さらに詳しく”- Features — The Cargo Book(英語):
[features]、オプションの依存クレート、dep:、そしてフィーチャーがどのように統合されるかについての完全なガイドです。 - Conditional compilation — The Rust Reference(英語):
cfgのすべてのオプションと、#[cfg]、#[cfg_attr]、cfg!の正確なルールです。 cfg!マクロ — Rust 標準ライブラリ(英語):cfg!が何を返すのか、#[cfg]属性とどう違うのかを説明しています。- Specifying Dependencies — The Cargo Book(英語):依存クレートごとの
features、default-features = false、optionalの書き方です。 - cfg — Rust By Example 日本語版:
#[cfg]とcfg!を並べた短い例が載っています。