Open8

miseのコードリーディングをしながらRustの勉強するメモ

castor4bitcastor4bit

エディタはJetBrainsRustRoverが非商用だと無償利用できるのでインストールした。VSCodeでもよかったのだけれど、 .vscode/extensions.json などがなかったので専用IDEのほうが安心できる。

Rust自体は以前にインストール済みだったけれど、結構前なので最新版に更新する。

$ rustup update
$ rustc --version
rustc 1.81.0 (eeb90cda1 2024-09-04)
castor4bitcastor4bit

まずはエントリポイントを探す。

The Rust Programming Languageによると、 main が最初に呼び出される関数になるらしい。こんな基本的なことから忘却している。

These lines define a function named main. The main function is special: it is always the first code that runs in every executable Rust program.

ビルドはCargoと呼ばれるツールで行う。設定ファイルの Cargo.toml を見ると path = "src/main.rs" という記述があるので、おそらくここがエントリポイントだろう。

[[bin]]
name = "mise"
path = "src/main.rs"

それっぽい main 関数があるので、ここから読み解いていく。
https://github.com/jdx/mise/blob/aba9ae5928e5cb0347ca77ce73af2e4d0571a237/src/main.rs#L61-L74

castor4bitcastor4bit

main を読み解く前に関数についての基本。
https://doc.rust-lang.org/book/ch03-03-how-functions-work.html

fn sum(a: i32, b: i32) -> i32 {
    a + b
}
  • 関数は fn ではじまる
  • 関数や変数の命名規則は慣例的にスネークケース
  • 関数名の後の () 内に引数を定義する
    • 引数は 変数名: 型 で記述する
  • 戻り値は -> の後に型を記述する
  • 戻り値は暗黙的に最後の式が返される
    • セミコロンをつけないと式として評価される
    • 明示的に return で返すことも可能
  • 関数本体は {} で囲まれる

慣れ親しんだ他言語に近い文法なので、特に迷わず書けそうなのは有り難い。

これを踏まえて main 関数を見てみると、引数はなく戻り値として eyre::Result<()> が宣言されている。

fn main() -> eyre::Result<()> {
    ....
}

Rust By Exampleによると main の戻り値に Result を返した場合はエラー発生時にエラーコードなどを出力してくれるようである。

If an error occurs within the main function it will return an error code and print a debug representation of the error (using the Debug trait).

ここでの Resultcore::result::Result だけれど、 eyre::Resultcore::result::Result の型エイリアスなので同じ文脈で解釈してよさそうか。型エイリアスについては Advanced Types に解説がある。

pub type Result<T, E = Report> = core::result::Result<T, E>;

Result はScalaでのEitherみたいなものと思ってよいのかな(Scalaもよく知らないけれど)。

Result を使ったエラーハンドリングについては Recoverable Errors with Result に解説がある。 return Err(e) のようにエラーを返すとエラー出力させたりできるのだろうけれど、miseのmain関数では特にエラーを返してはおらず、eyre::Result<()>E を含まないので慣例的に(あるいは将来の拡張として)エラーハンドリングを意識した実装にはなっているけれど、実際には未使用のような感じな気がする。

castor4bitcastor4bit

eyre::Result についてもう少し深堀りする。

eyre の部分は名前空間であろうから(参考: Non-operator Symbols)、eyre:: ということは外部ライブラリなどに定義があるのだろうと思われる。

より詳しい概念の説明があった。
https://doc.rust-lang.org/book/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html

外部ライブラリのような考え方は、だいたいcreteの単位で扱われると思ってよいのかな。
eyre もcreteということでよいと解釈した。
https://docs.rs/eyre/latest/eyre/

Type Definition eyre::Resultには main で使うコード例が記載されているがどう動くのかあまりイメージできないな…。

Struct eyre::Reportを読むと Box<dyn std::error::Error> というキーワードが登場し、 Box<dyn error::Error>とは? を読むとエラーハンドリング時のError traitの型を動的に決定するための仕組みを示していそうなので、ここでいうラッパーの役割を担っているようにも思われる。

この実装は Rust By Example のBoxing errors にも記載があるけれど、実際どのように影響するのか現時点だと理解できていないので一旦後回しにする。

castor4bitcastor4bit

さて、 fn main() に戻って処理を追ってみる。

ちなみに実装や仕様を確認するような場合にはこんなに1行ずつ細かく処理を追ったりはしないけれど、今回はRustの勉強をしたいのが主目的なのでじっくりやっている。

fn main() -> eyre::Result<()> {
    output::get_time_diff("", ""); // throwaway call to initialize the timer
    ...

最初は output::get_time_diff("", "") が呼び出されている。output.rs に定義されており、 output:: の部分は他言語では namespace などを示すことが多い気がするけれど、どうもそのような定義はなさそうにも読める。深堀りしていく。

まず呼び出し側では、コードの上部で以下のような宣言(?)が見られた。

#[macro_use]
mod output;

ModulesFile hierarchy によると、 mod output;output.rs の内容を output:: で呼び出せるような仕組みらしい。Rustにおいてはファイル単位でモジュールのスコープが分割されているようである。

同一ファイル内でモジュールを使うには mod xxx { ... } のように括弧で囲う。こちらはなんとなく馴染みがある記法である。

モジュール内の関数などは関数宣言の前に pub を付加することでモジュール外から呼び出せるようになる。 get_time_diffpub fn get_time_diff(module: &str, extra: &str) と宣言されているので外部参照が可能になっていることが分かる。

この modTypeScriptimport と同じようなイメージで使えるのだろうと考えていたけれど、まさにそうではないという記事を読んでモジュールがどのような扱いになるのか理解できた気がする。クレートとモジュールも微妙に境界が分からず混乱していたので、分かりやすく解説されており大変に有り難い。
https://zenn.dev/kengoku123/articles/rust-crate-basics

多少まだ誤解があるかもしれないけれど、今のところこのような理解をした。

  • Rustにおいてはファイルパスに対応してモジュールの階層が切られ、 mod はその階層からの相対指定でモジュールを読み込む
  • src/main.rsmod を宣言した場合はルートモジュールに読み込まれる
    • そして、読み込まれたモジュールは下位のモジュールでも参照することができる
    • ここで crate::mod1::hello(); のような記法になるのでクレートと混乱してしまう…
  • クレートはビルドの単位であり、1クレートごとに1つのバイナリファイル(バイナリクレートまたはライブラリクレート)が生成される

最後に #[macro_use] については、 The macro_use attribute などを読んだ限りでは mod output の内部で #[macro_export] ラベルが付けられたマクロ定義を読み込み側で参照可能にするもの…と解釈した。

castor4bitcastor4bit

補足として、マクロについては Macros に記載がある。基本的な考え方は一般的なマクロと概ね同じようだけれど、 procedural macros はまだ読んでいないので後で理解する。

castor4bitcastor4bit

fn get_time_diff() を読んでいく。
https://github.com/jdx/mise/blob/796ff8293b3482dbbdc9fb6858ced746a39354c8/src/output.rs#L150-L180

本題の前に #[cfg(feature = "timings")] の記述について。
Features によると、これは所謂Feature Flagsで Cargo.toml[features] セクションに定義があれば関数定義が有効になるという仕組みらしい。

呼び出される関数が未定義のままだとビルドエラーになるので、通常は #[cfg(not(feature = "timings"))] のようにフラグが無効な場合の定義も併記するのだろう。get_time_diffについても以下のような関数定義が存在する。
https://github.com/jdx/mise/blob/796ff8293b3482dbbdc9fb6858ced746a39354c8/src/output.rs#L182-L185

さて、 get_time_diff() である。関数定義である get_time_diff(module: &str, extra: &str) -> String の引数と戻り値はよいとして &str という記述は他言語でも馴染のある 参照(Shared references) だった。str は文字列スライスを示す型で、文字列周りは奥が深いので詳細は別途整理する。