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

今回はまとめて3章です。

第10章

ウィンドウを表示して操作できるようにする章です。 一気にGUIっぽくなりますね。

マウスカーソルが画面外に飛び出すのを修正 (day10a)

マウスカーソルを画面端に動かすと画面端から飛び出してしまうのを修正しました。

github.com

C++版では画面から飛び出したマウスが反対側の端から現れるようになっていました。 Rust 版では描画時に座標の範囲チェックを行っているためそのような動作にはなっていませんでしたが、 マウスが画面端から移動して隠れてしまうようにはなっていたため修正しました。

メインウインドウを追加 (day10b)

メインウインドウを追加します。

github.com

メインウインドウ処理専用の CoTask を作成し、表示するようにしています。

高速カウンタを追加 (day10c)

イベントループのループ毎にカウントアップするカウンタを作成し、値をメインウインドウに表示します。

github.com

C++版とは構造が異なりイベントループは async/await の executor として実装されているため、ループ内に簡単にカウントアップ処理を追加することはできません。 このため、一度イベントループに制御を戻した後即復帰するような Future である Yield を作成し main_window のイベント処理中で利用するようにしました。 これにより、 main_window の CoTask からイベントループに一旦制御を戻せるようになります。 制御を戻した回数をカウントすることでカウンターの代替としました。

描画範囲の制限による高速化 (day10d)

従来処理では画面の一部分が変更された場合でも画面全体を再描画していました。 これを更新があったウインドウの範囲のみ再描画するように変更し、画面描画を高速化します。

github.com

実装方針はC++版と同じです。

Window と WindowDrawer を統合する

Window と WindowDrawer はそれぞれ別の構造体として定義していましたが、両者を統合し、 Window が Draw を実装するようにしました。

github.com

コードがシンプルになりました。

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(...);
});

github.com

後者の方がロック区間が明確になって良いかなーという気持ちです。

バックバッファによりちらつきを解消する (day10e)

これまでは画面の描画時に直接フレームバッファに描画していました。 このため、描画途中の状態が画面に表示されるため、マウスカーソルなどがちらついて表示されることがありました。 これを解消するため、バックバッファをというバッファを導入します。 各ウインドウの描画時に直接フレームバッファに描画するのではなく、一旦バックバッファにすべてのウインドウを描画し、 完了後にバックバッファの内容をフレームバッファにコピーするという実装へと変更します。 これによりちらつきが完全に解消します。

github.com

ウインドウをドラッグできるようにする (day10f)

ウインドウをドラッグすることで移動できるようにします。

github.com

mouse の CoTask でマウスのボタン押下を検知できるようにし、それに応じてウインドウをドラッグできるようにします。 マウスカーソルの下にあるウインドウの LayerId を取得するため、 LayerManager へ問い合わせるようなインタフェースを用意しています。 これまでの LayerManager 関連処理と異なり、 LayerManager 側関数からの戻り値を呼び出し元へ返す必要がありますが、mouse と layer_manager はそれぞれ異なる CoTask で動作しているため、通常の関数のように値を渡すことはできません。 CoTask 間を跨がって値をやりとりするためにはチャンネルが利用可能ですが、これまでに作成したチャンネルは何度も繰り返して値を送信するためのものであり、関数の戻り値といった値を一度だけ渡すような使い方には向いていません。 このため、 oneshot というチャンネルを作成し使うようにしています。

マウスのドラッグ関連処理を layer の CoTask へと移動する

先ほどの節でマウスのドラッグ処理を実装したばかりですが、今後このようなマウスからの入力に応じてウインドウを制御するような処理が増えてくると、 mouse と layer の CoTask 間でのやりとりが増えることとなり、効率が悪いですし、なによりもプログラミングがめんどくさいです。 このため、 mouse の CoTask はマウスボタンの押下有無等を判定するだけとし、 layer の CoTask でドラッグ等の処理を行うようにしました。

github.com

CoTask 間の役割分担が明確になって良い感じですね。

メインウインドウだけをドラッグ可能にする (day10g)

従来の実装では Window により実装されているすべての要素がドラッグ可能だったため、コンソールやデスクトップの背景もドラッグ可能になってしまっていました。 main_window だけドラッグできるように改造します。

github.com

Layer に draggable というメンバーを追加し、当該メンバが true の場合のみドラッグ処理を実行するようにします。 先の節で layer の CoTask にドラッグ関係の処理を移動したことで、簡単に実装することができました。 (mouse の CoTask で各レイヤのドラッグ可否を取得しようとすると、layer の CoTask とのメッセージやりとりを増やす必要があるため)

第11章

Local ACPI によるタイマーを実装する章です。

ソースコードの整理 (day11a)

ソースコードのモジュール構造を整理する節です。 Rust版では最初からモジュール構造を整理していたため、特に何も行っていません。

タイマー割り込み (day11b)

Local ACPI によるタイマー割り込みを実装します。

github.com

実装は xHC の割り込みとほとんど同じですが、割り込みの発生有無だけが分かれば良い xHC と異なり、割り込みが発生した回数が重要なため、割り込み発生回数を AtomicU64 でカウントするようにしています。

タイマー間隔の短縮とタイマーマネージャーの追加 (day11c)

タイマーの設定により割り込み間隔を短縮するのと、タイマーを管理する TimerManager を追加します。

github.com

複数のタイマーへ対応する (day11d)

プログラムの複数箇所で同時にタイマーによる待ち合わせができるようにします。

github.com

前の節で追加した TimerManager に、タイマーの登録とタイムアウトの通知機能を実装します。 timer の CoTask では ACPI タイマーの割り込みと他の CoTask からのタイマー登録依頼という異なるキューからの二種類のイベントを処理しないといけないため、 futures_util::select_biased マクロを利用しています。 select マクロは std が必要ですが、 select_biased は std 不要なのでフリースタンディング環境でも利用できます。

なお、タイマー割り込みで使うかと思い動的に CoTask を spawn できるようにする仕組みを Executor に追加しましたが、結局使いませんでした。 今後利用出来る場面があるかと思い、実装はそのままにしています。

RSDP を取得する (day11e)

正確な時刻が分かる ACPI PM タイマーを利用するための準備の節です。

github.com

Rust 実装で利用しているブートローダーである bootloader クレートでは、起動時のパラメータとして RSDP へのポインタが渡されるため、カーネル側の実装は特に難しいことはありませんでした (例によって RSDP の物理アドレスが仮想アドレスにマッピングされていなかったため、ページテーブルの書き換えは行っていますが)。

問題があったのは bootloader 側の実装でした。 具体的には、 ACPI v1 と v2 の両方の RSDP が存在する場合に、 ACPI v1 側の RSDP をカーネルへ渡す場合があるためです。 これは、UEFI のブートローダー実装で ACPI v1 と ACPI v2 の RSDP のうち、先に見つかった方をカーネルへ渡すようになっているためです。

github.com

筆者のQEMU環境では必ず ACPI v1 の RSDP が渡されるようでした。 ACPI PM タイマー利用のためには ACPI v2 の RSDP が必要なのでこれでは困ってしまいます。

ひとまず、 bootloader にパッチを当て、 ACPI v1 の RSDP は無視するようにしました。

github.com

また、 bootloader の GitHub リポジトリに issue を立てました。

github.com

作者の方にも反応頂いたので、そのうち解決されるといいなー。

第12章

ACPI PM タイマを使えるようにするのと、キーボードからの入力に対応する章です。

FADT を検索する (day12a)

前の節で検索した RSDP をたどって XSDT を取得、そこから更に FADT を検索するという節です。

github.com

だいたい C++ 実装と同じですね。 Rust のイテレーターではメソッドチェーンで検索処理を簡潔に書けるのが良いですね。

ACPI PM タイマーによりタイマー間隔を補正する (day12b)

正確な時間が分かるタイマーである ACPI PM タイマーにより、周期が不明なタイマーである Local APIC タイマーの周期を測定します。

github.com

これもまた C++ 実装と同じです。 余談ですが、筆者環境だとどうも時間が正しく計れていないような気がしています。 1秒間隔のはずが、3秒間隔くらいになっています。 筆者は WSL2 上で QEMU を動作させているのですが、 WSL2 では (まだ) Nested VM がサポートされていないため、 QEMU の動作が遅いことが原因なのでしょうか。

キーボードからの入力を処理する (day12c)

キー入力を受け取って画面に出力する節です。

github.com

C++ 実装と同じで特に書くことがありません...

修飾キーを処理する (day12d)

Ctrl や Shift などの修飾キーを処理できるようにします。

github.com

C++ と同じですね。

テキストボックスを表示する (day12e)

テキストボックスを含むウインドウを作成し、キー入力に応じてテキストボックス内に文字を表示します。

github.com

テキストボックスのウインドウを独立した CoTask として実装しています。 また、キーボード入力を処理する keyboard CoTask からは mpsc::Sender 経由でキー入力イベントをテキストボックスの CoTask へ直接送信するようにしています。 将来的には送信先 CoTask を動的に切り替える処理が必要になるでしょうが、とりあえずはこのような簡単な実装にしておきます。

点滅するカーソルを描画する (day12f)

テキストボックスに入力位置を示すカーソルを描画します。

github.com

0.5秒ごとにタイマーイベントを発生させ、イベント契機ごとにカーソルの描画、削除を繰り返します。 定期的に実行されるタイマーである timer::interval を追加し、簡単に利用できるようにしています。

まとめ

C++ 実装とだいたい同じで書くことがだんだん無くなってきましたが、メモ代わりにブログ記事は残しておこうかとは思っています。

次章はついにプリエンプティブマルチタスクの実装です。Rust でうまく実装できるのか。楽しみですね。