Rust のStdinを読む

Rust で Command::new した子プロセスに対して標準入出力でやりとりをするコードを書いていて、std::io や std::fs ってどうなっているんだっけ?というのが気になったので、気合いを入れてコードを読むことにした。

Rust における入出力

Rust の入出力機能は、std::io や println マクロを用いて標準入出力するものや、std::fs を用いてファイル入出力するものが一般的である。

Rust で標準出力や標準エラー出力をやりたいだけなら println! や eprintln! で事足りるし、標準入力をするなら std::io::stdin().read_line(&mut buf) を呼ぶだけでいい。ファイル入出力もstd::fs::File を用いて開いたファイルに対して read_line を呼び出すだけで済む。

このように入出力をするだけなら簡単に済むが、その裏側はどうなっているのだろうか?

入出力に関連するトレイト

Rust は入出力の機能を次のトレイトや構造体で抽象化する。

各トレイトは library/std/src/io/mod.rs に実装されている。

BufReader は library/std/src/io/buffered/bufreader.rs に実装されている。

BufWriter は library/std/src/io/buffered/bufwriter.rs に実装されている。

  • Read
    • read() が実装されていることを要求する。バイトごとの読み込みが想定されている。
    • 実装した read() を用いて、read_to_string() などのデフォルト実装を提供する。
  • Write
    • write() と flush() が実装されていることを要求する。
    • 実装した write() や flush() を用いて、write_all などのデフォルト実装を提供する。
  • BufRead
    • Supertrait として Read を指定する。
    • 内部にバッファを持つことで、繰り返し入力する際に毎回システムコールを呼ばないようにするためのトレイト。
  • BufReader
    • Read が実装された構造体に BufRead を実装したものとして取り扱うための構造体。Read を実装した構造体 a を引数に入れて BufReader::new(a) とすることで、a に BufRead も実装したものとして利用することができる。
  • BufWriter
    • BufReader と同様にバッファを用いることで、write() を用いて繰り返し書き込む際に毎回システムコールを呼ばないようにするための構造体。BufRead に対応した BufWrite というトレイトがありそうだが、これは存在しない。BufRead のように追加のメソッドを必要としないためである。

標準入力

標準入力に関する実装を追っていく。

UNIX 系の OS においてファイルからメモリへバイト列を読み込みたい場合、通常 read システムコールを用いる。これを使うことで、裏で動いているファイルシステムやハードウェアの詳細に依存する処理をカーネルが受け持ってくれるようになる。Rust の入力でもどこかでシステムコールが呼ばれているはずだから、どういった流れで read システムコールが呼ばれるのかを探る。

std::io::Stdin

Stdin in std::io - Rust

library/src/io/stdio.rs に実装されている。

std::io::Stdin

以後Stdin と書く。

Stdin は、inner として Mutex<BufReader<StdinRaw>> への &'static な参照をもつ。

Mutex排他制御のために用いられるスマートポインタである。StdinRaw については後ほど説明する。

std::io::stdin() によって Stdin 型の値が得られる。

Stdin には標準入力のための Read が実装されている。 Stdin において、Read の各メソッドは自身の inner に対する lock を取って StdinLock の対応するメソッドを呼び出しているだけである。

StdinLock に頼り切りであることがよく分かる図

Stdin::lock によって明示的に StdinLock 型の値を得ることもできる。StdinLock には Read および BufRead が実装されている。

Stdin には lock して read_line を呼び出すだけの read_line メソッドが実装されている。std::io::stdin().read_line(&mut buf) はこれを呼び出している。

std::io::Stdin::read_line

次に、StdinLock について説明する。

StdinLock

StdinLock in std::io - Rust

library/std/src/io/stdio.rs に実装されている。

std::io::StdinLock

StdinLock は BufReader<StdinRaw> を MutexGuard でくるんだものを inner としてもつ。

各実装は inner のメソッドを呼び出しているだけである。MutexGuard はMutex に対して lock() を呼び出したときに返ってくる型で、 Deref 及び DerefMut を実装しているから、実質 BufReader<StdinRaw> のメソッドが呼ばれている。

StdinRaw に頼り切りであることがよくわかる図

次に、StdinRawについて説明する。

StdinRaw

StdinRaw in std::io::stdio - Rust

library/std/src/io/stdio.rs に実装されている。

std::io::StdinRaw

std::sys::pal::{環境に対応したモジュール}::stdio::Stdin をinner として持つ。ややこしいが、最初に出てきた std::io::Stdin とは異なる。StdinRaw は公開されていない。

library/std/src/io/stdio.rs の中で use crate::sys::stdio としている。これは実際には library/std/src/sys/pal/mod.rs 中でターゲットOS向けに use されたモジュールを指す。以降、UNIX 環境であることを前提とする。(余談だが、2024/01/14 あたりで sys 周りのコードを sys/pal に移す動きがあったらしく、記事を書いてる途中で git pull してみたら全然違うディレクトリ構造になっていて驚いた)

対応するモジュールを定義して、その下にあるものを全て use している

各実装はターゲットOSに対応する stdio の実装を呼び出して、帰ってきた エラーが EBADF の場合は handle_ebadf を用いてOkに変換する。なぜかはわからない(ご存知の方がいれば教えてください...)EBADF は errno = 9 に対応するエラーで、 Bad file number の意味。書き込み専用に開いたファイルに read() システムコールを呼んだ場合などに発生するものらしい。

次に、std::sys::pal::unix::stdio::Stdin について説明する。

std::sys::pal::unix::stdio::Stdin

Stdin in std::sys::unix::stdio - Rust(注: おそらくドキュメントのリンクはそのうち変わる。上記の変更によって sys が sys/pal になるはず。以降貼るリンクも同様。)

library/std/src/sys/pal/unix/stdio.rsに実装されている。

この構造体はユニット型構造体(フィールドを一つも持たない構造体)である。

各メソッドは以下のように実装される。

Stdin

ManuallyDrop はスコープから抜けたときに Drop しないようにするために用いるスマートポインタである。(Deref と DerefMut が実装されている。) UNIX の場合、FileDesc には Drop が実装されていないため、これを用いる。

FileDesc::from_raw_fd は、FramRawFd トレイトを実装した際に定義された関数であり、ファイルディスクリプタの値を受け取ってFileDesc 型の構造体を返す関数である。ファイルディスクリプタとはOSがファイルを扱うときに用いる番号で、libc::STDIN_FILENO は 標準入力に対応する。 (POSIX により 0 と定められていて、実際に中身は整数の0である。)

この関数が unsafe なのは、File::from_raw_fd を使うときの事情らしい (https://github.com/rust-lang/rfcs/issues/1043)

次は std::sys::pal::unix::fd::FileDesc について説明する。

std::sys::pal::unix::fd::FileDesc

FileDesc in std::sys::unix::fd - Rust

library/std/src/sys/pal/unix/fd.rs に実装されている。

FileDesc::read の実装を見ると libc::read を呼び出している。これは read システムコールに対応する。cvt という関数によって、システムコールでエラーが起こった際に適切なエラーを返すようになっている。

まとめ

この記事では、 Rust において入出力がどのように実装されているかをシステムコールに着目して読み進めた。 最初は標準出力/標準エラー出力や、ファイル入出力についても書くつもりだったけれど、そんなに変わらないのでこの記事では書かないことにした。そのうちまとめるかも...

Rust の標準ライブラリは大部分が Rust 自身で実装されており、非常に読みやすかった。VSCode の rust-analyzer を使いたいので手元でビルドしたが、Rust Compiler Development Guide を読めばそこまで苦しまずに環境構築できた。

今後も気になった標準ライブラリの実装には目を通していきたい。