「ゼロからのOS自作入門」を Rust でやる (第5章)

引き続き「ゼロからのOS自作入門」を Rust でやっていきます。

完走できるといいなぁ。

第5章に入る前に

前回作った部分でいくつか気になった箇所があったので修正しました。代表的なものを紹介します。

unwrap / expect を減らす

main 以外の処理では unwrap / expect を使わず Result で呼び出し元にエラーを通知するようにしました。

github.com

unwrap / expect の利用箇所を見つけるためには clippy の以下 lint が便利です。

#![warn(clippy::unwrap_used)]
#![warn(clippy::expect_used)]

今回独自の Error 型を定義した訳ですが、 thiserror などが使えないのはやはりめんどくさい。 thiserror の no_std 対応は Pull Request はあるのですが、マージされないまま1年以上経っており状況がよくわからないです。。。

描画関連処理を trait へ括りだす

framebuffer::Drawer の描画関連処理を graphics::Draw トレイトへ括りだし、 framebuffer::Drawer が当該トレイトを実装するようにました。

github.com

今後 &dyn Draw により描画のバックエンドを切り替えられるようにすることが必要になってくるので、このような対応をしました。 Draw が object safe であることを保証するため、 static_assertions::assert_obj_safe を使っています。

最初からこのような形にしなかったのは、トレイトの関数の引数では型パラメータや impl Trait を使いたかったためです。 例えば、以下のような関数を持つトレイトは object safe ではありません。

impl Draw {
  fn draw(&mut self, point: impl Into<Point<i32>>);
}

上記のように引数を impl Into<T> で受けられると便利なのですが、今回は諦めました。

もう一点、細かい話ですが画面上の座標や長さを表す型は Point<i32>, Size<i32> としています。 どちらも Vector<i32> の別名で同じ型なのですが、コードを見やすくするために別名を与えています。 数値として i32 を利用しているのは、座標が負値になることがあったり、座標の引き算などを簡単にできるようにしたかったためです。

配列のインデックスの usize 型との変換がめんどくさい (そして asunwrap が登場しがち) という問題はありますが、 細かな工夫をすることで乗り越えることにします (例えば、 iter.enumerate() の代わりに (0..).zip(iter) を使うなど)。

第5章

文字列の出力をする章です。

文字 'A' の出力 (day05a)

github.com

特に難しいことはやっていません。 スクリーン範囲外に描画しようとした場合に発生したエラーを無視するための手段として Result<(), DrawError>::ignore_out_of_range() なんて関数を定義しているあたりが少し面白いくらいでしょうか (この後のコミットでスクリーン範囲外に描画した場合もエラーにしないよう変更しており、結局この関数は削除されていますが)。

分割コンパイル (day05b)

C++ のファイルを複数に分割する話です。 最初からモジュール分割してプログラムを作成していたため、スキップしました。

フォントを増やす & 文字列の出力 (day05c)

'A' 以外の文字を出力するために、テキストファイルに定義された字形 (フォント) をプログラム中から利用するという節でした。 C++ 実装ではテキストからオブジェクトファイルを生成していました。 Rust 実装では build.rs でコード生成しモジュールとしてインポートするようにしました。 Rust ではよくあるパターンですね。

github.com

C++ で写経していたときは objcopy 実行ディレクトリと入力ファイルの (hankaku.bin) のパスが変わると生成されるシンボル名が変わる現象に悩まされたりしましたが、 Rust のソースコード生成ではこのあたりの悩みとは無縁なので良いですね。

write / writeln で文字列を出力できるようにする (day05d)

github.com

C++ では sprintf を使えるようにしていましたが、 Rust では write!writeln! を使えるようにします。 まだヒープアロケーションはないので format! は使えません。

やったことは簡単で、 fmt::Write を実装した font::StringDrawer を用意しました。 write! 呼び出し1回につき複数回 fmt::Write::write_fmt が呼び出されることがあるので、 font::StringDrawer では次に描画する位置を覚えておき、1回出力することに更新する必要があります。

最初この処理を忘れてしまったため、文字が同じ場所に重ねて出力されて大変な事になっていました。 書いたプログラムが滅茶苦茶な動作をするのを見るのも自作の楽しさかなという感じです。

コンソールを追加する (day05e)

最終的に C++ 版をベタ移植したようなコードに落ち着きました。

github.com

Console::buffer の持ち方をどうするか悩んだのですが、どうせ ASCII の範囲しか表示されないだろうということで割り切って u8 の配列としました。 ASCII の範囲で表せない文字を表示しようとした場合、 font::char_to_byte により b'?' に変換されます。 また、前の章で &str を受け取る描画関数 font::draw_str を用意したのですがこれは使わず、 u8 の配列を受け取る font::draw_byte_str を使うようになっています。

このあたりの文字列処理は後々ちゃんとしたいですね。

printf! / println! を追加する (day05f)

先ほど作った console::Consoleグローバル変数にし、カーネル内の各所から printf! / println! により文字列を書き込めるようにしました。

github.com

グローバル変数の排他の取り方に苦労しています。 元々の Console の実装ではフレームバッファへの可変参照 &mut framebuffer::FrameBuffer を保持していましたが、これをそのままにして console::Consoleグローバル変数にするのは論外です。 console::Console 以外から画面への出力ができなくなってしまうためです。

ちょっと悩んだのですが、以下のようにしています。

  • console::Console には framebuffer::Framebuffer を含めず初期化
  • コンソールへの出力処理 (_print) でフレームバッファとコンソールのロックを取得し、 Console::writer() により fmt::Write を実装した ConsoleWriter を取得する
  • ConsoleWrite に対して write! する

framebuffer のロックは様々なタスクで奪い合いになりそうなのでなんだか不安な感じがしますが、ひとまずはこれでいこうかと。 そのうちちゃんとします。

おまけ

今回作成した print!, println! を使い、 panic 時にメッセージを出力するようにしました。

github.com

panic 時に行っていた処理で framebuffer やコンソールのロックを取得していた場合デッドロックしますが、まあ何もしないよりはマシかなと。

unsafe なコードを書き始めたら、割り込み発生時の情報などの出力できるようにしたいですね。

まとめ

文字列の描画関連を用意しました。 ロック周りや文字/文字列の扱いで少し悩んだところはありましたが、まだまだ全然簡単ですね。

次章は Rust 実装最大の (?) 鬼門であるUSBデバイス対応です。 この難題にどう立ち向かうのか。お楽しみに。