条件付きコンパイルと cfg

17 分で読了
このページの目次

cfg は、特定の実装がビルドに含まれるかどうかを決定します。プラットフォーム固有のモジュール、オプショナルな依存関係、テスト補助コードに適しています。これはプログラムの起動環境に基づいてランタイムに選択を行うものとは異なり、サポートされている構成に対するすべてのコンパイル時チェックを代替するものではありません。

cfg と cfg! の重要な違い

#[cfg(unix)] fn platform()無効なコードは名前・型検査から除去if cfg!(unix) { ... } else { ... }両方の分岐は有効な名前と型が必要存在しうるコード項目を選択プラットフォーム固有の API に適している定数条件の分岐を選択両側がコンパイル可能な式に適しているcfg 属性はコードを除去し、cfg! はブール定数を生成するだけ条件付きコンパイルは、無効化された領域に任意の解析不可能な構文を含めることを許可するものではありません。
#[cfg(unix)]
fn platform_name() -> &'static str { "unix" }

#[cfg(not(unix))]
fn platform_name() -> &'static str { "other" }

fn main() {
    println!("{}", platform_name());
    let word = if cfg!(debug_assertions) { "checks" } else { "optimized" };
    println!("{word}");
}

cfg! は true/false に展開されますが、その外側の通常の if の両方の分岐は、名前解決と型チェックの対象となります。Linux でのみ存在する関数を、他のプラットフォームでは if cfg!(target_os="linux") で隠すことはできません。そのような場合は、項目またはブロックに対して #[cfg] を使用してください。条件付きコンパイルのルール

#[cfg] は構文レベルでの構成除去であり、ソースファイルは依然として Rust の構文として読み込める必要があります。無効化された領域に任意の壊れたトークンを置くことはできません。モジュールレベルのプラットフォーム実装では、異なるファイルを cfg で選択し、各実装が同じ公開インターフェースを維持するようにします。

プラットフォーム固有の差異を実装する前に、分岐のカバーを確保する

同じ関数の複数の実装には、以下の2つの条件を満たす必要があります:サポートされている構成のいずれにおいても少なくとも1つが存在し、かつ同時に2つが選択されないこと。最も保守しやすい二分法は、条件 P と not(P) を使用することであり、「現在思いつく他のプラットフォーム」を手動で列挙することではありません。

#[cfg(all(unix, target_pointer_width = "64"))]
fn implementation() -> &'static str { "unix-64" }

#[cfg(not(all(unix, target_pointer_width = "64")))]
fn implementation() -> &'static str { "portable" }

fn main() {
    assert_eq!(implementation(),
        if cfg!(all(unix, target_pointer_width = "64")) {
            "unix-64"
        } else {
            "portable"
        });
}

ここで2つの実装は同じシグネチャを持ち、呼び出し側はプラットフォーム判断を繰り返す必要がありません。all はすべての条件が成立することを示し、any は少なくとも1つが成立することを示し、not は条件を反転させます。空の all() は真、空の any() は偽です。これらの空条件は、最小限の実験を構築するためにも適しています。未定義の名前を含む関数を #[cfg(any())] としてマークすると、名前チェックの対象から外れます。これを通常の if cfg!(any()) に変更すると、未定義の名前に対してエラーが発生します。

OS に基づいて実装を選択する場合、重複する条件で同名の関数をそれぞれ定義しないでください。例えば、unix と target_os="linux" は同時に成立する可能性があります。feature に基づいて選択する場合も、和集合を考慮する必要があります。2つのユーザー機能は同時に有効になっている可能性があります。製品契約で組み合わせが明確に禁止されている場合のみ、compile_error! を使用してその組み合わせを拒否してください。通常、機能は共存を許可すべきです。構成述語

false の分岐でも型検査に失敗する理由

実行時に分岐へ入るかどうかでは、コンパイル時の検査は決まりません。linux_only を Linux のみの定義とし、名前検査まで残るコードを比較します。#[cfg] は呼び出しを除去し、cfg! は真偽定数に展開するだけです。

図を準備しています
false の分岐でも型検査に失敗する理由

#[cfg] は無効なコードを除去し、if cfg! は両分岐に名前・型検査が必要な真偽分岐です。

一般的な条件の次元による分類

条件表現される事実注意
target_oslinux、windows、macos などのターゲットOSビルドスクリプトが実行されるホストOSではない
target_archx86_64、aarch64、wasm32 など全 ABI を代表するものではない
target_envgnu、musl、msvc などのターゲット環境一部のターゲットでは値が空の場合がある
target_pointer_widthポインタの幅すべての整数やアドレスの実装幅と等しくない
target_featureこのターゲットビルドで有効化された命令セットの機能任意のデプロイメントマシンがサポートしているわけではない
debug_assertions対応するコンパイルフラグプロファイルで上書き可能であり、固定の dev/release タグと等しくない
testクレートがテストモードでコンパイルされている統合テストが依存する通常のライブラリは、これにより自動的に cfg(test) を持つわけではない
feature="json"Cargo で有効化された現在のパッケージの機能CPU の機能ではない

現在のターゲット構成を確認するには:

rustc --print cfg
rustc --print cfg --target aarch64-unknown-linux-gnu

2番目のコマンドは、コンパイラが認識しているターゲット構成を表示しますが、ターゲット標準ライブラリのインストールやリンクは行いません。ターゲットに対して名前が取得できたからといって、その環境でビルド成果物が実行できるわけではありません。

cfg_attr は属性を選択し、実行フローを変更しない

#[cfg_attr(debug_assertions, derive(Debug))]
struct State { value: u32 }

fn main() {
    let state = State { value: 7 };
    assert_eq!(state.value, 7);
    #[cfg(debug_assertions)]
    println!("{state:?}");
}

条件が満たされると derive(Debug) が付加され、そうでない場合は付加されません。例の Debug の使用箇所も同じ条件によって制御されます。println! を if cfg!(debug_assertions) の中に置くと、デバッグアサーションを無効にしたビルドでも State が Debug を実装していることが要求されます。同様に、リンク属性、ドキュメント注釈、または表示属性を選択することもできますが、プラットフォーム間で repr を変更する場合は、FFI、ストレージ形式、および依存側のレイアウトの期待値を考慮する必要があります。

同じ条件のセットが繰り返し出現する場合、モジュールの境界に集約できます。散在する cfg は、特定の構成でメソッド、インポート、またはトレイト実装が漏れる原因となる可能性があります。デフォルト構成でのコンパイルが成功することは、その1つの分岐の組み合わせが有効であることを証明するだけでしかありません。

コンパイル時の CPU 機能とランタイムのディスパッチ

#[cfg(target_feature="avx2")] は、現在のビルド環境で AVX2 が有効化されていることを示しており、実行マシンに対して検出を実行するものではありません。target-cpu=native を使用してビルドし、異なる CPU に配布すると、互換性が失われる可能性があります。

汎用バイナリでマシンごとに専用実装を選択する必要がある場合は、ターゲットプラットフォームのランタイム機能検出(例:x86 における is_x86_feature_detected!)を使用し、#[target_feature] の安全条件を満たす関数を呼び出すと同時に、ベースライン実装を保持してください。専用命令を無条件で実行し、その後にサポートされているかどうかを if で判断することはできません。x86 機能検出

build.rs によるカスタム cfg の生成

例えば、ネイティブライブラリが API を提供しているかどうかは、ビルドスクリプトによって決定する必要があります。cfg の有効な名前と値の両方を同時に宣言し、rustc のチェックでスペルミスを検出できるようにします。以下では、明示的な環境入力を示しており、実際のシステムライブラリが検出されたことを主張するものではありません。

fn main() {
    println!("cargo::rustc-check-cfg=cfg(has_demo_api)");
    println!("cargo::rerun-if-env-changed=DEMO_API");
    if std::env::var("DEMO_API").as_deref() == Ok("1") {
        println!("cargo::rustc-cfg=has_demo_api");
    }
}
ホスト:build.rs を実行DEMO_API またはターゲットライブラリの情報を取得Cargo のターゲット構成TARGET / CARGO_CFG_TARGET_OSrustc-check-cfg有効な名前 has_demo_api を宣言rustc-cfg(条件付き出力)実際に has_demo_api を有効化ターゲットコード:有効性チェックと条件選択は別々に発生build.rs はホストで実行され、出力は現在のパッケージのターゲットコードに影響を与えるbuild.rs 内の cfg!(target_os) はホストを記述します。ターゲット情報を取得して初めて、クロスビルドを指導できます。

スクリプトをプロジェクトルートの build.rs として保存し、src/main.rs で完全な二分インターフェースを使用します。

#[cfg(has_demo_api)]
fn capability() -> &'static str { "enabled" }

#[cfg(not(has_demo_api))]
fn capability() -> &'static str { "fallback" }

fn main() {
    println!("{}", capability());
}

POSIX シェルで以下のコマンドを順に実行します。2 回目と 3 回目では、target ディレクトリを手動でクリーンアップする必要はありません。環境入力が変化すると、スクリプトは再実行されます。

DEMO_API=0 cargo run --quiet
DEMO_API=1 cargo run --quiet
DEMO_API=0 cargo run --quiet

出力は順に fallback、enabled、fallback となります。スクリプトは文字列 "1" のみが有効化されると明確に規定しています。"true"、空文字列、および欠落値は有効化されません。実際のライブラリ検出では、「サポートされていない」ことと「検出に失敗した」ことを区別する必要があります。そうしないと、ツールチェーンの欠如が機能の欠如として誤って解釈され、フォールバック実装が静かに選択される可能性があります。

メインコードでは #[cfg(has_demo_api)] を使用します。check-cfg は条件が合法であることを宣言するだけであり、条件を有効化しません。rustc-cfg によって初めて有効化されます。cargo::KEY 形式は対応する Cargo のバージョンをサポートしている必要があります。より古いプロジェクトは、MSRV に応じて cargo:KEY 形式を使用できます。ビルドスクリプトの出力

検出対象がクロスターゲットである場合、ターゲットライブラリとそのヘッダーファイルを確認する必要があり、ホストにインストールされたライブラリを検出して、その結論をターゲットに直接書き込むことはできません。カスタム cfg は、それを使用する他のクレートには自動的に伝播しません。

スペルチェックと有効化スイッチの独立した検証

上記の main.rs を一時ディレクトリにコピーし、rustc の2種類の入力を直接検証できます。

rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo
rustc --edition 2024 --check-cfg 'cfg(has_demo_api)' --cfg has_demo_api -D unexpected_cfgs main.rs -o cfg-demo
./cfg-demo

1 回目は有効な条件を登録するだけで、fallback を出力します。2 回目は追加でその条件を有効化し、enabled を出力します。ソースコード内の名前を has_demo_ap とスペルミスすると、unexpected_cfgs によって拒否され、スペルミスが「サポートされていない」として静かに解釈されるのを防ぎます。考えられるすべての名前を宣言する際は、無条件で実行する必要があります。検出が成功した分岐内でのみ宣言すると、機能がオフのときに予期しない構成警告が発生する可能性があります。rustc 構成チェック

構成マトリックスはインターフェースの組み合わせをカバーすべきである

オプショナルな機能をサポートすると謳うライブラリに対しては、少なくともデフォルト、デフォルトをオフ、単一の重要な機能、および約束された組み合わせを個別にチェックする必要があります。all-features は和集合に過ぎず、デフォルト機能をオフにしたテストを代替するものではありません。cfg(test) の補助的なインポートが、通常のビルドでの欠如を隠している可能性もあります。

cargo check、cargo test、および --target を組み合わせて、異なるレベルを確認できます。クロス cargo check は、ターゲットリンカー、ネイティブ依存関係、またはランタイムの動作を検証しないため、これらの結論を正確に区別する必要があります。

失敗の原因に基づいてマトリックスを構成し、コマンド数を増やすだけでなく

ビルドの次元発見すべき具体的な問題有用なチェック
デフォルト機能のオフデフォルトのインポートによる依存関係の欠如の隠蔽、フォールバック実装の未定義cargo check --no-default-features
単一機能のオンその機能が別の機能を誤って依存している個別に --no-default-features --features 名前で
許可された機能の組み合わせ重複定義、競合する実装、依存機能のマージ約束された組み合わせに対して個別に check/test
debug_assertions のオフDebug などの属性の使用箇所が同期して保護されていないrelease ビルドまたはそのスイッチを明示的に上書き
カスタム条件の真/偽フォールバック分岐、宣言の漏れ、入力変化による再ビルドの欠如上記の 0 → 1 → 0 の実験
異なるターゲットプラットフォーム API、型幅、ネイティブライブラリのリンクターゲットに対して check し、利用可能な環境でリンクおよび実行

例えば、「json 機能 + has_demo_api」は2つの独立した軸であり、両方がオフ、json のみ、API のみ、両方がオンという4つの構成を考慮する必要があります。実際の契約で、特定の軸が特定のターゲットでのみ出現することを許可している場合、制限を明確に記述し、サポートされていない組み合わせに対する診断を検証する必要があります。現在のマシンが通過するようにするために、rustc に unix などの組み込み条件を手動で注入しないでください。これではターゲット ABI は変更されず、対応する標準ライブラリもインストールされません。