55
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[Rust] ファイル分割によるクレート分割、モジュール分割、パッケージ分割

Last updated at Posted at 2021-10-21

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 Targets - The Cargo Book

2.3. バイナリクレートからライブラリクレートの関数を呼ぶ

前述の性質より、バイナリクレートからライブラリクレート内の関数を呼ぶときは、外部クレートから呼ぶのと同様の手法で呼べます。

※ Rust 2018 以降では extern crate は不要です。

パッケージ名 foo かつ Cargo.toml で各ターゲットの名前 name を指定していない場合:

src/main.rs
fn main() {
    foo::run(); // メモ: 同一パッケージのライブラリクレートが「foo クレート」として見える
}
src/lib.rs
pub fn run() {
    println!("Hello, world!");
}

2.4. クレート名の命名規則

『Rust APIガイドライン』にあるクレート名の命名規則の概要は以下の通りです:

  • クレート名の前後に -rs-rust を付けない
    • ただし、リポジトリ名とクレート名を同一にする必要はなく、リポジトリ名は自由に決めて良い
  • クレート名は snake_casekebab-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_casekebab-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
src/lib.rs
mod bar {
    pub mod baz {
        // ... 略
    }
    // ... 略
}
// ... 略

※その他のファイル内容は略。

3.1.3. ファイル分割ありの例

ディレクトリ構成例
.
├── Cargo.lock
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── main.rs
    ├── bar.rs
    └── bar/
        └── baz.rs
src/lib.rs
mod bar;
// ... 略
src/bar.rs
pub mod baz;
// ... 略

src/bar/baz.rs やその他のファイル内容は略。

3.1.4. おまけ: 孫モジュールのみファイル分割する

ファイル分割なしの mod <子モジュール名> { /* ... */ } とファイル分割ありの mod <子モジュール名>; を併用することで、孫モジュールのみ別ファイルにすることも可能です。

ディレクトリ構成例
.
├── Cargo.lock
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── main.rs
    └── bar/
        └── baz.rs
src/lib.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 で結合テストするときに意図しないモジュールが結果に表示されたりします。

上記の理由で子モジュールのファイルを置けない場合は、さらにサブディレクトリを作り、その中にクレートルートを置くことで対処可能です。

悪いディレクトリ構成例: baz を子モジュールとして扱いたいが、クレートルートとして扱われる場合がある
.
├── Cargo.lock
├── Cargo.toml
└── src/
    ├── lib.rs
    ├── main.rs
    └── bin/
        ├── bar.rs
        └── baz.rs
良いディレクトリ構成例: baz は bar の子モジュール
.
├── 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 キーの値に追加します。

Cargo.toml: パッケージディレクトリ foo, bar, baz を利用する場合
[workspace]
members = ["foo", "bar", "baz"]

[workspace] セクションの members キーに追加するのはパッケージの「ディレクトリのパス」であり、パッケージ名ではありません (同一である場合が多いですが、異なる場合もあります) 。
※ディレクトリのパスにワイルドカード * 等のグロブを利用できます。
※既存のパッケージをワークスペースにした場合は、カレントディレクトリの記述は追加しません。

参考「The [workspace] section - Workspaces - The Cargo Book

4.3. デフォルトパッケージを指定

cargo run 等でデフォルトで選択されるパッケージを default-members キーで指定します。

Cargo.toml: パッケージディレクトリ foo, bar, baz を利用し、foo をデフォルトパッケージにする場合
[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 オプション等でパッケージを選択します。

※ここではパッケージディレクトリのパスでなくパッケージ名を指定します。

パッケージ foo を実行する場合
cargo run -p foo

コマンドによってはクレートの指定でも実行可能です。

クレート 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] セクションに「パス形式」で依存関係を追加します。

foo/Cargo.toml: パッケージディレクトリ foo から bar を利用する場合
[dependencies]
bar = { path = "../bar" }

crates.io で公開する場合は依存パッケージも公開し、[dependencies] セクションで crates.io 用の version キーの記述を追加します。

foo/Cargo.toml: パッケージディレクトリ foo から bar を利用し、crates.io で公開する場合
[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 の仕様)

55
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?