Rust 2018 で以下のようにモジュールシステムが改善されています:
-
extern crate
は 99% 不要になりました (※公式ドキュメントより) 。 - mod.rs はサブディレクトリにサブモジュールを置く場合に不要になりました。
※公式ドキュメントに、「結合テスト」で mod.rs を用いる例が載っていますが、mod.rs を用いない方法があります (※後述) 。
参考「Path and module system changes - The Edition Guide」
※本記事では pub
キーワードについては不説明。公式ドキュメント等参照。
参考「Visibility and privacy - The Rust Reference」(pub
キーワード)
1. Rust におけるファイル分割の種類
Rust においてソースコードの「ファイルやディレクトリの分割」は以下の種類があります:
- モジュール分割 (※モジュール分割はファイルを分割しなくてもできます)
- クレート分割
- パッケージ分割 (ワークスペースの作成)
Rust のモジュールシステムおよびワークスペースの用語と役割は以下の通り:
- モジュール: 関数定義や型定義等や、またはモジュールを入れ子状に持つことができる (詳細略)
-
クレート: モジュールツリー。(子孫を含めて) 複数のモジュールを持つことができる。Rust のコンパイルの単位
- クレートルート: クレート内のルートモジュール
- パッケージ: 0 または 1 個のライブラリクレート、および複数個のバイナリクレートを持つことができる
- ワークスペース: 複数のパッケージを持つことができる (つまり複数のライブラリクレートを持てる)
参考「Modules - The Rust Reference」(モジュール)
参考「Items - The Rust Reference」(モジュールが持てるアイテム)
参考「Managing Growing Projects with Packages, Crates, and Modules - The Rust Programming Language」(パッケージ・クレート・モジュール)
参考「Cargo Workspaces - The Rust Programming Language」(ワークスペース)
2. クレート分割
バイナリプロジェクトにおいて、バイナリクレート (ルートファイルは main.rs) とライブラリクレート (ルートファイルは lib.rs) に分割します。
※「ルートファイル」=「ルートモジュールのファイル」
※ src/bin 以下や結合テストのディレクトリ構成に関しては「モジュール分割」で後述。
2.1. main.rs と lib.rs に分割する理由
大きな理由は以下の 2 つです:
-
バイナリクレートに含まれる関数等の結合テストができないから
- バイナリクレートは実行バイナリを生成するためのものであり、外部から関数等が呼ばれることを想定していない
- バイナリクレート内で
pub
を付けてもクレート外からアクセスできない - (ちなみに、結合テスト時に (単体テストでない) 実行バイナリ自体はビルドされるため、環境変数
CARGO_BIN_EXE_<name>
から実行ファイルパスを取得し、std::process::Command
等を用いて実行バイナリ自体をテストすることは可能)
- モジュール分割したいから
- モジュール強度を強化
- 可読性の向上
- 保守性の向上
- モジュール性・解析性・修正性等の向上
- 試験性・テスト容易性の向上
- バグ数削減
「モジュール分割したいから」に関しては実際「モジュール分割」で実現可能なので、「クレート分割」したい一番の理由は「結合テストできるから」かと思います。
参考「Integration Tests for Binary Crates - Test Organization - The Rust Programming Language」
参考「Integration tests - Cargo Targets - The Cargo Book」
参考「Target Selection - cargo test - The Cargo Book」
参考「Separation of Concerns for Binary Projects - Refactoring to Improve Modularity and Error Handling - The Rust Programming Language」
2.2. バイナリクレートとライブラリクレートの性質
バイナリクレートとライブラリクレートは以下の違いがあります:
- バイナリクレート
- そのクレートの外部から関数等にアクセスできない (
pub
があっても不可)
- そのクレートの外部から関数等にアクセスできない (
- ライブラリクレート
- そのクレートの外部から関数等にアクセスできる (
pub
によってクレート外まで公開されている場合) - 同一パッケージ内のバイナリ、結合テスト、Examples からライブラリクレートにアクセスできる
- ライブラリクレートが標準で Cargo.toml の
[dependencies]
セクションに追加されているような扱い
- ライブラリクレートが標準で Cargo.toml の
- そのクレートの外部から関数等にアクセスできる (
参考「Cargo Targets - The Cargo Book」
2.3. バイナリクレートからライブラリクレートの関数を呼ぶ
前述の性質より、バイナリクレートからライブラリクレート内の関数を呼ぶときは、外部クレートから呼ぶのと同様の手法で呼べます。
※ Rust 2018 以降では extern crate
は不要です。
パッケージ名 foo
かつ Cargo.toml で各ターゲットの名前 name
を指定していない場合:
fn main() {
foo::run(); // メモ: 同一パッケージのライブラリクレートが「foo クレート」として見える
}
pub fn run() {
println!("Hello, world!");
}
2.4. クレート名の命名規則
『Rust APIガイドライン』にあるクレート名の命名規則の概要は以下の通りです:
- クレート名の前後に
-rs
や-rust
を付けない- ただし、リポジトリ名とクレート名を同一にする必要はなく、リポジトリ名は自由に決めて良い
-
クレート名は
snake_case
かkebab-case
にする (統一されていない) - 名前を複数の単語に区切るとき、原則として単語を 1 文字にしない。ただし一番最後の単語を除く
- 例:
,b_tree_map
btree_map
,,pi2
pi_2
- 例:
- 名前に含まれる頭字語や複合語は 1 単語扱いで、単語の文字を全て小文字にする
- ※『Rust APIガイドライン』では
kebab-case
における単語の規則について書かれていませんが、snake_case
と同様と考えて良いかと思います
"The Cargo Book" にあるターゲット名の概要は以下の通りです:
- ライブラリクレートおよびデフォルトのバイナリクレート (ルートファイル: src/main.rs) のクレート名はデフォルトでパッケージ名になる
- パッケージ名はアルファベット、数字、ハイフン
-
、アンダースコア_
が使用できる - ※ Rust の仕様上は「アルファベット」に漢字等を含みますが、crates.io では ASCII 文字に限定されるため、結局のところ ASCII 文字の英字にするのが良いかと思います
- パッケージ名はアルファベット、数字、ハイフン
- 他に自動検出されるターゲット (src/bin 以下や 結合テスト等) のターゲット名はディレクトリ名かファイル名になる
- ハイフン
-
はアンダースコア_
に置き換えられる
総合的に考えると、「パッケージ名やクレート名を決めるときは snake_case
か kebab-case
」で、ソースコード中でのクレート名の扱いは常に snake_case
になります。
参考「Naming - Rust API Guidelines」(命名規則)
参考「The name field - Cargo Targets - The Cargo Book」(ターゲット名・クレート名)
参考「The name field - The Manifest Format - The Cargo Book」(パッケージ名)
Rust のドキュメントではクレート名は kebab-case
にしているようです。
参考「Package Layout - The Cargo Book」
3. モジュール分割
3.1. 基本
3.1.1. 子モジュールを定義する方法
子モジュールを定義する方法は以下の 2 つあります:
-
mod <子モジュール名> { /* ... */ }
を記述する (ファイル分割なし) -
mod <子モジュール名>;
を記述し、<子モジュール名>.rs を以下の場所に配置する (ファイル分割あり)- 自モジュールがクレートルートならルートファイルと同じ階層のディレクトリ (※置けない場所あり。後述)
- 自モジュールがクレートルート以外ならサブディレクトリ <自モジュール名> 直下
※「mod
を書く場所」=「自モジュール」=「カレントモジュール」
※「クレートルート」=「クレート内のルートモジュール」
※ mod
は子モジュールを定義するキーワードで、use
は既に定義されたモジュール名等を含むパスに別名を付けるキーワードです。
参考「Defining Modules to Control Scope and Privacy - The Rust Programming Language」
参考「Separating Modules into Different Files - The Rust Programming Language」
3.1.2. ファイル分割なしの例
.
├── Cargo.lock
├── Cargo.toml
└── src/
├── lib.rs
└── main.rs
mod bar {
pub mod baz {
// ... 略
}
// ... 略
}
// ... 略
※その他のファイル内容は略。
3.1.3. ファイル分割ありの例
.
├── Cargo.lock
├── Cargo.toml
└── src/
├── lib.rs
├── main.rs
├── bar.rs
└── bar/
└── baz.rs
mod bar;
// ... 略
pub mod baz;
// ... 略
※ src/bar/baz.rs やその他のファイル内容は略。
3.1.4. おまけ: 孫モジュールのみファイル分割する
ファイル分割なしの mod <子モジュール名> { /* ... */ }
とファイル分割ありの mod <子モジュール名>;
を併用することで、孫モジュールのみ別ファイルにすることも可能です。
.
├── Cargo.lock
├── Cargo.toml
└── src/
├── lib.rs
├── main.rs
└── bar/
└── baz.rs
mod bar {
pub mod baz;
// ... 略
}
// ... 略
※ src/bar/baz.rs やその他のファイル内容は略。
3.2. 子モジュールのファイルを置いてはいけない場所
パッケージのディレクトリの中で、以下の場所に子モジュールのファイルを置くとルートファイルとして認識される場合があるため、置いてはいけません:
- src/bin 直下: src/bin は複数のバイナリクレートを作りたいときに使用する
- tests 直下: tests は結合テストを作りたいときに使用する
- examples 直下
- benches 直下
※「ルートファイル」=「ルートモジュールのファイル」
src/bin 直下のクレートを名前を指定して単体でコンパイルする場合等には問題が起きませんが、cargo build --all
で実行バイナリを全てコンパイルしようとしてエラーが発生したり、cargo test
で結合テストするときに意図しないモジュールが結果に表示されたりします。
上記の理由で子モジュールのファイルを置けない場合は、さらにサブディレクトリを作り、その中にクレートルートを置くことで対処可能です。
.
├── Cargo.lock
├── Cargo.toml
└── src/
├── lib.rs
├── main.rs
└── bin/
├── bar.rs
└── baz.rs
.
├── Cargo.lock
├── Cargo.toml
└── src/
├── lib.rs
├── main.rs
└── bin/
└── bar/
├── main.rs
└── baz.rs
※上記は src/bin での例ですが、tests 等でも同様。
※公式ドキュメントに、結合テストで mod.rs を用いる例が載っていますが、上記の方法では mod.rs は不要です。
参考「Package Layout - The Cargo Book」
3.3. モジュール名の命名規則
『Rust APIガイドライン』にあるモジュール名の命名規則の概要は以下の通りです:
- モジュール名は
snake_case
にする - 名前を複数の単語に区切るとき、原則として単語を 1 文字にしない。ただし一番最後の単語を除く
- 例:
,b_tree_map
btree_map
,,pi2
pi_2
- 例:
- 名前に含まれる頭字語や複合語は 1 単語扱いで、単語の文字を全て小文字にする
参考「Naming - Rust API Guidelines」
4. ワークスペース
パッケージはライブラリクレートを最大 1 つしか持てないため、ライブラリクレートを複数管理するには複数パッケージを持つ「ワークスペース」を作成します。
※ crates.io で公開する場合はパッケージ単位で行います。
参考「Workspaces - The Cargo Book」
参考「Cargo Workspaces - The Rust Programming Language」
4.1. ワークスペースの作成
ワークスペースを作成するには以下の 2 種類の方法があります:
- 新規ディレクトリを作成し、直下にワークスペース用の Cargo.toml を作成する (※内容は後述)
- 既存のパッケージの Cargo.toml にワークスペース用の記述を追加する (※内容は後述)
4.2. ワークスペースにパッケージを追加
ワークスペースディレクトリ以下 (直下でなくても良い) にパッケージを追加し、以下のように、ワークスペースで扱うパッケージディレクトリのパスの記述を、Cargo.toml [workspace]
セクションの members
キーの値に追加します。
[workspace]
members = ["foo", "bar", "baz"]
※ [workspace]
セクションの members
キーに追加するのはパッケージの「ディレクトリのパス」であり、パッケージ名ではありません (同一である場合が多いですが、異なる場合もあります) 。
※ディレクトリのパスにワイルドカード *
等のグロブを利用できます。
※既存のパッケージをワークスペースにした場合は、カレントディレクトリの記述は追加しません。
参考「The [workspace] section - Workspaces - The Cargo Book」
4.3. デフォルトパッケージを指定
cargo run
等でデフォルトで選択されるパッケージを default-members
キーで指定します。
[workspace]
members = ["foo", "bar", "baz"]
default-members = ["foo"]
※既存のパッケージをワークスペースにし default-members
キーを記述していない場合は、そのパッケージがデフォルトパッケージになります。
参考「Package selection - Workspaces - The Cargo Book」
4.4. cargo
でワークスペース内のパッケージを管理する
具体的なコマンドにより異なりますが、cargo run
等の「パッケージ選択」("Package Selection") の機能があるコマンドに関しては、-p
/ --package
オプション等でパッケージを選択します。
※ここではパッケージディレクトリのパスでなくパッケージ名を指定します。
cargo run -p foo
コマンドによってはクレートの指定でも実行可能です。
cargo run --bin foo
「パッケージ選択」をしない場合はデフォルトパッケージに対してコマンドが実行されます。
cargo run
cargo publish
等の「パッケージ選択」の機能を持たないコマンドに関しては、扱いたいパッケージのディレクトリに移動して利用します (カレントディレクトリが操作の対象) 。
※コマンドごとに「パッケージ選択」機能の有無や何のオプションがあるかについては、コマンドのヘルプや "The Cargo Book" を参照。
※バージョン 1.55.0 現在、パッケージを選択せずデフォルトパッケージが不明な場合 (ワークスペースのルートディレクトリがパッケージでない場合) に、「[package]
セクションの default-run
キーで値を指定」するようにエラー表示されますが、その場合はルートディレクトリがパッケージではないのでそのエラー表示は無視します (その他の手段で対応) 。
参考「Cargo Commands - The Cargo Book」
4.5. ワークスペース内のパッケージ間のやりとり
ワークスペース内のパッケージ間でクレートを利用するために、必要に応じて各パッケージの Cargo.toml の [dependencies]
セクションに「パス形式」で依存関係を追加します。
[dependencies]
bar = { path = "../bar" }
crates.io で公開する場合は依存パッケージも公開し、[dependencies]
セクションで crates.io 用の version
キーの記述を追加します。
[dependencies]
bar = { path = "../bar", version = "0.1.0" }
参考「Specifying path dependencies - Specifying Dependencies - The Cargo Book」
5. その他参考
参考「The Manifest Format - The Cargo Book」(Cargo.toml のマニフェストフォーマット)
参考「toml.io/v1.0.0.md at main · toml-lang/toml.io · GitHub」(TOML の仕様)