「ゼロからのOS自作入門」を Rust でやる (第5章)
引き続き「ゼロからのOS自作入門」を Rust でやっていきます。
- 前回: 「ゼロからのOS自作入門」を Rust でやる (第1章~第4章) - gifnksmの雑多なメモ
- 次回: 「ゼロからのOS自作入門」を Rust でやる (第6章 その1) - gifnksmの雑多なメモ
- 関連記事一覧: ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
完走できるといいなぁ。
第5章に入る前に
前回作った部分でいくつか気になった箇所があったので修正しました。代表的なものを紹介します。
unwrap
/ expect
を減らす
main
以外の処理では unwrap
/ expect
を使わず Result
で呼び出し元にエラーを通知するようにしました。
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
が当該トレイトを実装するようにました。
今後 &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
型との変換がめんどくさい (そして as
や unwrap
が登場しがち) という問題はありますが、
細かな工夫をすることで乗り越えることにします (例えば、 iter.enumerate()
の代わりに (0..).zip(iter)
を使うなど)。
第5章
文字列の出力をする章です。
文字 'A' の出力 (day05a)
特に難しいことはやっていません。
スクリーン範囲外に描画しようとした場合に発生したエラーを無視するための手段として Result<(), DrawError>::ignore_out_of_range()
なんて関数を定義しているあたりが少し面白いくらいでしょうか (この後のコミットでスクリーン範囲外に描画した場合もエラーにしないよう変更しており、結局この関数は削除されていますが)。
分割コンパイル (day05b)
C++ のファイルを複数に分割する話です。 最初からモジュール分割してプログラムを作成していたため、スキップしました。
フォントを増やす & 文字列の出力 (day05c)
'A' 以外の文字を出力するために、テキストファイルに定義された字形 (フォント) をプログラム中から利用するという節でした。
C++ 実装ではテキストからオブジェクトファイルを生成していました。
Rust 実装では build.rs
でコード生成しモジュールとしてインポートするようにしました。 Rust ではよくあるパターンですね。
C++ で写経していたときは objcopy 実行ディレクトリと入力ファイルの (hankaku.bin
) のパスが変わると生成されるシンボル名が変わる現象に悩まされたりしましたが、 Rust のソースコード生成ではこのあたりの悩みとは無縁なので良いですね。
write
/ writeln
で文字列を出力できるようにする (day05d)
C++ では sprintf
を使えるようにしていましたが、 Rust では write!
と writeln!
を使えるようにします。
まだヒープアロケーションはないので format!
は使えません。
やったことは簡単で、 fmt::Write
を実装した font::StringDrawer
を用意しました。
write!
呼び出し1回につき複数回 fmt::Write::write_fmt
が呼び出されることがあるので、 font::StringDrawer
では次に描画する位置を覚えておき、1回出力することに更新する必要があります。
最初この処理を忘れてしまったため、文字が同じ場所に重ねて出力されて大変な事になっていました。 書いたプログラムが滅茶苦茶な動作をするのを見るのも自作の楽しさかなという感じです。
コンソールを追加する (day05e)
最終的に C++ 版をベタ移植したようなコードに落ち着きました。
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!
により文字列を書き込めるようにしました。
グローバル変数の排他の取り方に苦労しています。
元々の Console
の実装ではフレームバッファへの可変参照 &mut framebuffer::FrameBuffer
を保持していましたが、これをそのままにして console::Console
をグローバル変数にするのは論外です。
console::Console
以外から画面への出力ができなくなってしまうためです。
ちょっと悩んだのですが、以下のようにしています。
console::Console
にはframebuffer::Framebuffer
を含めず初期化- コンソールへの出力処理 (
_print
) でフレームバッファとコンソールのロックを取得し、Console::writer()
によりfmt::Write
を実装したConsoleWriter
を取得する ConsoleWrite
に対してwrite!
する
framebuffer のロックは様々なタスクで奪い合いになりそうなのでなんだか不安な感じがしますが、ひとまずはこれでいこうかと。 そのうちちゃんとします。
おまけ
今回作成した print!
, println!
を使い、 panic
時にメッセージを出力するようにしました。
panic 時に行っていた処理で framebuffer やコンソールのロックを取得していた場合デッドロックしますが、まあ何もしないよりはマシかなと。
unsafe
なコードを書き始めたら、割り込み発生時の情報などの出力できるようにしたいですね。
まとめ
文字列の描画関連を用意しました。 ロック周りや文字/文字列の扱いで少し悩んだところはありましたが、まだまだ全然簡単ですね。
次章は Rust 実装最大の (?) 鬼門であるUSBデバイス対応です。 この難題にどう立ち向かうのか。お楽しみに。