モジュールとワークスペース――Rustのプロジェクト管理を理解する:基本からしっかり学ぶRust入門(13)
Rustについて基本からしっかり学んでいく本連載。第13回は、モジュールをクレートに分割する方法、そして複数のパッケージから成るプロジェクトを管理するワークスペースの仕組みについて。
別クレートのモジュールを利用する
本連載第12回では、同一のクレート内にあるモジュールの利用について見てきましたが、実際の開発では別のクレートにモジュールを切り離し、それを利用するというケースの方が多いでしょう。あるクレートにあるモジュール定義を別クレートから利用できるようにするには、以下のようにします。
- モジュール名と同名のファイル(拡張子は.rs)をクレートルートと同じフォルダに作成する
- このファイルにはモジュール定義の内部だけを記述する
- 利用側のクレートではmod文でこのモジュールを指定する
src/bin/graphics.rsは、この規則に従って命名、配置したファイルです。規則の通り、graphicsモジュールを定義するmod文のブロックが存在しないことに注意してください。なお、本節のサンプルはmodules2パッケージに作っていきます。
// calcモジュールの定義 pub mod calc { pub fn get_x() {} pub fn get_y() {} } // drawモジュールの定義 pub mod draw { pub fn point() {} pub fn line() {} pub fn triangle() {} pub fn square() {} }
以下は、graphicsモジュールを利用する側のクレートです。
// graphicsモジュールの定義は同名のファイルにあるとする mod graphics; (1) // 名前空間crate::graphics::drawをインポート use crate::graphics::draw; (2) fn main() { // インポートした名前空間での省略記法 draw::point(); (3) draw::line(); draw::triangle(); draw::square(); }
(1)のようにmod文でブロックを省略すると、同名のファイル(拡張子は.rs)にモジュールの定義があるとみなします。これで別クレートにあるモジュールが利用できますが、(2)でuse文を使うことで、モジュール定義の名前空間をスコープに取り込んでいます(インポート)。この結果、(3)のようにインポートした名前空間でフルパスを用いずに関数を呼び出せます。
【補足】クレートの階層化
本連載第12回を含めて、ここまでクレートの中に複数のモジュールを配置する例を紹介してきましたが、一般的には、1つのクレートには1つのモジュールのみを含めるべきです。これにより、モジュールの独立性が高まり、再利用しやすくなります。
graphicsモジュールであれば、その中にはcalcとdrawという2つのモジュールがありますが、これらは別クレートにできます。その場合には、クレートルートのあるフォルダにモジュールと同名のgraphicsフォルダを作成し、その下にモジュール名と同名のcalc.rsとdraw.rsをさらに作成することになります。
クレートルート └── graphicsフォルダ(graphicsモジュール) ├── calc.rsクレート(calcモジュール) └── draw.rsクレート(drawモジュール)
このように、クレートルートを起点に物理的にモジュールツリーを構成することになり、モジュールの階層が簡潔になります。
ワークスペース
ここまでは、同一パッケージで完結したモジュールシステムについて紹介してきました。しかし、ワークスペースを利用することで、複数のパッケージにまたがるアプリケーションを作成できます。ここからは、ワークスペースを用いてパッケージをまとめる方法を紹介していきます。基本的な流れは以下の通りです。
- ワークスペースのパッケージを作成する
- ワークスペースを構成するバイナリパッケージを作成する
- ワークスペースを構成するライブラリパッケージを作成する
- パッケージの依存関係を設定する
ワークスペースのパッケージを作成する
ワークスペースもまた、パッケージの一種です。ワークスペースを利用するには、まずはこのパッケージを作成する必要があります。通常のパッケージとは異なるので、cargo newコマンドは使えません。フォルダを手動で作成し、パッケージ管理のためのCargo.tomlファイルも手動で作成します。なお、本節のサンプルはworkspaceパッケージに作成していきます。
まずは、ワークスペースのためのフォルダをatmarkit_rustフォルダの直下にworkspaceという名前で作成し、そこに移動しておきます。
% pwd atmarkit_rust % mkdir workspace % cd workspace
このフォルダに、適当なテキストエディタ(VSCodeなど)でCargo.tomlファイルを以下の内容で作成します。
[workspace] (1) members = [ (2) "tool" (3) ]
本連載第1回でCargo.tomlファイルの基本的な形式について紹介しましたが、そこには[project]セクションがありました。ワークスペースでは、(1)のように[workspace]セクションを記述します。ここには、(2)のようにワークスペースを構成するパッケージをmembersエントリに列挙しますが、まずは(3)のように最初のパッケージを記述しておきます。このパッケージはこれから作成するので、存在している必要はありません。
ワークスペースを構成するバイナリパッケージを作成する
次に、ワークスペースを構成するバイナリパッケージを作成します。このパッケージは、ワークスペースを構成する最初のパッケージとなります。上記のCargo.tomlファイルのmembersエントリにあったtoolパッケージを作成します。このパッケージの作成は、cargo newコマンドで実行できます。
% cargo new --bin tool Created binary (application) `tool` package
ご覧の通り、いつも通りのメッセージが出力されて、パッケージの作成が完了しました。ワークスペースworkspaceにはtoolパッケージがあるだけですが、すでにワークスペースのビルドと実行が可能です。まずはビルドしてみましょう。
% cargo build Compiling tool v0.1.0 (/Users/nao/Documents/atmarkit_rust/workspace/tool) Finished dev [unoptimized + debuginfo] target(s) in 1.58s
メッセージを見ると、コンパイルされたのはtoolパッケージで、通常通りのメッセージです。いつもと何かが違うのかと思われるでしょう。ここで、ワークスペースのフォルダ構成を見てみます。VSCodeのフォルダツリーを以下の図に示します。
toolパッケージにtargetフォルダが存在せず、workspaceパッケージにのみtargetフォルダがある点に注目です。workspaceパッケージは手動で作成したので、最初はCargo.tomlファイルがあるだけでしたが、そのあとにバイナリパッケージを作成してビルドしたので、targetフォルダが作成されたわけです。
このように、ワークスペースを使う場合には、ビルドの成果物はワークスペースでまとめられるようになり、個別のパッケージには出力されません。toolパッケージのコンパイル結果であるtoolファイルも、workspaceパッケージのtarget/debugフォルダに生成されています。
上記の例ではビルドのみ実行しましたが、cargo runコマンドで実行すれば、バイナリパッケージのデフォルトの処理である「Hello, world!」の出力が確認できます。
% cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.05s Running `target/debug/tool` Hello, world!
ワークスペースを構成するライブラリパッケージを作成する
前項では、ワークスペースにバイナリパッケージを1個作成してビルドしました。ただし、バイナリパッケージが1個あるだけではワークスペースの意味がないので、ライブラリパッケージを作成して、これもワークスペースに含めてみましょう。
これにはまず、バイナリパッケージと同様にワークスペースのCargo.tomlファイルを編集し、membersエントリにライブラリパッケージを追加します。
[workspace] members = [ "tool", "libs" (1) ]
(1)でライブラリパッケージlibsを追加しています。特に説明は不要ですね。"tool"の後にカンマ(,)の付加を忘れないようにしましょう。このように、Cargo.tomlファイルにパッケージを追記し、そのあとにパッケージを作成していくのが基本です。ライブラリパッケージも作成します。
% cargo new --lib libs Created library `libs` package
これで、workspaceパッケージにはtoolパッケージとlibsパッケージが存在することになりました。この時点でのワークスペースの構成を以下の図に示します。
libsパッケージには何もコードがないので、このままではパッケージがまとめられたという実感は湧かないでしょう。そこで、バイナリパッケージからライブラリパッケージの関数を呼び出してみて、パッケージがまとまることを確認していきましょう。
依存関係を記述する
あるパッケージから別のパッケージの関数などを利用するには、依存関係の設定が必要になります。依存関係は、利用する側のパッケージのCargo.tomlファイルの[dependencies]セクションに(デフォルトで存在しています)、利用するクレート名とパッケージの実体のパスをpathエントリで設定します。以下は、libsクレートはパッケージlibsに依存するという設定です。Cargo.tomlファイルがtoolパッケージのものであることに注意してください。
[dependencies] (1) libs = { path = "../libs" } (2)
(1)は、依存関係を記述する[dependencies]セクションです。(2)はクレート名とクレートのあるパッケージの場所(フォルダ)です。この場合は、toolパッケージとlibsパッケージは同階層にあるので、「..」を用いて指定しています。なお、libsで始まるエントリは、必ず1行で記述してください。「{ }」があるからといって行を分けるとパースエラーになります。
依存関係のあるクレートを参照するには、extern crate文を使います。この「extern」は、C/C++言語におけるものと同じで、「外部にあるものを持ってくる」という意味合いになります。ここでは、toolパッケージのmain.rsファイルの冒頭にlibsクレートの参照を追加しておきましょう。
extern crate libs;
この記述で、libsを使用した外部関数呼び出しの準備ができました。
【補足】extern crateの省略
ここでは、extern crate文で外部のクレートの参照を指定しましたが、Rust 2018以降では省略可能になっています。省略しても、Cargo.tomlファイルの[dependencies]セクションに記述があれば、自動的にクレートを検索して参照を解決します。
graphicsモジュールの関数を呼び出す
せっかくなので、libパッケージに冒頭で紹介したgraphicsモジュールを導入し、モジュール内にある関数を呼び出してみます。graphicsモジュールではクレートをgraphics.rsで表しているので、これをそのままlibパッケージのsrcフォルダにコピーします。
そして、libs/src/lib.rsファイルにも、モジュールの利用を宣言するmod文を追加しておきましょう。lib.rsファイルにデフォルトで記述されているテストのコードは、削除してしまっても構いません。外部から利用するので、mod文にpubキーワードを付記するのを忘れないでください。
// graphics.rsをgraphicsモジュールとして使うことを宣言 pub mod graphics;
また、graphicsモジュール内の関数(正確には、graphicsモジュールに含まれるcalcモジュールとdrawモジュール内の関数)が呼び出されたことを確認するために、それぞれの処理内容としてprintln!()マクロを追加しておきましょう。図形を実際に描画する必要はなく、呼び出されたことが分かれば十分だからです。
…略… // 関数の処理内容にprintln!()マクロを追加 pub mod draw { pub fn point() { println!("Draw point."); } pub fn line() { println!("Draw line."); } pub fn triangle() { println!("Draw triangle."); } pub fn square() { println!("Draw square."); } }
toolパッケージのmain.rsファイルに、この関数を呼び出すコードを追記します。
fn main() { // libsクレートのgraphics::draw::point()関数を呼び出す libs::graphics::draw::point(); //println!("Hello, world!"); }
同一のクレートにあったときには不要だった、libsクレートの参照からモジュールを指定していることに注目してください。ここではフルパスで関数を指定しましたが、use文で名前空間をインポートし、省略記法で呼び出すこともできます(本連載第12回を参照)。実行してみると、libsパッケージにある関数point()をtoolパッケージのmain()関数から呼び出せていることが分かります。
% cargo run Finished dev [unoptimized + debuginfo] target(s) in 0.00s Running `target/debug/tool` Draw point.
【補足】ワークスペース外部の関数等の利用
今回の例のように、全て自前のパッケージでアプリケーションが完結していればよいですが、実際にはワークスペースの外にある関数などを利用するケースも多いでしょう。
例えば、サードパーティー製のライブラリを使うようなケースです。このような場合も、ワークスペース内の個々のパッケージにおいて、下記のようにCargo.tomlファイルに依存関係を記述します。この場合は、RustのGitHubリポジトリからhttpクレートを検索し、一致するバージョンのクレートをプロジェクト(ワークスペース)内で利用できるようにします。
[dependencies] http = "0.2.8"
まとめ
今回は、モジュールシステムの続きとして、モジュールをクレートに分割する方法、そして複数のプロジェクトをまとめるワークスペースについて紹介しました。小さなサンプルだけを動かす場合はメリットを実感しづらいかもしれません。しかし、大規模開発など外部のクレートを複数利用する際に便利さを実感できるでしょう。
次回は、Rustが標準で備えるテスト機能を紹介する予定です。
筆者紹介
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.