Rustは最近の言語だけあって、いろんな言語のベストプラクティスがうまく取り入れられている。特にツールまわりは最初から簡単に使える上に、柔軟性もあって使い勝手がよい。さらに、より現在的な開発のあり方をサポートするために一歩進んでいると感じるところもある。RustのビルドツールであるCargoとテスト・ドキュメンテーションについて、面白いと思った所を紹介する。
言語自体の話はこちらに書いた。
Cargo
依存ライブラリの設定
Cargoの設定ファイル(Cargo.toml)では、依存するライブラリを下記のように指定する。ここで指定されるライブラリの単位をRustではCrateと呼ぶ。
[dependencies]
lambda_runtime = "^0.2"
futures = { version = "0.3.4", features = ["compat"]}
common = { path = "../common" }
1つめのlambda_runtime
はシンプルにバージョンが0.2以上を要求している。
2つめのfutures
はバージョンだけでなく、利用する機能(features)を指定をしている。
ライブラリ側では下記のようにすると、そのfeaturesが指定された時のみ、そのコードが含まれるようにできる。
#[cfg(feature = "compat")]
pub mod compat { ... }
3つめのcommon
はローカルのCrateを相対パスで指定している。ここで指定したディレクトリのCargo.toml
を読み込む。
CargoはCrateを作ったり参照したりするのが楽なので、細かく分けやすい。
下記のようにアーキテクチャでコードを切り替えるとかもできる。1
[target.'cfg(target_arch = "x86_64")'.dependencies]
hoge = { path = "hoge/x86_64" }
ところで、Rustの非同期処理ライブラリであるfutures
は0.1系から0.3系に変わる際に大幅にアップデートされたが、0.3系には0.1系と相互に変換することが可能になっている。
どうやって2つのバージョンを混在させているかを確認するとfutures-rs/futures-util/Cargo.toml
に下記の設定がある。
futures_01 = { version = "0.1.25", optional = true, package = "futures" }
この設定により、コードからはuse futures_01::...
という形で0.1系のfuturesのモジュールを参照できる。
RustはそれぞれのCrateの中で名前空間を構成しており、他のCrateをインポートする時はその名前空間に別のCrateの名前空間を追加する形で構成される。そのおかげで上記のような柔軟なことができる。
逆に見た目は同じ名前空間でも実体が異なる状況になってコンパイルエラーになったりすることもあるので注意。
build.rs
Cargo.toml
と同じディレクトリにbuild.rs
という名前のファイルを置くと、ビルド前にそのコードを実行して、コード生成などの副作用を起こすこともできる。
それだけでなく、標準出力にcargo:
で始まる文字列を出力すると、コンパイルのパラメータなどのcargoの設定を変えることもできる。この設定はどこでやってるんだって思ったら、build.rs
の中のprintln!
でやってたりする。
Cとの連携
仮想キーボードのsqueekboardは元々Cのプロジェクトを徐々にRust化している。レポジトリ( https://source.puri.sm/Librem5/squeekboard.git )を見ると、同じディレクトリにRustとCのコードが混在している。
特にimservice.c
とimservice.rs
のように、同一モジュール内でもCとRustが混在している。もちろんCとやりとりする値はborrow checkなどはできないので、中のコードを見るとunsafe
でどうにかしている。
ベターCとしてのRustの取り回しの良さの一例と言える。
すこしハマったこと
ライブラリにパッチを当てたくて、そのライブラリをローカルでビルドしたらコンパイルが通らなかった。
他のCargoから参照したらコンパイル通るのに、何故と調べていたところ、参照されるライブラリがローカルかノンローカルでコンパイルオプションが違うらしい。( https://github.com/rust-lang/cargo/issues/5998 )
ローカルでは厳しくlintチェックするが、ノンローカルはlintチェックはしない。
テストとドキュメンテーション
Rustはテストやドキュメントを書くことを重きを置いており、言語としてサポートしている。例として、std::io
モジュールのrustdoc( https://doc.rust-lang.org/std/io/index.html )を見てみよう。
親切な説明やサンプルコードが豊富にあるが、これらはすべてコード中のコメントから生成されたドキュメントである。モジュール名の右側にある[src]
をクリックして、ソースコードを見るとコメントに同じような記述があることが確認できる。
このソースコードの中で#[test]
で検索するとテストコードが見つかる。Rustではユニットテストはプロダクトコードと同じファイルに書く。公開していないものにもアクセスしやすいし、テスト対象とテストコードが近くて分かりやすい。
テスト自体がドキュメンテーションとしての意味を持つので、その意味でもテスト対象の近くにテストコードがあるのは理に適っている。
ドキュメントコメントはmarkdownで書かれる。markdown中のコードブロックで書かれるサンプルコードはコンパイルされるし、assertionがある場合はテストとして実行される。コンパイルや実行には必要だが非本質的なコードはドキュメントから除外するように指定することもできる。サンプルコードも正しく動作することをチェックするおかげで、コードを変更したのにサンプルコードが追従していないなど、よくある間違いも防ぐことができる。
実際、自然言語で説明されるより、テストコードやサンプルコードで書いてあった方が分かりやすいし、不整合のチェックされているので安心感がある。ドキュメントとしてのテストコードをしっかり書く、そのコードを自然言語で補足してドキュメントとして見せるという考えは他の言語でも広まって欲しい。
-
ちなみに
[x.y.z]
みたいな書き方はtomlのネストしたテーブルの記法として定義されている( https://github.com/toml-lang/toml ) ↩