推奨ツール
所有権やモジュールのようなコア言語機能は、プロジェクトの規模や範囲に関係なく、Rustでの開発体験を特徴づけます。 成長を続けるライブラリエコシステムにより、多くの意欲的なプロジェクトが実現可能になります。つまり、他者が開発・保守している抽象化を活用できます。 組み込みのテストサポートは、そのような大胆なプロジェクトを自信を持って構築する助けになります。
ソフトウェアエンジニアリングには、最後に取り上げるべき、おそらくそれほど華やかではない側面があります。それは保守です。 モジュールを賢く使えばアーキテクチャ上の複雑さを制御する助けになりますが、よく整理されたプロジェクトであっても、コードベースの健全性を保つには追加のツールが必要です。
このセクションでは、Rustのファーストパーティのドキュメント生成、lint、コードフォーマット、ビルド再現ツールの基本を簡単に取り上げます。 また、既知の脆弱性について依存関係を監査するなど、さまざまなタスクのためのサードパーティユーティリティもいくつか試します。
ファーストパーティツール
rustdoc
Rustには、組み込みのドキュメントジェネレータであるrustdocがあります1。
これはツールチェーンの標準の一部であり、cargoとともにバンドルされています。
特殊なコメント構文により、Markdown2でドキュメントをコードのすぐそばに直接記述できます。
利点は2つあります。
-
ドキュメントWebサイトをローカルでレンダリングしたり、リモートで提供したりできます。その包括性はコメントの充実度に応じます。これはライブラリ利用者にとって大きな恩恵です。
-
ドキュメントの例は自動的に単体テストとして実行されます。これにより、少なくとも提供した例に関しては、任意の時点でドキュメントが最新であることが保証されます。また、テストスイートを構築していくうえで少し弾みがつきます。
rustdocの動作を見るために、新しいライブラリを作成してみましょう。
cargo new --lib prime_test
tree prime_testを実行すると、次のプロジェクトレイアウトが表示されます。
prime_test/
├── Cargo.toml
└── src
└── lib.rs
lib.rsに次を追加します。
//! This library does unoptimized primality testing.
/// Given a list of numbers, get the count of prime numbers present.
///
/// # Example
///
/// ```
/// use prime_test::count_primes;
///
/// let list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
/// assert_eq!(count_primes(&list), 4);
/// ```
#[doc(alias = "primality")]
pub fn count_primes(num_list: &[usize]) -> usize {
// Unnecessary, unidiomatic check
if num_list == [] {
return 0;
}
num_list.iter().filter(|n| is_prime(**n)).count()
}
// Prime number check.
// This is a naive implementation,
// there are much more efficient implementations.
// Returns `true` if `n` is prime, `false` if not.
fn is_prime(n: usize) -> bool {
if n <= 1 {
return false;
}
for i in 2..n {
if n % i == 0 {
return false;
}
}
true
}
-
最初のコメントは
//!で始まり、クレート全体のドキュメントを提供します。 -
pubでマークされたcount_primes関数は、クレートルート(lib.rs)からエクスポートされます。これは公開APIの一部です。-
3つのスラッシュ(
///)で始まるコメントは、レンダリングされるドキュメントの一部になります。 -
#[doc(alias = "primality")]は、この関数に別のキーワードをタグ付けするマクロです。これにより、関連する検索語primalityを入力したユーザーは、この関数が検索結果に表示されるのを見ることになります。
-
-
is_primeは非公開ヘルパーであり、エクスポート用のpub修飾子は持たず、通常の非ドキュメントコメント(//で始まる行)を使用しています。
cargo testを実行すると、記述した両方の単体テストと、すべてのドキュメント例が実行されます。
ドキュメントテストだけを実行するには、cargo test --docを使用できます。
running 1 test
test src/lib.rs - count_primes (line 9) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.16s
ドキュメントをローカルでレンダリングするには、cargo doc --openを実行します。
生成されたHTML/CSS/JavaScriptのWebページが、システムのデフォルトブラウザで開かれます。
ランディングページには、クレート全体のドキュメントが表示され、エクスポートされたモジュール、構造体、関数が列挙されます。
今回の場合、唯一の公開項目はcount_primes関数です。
それをクリックすると、例を表示するドキュメントページに移動します。
rustdocは標準ライブラリのドキュメントに使用されているため、その形式はRust開発者にすでに馴染み深いものです。
さらに、プロジェクトをRustの公式パッケージリポジトリであるcrates.ioに公開すると、そのプロジェクトのドキュメントは、Rustの公式ドキュメントホストであるdocs.rsで自動的にレンダリングされ、ホストされます。
明確で完全なドキュメントを書く責任は依然としてあなたにありますが、ツールとインフラストラクチャは、必要としているユーザーの手元にドキュメントを届けるための障壁を取り除いてくれます。
追加のヒントをいくつか示します。
-
エクスポートされたすべての項目について完全性を強制したい場合は、クレートルート内に任意の
#![deny(missing_docs)]を追加すると、ドキュメントの欠落がコンパイル時エラーになります。 -
コード例に、ユーザーが存在すると仮定できるボイラープレートが含まれている場合、その行の先頭に
#を付けることで、レンダリングされたドキュメントからは省きつつ、テスト実行時には存在させることができます。- たとえば、
count_primes関数の例が# use prime_test::count_primes;で始まっていた場合、ドキュメントにはこのimport行は表示されません。
- たとえば、
-
すべてのドキュメント例が、完全に単独で実行可能なコードであるとは限りません。
cargoに対して、例がコンパイルされることは確認させるが、ドキュメントテストとして実際に実行しようとはさせないようにするには、コードブロックの先頭にno_runを付けます。これは開始の3つのバッククォートの直後に追加します。コンパイルも実行もすべきでないコードについては、同様にignoreを使用できます。
clippy
clippyは公式のコードlintツールです。
本書のコンテナにはすでにインストールされていますが、一般的なセットアップは次のようになります。
rustup update
rustup component add clippy
clippyの公式ソースリポジトリのREADME.md3によると、このツールは次のカテゴリにわたり、500以上のlintをサポートしています。
| カテゴリ | 説明 | デフォルトレベル |
|---|---|---|
clippy::all | デフォルトで有効なすべての lint(correctness、suspicious、style、complexity、perf) | warn/deny |
clippy::correctness | 明らかに誤っている、または無用なコード | deny |
clippy::suspicious | 誤っている、または無用である可能性が非常に高いコード | warn |
clippy::style | より慣用的な方法で書かれるべきコード | warn |
clippy::complexity | 単純なことを複雑な方法で行っているコード | warn |
clippy::perf | より高速に実行されるように書けるコード | warn |
clippy::pedantic | かなり厳格であったり、ときどき誤検出があったりする lint | allow |
clippy::nursery | まだ開発中の新しい lint | allow |
clippy::cargo | cargo マニフェストに対する lint | allow |
完全な lint セットには、検索可能なドキュメントサイトがあります4。
clippy::correctness は実際のバグを見つけられることに注目してください(たとえば、clippy::style が指摘する、慣用的ではないが正しいコードとは対照的です)。
ただし、correctness チェックのうち、十分な確信をもって自動的に適用できるほど正確なものはごく少数です(例: MachineApplicable5 ルール)。
デフォルト設定で prime_test ライブラリに対して clippy を実行するには、次のようにします。
cd code_snippets/chp3/prime_test
cargo clippy
// Unnecessary, unidiomatic check というコメントが付いた、count_primes 内の if ブロックに対して、次の警告が表示されます。
warning: comparison to empty slice
--> src/lib.rs:18:8
|
18 | if num_list == [] {
| ^^^^^^^^^^^^^^ help: using `is_empty` is clearer and more explicit: `num_list.is_empty()`
|
= note: `#[warn(clippy::comparison_to_empty)]` on by default
= help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#comparison_to_empty
warning: `prime_test` (lib) generated 1 warning
この警告は実際に有用で、より明示的な is_empty() API を使うことで、コードをひと目で理解しやすくなります。
count_primes を次のように更新すれば、この警告は消えます。
pub fn count_primes(num_list: &[usize]) -> usize {
if num_list.is_empty() {
return 0;
}
num_list.iter().filter(|n| is_prime(**n)).count()
}
しかし、リンターには限界があります。 リンターは構文を解析しますが、意味を理解するわけではありません。 より複雑な lint は、あたかも意味を理解しているかのように見えることがありますが。
実際には、このチェックは必要ありません。イテレーターのフィルタリング(上記の .iter().filter(...))は、空入力というエッジケースをすでに正しく処理するからです。
この関数はワンライナーにできます。
pub fn count_primes(num_list: &[usize]) -> usize {
num_list.iter().filter(|n| is_prime(**n)).count()
}
それでも、clippy は一般的なコード品質を向上させ、維持するための強力で有用なツールです。
また、その lint は定期的に改善されています。
clippy は CI に加えるのに最適です。
「Trojan Source」攻撃についての補足
clippy::correctnessに含まれる、やや難解な lint の 1 つは、ソースファイル内の不可視 Unicode 文字を拒否します。 これは潜在的に意外なエッジケースを排除しますが、Rust コンパイラー自体も現在では、「trojan source」攻撃6 を防ぐために lint を行っています。この攻撃は、エンコーディングのトリックを使って、人間には(可視文字の観点で)ある見え方をする一方で、コンパイラーには(解析されたトークンの観点で)別の見え方をするソースコードを生成します。 この変更は、研究者がこの攻撃をサプライチェーンセキュリティへの脅威として報告した CVE-2021-425747 への対応として行われました。
rustfmt
ベストプラクティスや高レベルのイディオムをチェックするリンターとは異なり、rustfmt は低レベルのスタイルルールを強制します。
最大行幅、項目間で許される空白行の数、開始波括弧を同じ行に置くべきか次の行に置くべきか、といったものです。
特定のプロジェクトに適用される個々の書き換えは設定可能です8。 チームの好みに合わせてスタイルルールを調整できます。
rustdoc と同様に、rustfmt は cargo サブコマンドで実行できます。
デフォルトのルールセットを適用するには、次のようにします。
cargo fmt
この prime_test ライブラリの例では、このコマンドは何の効果もありません。
しかし、大規模で複数の開発者が関わるコードベースでは、一貫性と可読性の基準レベルを確保するための重要な手段になり得ます。
clippy と同様に、rustfmt も CI の理想的な候補です。
すべてのコミットが統一されたスタイルに従っていれば、コードレビューはより効率的になります。
Cargo.lock ファイル
ファーストパーティのリストにおけるこの最後の項目は、単独のツールではありません。むしろ、議論しておくべき cargo の重要な機能です。
cargo build や cargo run を実行したあと、Cargo.toml の横に新しいファイル Cargo.lock が現れたことに気づいたかもしれません。
Cargo.toml は頻繁に編集するもの(例: 新しい依存関係を追加するため)である一方、Cargo.lock は再現可能なビルドのためのメタデータを含む自動生成ファイルです9。
これは、ビルド時点で特定の依存関係バージョンを「固定」します。
ビルドするたびに cargo が依存関係を積極的にアップグレードすることは望ましくないでしょう。
再現可能なビルド
たとえば、Cargo.toml に rayon クレート10 への依存関係を次のように追加したとします。
[dependencies]
rayon = "^1.5"
Cargo Book の依存関係指定ガイド11 によれば、これはプロジェクトが 1.5.0 より大きく 2.0.0 未満の任意のセマンティックバージョン12 の rayon を使用できることを意味します。
あなたが最初にプロジェクトをビルドした時点で、利用可能な rayon の最新バージョンが 1.5.0 だったと仮定しましょう。
バージョン 1.5.1 がリリースされると何が起こるでしょうか。
何も起こりません。
その最初のビルド中に生成された Cargo.lock ファイルは、使用する rayon のバージョンとして 1.5.0 を記録しています。
Cargo.lock を含むプロジェクトディレクトリのコピーを同僚と共有すれば、その同僚はあなたとまったく同じ依存関係バージョンを使ってプロジェクトをビルドできます。
Chapter 2 の CLI ツールのように、実行可能ファイルをビルドするプロジェクトでは、Cargo.lock ファイルをバージョン管理にコミットするのがよい考えです。
そうすれば、誰でも同等の実行可能ファイルをビルドできます。
さらに、同僚がまったく同じコンパイラーバージョンを使い、同じプラットフォームをターゲットにすることを確実にするために、rust-toolchain.toml13 ファイルを含めることもできます。
理論上は、依存関係のバージョンを固定する必要はないはずです。
セマンティックバージョニングでは、1.5.1 には完全に後方互換性のあるバグ修正のみが含まれることになっています。
しかし、それは自動的に強制できるものではなく、慣習にすぎません。
そしてソフトウェアは複雑であり、些細なバグ修正が、あなたのプロダクトや環境に固有の問題を引き起こす可能性は十分にあります。
だからこそ、プロダクションソフトウェアにとって再現可能なビルドは非常に重要です。
特に、継続的インテグレーションと継続的デプロイメント(CI/CD)に関してはなおさらです。
Cargo.toml で許可される最新の依存関係バージョンへ更新する準備ができたら、単に次を実行します。
cargo update
cargo test
前者のコマンドは、新しく公開されたバージョンを検索し、Cargo.lock を更新します。
後者はテストスイートを実行します。
念のためです。
セルフホストされた依存関係
依存関係は crates.io でホストされている必要はないことに注意してください。
第2章のCLIツールは、次のように clap をそのGitHubリポジトリから直接取得することもできました。
[dependencies]
clap = { git = "https://github.com/clap-rs/clap.git", features = ["derive"] }
git = ... は、あなたの会社やチームが管理するプライベートなセルフホストリポジトリを含む、任意のGitリポジトリURLに使用できます。
さらに、特定のブランチとコミットハッシュに手動で固定することもできます。
[dependencies]
clap = { git = "https://github.com/clap-rs/clap.git", branch = "master", rev = "31bd0b5", features = ["derive"] }
これは、他の依存関係を cargo update でアップストリームに追従させ続けながら、内部ライブラリの既知の良好なバージョンに留まる必要がある場合に便利です。
サードパーティ製ツール
組み込みのメンテナンスツールは氷山の一角です。
エコシステムには、しばしば cargo のプラグインとしてバンドルされる、さまざまな追加機能があります。
つまり、cargo に追加のサブコマンドを拡張するのは、多くの場合 cargo install <name_of_tool> を実行するのと同じくらい簡単だということです。
ただし、一部のツールには追加のセットアップ手順があります。
ここでは、3つの異なるサードパーティ製プラグインを試してみます。 このセクションの残りでは、この本のコンテナを使用しているものとします。このコンテナには、それぞれが事前にインストールされています。 そうでない場合は、各ツールのドキュメントを参照してください(脚注にリンクがあります)。
cargo-modules
大規模なRustプロジェクトの概要をすばやく把握する必要がある場合、cargo-modules14 が便利です。
これは、内部APIと外部APIの両方を含むモジュール階層をコンソールに出力します。
第2章で使用したCLI引数パーサーである clap で試してみましょう。
git clone git@github.com:clap-rs/clap.git
cd clap/
cargo modules generate tree --with-types --package clap
次のような内容で始まる、色分けされた出力が表示されるはずです。
crate clap
├── const INTERNAL_ERROR_MSG: pub(crate)
├── const INVALID_UTF8: pub(crate)
├── struct SubCommand: pub
├── mod build: pub(crate)
│ ├── mod app_settings: pub(self)
│ │ ├── struct AppFlags: pub
│ │ ├── enum AppSettings: pub
│ │ └── struct Flags: pub(self)
│ ├── mod arg: pub(self)
│ │ ├── struct Arg: pub
│ │ ├── enum ArgProvider: pub(crate)
│ │ ├── enum DisplayOrder: pub(crate)
│ │ ├── type Validator: pub(self)
│ │ ├── type ValidatorOs: pub(self)
│ │ └── fn display_arg_val: pub(crate)
...
cargo-audit
Rustのcrateエコシステムは、ある種の「諸刃の剣」です。
-
一方で、
cargoは外部依存関係のビルドと統合を簡単で楽しいものにします(GNUmake15 のような従来のシステムソフトウェア用ビルドツールと比べて)。 -
他方で、プロフェッショナルなプロジェクトでは、驚くほど大量の依存関係がすぐに蓄積されることがあります。その多くは推移的な依存関係(依存関係の依存関係)です。
長いコンパイル時間は厄介ですが、大規模な依存関係グラフの本当の欠点はメンテナンスです。 直接依存している依存関係を最新バージョンに更新したからといって、その作者たちも同じことをしているとは限りません。 既知のセキュリティ問題を持つcrateバージョンに、直接的または間接的に依存してしまう可能性があります。
ここで登場するのが、もう1つの cargo プラグインである cargo-audit16 です。
これは、依存関係グラフ全体をスキャンし、既知の脆弱性を持つcrateバージョンを探します。
Rust Secure Code Working Group17 が管理する Rust Security Advisory Database18 に登録された公開データを使用します。
第2章の rcli ツールの完全な依存関係グラフを監査できます。
コマンドは rcli フォルダではなく、ワークスペースルートから実行することに注意してください。
cd code_snippets/chp2/crypto_tool/
cargo audit
本稿執筆時点では、このスキャンは399件のセキュリティアドバイザリ(シグネチャデータ)を読み込み、29個の依存関係(完全な依存関係グラフ)をチェックしました。
Fetching advisory database from `https://github.com/RustSec/advisory-db.git`
Loaded 399 security advisories (from /home/tb/.cargo/advisory-db)
Updating crates.io index
Scanning Cargo.lock for vulnerabilities (29 crate dependencies)
第2章の静的/動的、既知/未知の象限を覚えていますか?
cargo-audit は、あなたのプロジェクトに固有のまったく新しいバグを発見することはありませんが、依存関係に対する重要な健全性チェックです。
今回の rcli プロジェクトには問題がなかったので、cargo-audit の警告やエラー出力がどのようなものか気になるかもしれません。
以下は警告のサンプルです。
Crate: difference
Version: 2.0.0
Warning: unmaintained
Title: difference is unmaintained
Date: 2020-12-20
ID: RUSTSEC-2020-0095
URL: https://rustsec.org/advisories/RUSTSEC-2020-0095
Dependency tree:
difference 2.0.0
└── predicates 1.0.8
cargo-binutils
cargo-binutils19 は、Linuxバイナリを調査するためのコマンドラインツール群であるGNU Binutils[^GNUBinutils] のラッパーです。
ここでは、Binutilsスイート内のすべてのツールを列挙することはしません。
感覚をつかむために、size サブコマンドを使って、第2章の rcli ツールの出力バイナリに含まれる各セクションの正確なバイト数を取得できます。
cd code_snippets/chp2/crypto_tool/rcli
cargo size --release -- -A
特定の1行には、ELF20 バイナリが実行可能コードを格納する .text セクションのサイズが出力されます。
section size addr
.text 598995 0x9080
報告される正確な数値は、コンパイラのバージョンとホストアーキテクチャによって異なります。
私たちの場合、最適化付き(--release)でビルドしたとき、rcli には 599 kB の実行可能コードが含まれます。
まとめ
ファーストパーティ製ツールを使うと、テストスイートも兼ねる最新のドキュメントを生成し、最新のベストプラクティスパターンに照らしてコードをリントし、大規模な開発チーム全体で一貫したフォーマットを保証し、再現可能なビルドを容易にできます。
サードパーティ製ツールは、さまざまな補助的タスクを実行します。
上記のリストは、エコシステムに存在するもののサンプルにすぎず、より多くのツールや cargo プラグインが毎年利用可能になっています。
Rust で本番ソフトウェアを構築するなら、その言語、ツールチェーン、エコシステムに投資していることになります。
エコシステムの依存関係のバージョンは十分簡単に管理できます。SemVer12 番号は Cargo.toml で設定できます。
では、言語そのものについてはどうでしょうか?
この章の締めくくりとして、Rust ツールチェーンのリリースサイクルを簡単に見ていきます。 心配はいりません。変更は常に後方互換であり、新しいバージョンがあなたのコードを壊すことはありません。 しかし、Rust のバージョニングがどのように機能するかを理解しておくことは有用です。 最新最高のものを追いかけたい場合でも、本番運用を順調に続けたいだけの場合でも同様です。
-
rustdoc とは?。The Rustdoc Book(2022年閲覧)。 ↩
-
基本構文、元の設計文書で概説された Markdown 要素。。Matt Cone(2022年閲覧)。 ↩
-
Clippy の lint。The Rust Team(2022年閲覧)。 ↩
-
MachineApplicable。The Rust Team(2022年閲覧)。 ↩ -
Trojan Source: 見えない脆弱性。Nicholas Boucher、Ross Anderson(2021年)。 ↩
-
rustc のセキュリティアドバイザリ(CVE-2021-42574)。The Rust Team(2022年閲覧)。 ↩
-
Rustfmt の設定。The Rust Team(2022年閲覧)。 ↩
-
Cargo.tomlとCargo.lock。The Cargo Book(2022年閲覧)。 ↩ -
セマンティック バージョニング 2.0.0。Tom Preston-Werner(2022年閲覧)。 ↩ ↩2
-
cargo-modules。Vincent Esche(2022年閲覧)。 ↩
-
cargo-audit。Alex Gaynor、Tony Arcieri、Sergey Davidoff(2022年閲覧)。 ↩
-
Secure Code Working Group。Rust Secure Code Working Group(2022年閲覧)。 ↩
-
Rust Security Advisory Database。Rust Secure Code Working Group(2022年閲覧)。 ↩
-
cargo-binutils。The Rust Embedded Working Group(2022年閲覧)。 ↩
-
Tool Interface Standard(TIS)Executable and Linking Format(ELF)仕様。TIS Committee(1995年)。 ↩