📝

RustのクレートとモジュールとCargo.tomlを整理

2023/08/22に公開

はじめに

これまでRustの簡単なサンプルプログラム程度を書いていただけなのであまり気にせず済んだのですが、いざ本格的にコードを書いてみようかと思ったときにモジュール周りの扱いで混乱して手が止まってしまったので、いったん整理するためにまとめてみました。

なぜRustのモジュールシステムがわかりづらかったのか

混乱した理由について、Rustは他の言語(ここでは比較としてTypeScriptを使います)とモジュールインポートの仕組み(というか考え方)が違うのに、そのことを理解していなかったためです。

TypeScriptではモジュールのインポートは次のように書きます。

sample1.ts
import { hello } from "./mod1.ts" 

そして他のファイルでもhelloが必要であれば同じようにインポートします。

sample2.ts
import { hello } from "./mod1.ts" 

このように同じ内容のimport文が複数箇所現れる可能性があります。

Rustでは他のモジュールを参照するにはmodを使います。modをtsのimportと同じように考えるとつまずきます。以下のようなモジュールsrc/mod1.rsを作成します。

src/mod1.rs
pub fn hello() {
    println!("Hello, world!");
}

これを下記のようにインポートすることを試みます。

src/sample1.rs
mod mod1;
pub fn call_hello() {
    mod1::hello();
}

しかし、このコードはビルドエラーとなります。上記のコードが意味するのは、

  • src/sample1.rsはsample1モジュールを定義する(ファイル名=モジュール名です)
  • mod mod1の記述はsample1のサブモジュールとしてmod1を取り込むことを意味します。
    • そしてmod1モジュールはsrc/sample1/mod1.rsに存在すると判断されます。
    • そんなファイルは存在しない → ビルドエラー

これを意図した通りに修正するにはどうすればいいのでしょうか。そのためにはmod宣言を記述する場所をsrc/main.rsに変えます。

src/main.rs
mod mod1;
mod sample1;

pub fn main() {
    sample1::call_hello();
}

main.rs内でmod mod1を記述することでルートモジュール内にサブモジュールmod1を取り込みます。

そしてsrc/sample1.rsは下記のように修正します。

src/sample1.rs
pub fn call_hello() {
    crate::mod1::hello();
}

もしくは下記のように書いてもOKです(絶対パスか相対パスかという違いだけです)。

src/sample1.rs
pub fn call_hello() {
    super::mod1::hello();
}

crate::はクレート内のルートモジュールからの絶対パスを意味します。

Rustにはuseが存在しますが、これは単純にショートカットを定義するだけでモジュールの取り込みとは何も関係がありません。よってuseを一切使わずにフルパスで指定しても構いません。上記のコードはフルパス指定の例となります。

useを使うと下記のように省略して書けるようになります。

src/sample1.rs
use crate::mod1;
pub fn call_hello() {
    mod1::hello();
}

さらに省略して次のようにも書けます。

src/sample1.rs
use crate::mod1::hello;
pub fn call_hello() {
    hello();
}

以上のことからRustにおいて同じモジュールを取り込むためのmod宣言が複数回登場することはありません(tsのimportとは異なる部分です)。

クレートとは何か

クレートというワードが登場しましたが、そもそもクレートとは何かについて言及していなかったので説明をしておきます(Crateは「木箱」「容器」を意味する英単語です)。

クレートはCargo.tomlに記述されるビルドの単位となります(その他の用途としては主に依存ライブラリを管理するための役割も持ちます)。

ビルドの結果として1クレート毎に1つのバイナリファイルが生成されます。具体的には実行可能バイナリを生成するmain関数を持つファイル(バイナリクレートと呼びます)及び、他のクレートから参照されるためのライブラリファイル(ライブラリクレートと呼びます)の二種類が存在します。

Cargo.tomlにはビルド対象のクレートについての明示的な記述が存在していなくても、

  • src/main.rs: デフォルトのバイナリクレート
  • src/lib.rs: デフォルトのライブラリクレート

が予約されています。上記の名前以外のクレートを定義したい場合はCargo.tomlに記述を追加する必要があります(追加のクレートを定義するための記述などについては後述します)。

ところでCargo.tomlには下記のような項目があります。

Cargo.toml
[package]
name = "rust_example"
version = "0.1.0"
edition = "2021"

このnameは一体何に使うのかですが、ここで指定された名前は他のクレートからライブラリとして参照するときの名前になります。

先ほどのmod1モジュールをライブラリに移動してみます。src/lib.rsを下記の内容で作成します。

src/lib.rs
pub mod mod1;

先ほどと違いmodの前にpubが指定されていることに注意です。他のクレートからアクセスさせるためにはpubを指定して外部に公開する必要があります。

そしてsrc/sample1.rsも下記のように修正します。

src/sample1.rs
use rust_example::mod1;
pub fn call_hello() {
    mod1::hello();
}

crate::となっていた箇所をrust_example::に修正しました。rust_exampleはCargo.tomlのnameで指定した名前です。このように他のクレートを参照するときはクレート名::というプレフィクスを指定します。

crate::の記述は自身が所属するクレートを意味します。バイナリクレートは名前を持たないのでバイナリクレート内から自身のクレートに絶対パスでアクセスするためにはcrate::を使う必要があります。

ネストしたモジュール

旧形式

ここまで見てきた例ではソースディレクトリ直下に平たく置かれていたモジュールだけを扱っていました。実際の業務ではモジュールを機能ごとにディレクトリで分割するはずです。次のようなモジュール構成を考えてみます。

  • ルートモジュール内にmod1モジュールを配置
  • mod1内にサブモジュールfooとbarを配置

これをディレクトリツリーとして表現すると以下のようになります。

src
  |- main.rs
  |- lib.rs
  |- mod1
     |- mod.rs
     |- foo.rs
     |- bar.rs

この構成では、謎のファイルsrc/mod1/mod.rsが存在します。mod宣言で指定した名前がファイルではなくディレクトリ名である場合は、ディレクトリ内のmod.rsが自動で読み込まれます。つまりmod.rsは仕様で決められた特別なファイル名となります。

mod.rsの内容は下記のようになります。

src/mod1/mod.rs
pub mod foo;
pub mod bar;

この状態でmain.rsからfooモジュールやbarモジュールを参照する場合は下記のようにアクセスすることになります。

src/main.rs
mod mod1;

pub fn main() {
    mod1::foo::hello();
    mod1::bar::hello();
}

(各サブモジュール内にそれぞれhello関数が定義されていると仮定しています)

ライブラリクレート経由でアクセスする場合はまずsrc/lib.rsを下記のようにします。

src/lib.rs
pub mod mod1;

そしてsrc/main.rsは下記のようになります。

src/main.rs
pub fn main() {
    rust_example::mod1::foo::hello();
    rust_example::mod1::bar::hello();
}

これが長ったらしいと思う場合は、もちろんuseを使うことができます。

モジュールをmain.rs内で取り込むのと、lib.rs内で取り込む場合では何が違うのでしょうか?基本的にはどちらでも問題なく動作しますが、testsディレクトリからモジュールのテストを書く場合はlib.rsにmod宣言を置く必要が有ります。

testsディレクトリ内のテスト(結合テスト)は、別クレートから公開された機能をテストするという位置付けになります。よって他のクレートからバイナリクレートへのアクセスができないため(すでに言及した内容です)、テスト対象の機能はライブラリクレートに配置する必要があります。

新形式

Rustの2018エディション以降では新形式を使うことができます。

先ほどの旧形式を新形式に変換すると以下のようになります。

src
  |- main.rs
  |- lib.rs
  |- mod1.rs
  |- mod1
     |- foo.rs
     |- bar.rs

変更点は以下の通りです。

  • mod.rsが不要になりました。
  • 代わりにmod1ディレクトリと同階層にmod1.rsが配置されました。

mod1.rsの記述内容は、旧形式のmod.rsと同内容です。

Cargo.tomlについて

Cargo.tomlではデフォルトでsrc/main.rsという名前のバイナリクレートおよびsrc/lib.rsという名前のライブラリクレートが予約されています。上記の名前以外のクレートを定義する場合はCargo.tomlに追記が必要です。

バイナリクレートは任意の数を定義することができますが、ライブラリクレートは一つだけです。

複数のバイナリクレートを定義する

Cargo.tomlに[[bin]]セクションを記述することで追加のバイナリクレートを定義できます。

Cargo.toml
[[bin]]
name = "main2"
path = "src/main2.rs"

[[bin]]
name = "main3"
path = "src/main3.rs"

この場合は、デフォルトのsrc/main.rsと追加したsrc/main2.rssrc/main3.rsの三つのバイナリクレートが存在することになります。さらにsrc/bin以下に置かれたファイルはCargo.tomlに記述しなくても自動的にバイナリクレートとして扱われます。

src
  |- bin
  |  |-main4.rs
  |- main.rs
  |- main2.rs
  |- main3.rs

上記のディレクトリ構成がある場合、次のように実行するバイナリを指定することができます。

$ cargo run --bin rust_example # src/main.rsを実行する
$ cargo run --bin main2 # src/main2.rsを実行する
$ cargo run --bin main4 # src/bin/main4.rsを実行する

追記: extern crate について

昔のソースなどを見ていると(もしくはChagGPTにソースを書かせると)、extern crate ...という記述を見かけることがあります。これは一体なんでしょうか?

結論から言うとこの記述は新しいRustでは不要なので使うことはありません。一応どんな意味があるかを説明しておくと、Cargo.tomlに依存ライブラリを追加して、そのライブラリを参照する際に必要だったもののようです。例えばserde_jsonライブラリを例にすると、

Cargo.toml
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

上記のような依存ライブラリが設定されている時に、

src/main.rs
extern crate serde;
extern crate serde_json;

use serde::{Deserialize, Serialize};
(...省略...)

のように追加したライブラリを参照する際にはまずextern crate ライブラリ名を指定してから使う必要があったようです。現在のRustではこの記述は不要になりました。

おわりに

Rustのモジュールシステムって他の言語と比較するとわかりにくいですよね。より正確に言うとわかりにくいと言うより、他の言語と違いますよね。下手に他の言語での経験があると、その経験を当てはめようとしてうまくハマらなくて混乱するのかもしれません。

今回の記事をまとめる過程でようやく頭を整理することができたと思います。この記事がRust初学者の参考になれば幸いです。

Discussion