torutkのブログ

ソフトウェア・エンジニアのブログ

Rust事始め(続)

Rustのソースファイル構成

Rust では、標準のビルド&パッケージ管理システム Cargo を使いビルドを行います。 このビルドでは、ソースファイルのディレクトリ及びファイル名にかなり独特の制約があります。

単一のソースファイルから単一の実行ファイルを作成する Cargoパッケージ

お馴染みの Hello worldプログラムの構成です。

任意の作業ディレクトリにて、次のcargoコマンドでバイナリクレートを1つ持つパッケージを作成します。

D:\work> cargo new hello_world
  • パッケージとは、クレートをビルド・テスト・共有する単位(ひとまとまりのビルド)です。
  • クレートとは、実行可能ファイルまたはライブラリファイルを生成する単位です。前者をバイナリクレート、後者をライブラリクレートと呼びます。
  • cargo newコマンドでパッケージを作成します。パッケージ名をコマンドの引数で指定します(ここでは hello_world )。デフォルトではバイナリクレートを1つ持つパッケージが生成されます。

生成されるディレクトリツリーは次となります。

hello_world
  +-- src
  |    +-- main.rs
  +-- Cargo.toml

srcディレクトリ直下に生成されるソースファイル main.rs は、バイナリクレートのクレートルートとなり、このファイルからビルドを開始し、このファイルから辿れるモジュールが芋づる的にビルドされます。 このバイナリクレートの名前は、パッケージ名と同じ(ここではhello_world)です。

次のcargoコマンドでクレートをビルドします。

D:\work\hello_world> cargo build
   Compiling hello_cargo v0.1.0 (D:\work\hello_world)
    Finished dev [unoptimized + debuginfo] target(s) in 1.16s

複数のソースファイルから単一の実行ファイルを作成する Cargoパッケージ

モジュール機能を使い、複数のソースファイルから構成されるプログラムをビルドします。 ソースファイルを分割する場合は、モジュールに切り出してモジュールの階層に基づくディレクトリおよびファイル名を持つソースファイルとする必要があります。

クレートルートからモジュール参照

クレートルート(ここではmain.rs)から、別ファイルに定義されるモジュールを利用します。 クレートルートから直接参照するファイルは、クレートルートが置かれているディレクトリ(src)に、モジュール名に基づくファイル名(ここではgreeting.rs)で配置します。次のディレクトリツリーに示します。

hello_world
  +-- src
  |    +-- main.rs
  |    +-- greeting.rs
  +-- Cargo.toml

main.rsに、モジュールgreetingを参照する記述を入れます。これにより、モジュール名に基づく名前のソースファイル(greeting.rs)をビルドに含めます。

mod greeting;
fn main() {
    println!("{}", greeting::greet());
}

greeting.rsに記述する要素は、mod greeting {...} と定義しなくても、モジュールgreetingに含まれたものとして扱われます。

pub fn greet() -> String {
    String::from("Hello, world!")
}
モジュールの階層化

上述のgreetingモジュールから、さらに別ソースファイルに記述された別モジュール(ここではgreeting::time_class)を参照します。 クレートルートからのモジュール階層に基づき、greeting::time_classモジュールを定義するソースファイルはgreeting/time_class.rs となります。

hello_world
  +-- src
  |    +-- main.rs
  |    +-- greeting.rs
  |    +-- greeting
  |          +-- time_class.rs
  +-- Cargo.toml

greetingモジュールのソースファイル中に time_class モジュールへの参照を記述します。

mod time_class;
pub fn greet() -> String {
    format!("Hello, world!, {}", time_class::classify(12))
}

time_classモジュールのソースファイルはモジュール名に基づき time_class.rs とし、パッケージ階層に基づき src/greeting/time_class.rs に配置します。

pub fn classify(hour: u32) -> &'static str {
    match hour {
        0 ..=4 => "midnight",
        5 ..=8 => "morning",
        9 ..=16 => "daytime",
        17 ..=19 => "evening",
        20 ..=23 => "night",
        _ => "out of range"
    }
}
さらにモジュール階層を深くする

モジュール greeting::time_class::morning を定義する場合、morningモジュールのソースファイルは次の様に配置します。

hello_world
  +-- src
  |    +-- main.rs
  |    +-- greeting.rs
  |    +-- greeting
  |          +-- time_class.rs
  |          +-- time_class
  |                +-- morning.rs
  +-- Cargo.toml
mod.rs というファイル(Rust 2015の場合)

Rust 2015 では、モジュールディレクトリに mod.rs という特定のファイル名を配置し、これがモジュールのソースファイルとして扱われます。 Rust 2018 でもこの振る舞いは継続して利用可能ですが、互換性のための利用に留めた方がよいと思います。

1つのパッケージに含めるクレートの制約

パッケージの制約として、1つのパッケージに含めることができるクレートは次となります。

  • 1個以上のクレートを持つ
  • ライブラリクレートは、0個または1個(2個以上は含められない)
  • バイナリクレートは、0個以上(複数個を含められる)

ワークスペースで複数パッケージを構成

プログラムの規模が大きくなると、複数のライブラリから構成したくなります。このときは、ワークスペースを用いて複数のパッケージを束ねます。

ワークスペース構成

まず、ワークスペースディレクトリを用意し、ワークスペースを定義するCargo.tomlファイルを配置します。

D:\work> mkdir hello_workspace
D:\work> cd hello_workspace
D:\work\hello_workspace> 

ワークスペースを作成するcargoコマンドはないようなので、手作業でディレクトリを作成します。

[workspace]

members = [
    "hello",
]

Cargo.tomlには、ワークスペースの定義を記述します。ワークスペースを構成するパッケージをmembersに記述します。まずは実行可能ファイルを生成するパッケージ hello を記述しました。後でライブラリを生成するパッケージを追記していきます。

helloパッケージをcargo newコマンドで生成します。

D:\work\hello_workspace> cargo new hello
     Created binary (application) `hello` package

D:\work\hello_workspace>

この時点でのディレクトリ構成は次となります。

D:\work\hello_workspace
    +-- Cargo.toml
    +-- hello
          +-- Cargo.toml
                +-- src
                      + main.rs
ワークスペースのビルド

ワークスペース直下のディレクトリで、cargo buildコマンドを実行します。

D:\work\hello_workspace> cargo build
   Compiling hello v0.1.0 (C:\Users\toru\study\rustw\learn\hello_workspace\hello)
    Finished dev [unoptimized + debuginfo] target(s) in 0.96s

D:\work\hello_workspace> 

ビルド成果物は、ワークスペース直下の target ディレクトリに生成されます。

D:\work\hello_workspace
    +-- Cargo.lock
    +-- Cargo.toml
    +-- hello
    |     +-- Cargo.toml
    |           +-- src
    |                 + main.rs
    +-- target
          +-- debug
ワークスペースにライブラリを生成するパッケージを追加

ワークスペースを定義するCargo.tomlにパッケージ名を記載しないで作成すると、次の様に警告メッセージが表示されます。

D:\work\hello_workspace> cargo new morning --lib
warning: compiling this new package may not work due to invalid workspace configuration

current package believes it's in a workspace when it's not:
current:   C:\Users\toru\study\rustw\learn\hello_workspace\morning\Cargo.toml
workspace: C:\Users\toru\study\rustw\learn\hello_workspace\Cargo.toml

this may be fixable by adding `morning` to the `workspace.members` array of the manifest located at: C:\Users\toru\study\rustw\learn\hello_workspace\Cargo.toml
Alternatively, to keep it out of the workspace, add the package to the `workspace.exclude` array, or add an empty `[workspace]` table to the package's manifest.
     Created library `morning` package

ワークスペースのCargo.tomlにmorningパッケージを追記します。

[workspace]

members = [
    "hello",
    "morning",
]

この時点のディレクトリ構成は次となります。

D:\work\hello_workspace
    +-- Cargo.lock
    +-- Cargo.toml
    +-- hello
    |     +-- Cargo.toml
    |           +-- src
    |                 + main.rs
    +-- morning
    |     +-- Cargo.toml
    |           +-- src
    |                 + lib.rs
    +-- target

ただし、この時点では hello クレートが morning クレートに依存していることが記載されていないのでビルドが通りません。

ワークスペース内のパッケージ間の依存性の記述

helloパッケージのCargo.tomlに依存性を記述します。

   :(前略)
[dependencies]
morning = { path = "../morning" }