コンテンツにスキップ

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!() マクロは同じ条件を受け取りますが、結果をふつうの 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/ana
▶ 実際に試してみよう

target_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 システムです");
}
}

実行結果:

デフォルトのシェル: sh
64ビットの 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)] で始まっていました。

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 = "名前" でフィーチャーを確認します。

src/main.rs
#[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…」の行を隠すだけのオプションです)。

Terminal window
cargo run -q # デフォルトのフィーチャーだけ
cargo run -q --features json # デフォルト + json
cargo 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 に聞けばわかります。

Terminal window
cargo tree -e features --features json --depth 1

pizza_order では、こう表示されます。

pizza_order v0.1.0 (/home/ana/pizza_order)
├── serde feature "default"
├── serde feature "derive"
└── serde_json feature "default"

--depth 1 を付けないと、依存クレートがさらにその依存クレートでオンにしているフィーチャーまで含めた、ツリー全体が表示されます。長くなりますが、「どうしてこのクレートがコンパイルされているんだろう?」と思ったときには、いちばん頼りになる道具です。

フィーチャーは機能を追加するだけにする。 フィーチャーをオンにしたことで、関数がなくなったり、既存の関数の動きが変わったりしてはいけません。理由はこうです。プロジェクトの中の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 に追加しましょう。

Terminal window
cargo check --all-features
cargo check --no-default-features
cargo test --all-features

大きなクレートなら、cargo-hack というツールでフィーチャーをひとつずつ確認できます(cargo hack check --each-feature)。

フィーチャー名のタイプミス。 #[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 に入れるべき理由です。

メモアプリが設定を保存する場所を返す関数 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"]
src/main.rs
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 -q
Ana: 120点
Bo: 95点
$ cargo run -q --features export
Ana: 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 の警告に気をつけましょう。

理解度チェック

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