項目26:忍び寄るフィーチャに注意しよう
https://effective-rust.com/features.html
LT;DR
フィーチャ名とクレート名は同じ 名前空間 を共有する
そのため、フィーチャを定義する際には、依存クレート名と衝突しないように注意すること
フィーチャ統合 が起こるため、フィーチャは加法的であるべき
そのためには、フィーチャは public な構造体のフィールドやトレイトのメソッドをクレートに依存させてはいけない
hr.icon
条件コンパイル
Rust は #[cfg] または #[cfg_attr] アトリビュートを用いることで、付与したプログラムの アイテム(e.g. 関数や行、ブロック)をコンパイル対象とするか制御できる
アトリビュートには名前だけ渡す("test")ことも、名前と値のペア(panic = "abort")を渡すことも可能
同じ名前に対して、複数の値を指定することも可能
code:rs
// --cfg myname="a" --cfg myname="b" をつけて実行
#cfg(myname = "a")
println!("cfg(myname = 'a') is set");
#cfg(myname = "b")
println!("cfg(myname = 'b') is set");
後述する フィーチャ 値以外では、ツールチェーンがビルド環境を表すために自動的に設定される値がよく利用される
e.g. OS(target_os)や アーキテクチャ(target_arch)、ポインタサイズ(target_pointer_width)、エンディアン(target_endian)、アトミック操作 をサポートしている ビット幅(target_has_atomic)
これらをセットすることで、特定のターゲットに固有の機能をそのターゲットに対してビルドする時のみコンパイルするので、移植性 が高まる
フィーチャ
クレートの機能をフィーチャ名を指定することで選択的に有効化できる機能
Cargo の機能なので、コンパイラ にとってはフィーチャも他の cfg と同じように処理される
あるクレートで使用できるフィーチャを確認するには、Cargo.toml の features セクションと、optional = true が指定されている dependencies セクションを確認 すれば良い
たとえば以下の Cargo.toml には 6 つ のフィーチャが存在する(5 つではない)
code:Cargo.toml
features
default = "featureA"
featureA = []
featureB = []
featureAB = "featureA", "featureB"
schema = []
dependencies
rand = { version = "^0.8", optional = true }
hex = "^0.4"
warning.icon
default は特別なフィーチャ名である
値にはデフォルトで有効にするフィーチャ名を指定する
無効にする際には、--no-default-features を指定する
Cargo.toml 上で記載することも可能
code:Cargo.toml
dependencies
somecrate = { version = "^0.3", default-features = false }
default はフィーチャ名なので、cfg 内で利用することも可能
code:rs
#cfg(feature = "default")
println!("This crate was built with the \"default\" feature enabled.");
#cfg(not(feature = "default"))
println!("This crate was built with the \"default\" feature disabled.");
依存クレートに optional = true を指定すると、クレート名がフィーチャとなる
この挙動は、[features] セクションで以下のように指定すれば無効にできる
https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies
code:Cargo.toml
features
rand = "dep:rand"
もちろん cfg 内でも利用できる
code:rs
#cfg(feature = "rand") // --features rand で有効
pub fn pick_a_number() -> u8 { rand::random::<u8>() }
#cfg(not(feature = "rand")) // --features rand で無効
pub fn pick_a_number() -> u8 { 4 }
つまり、クレート名とフィーチャ名は同じ 名前空間 にある
そのため、同じ名前だと衝突する可能性があるので「フィーチャ名を選ぶときは慎重に」
package を指定することも可能
code:Cargo.toml
dependencies
foo = { package = "foo-lib", version = "1.0" }
フィーチャ統合
一方、あるクレートが有効にしているフィーチャは、フィーチャ統合 が起こりうるため Cargo.toml からは把握することができない
たとえば以下のコードでは somecrate の featureA と rand フィーチャを有効にしているが、これ以外のフィーチャも有効になる可能性がある
code:Cargo.toml
dependencies
somecrate = { version = "0.3", features = "featureA", "rand" }
そのため、フィーチャは 加法的 である(それぞれのフィーチャを同時に有効にしたときに矛盾が起きない)ようにするべき
具体的には、
「構造体の public なフィールドがフィーチャに依存するのは避ける」
これにより、ユーザのコード上で予期せぬ動作が発生する可能性を回避できる
e.g.
code:rs
#derive(Debug)
pub struct ExposedStruct {
pub data: Vec<u8>,
#cfg(feature = "schema")
pub schema: String,
}
「public なトレイトのメソッドがフィーチャに依存するのは避ける」
構造体の場合と同じ理由
ただし、ユーザが オーバーライド する必要のない デフォルト実装 は例外
e.g.
code:rs
pub trait AsBbor: Sized {
fn serialize(&self) -> Result<Vec<u8>, Error>;
fn deserialize(data: &u8) -> Result<Self, Error>;
#cfg(feature = "schema")
fn cddl(&self) -> String;
}
フィーチャ統合により、あるクレートに N 個の独立したフィーチャがあった場合、理論的には 2^N 個の組み合わせのビルドが起こりうる
そのため、加法的であることを保証するにはこれらすべての組み合わせをテストする必要がある
とはいえ、フィーチャを用意するのは、拡大する依存グラフに対して公開するものを制御するのに大いに役立つ
特に no_std 環境でも利用できる 低レイヤ のクレートでは有用
実際、このようなクレートでは std や alloc をフィーチャとして、依存する機能の有効化を制御することが多い
#Rust #Effective_Rust_―_Rustコードを改善し、エコシステムを最大限に活用するための35項目