見出し画像

Rustのライブラリ管理は「mod.rs」が大事(Rust 2015)

 僕はC++畑な人でして、他の多くのC++erの方達同様に自分が作った再利用できるライブラリ(.hと.cpp)を特定のフォルダの下に次のように保存しています:

画像1

(上のはイメージです(^-^;)

C++はヘッダーファイル(.h)に関数やらクラスの定義が記述されていて、#includeでそのファイルの相対パス指定すると、それらを使えるようになります:

/* mian.cpp */

#include "oxlib/oxmath.h"
 
int main() {
    int a = OX::Math::add( 100, 200 );   // ライブラリ内の関数を使用
    return 0;
}

この「ライブラリを定義したヘッダーファイル名を指定する」という感覚はC++erの体には染みついているわけですが、同じ感覚をRustに持ってくるとある種の混乱を招いてしまいます。Rustでの内外のライブラリは、定義されたファイルパスを指定して使う…というC++的な形式にはなっていません。ライブラリがあるフォルダ構造に紐づいた独特の管理体制が取られています。

そこでここではRustで自作のライブラリを作って活用するために知っておくべき「Rustのモジュール管理」について見ていきましょう。

クレート(木箱)について

Rustは一つのアプリケーションを「パッケージ」という単位で呼んでいます。1つのパッケージには「クレート(木箱)」というコードの塊をたくさん入れる事が出来ます。クレートは要は.rsファイルの事で、これがコンパイルの一塊となります。

クレートには大きく2種類があります。バイナリクレートとライブラリクレートです。ライブラリクレートはlib.rsという名前のファイルで定義されるもので、ライブラリをコンパイルする時のエントリ(開始点)となります。src/lib.rsがありコンパイルフラグにtestを指定した場合、lib.rsから辿れるテストコードが実行されます。

一方のバイナリクレートはlib.rs以外のrsファイルです。その中でもmain.rsというファイルは特別で「エントリーバイナリファイル」と呼ばれています。src/main.rsがあってコンパイルオプションをbuildにした場合、コンパイラはこのファイルをエントリーとしてコンパイルを始め、最終的にexeファイルなどの実行ファイルをビルドしてくれます。

cargo new <パッケージ名>で環境を作った場合、main.rsが作られます。一方cargo new <パッケージ名> --lib とするとlib.rsが作られます。パッケージの中には0個か1個のlib.rsと複数のバイナリクレート、もしくは1個のmain.rsと複数のバイナリクレートを含める事が出来ます。

main.rsとlib.rsとを一緒に含める事は出来ると言えば出来るのですが、コンパイラオプションをtestにするのとbuildにするのとで違う物になるだけなのと、コンパイラの挙動がおかしくなる事があるので、混乱しないためにも別々の開発環境に分けるのをお勧めします。

クレートの下にモジュール

クレートは大きな機能の塊です。Rustはクレートファイル名を「クレート名」として区別します。なのでmian.rs以外の.rsの名前は実は結構重要です(^-^;。クレートとなる.rsの中には「mod」というキーワードを使う事で複数のモジュールを定義する事が出来ます。例えばこういう感じ:

/* hoge.rs */
 
pub mod foo {
    pub fn getVal() -> i32 {
        return 100;
    }
}
 
pub mod piyo {
    pub mod powa {
        ....
    }
}

Rustの中で特定のモジュールを指定する時、その絶対パスはクレートから辿る指定となります。上のgetVal関数は「hoge::foo::getVal()」ですし、powaモジュールは「hoge::piyo::powa」となります。

C++の#includeっぽいのも「mod」

 RustでのC++で言う#includeに近い働きをするのは、上で出てきた「mod」というキーワードです。これはmoduleの略で、ある機能の塊を指します。使い方の感覚自体は#includeにやや近く、モジュールファイルを次のように指定します:

/* main.rs */

mod math;
mod network;
 
 fn main() {
     let a = math::add( 100, 200 );
 }

上の記述はイメージなので悪しからず(^-^;。標準でmathとかnetworkというモジュールはありません。

見た目で#includeと違うのは、ファイルのパスっぽくない所です。実際これはパスでは無くてモジュール名をダイレクトに指定します。これは逆に言えば「コード上ではモジュールを定義しているパスを解決していない」とも言えます。ここが#includeと最大に違う所です。勿論C++もコンパイルオプションで#includeが参照するフォルダパスを複数指定出来ます。指定すればそのパス以下を記述するだけで済むようになります。でもRustはそうじゃないんです。では上のモジュールを自前で作るとして、それらをどこに置いてどう取り扱えば上のように呼び出せるようになるのか…。これがこの章の肝です。

modはmain.rs、lib.rs、mod.rs内のみ

modによるモジュールの指定はmain.rs、lib.rs、mod.rsという特別な.rsファイル内でのみ使用できます。基本的にmodは記述したファイルと同じフォルダに含まれている.rsクレートのみ指定可能です。mod.rsファイルについては後述します。

modは同じフォルダにある.rsを指す…のだけど…

 Rustはsrcフォルダ以下に.rsを追加してコードを育てていきます。この内エントリーはsrc/main.rsと決まっていますが、その他の.rsファイルもsrc直下に置かれます。そしてそれらファイルはすべてクレートでありモジュールとして扱う事が可能です。よって、例えばmath.rsに次のようなadd関数を定義して、

/* src/math.rs */
 
// 加算
// ※「pub」を付けないと公開されない
pub fn add( a : i32, b : i32 ) -> i32 {
    a + b   // 戻り値
}

srcフォルダ下に置けば、

/* src/main.rs */

mod math;
 
fn main() {
    let a = math::add( 100, 200 );    
}

このようにmod mathで自前のmathモジュールを使う事を宣言し、math::add()などモジュール名::関数と記述する事でmath.rs内の関数や構造体などを利用する事ができるようになります。

しかしながら、modはパスでは無くてあくまでクレート名を指定する物なので、フォルダ階層を記述出来ません。つまり、先のmath.rsをsrc/hoge/fooフォルダ下に移動して、

mod hoge/foo/math;  // NG

というパス的な記述をしても文法エラーになってしまいます。なので基本的にmodは「同じフォルダに所属する他の.rsクレート」しか指定できない…となります。

 でもそれだとmain.rsはsrcフォルダ直下の.rsしか使えない事になってしまいます。作っているアプリケーションにバリバリに依存する.rsも、再利用可能な.rsもsrcフォルダ下にごっちゃ…それは流石に嫌なわけです。再利用できそうなものはsrcフォルダ下に例えばmylibフォルダを作り、そこにmath.rsとかnetwork.rsなどを入れて、アプリケーション固有の.rsと分別整理したい。そうできないとちょっと困ります。もちろんそういうフォルダ構成にはできますが、ちょっと制約もあります。

mod.rsがポイント!

 では、src/mylibフォルダを作り、そこにmath.rsを放り込んで、math.rsにあるadd関数をmain.rsから呼び出すにはどうするか?残念ながらただファイルを放り込んだだけでは参照する事は出来ません。これには独特な方法が取られています。

 まずsrc/mylibフォルダ下に「mod.rs」という特別な名前の.rsファイルを追加します。デフォルトは空で構いません。このファイルがあると、Rustはそのフォルダをモジュールであると認識してくれます。こうすると、

/* src/main.rs */
mod mylib;  // OK

このようにフォルダ名をそのままモジュールとして指定できるようになります。mylibフォルダはmain.rsと同じsrcフォルダ下にあるので、modの適用範囲なんですね。

 ただし、これだとsrc/mylib/math.rsに定義したadd関数はまだ認識出来ません。上の状態だとmain.rs内は「src/mylib/mod.rs」しか実は認識しておらず、mylibフォルダ下にあるmath.rsは見えていないのです。それを見えるようにするには、mylib/mod.rs内でmathをモジュールとして指定します:

/* src/mylib/mod.rs */
pub mod math;  // OK: 同じ階層にあるので指定可能

ここで大切なのはmodの前の「pub」アクセッサ。これはそのモジュールを上の階層のフォルダに公開(public)するという宣言です。これを付けないとmath.rsは非公開となり、math.rs内で定義されている物は一切触れなくなります。触って良い物をpub宣言で公開にすれば、次のように関数を呼び出せます:

/* src/main.rs */
mod mylib;
 
fn main() {
    mylib::math::add( 100, 200 );  // OK: 300が返る
}

ただしこのフォルダ階層だとmathモジュールはmylibクレート(モジュール)のサブモジュールになるので、main.rsで指定するクレートはmylibで、関数の指定は上のように「mylib::math::add関数」というような呼び出し方になります。

さらに下の階層は?

 自前の数学機能(クレート)であればmath.rsの1ファイルだけで何とか済むかもしれませんが、例えばより実装規模の大きいネットワーク関連のツールをnetwork.rsの1ファイルだけで完結する事は一般的には難しい…というよりとてつもない行数になって開発効率も管理もカオスになるため避けるのが得策です。つまりファイル分割をした方が良いと。でもそうなるとmylib下にネットワーク絡みの.rsがずらずらと並んでしまいます。それはそれで管理上やっぱり避けたい所だったりします。そういう一連の機能を記述する.rsの数が多くなりそうな物は、mylibフォルダの下にさらにサブフォルダをさらに掘って、そこに機能を実現する.rsファイル群を突っ込んでまとめたい。自然とそう考えたくなる訳です。

 フォルダ階層が深くなった場合も、そのフォルダにmod.rsを入れるとサブモジュールフォルダとして認識してくれます。例えばnetworkフォルダをmylibフォルダ下に作り、そこにmod.rsを新設します。そして、

/* src/mylib/network/mod.rs */
pub mod connection;
pub mod http_tool;

このようにnetworkフォルダ下にある公開して良いモジュール(.rsファイル)をmodで指定します。

ただし、これだけだとmain.rsからは見えません。なぜなら、networkフォルダはmylibクレートが管理するサブモジュールであって、mathの時と同様その公開権限はmylibにあるためです。networkサブモジュールを公開して良いならmylib下にあるmod.rsにそれを記述します:

/* src/mylib/mod.rs */
pub mod math;
pub mod network;  // OK: mod.rsを追加した直属のフォルダ名は指定可能

src/mylib/network/mod.rsを追加しnetworkフォルダをモジュールとして認識させ、src/mylib/mod.rsで直属のnetworkモジュールを公開すると、main.rsから、

/* src/main.rs */
mod mylib;  // mylibにnetworkが含まれる!
 
fn main() {
    mylib::network::connection::connect( "192.168.0.5" );
}

大本のmylibモジュールのサブモジュール(mylib::network)として認識してくれるようになります。

ファイルとフォルダの見た目の構成はこういう感じです:

画像2

Rust2018からはサブモジュールの定義の仕方が変わり、mod.rsではなく.rsと同名のフォルダを作りその下にサブモジュールとなる.rsを入れる形式になりました。互換のためmod.rs方式も使えます。

 mod.rs内に関数などを直接書いてもOKです。その場合「フォルダ名::関数」というアクセスの仕方になります。上のconnect関数をnetwork/mod.rsに記述していれば、

/* src/main.rs */
mod mylib;
 
fn main() {
    mylib::network::connect( "192.168.0.5" );
}

このように呼び出しをちょっと短く出来ます。でもこれは設計のお話なので、mod.rsに記述すべきか否かというのは状況で判断すべきでしょう。

フォルダ階層=モジュールパスの認識を

以上のようにRustではsrc下のフォルダ階層が基本的にはそのままモジュールのパスとして認識されます。この辺りはフォルダ階層とは独立してnamespaceでモジュール階層を表現できるC++erの感覚とかなり違います。ですから、フォルダの階層を変えるのは結構な事件です。今までmylib::network::connectionとアクセスしていた物が、mylib::network::hoge::connectionに変わってしまうと、旧パスを使っていた箇所を全部修正する必要が出てくるためです。よってフォルダ構成(サブモジュール化)はより慎重に設計する必要があります。

長い呼び出しを簡略したい!

 mod.rsでモジュールを階層化して自前のライブラリを作っていく事ができるようになるのですが、上のconect関数の呼び出しに見られるように、階層が深くなると呼び出しがひじょーに長くなってしまいます。VSCodeは補完機能があるので打ち込み時にキーを叩く回数は少なくて済むにしても、これは見栄え(可読性)としてシンドイのです(-_-;

このようなネストするモジュールの冗長な呼び出しを回避するには「use~as」を用いると便利です:

/* src/main.rs */
mod mylib;
use oxlib::network::connection as NWConect;   // エイリアス設定
 
fn main() {
    NWConect::conect( "192.168.0.5" );
}

useの後ろに元のモジュールパスを、asの後ろに任意の別名(エイリアス)を指定すると、main内にあるようにその別名でモジュール内の関数等にアクセスできるようになります。余り短い名前にするとそれはそれでよく分からなくなるのですが、上手に使えばコードが読みやすくなります。

読みやすくなるだけでなく、エイリアスを使うと先に懸念したライブラリ内のフォルダ構成が変わった場合にuse部分だけをそれに対応させるだけで済むようになります。もしエイリアスを使わずフルパスであちこちに書いていたら…割と地獄です(-_-;;

という事で、Rustでのライブラリ管理のしくみを見てきました。C++erの観点からすると「ちょっと面倒臭いなぁ」と感じるのは正直否めないのですが、mod内のpub指定の有無で公開して良いモジュールをちゃんと選択できる、そのモジュールアクセス権限による安全性の確保はC++には無い利点です。流石後発言語Rust。ではまた(^-^)/

この記事が気に入ったらサポートをしてみませんか?