「ゼロからのOS自作入門」を Rust でやる (第10章~第12章)
今回はまとめて3章です。
- シリーズ最初の記事: 「ゼロからのOS自作入門」を Rust でやる (第1章~第4章) - gifnksmの雑多なメモ
- 前回: 「ゼロからのOS自作入門」を Rust でやる (第9章) - gifnksmの雑多なメモ
- 次回: 「ゼロからのOS自作入門」を Rust でやる (第13章) - gifnksmの雑多なメモ
- 関連記事一覧: ゼロからのOS自作入門 カテゴリーの記事一覧 - gifnksmの雑多なメモ
第10章
ウィンドウを表示して操作できるようにする章です。 一気にGUIっぽくなりますね。
マウスカーソルが画面外に飛び出すのを修正 (day10a)
マウスカーソルを画面端に動かすと画面端から飛び出してしまうのを修正しました。
C++版では画面から飛び出したマウスが反対側の端から現れるようになっていました。 Rust 版では描画時に座標の範囲チェックを行っているためそのような動作にはなっていませんでしたが、 マウスが画面端から移動して隠れてしまうようにはなっていたため修正しました。
メインウインドウを追加 (day10b)
メインウインドウを追加します。
メインウインドウ処理専用の CoTask
を作成し、表示するようにしています。
高速カウンタを追加 (day10c)
イベントループのループ毎にカウントアップするカウンタを作成し、値をメインウインドウに表示します。
C++版とは構造が異なりイベントループは async/await の executor として実装されているため、ループ内に簡単にカウントアップ処理を追加することはできません。
このため、一度イベントループに制御を戻した後即復帰するような Future
である Yield
を作成し main_window
のイベント処理中で利用するようにしました。
これにより、 main_window
の CoTask
からイベントループに一旦制御を戻せるようになります。
制御を戻した回数をカウントすることでカウンターの代替としました。
描画範囲の制限による高速化 (day10d)
従来処理では画面の一部分が変更された場合でも画面全体を再描画していました。 これを更新があったウインドウの範囲のみ再描画するように変更し、画面描画を高速化します。
実装方針はC++版と同じです。
Window
と WindowDrawer
を統合する
Window
と WindowDrawer
はそれぞれ別の構造体として定義していましたが、両者を統合し、 Window
が Draw
を実装するようにしました。
コードがシンプルになりました。
Mutex::with_lock
を追加
各 CoTask
のイベントループ内で一時的に Window
のロックを取得し、描画完了後アンロックするという処理が何度も登場しています。ロック & アンロックの区間を制御するため以下のようにブロックが必要なのですが、コードが読みづらく感じたため、引数のクロージャにロックを取った値を渡す Mutex::with_lock
を用意しました。
// 既存処理 let mut window = ...; { let mut window = window.lock(); window.fill_rect(...); window.fill_rect(...); } // 改造後処理 window.with_lock(|window| { window.fill_rect(...); window.fill_rect(...); });
後者の方がロック区間が明確になって良いかなーという気持ちです。
バックバッファによりちらつきを解消する (day10e)
これまでは画面の描画時に直接フレームバッファに描画していました。 このため、描画途中の状態が画面に表示されるため、マウスカーソルなどがちらついて表示されることがありました。 これを解消するため、バックバッファをというバッファを導入します。 各ウインドウの描画時に直接フレームバッファに描画するのではなく、一旦バックバッファにすべてのウインドウを描画し、 完了後にバックバッファの内容をフレームバッファにコピーするという実装へと変更します。 これによりちらつきが完全に解消します。
ウインドウをドラッグできるようにする (day10f)
ウインドウをドラッグすることで移動できるようにします。
mouse
の CoTask
でマウスのボタン押下を検知できるようにし、それに応じてウインドウをドラッグできるようにします。
マウスカーソルの下にあるウインドウの LayerId
を取得するため、 LayerManager
へ問い合わせるようなインタフェースを用意しています。
これまでの LayerManager
関連処理と異なり、 LayerManager
側関数からの戻り値を呼び出し元へ返す必要がありますが、mouse
と layer_manager
はそれぞれ異なる CoTask
で動作しているため、通常の関数のように値を渡すことはできません。
CoTask
間を跨がって値をやりとりするためにはチャンネルが利用可能ですが、これまでに作成したチャンネルは何度も繰り返して値を送信するためのものであり、関数の戻り値といった値を一度だけ渡すような使い方には向いていません。
このため、 oneshot
というチャンネルを作成し使うようにしています。
マウスのドラッグ関連処理を layer
の CoTask
へと移動する
先ほどの節でマウスのドラッグ処理を実装したばかりですが、今後このようなマウスからの入力に応じてウインドウを制御するような処理が増えてくると、 mouse
と layer
の CoTask
間でのやりとりが増えることとなり、効率が悪いですし、なによりもプログラミングがめんどくさいです。
このため、 mouse
の CoTask
はマウスボタンの押下有無等を判定するだけとし、 layer
の CoTask
でドラッグ等の処理を行うようにしました。
CoTask
間の役割分担が明確になって良い感じですね。
メインウインドウだけをドラッグ可能にする (day10g)
従来の実装では Window
により実装されているすべての要素がドラッグ可能だったため、コンソールやデスクトップの背景もドラッグ可能になってしまっていました。
main_window
だけドラッグできるように改造します。
Layer
に draggable
というメンバーを追加し、当該メンバが true
の場合のみドラッグ処理を実行するようにします。
先の節で layer
の CoTask
にドラッグ関係の処理を移動したことで、簡単に実装することができました。
(mouse
の CoTask
で各レイヤのドラッグ可否を取得しようとすると、layer
の CoTask
とのメッセージやりとりを増やす必要があるため)
第11章
Local ACPI によるタイマーを実装する章です。
ソースコードの整理 (day11a)
ソースコードのモジュール構造を整理する節です。 Rust版では最初からモジュール構造を整理していたため、特に何も行っていません。
タイマー割り込み (day11b)
Local ACPI によるタイマー割り込みを実装します。
実装は xHC の割り込みとほとんど同じですが、割り込みの発生有無だけが分かれば良い xHC と異なり、割り込みが発生した回数が重要なため、割り込み発生回数を AtomicU64
でカウントするようにしています。
タイマー間隔の短縮とタイマーマネージャーの追加 (day11c)
タイマーの設定により割り込み間隔を短縮するのと、タイマーを管理する TimerManager
を追加します。
複数のタイマーへ対応する (day11d)
プログラムの複数箇所で同時にタイマーによる待ち合わせができるようにします。
前の節で追加した TimerManager
に、タイマーの登録とタイムアウトの通知機能を実装します。
timer
の CoTask
では ACPI タイマーの割り込みと他の CoTask
からのタイマー登録依頼という異なるキューからの二種類のイベントを処理しないといけないため、 futures_util::select_biased
マクロを利用しています。 select
マクロは std
が必要ですが、 select_biased
は std
不要なのでフリースタンディング環境でも利用できます。
なお、タイマー割り込みで使うかと思い動的に CoTask
を spawn
できるようにする仕組みを Executor
に追加しましたが、結局使いませんでした。
今後利用出来る場面があるかと思い、実装はそのままにしています。
RSDP を取得する (day11e)
正確な時刻が分かる ACPI PM タイマーを利用するための準備の節です。
Rust 実装で利用しているブートローダーである bootloader
クレートでは、起動時のパラメータとして RSDP へのポインタが渡されるため、カーネル側の実装は特に難しいことはありませんでした (例によって RSDP の物理アドレスが仮想アドレスにマッピングされていなかったため、ページテーブルの書き換えは行っていますが)。
問題があったのは bootloader
側の実装でした。
具体的には、 ACPI v1 と v2 の両方の RSDP が存在する場合に、 ACPI v1 側の RSDP をカーネルへ渡す場合があるためです。
これは、UEFI のブートローダー実装で ACPI v1 と ACPI v2 の RSDP のうち、先に見つかった方をカーネルへ渡すようになっているためです。
筆者のQEMU環境では必ず ACPI v1 の RSDP が渡されるようでした。 ACPI PM タイマー利用のためには ACPI v2 の RSDP が必要なのでこれでは困ってしまいます。
ひとまず、 bootloader
にパッチを当て、 ACPI v1 の RSDP は無視するようにしました。
また、 bootloader
の GitHub リポジトリに issue を立てました。
作者の方にも反応頂いたので、そのうち解決されるといいなー。
第12章
ACPI PM タイマを使えるようにするのと、キーボードからの入力に対応する章です。
FADT を検索する (day12a)
前の節で検索した RSDP をたどって XSDT を取得、そこから更に FADT を検索するという節です。
だいたい C++ 実装と同じですね。 Rust のイテレーターではメソッドチェーンで検索処理を簡潔に書けるのが良いですね。
ACPI PM タイマーによりタイマー間隔を補正する (day12b)
正確な時間が分かるタイマーである ACPI PM タイマーにより、周期が不明なタイマーである Local APIC タイマーの周期を測定します。
これもまた C++ 実装と同じです。 余談ですが、筆者環境だとどうも時間が正しく計れていないような気がしています。 1秒間隔のはずが、3秒間隔くらいになっています。 筆者は WSL2 上で QEMU を動作させているのですが、 WSL2 では (まだ) Nested VM がサポートされていないため、 QEMU の動作が遅いことが原因なのでしょうか。
キーボードからの入力を処理する (day12c)
キー入力を受け取って画面に出力する節です。
C++ 実装と同じで特に書くことがありません...
修飾キーを処理する (day12d)
Ctrl や Shift などの修飾キーを処理できるようにします。
C++ と同じですね。
テキストボックスを表示する (day12e)
テキストボックスを含むウインドウを作成し、キー入力に応じてテキストボックス内に文字を表示します。
テキストボックスのウインドウを独立した CoTask
として実装しています。
また、キーボード入力を処理する keyboard
CoTask
からは mpsc::Sender
経由でキー入力イベントをテキストボックスの CoTask
へ直接送信するようにしています。
将来的には送信先 CoTask
を動的に切り替える処理が必要になるでしょうが、とりあえずはこのような簡単な実装にしておきます。
点滅するカーソルを描画する (day12f)
テキストボックスに入力位置を示すカーソルを描画します。
0.5秒ごとにタイマーイベントを発生させ、イベント契機ごとにカーソルの描画、削除を繰り返します。
定期的に実行されるタイマーである timer::interval
を追加し、簡単に利用できるようにしています。
まとめ
C++ 実装とだいたい同じで書くことがだんだん無くなってきましたが、メモ代わりにブログ記事は残しておこうかとは思っています。
次章はついにプリエンプティブマルチタスクの実装です。Rust でうまく実装できるのか。楽しみですね。