飽きっぽい人のブログ@qwerty2501

プログラマとしてもテスターとしても中途半端な人のブログ

RustでOS作成したという論文を訳してみた その3

ここの翻訳になります
原文は2015年に書かれたものなので現在のRustの仕様と異なることが書いてある場合があります
本文章はGithubで管理されています
この翻訳は7割方間違っているので信用しないでください

前:その2

3 Rustの評価

小節1.2で言及した通り、Rustはかなり新しく、メモリと型安全に注目したシステム指向型プログラミング言語である。Rustのより包括的な型とメモリ分析は、ますます速度と安全性の両方を求められるようになった状況に置かれているCの素晴らしい代替にするためにある。以前はRustでOS作成など誰も真剣に試みたことはなかったし、Reenix実装を通して私はこの界隈でのRustの強みと弱みの深い認識を得ることができた。また、私は様々な分野においてRustが変更されてこの領域での利便性が向上するのを体験することができた。
ここではRustを使うことによる利点とプロジェクト全体を通して発見したことについて解説する。次に、Rust使用上の問題について検証し、現在起こっている言語上の主要な問題について見ていく。最後に、RustのパフォーマンスをCと比較して短い検証をして終える。

3.1 Rustの利点

このプロジェクトの過程で、私はシステムプログラミングにおいてRustを使うことは、CまたはC++を超える沢山の利点があることを見つけた。これらを並べると、利便性における問題が簡単であること、可用性における主要な進歩の明快であること、正確なコード作成のための機能からなる。

3.1.1 高水準言語

最も明確である、OS開発(もちろん一般的な開発も)において、Cのような言語を超えるRustを使用することの利点は、C++JavaPython、そして他の言語から多くの期待した高水準言語プログラマで構成されることが事実であるということだ。Rustはオブジェクトに付けられているメソッド、データとメソッドの明確な関係の許可、(健全な)演算子オーバーロード、そして単一の名前空間で機能を纏めてグループに関連さることを許可するモジュールシステムなどといった標準的なプログラミング抽象化を提供する。これら全ては、遥かに強く関連付けられた部品間の概念的な繋がりの作成による冗長さと曖昧さをより少なくすることができる。
糖衣構文より、Rustの簡単で便利な実装の更なる高水準な特質はまた、どのように我々がこの言語を使うかという主要な効果をもつ。トレイトを通すことにより、Rustはインターフェースの利用による仮想メソッドテーブル(vtable)の作成と自動的な仕様をより簡単に提供する。これはまた、C関数のような仮想メソッドの定義を書く必要性があるものよりも難解さを少なくしたキャストを実行する。さらに優れているのが、Rustは使用される前に正確な型を知っていることを証明できる場合、vtableを省略してコードをコンパイルするのために十分賢いということだ。これにより、通常のCでできることよりも、多くの一般的なシステム作成と、前よりもより多くのインターフェースの利用を簡単にすることができる。Rustは自動的にスコープ内でオブジェクトを破棄するセマンティクスを持つ上、破棄されたオブジェクトの型を基底とするカスタムデストラクタをサポートする。これでできるリソース取得の利用は初期化(RAII)セマンティクスとなる。我々は例えば、関連付けられたMutexのロックなしで同期されたデータへのアクセス、またはデータの保護が終わった場合に解放しないMutexといった、今までにないことを確実にするためにこれを使用することができる。さらにこれは常に正確なカウントを保障する参照カウントポインタ作成のために使われることができる。これは、関数が失敗するときに単純にエラーを返すことができるので、多くの関数での失敗のハンドリングを大幅に簡素化し、Rustのコンパイラは全ての初期化されたオブジェクトのデストラクタが正確に実行されるのを確実にする。
最後に、Rustは可能ならデータ量が大きなコピーを避けるため、関数のシグネチャを少しばかり自動的に書き直す。これはRustがしばしば戻り値に外部ポインタの代わりに巨大なオブジェクトを返す関数のシグネチャを書き換えることを意味する。返されたオブジェクトの配置を定義することを呼び出したものに認めることにより、例えばmoveでヒープ上から返されたオブジェクトのような、メモリコピーを防ぐ。rustcがRustソースコードの可読性を向上させるこの書き換えと、実関数が行うことの曖昧さを減少させる。
例えば、図5で見れるstat関数と図9で見れるそのCバージョンの比較をしよう。両関数はセマンティクスとして'stat'構造体またはエラーを返す。しかしながら、Cではコストのかかるメモリコピー操作を回避するために'stat'構造体を外部ポインタとして取るためにシグネチャを変更する必要がある。しかも、この曖昧さは少しばかり関数の監視を要する。だがRustの場合、この関数は'Stat'またはエラーを返すというこの型シグネチャの本来のセマンティクスを持つ。この関数をコンパイルする時のRustコンパイラは、同様にコストの高いコピー操作を避けるために、シグネチャをCのそれと一致するように変更する。同じことをするのにCのコードと同じことをするのに、速さでは匹敵するが、セマンティクスはよりきれいになることを意味する。

typedef struct vnode_ops {
    // ...
    int (* stat )( struct vnode * vnode ,
    struct stat * buf );
    // ...
} vnode_ops_t ;

図9: C stat関数インターフェース

3.1.2 型及びライフタイムと借用チェッカー

他の主要なRustを使うことでOS開発者を招く利点として包括的な型、ライフタイム、借用チェックシステムがある。これら三つの道具とともに、コード上のエラークラスを完全に除去する手助けを行う。これらのシステムはCで可能な方法よりもよりリッチなコードの意味を表すことを認める。
Rust言語が型安全で手続き指向的であることは小節1.2で述べた。標準ライブラリが提供するいくつかのラッパー型は型安全なコードを正しく書くのを手助けする。これらラッパー型の最も重要なものは二つあり、一つは Result<R,E> 型で、成功した場合は操作の(型Rの)値を返し、失敗した場合(型Eの)値を示すものと、もう一つは Option<T> 型で、T型の値またはその型の値がないことを示す None からなる。操作が失敗する可能性があるか、値が提供されない可能性を示すために使用される。これらの型はRustのコードの全ての場合で使用され、インターフェース内に通知可能性があることは事実で、上記で見れる(図4、図5、図6を見よ)最も関数が返すKResultオブジェクトは Result<T, errno::Errno> のシンプルなエイリアスだ。これらの型の使用を通して、コメントによる説明なしで正確な戻り値の意味をさらに具体的に伝達することができる。さらには、型システムがCでは一般的な、このような関数のふるまいによって多くの失敗が起きてしまうのを防ぐ。Rustでは明示的にハンドルされなければならないエラーが発生してないことを確認することなしで、結果オブジェクトを返す関数の結果にアクセスすることが可能である。これは例えば、ほぼ完全に明確に、図5でのstat関数のセマンティクスを作り、型システムによってチェックされるが、一方図9におけるCのそれは同様のセマンティクスを持っているのにもかかわらず、より少ない明示と全てにおいてチェックされない。
型チェッカーに加えて、借用チェッカーとライフタイムシステムもまた多くのエラーの一般的な型を防ぐことを、幾分コードをきれいにすることで助ける。ライフタイムシステムは全ポインタ参照が安全で、かつ未初期化のものや可能ではあるが解放済みのデータを参照しないことを要求する。時々この証明が得るためには、違う型とそれらのライフタイムの相互関係を明確にするアノテーションを与える。加えて、それはまた、オブジェクトのポインタが解放された後、保持されるポインタがないことを明確にする。それらとともに、ほぼすべての"解放後の使用"バグと"未初期化データ"バグ発生を防ぐ。加えて、ライフタイムシステムはどの程度の間相違する型の間でポインタを保有している時に有効かを重んじることに依存する型シグネチャを正確に指定することを強要する。最後に、借用チェッカーはデータ構造体間の依存を確認するためにライフタイムシステムを使用することで、同時に変更することを防ぐ。これにより他のコードによって、コードが無効なポインタを保持するバグを防ぐ。

3.1.3 マクロとプラグイン

他のRustを使うことでCより優れている利点はRustは包括的で力強いマクロとプラグインシステムを有しているということだ。小節1.2.1で述べたように、Rustのマクロは衛生的なRust構文抽象化ツリー(AST)の変換である。これらは全く別物で、Cプリプロセッサを超えてさらに力強くそして純粋なテキスト変換マクロである。Cマクロではマクロが使用されたコンテキストで展開されることを常に承知しておかなければならない。これはCではマクロは書いた人間が注意しないと上書きか変数の再定義する可能性があることを意味する。Rustでのマクロは、明示的にマクロに渡されたローカル変数を上書きすることはない。
Rustでのマクロ定義の標準的な方法は、パターンマッチドメイン固有言語(DSL)を使用することだ。このDSLを使用するときに、マクロ引数は基本的な型情報を持つ。各引数は、式、構文、型、識別子、そして大体他6つのいずれかである。これらは正しくマクロパースを確認するために、余分な括弧や区切り文字を通常必要としないことを意味している。さらには、マクロ展開する時に完全に力強いパターンマッチが行える、正しいフォーマットや引数の数に依存する違うコードの作成できる、Cのマクロシステムではできない何かができることを意味する。
加えて、常に強力なマクロシステムでRustコンパライラであるrustcはまた、コンパイラの振る舞いを変更するコンパイラプラグインの作成をサポートする。これらのプラグインは、さらに複雑で、標準マクロDSLを使うことで作成することが不可能な(そうでなければ非常に難しい)マクロを実装するために使用されることができる。例えば、quasiquote操作と、コンパイラプラグイン内だけでファイルシステムにアクセスを利用することができる。各コンパイラプラグインの使用によって、望むなら(そして実際にこれはどのようにマクロDSLが実装されるか)全ての新しいDSLを作ることができる。さらには、これらは図にで見た #[derive(…)] と同様に前面の項目を変更するタグの作成に使用される。最後に、様々な種類のエラーとレポートのためのAST確認に使用されるリント作成に使用される。これら全てでできることは、全ての開発者にとって非常に有用であり、それらがカーネル開発に使える純粋なコンパイルタイムプラグインであることに感謝する。

3.1.4 エコシステムの成長

最後の利点として、Rustはとても堅牢でモダンな標準ライブラリを備えており、カーネル開発において最も小さな労力で使うことができる点でCを超える。Rust標準ライブラリは他のどの高水準言語よりも期待するもの全てをを備えている。Rust標準ライブラリはlist、map、setを含む複数のコンテナ型、参照カウントスマートポインタと合理的に優れたstringモジュールを有する。一方、C標準ライブラリはpipe、file、シグナルハンドル、そしてメモリマッピングのようなカーネル機能には巨大なインターフェースであるため、カーネルセッティングでほとんど使えない。カーネルコンテキストで使われる沢山のC標準ライブラリのパーツがあるとはいえ、これらのパーツは遥かに限られていてそれらをRustに対応させて使用するには難しい。さらには、list型とmap型のようなRustで提供されているこれら多くの一般的な型はC標準ライブラリには含まれていない。
さらに多くのコミュニティのRustプロジェクトはほとんど標準ライブラリを使用するので、変更がほとんどまたは全くなく使用される。これはソースの大部分のOSのように複雑なもの作るときに非常に使えるパッケージをかなりシンプルにインクルードすることを意味する。例えば、Reenix作成に取り掛かった時、私は関数で実行されるテストを書くために、外部コンパイラプラグインを利用した。このプラグインは非常に便利で、システム初期化を実行していた方法に関連する複数のバグを見つける手助けになった。ついてないことに、多くのReenixコミュニティプロジェクトで利用することが出来なかった。これはほとんど私がReenix作っていた時に、Rust変更ペースが早くて機会を逃したことに起因し、Rustの最新バージョンで作業しようとするとプロジェクトが停滞することを意味している。Rustの最初の安定版リリース(現在の計画では2015年5月15日となっている)を過ぎるとこれは開発者が言語の安定バージョンだけにターゲットできるので、問題は少なくなるだろう。

3.2 Rustでの問題

Reenixを作っている間、私は主にRustコンパイラに起因する多くの問題に直面した。これらは私が知る限り最善の方法で解決するための必要なツールは、ほとんどの言語で提供されていなかった。これらのどの問題もRustがOS開発言語として使われる妨げにはならなかったが、その間私はOS開発の作業をできたし、問題の場所を特定できたなら、言語は改善するだろうと感じている。

3.2.1 構造体継承

一つ目の問題は、RamFSの実装をしていた時に構造体間の継承全体の欠如である。大体のケースだと複数の型が違う関数へのアクセスを要求するが、多くの同じデータを保持する。これの最適な例が図5で見せたVNodeである。これを見て8,9行目のget fsと get modeが同様にシンプルなアクセサメソッドであることに気付くかもしれない。両方のメソッドは全てVNodeのフィールドであるため簡素であり、実装に関係しないが、動作するために持っていないといけない。確かに、VNode実装するもの全てがNodeの型と、簡素なファイルシステムからなるフィールドのペアを持つ。
全ての一般的なVNodeのフィールド全てを保持し、抽象化された基底クラスがあったなら、よりシンプルになり、そしてサブタイプはこれらのフィールドを加えられられたし、クラスのメソッドのいくつかを実装するだけでよかった。このアイディアはRustコミュニティにとって新しくなかったし、それどころか半ダースを超える提案がRustコミュニティにされたし、継承と同じような提案もあったが、Rust1.0リリースまでの間ペンディングされることになった。

3.2.2 匿名列挙型

RamFS を実装している間、カーネルの残りであったRamFSとVNodeとしての型を描く必要があったのでかかりっきりになるという問題があった。これは全ての同じ型でのVNode作成方法を確認するため、また、そうでなければRustはそれらを型チェックできないためである。しかしながら、内部的にそれはVNode自身を分割した型の、それぞれ違う型を持つよりも遥かに理にかなった、VNodeが必要とする型を指定するメソッド実装である。さらに、この方法で行うことは、ほとんどすべてのVNodeメソッドが任意のものであり、かつ図5のような与えられた型のVnodeで使用される場合、予想されるエラーを返すことを許可する。この制限を満たすために、 RamFSVNode の分割変数の全てを保持することができた自前の列挙型を作った。そしてこの列挙型は全ての VNode メソッドが実際の VNode へ引数を簡単に伝達するための実装を持っていた。他の全ての VNode 型はその後作成とメソッド参照で、この列挙型の戻りとして自身を宣言した。これはうまくいったが、どれほど多くのVNodeトレイトの関数があっただろうか、それはもうとてつもなくコードを書くのに時間を浪費した。これを改善する方法であるが、人間工学に基づいて、Rustで何らかの匿名列挙型を認めることだ。そのような型が自動的に実装できて伝達できたなら、構成要素型の共有関数はこの手のコードであふれるようなことから回避することができた。さらに、この型はコンパイル時にサイズを知られていることを保障し、トレイトを返すことを要求するといった間接参照ポインタなしに使用されることを認める。これは適合する一つの型に関連付けられたデータ構造体のコレクションを簡単に提供する。

3.2.3 静的な所有権

最後のRustの改善点は、静的アロケートされたデータのハンドリングだ。OSカーネルでは、グローバルで全てのプロセスによって共有される多くのデータがある。これにはスケジューラのキュー、読み込まれたドライバのリスト、ファイルシステム、メモリのキャッシュページ、そして多くのものが含まれる。これは全てグローバルデータであり、実在する単一コピーで、規定値として None をセットする Option<T> を静的アロケートとしてこれら全てのデータ構造体を保持することは理に適っている。これは明確に型で値保持が全システムによって静的に所有され、初めは未初期かであり使用前にセットアップを要求することを表している。
不幸にも、アクティブなプロセスの Vec 、もしくは初期化済みデバイスドライバのコレクションのようなものの多くは、コンテンツが初期化されていないプロパティでそのバッキングメモリが破棄されていることを確認する特殊なデストラクタコードを持つデータ構造体である。現状のRustはこのような構造体が静的にアロケートされることを認めていない。恐らくこれはプログラム終了時にデストラクタが確実に実行されるための方法が何もないためだ。これは理にかなっているが、デストラクタが実行終了時に実行されることを気にしないケースも割とあるというのに、この些細なことを無効にするためのコンパイラへの命令方法を提供しないことはいささか奇妙に思う。
私がReenixで使った回避策は全ての構造体を動的アロケートすることだった。そしてそれら構造体を使うのに必要とあればいつでも逆参照される静的なポインタとして配置した。これが動いていた間、他のことに使われるべきだったヒープ領域を浪費するという残念な結果になった。さらには、それはヒープで確保されたデータはあまり効率にパックされないことを意味しており、静的データで消費するメモリよりもさらに多くのメモリを消費することになった。最後に、任意ポインタの逆参照は安全ではない操作(ポインタが初期化されていない、間違っているか、確保済メモリ)で、Rustのこの言語機能はカーネルを確認するのには不必要で邪魔だ。

3.3 致命的な問題:アロケーション

比較的マイナーではあるが、OS開発言語として本格的に使われる前に絶対対処しなければならない熟慮されるべき問題が、私がRustを使用している時に直面した。この問題はRustでのヒープメモリアロケーションに関するセマンティクスであり、より具体的にはアロケーションが失敗した時に起こる問題である。

3.3.1 Rustでのアロケーション

RustでのヒープメモリーはBoxと呼ばれるコンストラクトにより表現される。ヒープでアロケートされたメモリに属する オブジェクトはBox化されたオブジェクトと呼ばれる。Boxは値をヒープに保存するために、値の前にboxキーワードを書くことによって作られ、新しく作られたBoxに値の所有権を渡す。boxキーワードはプログラマのために、ヒープ領域のアロケーションとその初期化両方を扱う。
Rustにおいての標準的な推奨はBox化されたオブジェクトを直接返す関数を書かないことだ。代わりに、関数は値オブジェクトを返すべきで、ユーザはboxキーワードを使ってその値をBox内に配置すべきだ。これは(小節3.1.1で論じたように)Rustは、自動的にコピーを回避するために外部ポインタを使う代わりに、オブジェクトを返す関数の多くを書き換えるためだ。図10ではヒープメモリアロケーションを利用するプログラムの例を見てみよう。このプログラムは通常のRustが推奨するスタイルに従っていて、値によるバッファを返す関数のみを定義している。Buf::new()でヒープアロケートによって返されたバッファ作成は17行目でboxキーワードを使用している。セマンティクスとしてこの行は(1)Buf::new関数を使用してBufオブジェクトを作成するべき(2)新しく作成されたBufを保持するためにヒープ領域のアロケートをするべき(3)ヒープ領域にBufを移動するべきということを述べている。コンパイラはこの操作をより効果的にローカルコピーの除去によって行い、よりシンプルにBuf::newを直接ヒープメモリに書いて、同様のセマンティクスをわずかに違う手順を用いる。
このコードをコンパイルする時Rustコンパイラは図11で見れるように、バッファに必要なメモリアロケートのためにexchange_malloc関数を使用する。exchange_malloc関数は言語アイテムと呼ばれる関数の特殊な型である。言語アイテムはRustコンパイラが知っている追加情報の関数または型で、それらは当然不変であることが既定である。このケースにおいてrustcはexchange_mallocがnullまたはアロケーションした長さが0ではない無効なポインタを決して返さないことを知っている。これは行の確認によって関数自身で保証され、アロケーション失敗した場合はabortが呼ばれる。
コンパイラはこの情報を使用して、コンパイル済みのコードを最適化して、アロケートされたポインタがnullではないことの確認をすることによって作成する。さらにそれはコンストラクタが図10の17行目のようにコピーなしで実行を許可するために外部引数を使用するために書き直される。これらはコンパイラが図10から図12と同様のコード作成するためよるので、統合する。メモリをアロケートできない時に中断と決してnullを返さないことによって、Rustは完全にプログラマからアロケーションが失敗することを隠す。アロケーションが誤りなく完全であることをRustプログラマ(そしてコンパイラ)までは知っている。

 #![ feature ( box_syntax )]

    /// バッファ
    struct Buf {
        val : [u8 ;16] ,
    }

    impl Buf {
        /// バッファを作成する
        #[ inline ( never )]
        fn new () -> Buf {
            Buf { val : [0;16] , }
        }
    }

    fn main () {
    let x = box Buf::new ();
    // バッファで何かする...
 }

図10: ヒープアロケートを使用するプログラム

// Copyright 2014 The Rust Project Developers .
// Licensed under the Apache License , Version 2.0
// liballoc / heap .rs からもってきたもの

/// ユニークポインタのためのアロケータ
#[ cfg ( not ( test ))]
#[ lang =" exchange_malloc "]
#[ inline ]
unsafe fn exchange_malloc ( size : usize , align : usize ) -> * mut u8 {
    if size == 0 {
        EMPTY as * mut u8
    } else {
        let ptr = allocate ( size , align );
        if ptr . is_null () {::oom () }
        ptr
    }
}

// liballoc /lib.rs からもってきたもの

/// 一般的なOut of memoryルーチン
#[ cold ]
#[ inline ( never )]
pub fn oom () -> ! {
    // FIXME (#14674): ここには中断以外の動作を必要とされて
    // いるが、 どの完了したprintingもアロケートしないことを保障されることが
    // 必須となっている。
    unsafe { core::intrinsics::abort () }
}

図11: Rustのliballocコードのアロケート定義

main :
# 除去前の関数スタック確認。
# 詳細は図7を見ること。
# 関数の領域を確保。
    subl $56 , % esp
# exchange_mallocに引数をわたす。
    movl $1 , 4(% esp )
# exchange_mallocに引数のサイズをわたす。
    movl $16 , (% esp )
# exchange_malloc呼び出し。
# nullチェックなしだと返されたポインタで
# 実行されることに注意。
    calll heap::exchange_malloc
# 返されたポインタをスタック上に配置。
    movl % eax , 36(% esp )
# 返されたポインタを第一引数として
# Buf::new に渡す。
    movl % eax , (% esp )
# Buf::newを呼び出す。これは何も返さないことに注意すること。
# 第一引数は外部ポインタである。
    calll Buf::new
# 36(% esp ) は初期化されたBufオブジェクトへの
# ポインタである。

図12: (わかりやすくした)図10でコンパイル時に作成されるアセンブリ

3.3.2 どんな失敗か、そしてなぜやっかいなのか

Rustがヒープアロケート失敗することに触れさせないのか不思議に思うかもしれない。ドキュメントにはなぜこれが隠されたままなのか公式的見解が書いていないが、様々なRust開発者がそれについ議論していた。一般的なアイディアはBoxが露にするアロケーションが失敗することができるようにすることで、どの一般的ではないアイディアに対して、"重い"。さらにはアロケーションが失敗するRustは現在のタスクのスタックを解きほぐし、全てのデストラクタが実行されることを確実にするため、C++で作られたコールフレーム情報(CFI)ベースの例外ハンドリングシステムを使用する。
これはいくつかの方法において最も使用するケースに対して確かに理解できる解決策だ。ユーザコードを書くときにこれは一般的にはページング要求やスワップ、そしてほとんどのシステムでの多くの物理メモリが実際のアロケート失敗するのが実際のアロケーションで失敗するのはかなりレアで問題にはならない。事実なのは、プロセスはmallocmmap、またはbrk呼び出し(linuxの最新版では)に失敗するよりも、とても小さなメモリがある時にkillされる可能性が高いということだ。さらにCFI例外のハンドリングはC++の人気のおかげで、ユーザスペースで多くのよい実装があり、(信じられないほどレアな)Rustプロセスでの別スレッドのアロケート失敗というケースにおいては一貫した状態を持ち続けるということを意味している。不幸にも、これはユーザにとっては容認可能な方法で、カーネルコードにとっては絶対的に致命的であるということだ。カーネルコードは一般的にページング要求を行えない。これにはいくつかの理由がある。第一に、ユーザスペースプロセスがカーネルメモリのほぼすべては実際に使われるのは好ましくなく、最たるものはディスクからのデータのキャッシュページと全システムプロセスの実メモリの保持である。これはどのメモリもカーネルのために特徴づけられるが使われないのは本当に無益で、そのようなメモリがページング要求のおかげでアロケートされていない可能性が高い場合にユーザプロセスは似ていないということは筋が通っている。実際、多くのカーネルは他の継続作業がない場合ハードディスクからのデータについて全て空のメモリで埋める。次に、カーネルの大部分は、頻繁にアクセスされるため、セカンダリページにページアウトできないか(例として、システムコールコードなど)またはそれはページを戻すのは不可能にさせる(例として、ディスクドライバまたはスケジューラ)ことなしにページアウトされることができないのは致命的であるかのどちらかである。これは結論としてはアロケーション失敗はOSカーネルにおいては当然普通に発生する事故だということだ。さらに悪いことに、それは一般的にカーネルコードでは大きな問題にならない。普通は何かアロケートに失敗した時に、解決策はその失敗(ENOMEM)のエラー番号を返すか、単純にキャッシュされていて使用されていないページメモリ削除システムに伝えて、もう一度アロケートを試みる。最後になるが、これがもっとも厄介な問題で、学生が実装したWeenixの通す必要があるテストはプロセスが使用可能なメモリを全て消費している時でさえ実行し続ける。このテストが実行されている時、だいたい他のWeenixのパーツほとんど全てのアロケーションが失敗する。
これは主要なカーネル開発におけるエラーハンドリング構造の実際の問題にさえ入らない。これらで真っ先に挙げられるものは、CFIスタックの解明を使用することはユーザランドのものよりもカーネルコンテキストにおいてさらに困難ということだ。カーネルコンテキストにおけるC++の多くのユーザはアップルのI/Oドライバシステムのようなもの全てを禁止する。さらには、CFI解明 が実装された場合、OS開発に置いてより一層多くのシステム全体にわたるデータの操作が通常のユーザプロセスにおけるものよりも必要とされ、これらの変更を完全に戻すデストラクタを作成するのは困難だ。

3.3.3 解決策と応急処置

ありがたいことに、私が出くわしたRustの他の問題の多くはそうでもなく、これはセオリーに基づいてかなりシンプルな解決策があった。行われる必要があった全てのものはヒープメモリアロケートが失敗するために変更するというもので、全てのヒープメモリは使用される前に明示的に確認されなければならない。これは理論的には標準ライブラリコードにパッチを当てることによって解決されたのだ(が、一つはbox構文を使用することができなかった)。運悪くこれを行うにはいくつか障害があった。
第一に、上で論じたように、Rustのアロケート用の関数は特殊な言語アイテム関数であるということだ。これは図12で見て取れるように、exchange_malloc関数がコンテンツのコンストラクタを呼ぶ前に呼ばれることができるためだ。他の関数に使用するように切り替えることは、この最適化がRustコンパイラの変更なしに実行されないということを意味している。これはすべてのスタックからコピーされ、ヒープに割り当てられたオブジェクトを全て強制することによってRustのコードはとても重くなる。オブジェクトのサイズに依存することでオブジェクトはスタック上一時的に配置されるので、いともたやすくスタックオーバーフローが起きる。RFC80932の最近の承認をうけて、これは近い将来には問題とはならなくなるだろうが、しかし2015年4月のRustコンパイラのパッチを当てる必要がある。
第二に、Rust標準ライブラリの大部分は、誤ってるものから正しいものまである今現在のアロケートの性質に依存する。絶対最小値において、データ構造体とスマートポインタ一つあたりが謝りやすいアロケートモデルを,それに直接依存するデータ構造体と同様にサポートするために書き直されることを必要とする。さらに、多くの一般的なインタフェースは、例えばリストへの値の挿入が成功するといったことはもはや想定されないので、変更されることを必要とする。最終的には恐らくほぼすべての標準ライブラリが書き直しを必要とする変更を行い、謝りやすいメモリの性質をさらす関数を追加する。   最終的に、この問題には応急処置を施さなければならなかった。これには、システムブート上で直ちに数パーセントのメモリが与えられる、Rustにおける単純なアロケータを作成することが含まれる。関数が通常の方法を通してメモリをアロケートできなかった時、バックアップアロケータを使用するのだ。いつでもカーネルは、このバックアップアロケータが使用されているか確認するためのマクロにラップされた、メモリアロケート操作をできた。もし、マクロがアロケートが失敗することを予想して、操作の結果を破棄するものだったのなら、うまくすればメモリの解放はバックアップアロケータで使用されている。これはむしろ明確な理由のために壊れやすく、そして、非常によくスケールすることが期待されることができない。代わりに、この解決策がこのプロジェクトで作用したとしても、メモリアロケート失敗が仮想メモリ実装のVMプロジェクトに入るまで実際の問題のようにはならないため、明らかにはならない。
不幸にも、この問題はOSカーネル作成においてRustの使用を大きく妨げる。一つは安全かつ明確に中断することなくメモリアロケート失敗から復旧できなくてはならない。多大な速度の犠牲なしにこれを行うことができるまでRustはOS開発において実用的になることはないだろう。

3.4 パフォーマンス

最後の一つは、新言語の試験をしている時にされなければならない比較はパフォーマンスだ。これら測定する型を用いることはより難しく、ホストコンピュータ上で行き当たりばったりでバラバラになる傾向が多い。さらに、二つの実装の間で有効な比較を見出すのは難しい。ドライバが終わった後、私は現在のReenixとWeenixの私の実装二つで同一のテストケースをずっとせっとあっぷしていた。全ての通常パフォーマンステストやトラッキングツール呼び出しをするには足りなかったので、Reenixのプロセスコントロールサブシステムといくつかのテストのみの実行をするための比較を制限した。
私は二つのOSでのパフォーマンステストを終えた。初めに、私はマルチスレッドが全てがこの共有された整数のリソースアクセス競合される場合のシステムをシミュレートした。全てのスレッドは協調してともにこの整数に追加し、時たま奪う。このためのコードと時間は図13で見れる。Rustのコードを用いたこのためのテストの平均はCコードの実行したものの3.040倍と同じくらいだ。この数字は全ての応急処置に渡ってとても誤りがないため、これらのコストはRustが行っている作業量に合わせてスケールする。
また、二つのシステムで一般的なシステムコールであるwaitpidの実行を行った場合の時間をテストした。これらの時間は図14で見れる。この関数により、我々はRustが平均的に2.036倍Cよりも遅いことを知った。
これは違う関数においての遅延量が違うのと、言語コンパイラよりも他の何かと関連づいていることを指示していると考えれられる。最もこの遅くなる原因の候補として挙げられるのはアロケートだろう、なぜならRustはCのコードのアロケートよりも幾分多いと考えられ、そしてRustの全てのアロケートは正しいアロケータをリストから見つけ出さなければならないため高コストであるためだ。他に挙げる要因は、Rustではスタックチェックコードが含まれることで、多大な量のコードが全ての要求されたエラー確認を実行するか、Rustトレイトの利用により、生成された仮想関数テーブルの検索の量が多かったかによるものだ。

RustでのMutexテスト関数

extern "C" fn time_mutex_thread ( high : i32 , mtx : * mut c_void ) -> * mut c_void {
    let mtx : & Mutex  = unsafe { transmute ( mtx ) };
    let mut breakit = false ;
    loop {
        if breakit { kthread :: kyield (); breakit = false ; }
        let mut val = mtx . lock (). unwrap ();
        if * val == high { return 0 as * mut c_void ; }
        * val += 1;
        if * val % 4 == 0 { kthread :: kyield (); }
        else if * val % 3 == 0 { breakit = true ; }
    }
}

CでのMutexテスト関数

typedef struct { int cnt ; kmutex_t mutex ; } data_mutex ;

void * time_mutex_thread (int high , void * dmtx ) {
    data_mutex * d = ( data_mutex *) dmtx ;
    int breakit = 0;
    while (1) {
        if ( breakit ) { yield (); breakit = 0; }
        kmutex_lock (& d - > mutex );
        if (d - > cnt == high ) { break ; }
        d - > cnt ++;
        if (d - > cnt % 4 == 0) { yield (); }
        else if (d - > cnt % 3 == 0) { breakit = 1; }
        kmutex_unlock (& d - > mutex );
    }
    kmutex_unlock (& d - > mutex );
    return 0;
}

アクセス回数 #内スレッド Rust実行時間(秒) C実行時間(秒)) 遅延
100 2 0.0238 0.0079 2.999
100 10 0.0851 0.0294 2.889
100 100 0.7865 0.2782 2.827
10000 2 1.9372 0.5920 3.272
10000 10 6.7841 2.1688 3.128
10000 100 61.611 19.697 3.128

図13: RustとCのMutexタイミングコードと実行10回あたりの平均

子プロセスの状態 C実行時間 Rust実行時間
Alive 0.51325 ms 1.1435 ms
Dead 0.37493 ms 0.6917 ms

図14: 1000回超実行での平均waitpid関数実行のミリ秒時間

4 あとがき

私がこのプロジェクトを開始した時、RustでWeenixの水準でUnixライクなOS作成をするということを目標に定めた。この目標は、プロジェクトを完遂するための時間が与えられるのをとても必要としていることを証明した。なんとかできたが、とはいえ、まず手始めにReenixでも同様に全て完備できる、Weenixプロジェクトの二つのパーツを作った。さらに、このプロジェクトからカーネル開発環境においてRustの使用について重大な結論を見出すことができた。これは、取り分け重要な、私の知る限りで今までのRustでこの範囲におけるOSプロジェクトで他の誰もが試みたことがないか、多くが進行中でありるものを与えられた。UnixライクなOSカーネルの基礎デザインは、Rustを使用してのビルドを実行できることを示せたと私は確信している。
さらに、この経験は、まだまだ粗削りで粗削りではあるが、実装作業がWeenixプロジェクトをを行っている生徒によってCよりもRustの方がより簡単に達成できたを私に示した。しかし、サポートコード実装はしばしばRustでは難しいときがあり、わかりづらいコードに結果的にはなった。まだまだ全体的にRustは改善の余地がある。
このプロジェクトを通して、RustがOSカーネルプログラミングだと考えられる限りの改良できたいくつかの方法の感覚を掴むことができた。 これら多くの望まれた問題の修正の実装は苛立ちのレベルで、簡単にRustに追加するか、将来的にどこかでRustに追加されるかのどちらかで、あるいは応急処置で事足りるのだ。私は一つだけプロジェクトを行っている最中にメモリアロケーションに関する、実際カーネルプログラミングにおいてRustを真剣に選択して作るのを本気で取り掛かるには致命的な問題を見つけた。不幸にもこの問題は、すぐに回避するにはほぼ不可能で、修正するのが困難だったし、そしてRustの開発者によって重要な問題ではないと考えられてしまった。
Rustが事実上Cよりも遅いままで終わってしまったという事実は、より落胆させられた。一方、同様にこの遅延の多くがReenixでのメモリアロケートのオーバヘッドと関係している。この問題の領域は私がここでできたことよりも遥かに多い研究をされなければならないということを完全に決定づけられた。さらに、Cの場合ではできる上、しばしばRustコンパイラが常に埋め込む確認を無視できる。これらの全ては共に結論に至るまでイライラさせられるだろうが、Rustの速度減少は乗り越えられないものではない。OSプロジェクトでRust使用を考えることを望むなら、これはまだ熟慮される必要がある。
長々と述べたが言いたかったこととしてはReenixはまあまあ成功したということだ。私が行った限りで得たことは、このプロジェクトに隠れたアイディアが基本的に健全であることを証明することができたということだ。また、全体を通してRust言語は、この手の高水準な完成されたシステムを作る手助けをすること、そしてこの領域において少量の変更で効果的にCを置き換えることができることを示すことができた。

<<前

RustでOS作成したという論文を訳してみた その2

ここの翻訳になります
原文は2015年に書かれたものなので現在のRustの仕様と異なることが書いてある場合があります
本文章はGithubで管理されています
この翻訳は7割方間違っているので信用しないでください

前:その1 次:その3

2 Reenix

ReenixはRustで可能な部分のWeenix OSの大部分の再実装するという私のプロジェクトの名前だ。このプロジェクトでは仕事をWeenixプロジェクトでほとんど行ったように分けた。Weenixの5つのセクションで(小節1.1を見てほしい)、十分に1番目と2番目、プロセスとドライバを実装できた。また、VFSの非自明な部分のプロジェクトとS5FSとVMを完成させるために必要なコード作成を終えることができた。これを行う上でまた、Weenixの大部分をRustのコードをサポートするために変換と書き直しをする必要があった。プロジェクトの結果を含んだ全てのコードとオリジナルのWeenixへのパッチはGithubで見つけることができる。

2.1 機構

Reenix立ち上げにおいて、Rust言語のcratesのコンセプトを使った。Rustのcrateは関連するライブラリにコンパイルされることができるコードのパッケージ、または(いくつかのケースにおいて)バイナリだ。それらは一般的に'lib.rs'ファイルがあるフォルダを見つけることにより識別され、crateルートのための標準の(必須ではない)名前となる。Reenixのためプロジェクトを完全に新規の9つのプロジェクトに分けた。それらはまた、このプロジェクトで使っていた三つのコンパイルプラグインで、内二つは筆者が作成した。最後に、いくつかの標準ライブラリの枠組を利用し、Reenixのためだけに使う標準ライブラリのバージョンを作った。
相対的にReenixは19のcrateと完全にカスタムした12のものを利用する。
Reenixのcrateの大部分がWeenixプロジェクトの機構をミラーしている。メモリマネージメントアーキテクチャはmm crateを含んでおり、プロセス関連コードはprocs crateなどがある。しかしながらそれらはいくつかのcrateはWeenixの本当の類似物ではなく、Reenixに存在する。最初は、それらのうち最も基本的な基礎crateだ。このcrateは大きくそしてやや異なった型とトレイトのコレクションを持っている。これはcrate作成を可能にするためさらに下の依存ツリーの他のcrateのいくつかを部分的に知ることだ。このcrateにトレイト宣言があることで全てのcrateでトレイトが実装されている場合の依存で使われることができた。このcrateの別の機能はReenixでerrorsとして使われている基礎データ型の大部分を持っている。また'dbg'マクロを難なく使われることができるようにこのcrateに配置した。他の新しいcrateは(幾分悪い名前ではあるが)startup crateという。このcrateは最もCコード関数実装へ関連づけるためのACPI,PCIそしてGDTへのstubを最も保持しており、ブートされている間最も使われるためそう名づけられた。これらは、実際のハードウェアと複雑なbit-twiddling実行管理を要求する能力とメモリ操作、Rustでできる最善なことのいくつかなどがとても緊密に関連付けられている。また、データ関数のスレッド仕様の実装に含める。これらは主にここに置かれ、Reenixスレッドが作ったRustのスタックオーバーフロー検知実装をとても簡単にするという事実により全て作成した。最後の総合的に新しいcrateはumem crateだ。このcrateは完全に終わっておらず、そして現在いくつかのユーザスペース仮想メモリとページフレームキャッシュ実装を実装するためのメカニズムの必要性を持っている。Weenixではこれらはいくつかのmm階層になっているが、Rustでそれをするには、保持するcrateとRustの標準ライブラリの利用だけではより難しい。

2.2 ブート

一つ目の問題として、Reenixを手に入れるためにシステムブートを書いていた。Weenixは(私が開始した時点では)カスタム16bitアセンブリコードのブートローダを利用していた。
このブートローダは残念なことに4MBより大きなカーネルイメージを読み込むことができなかった。これは判明してすぐに問題になった。簡潔な出力を作成することをするのにrustcはgccよりも熟達していない。事実、この問題に直面しはじめたころからrustコードでの作業をやめる前は、かろうじて"Hello World"を作ることができた。
この問題の修正は多くの早期のすべてx86アセンブリなブートコードの修正を要求した。また、Linuxブートローダで有名なGRUBを使っていたブートディスク作成のためのビルドシステムの一部書き直しと、マルチブートをサポートするためのブートシーケンス仕様の変更を要求した。それ自体がひどく難しいというわけではないが、これはいくつかのシンプルなハックでCだと可能でRustだとできず、このプロジェクトを公開する試みのソートするために、絶対に必要なことだった。CはWeenixのような4MB以下の適度に複雑なカーネルを維持するのに完全に実現可能で、CS169の歴史でこの制限で実行するものはほぼ誰もいなかったのは事実だ。しかしながらRustにおいてこの制限はほぼすぐに知らしめられた。これはもっとRustコンパイラ最適化(またはその欠如)が言語自身で行っていることよりも多くのことを行っているようで、何人かのソフトウェア開発者の間で最適化の問題があることは事実だ。より確立された言語と比較した場合のRustの相対的な不足を考察する必要がある。

2.3 メモリと初期化

他の初期の問題として、メモリアロケート作業に直面していた。たくさんの理由により、気持ちとしてはむしろこのプロジェクトの範囲外でいたので、Rustでメモリアロケータ実装をせずに、代わりにWeenixに存在するアロケータを使うことにした。これはSlabアロケータというWeenixのアロケータから小規模な問題を引き継いでおり、連続したFixedサイズなデータ構造体のSlabメモリだ。これらのアロケータの型は実際非常に使いやすく、カーネルタスクと使用されたFreeBSDLinuxのような現実のカーネルに使われた。それらはまた、一般的にオブジェクトキャッシュスキームを含んでいるが、Weenixはそのようなスキームを使用していなかった。それらは一般的に多くのOSにて素早くアロケートされるために、サイズが知られている少数の構造体からなる。
問題となっているのはRustがmallocスタイルなメモリアロケータのアイディアでビルドされていることだ。この手のアロケータはどんなサイズでもバッファをアロケートできなければならないmallocからなるslabアロケータを使用して実装するのは逆に大変だ。一つの要求として、いくつかのシステムで、malloc関数はたくさんの違うサイズのslabアロケータから適切なアロケータを探し出す必要がある。slabアロケータを簡単に使用するのを認めるためカスタムアロケータサポートをRustに追加に関していくつかの議論があるが、これはRust1.0では盛り込まれず、延期された。さらに、言語とコンパイラはまた、いくつものセンスによるアロケーションのアイディアから成り立っているのは間違いない。OSカーネルにおいて、これは平常を保つことができない。これはRustコンパイラがこれらの課程を組み込むことは残念なことにかなり深い問題だ。この問題の詳細を小節3.3にて解説する。これをサポートするためにWeenixアロケータがそうであるように、かなり複雑なshim周りをサポートするRustアロケータモデルを最終的に書く必要があった。結局全ての基地のアロケータは常にメモリが確保された状態かつ、有効なメモリを選択するコードを書くことになった。しかしこれはアロケータがカスタムタイプ向けに完全にサイズ計算されたものありつづけるためと、利用するのに良いスペースを確保するという二つの利便性の点で問題を作った。しかしこれを行うには一般的に使われる全てのアロケータのリストを取得する必要があった。これは幾分奇妙なブートのためのマルチステージ初期化案を作ることを要求した。アロケートがまだセットアップされていない場合の初期化を最初のステージとした。このフェーズの間、各部はアロケータに予約するかほかのセットアップに関連付けられたアロケートを要求しないタスクが含むCのプロジェクトの初期化パーツが行うのを要求する。これはWeenixからのtapdance初期化の他の二つのフェーズに追加される。一度これは我々が全ての他のリアルスレッドコンテキストに実行可能な初期化をされ、アイドルプロセスに入った時に最後の初期化を行う。

2.4 プロセス

この小節ではどのようにプロセスが作られ、管理と停止をカバーするプロセスシステムのもっとも基礎的な関数2つについて解説する。次に、内部スレッドの同期が実行と維持されることについて解説する。最後に、Reenixスケジューラの調査とどのように作成と振舞いを行うかについて説明する。最初のReenixの主要な部分である包括的なプロセスコントロール、スケジューリングと同期はまた、CS169 WeenixプロジェクトのPROCSで実装される。Weenix基礎設計に従って、プロセス構造を選択した。実行中のスレッドの各プロセスのヒエラルキーがある場合に、プロセスとスレッドは分割される。スレッドとプロセスは分けてかたどられ、プロセスは少なくとも最低一つのスレッドを持つ。標準的なUnixでは、プロセス内に親子関係の追跡とプロセス初期化時にはぐれたプロセスを退避させる。プロセスは子プロセス情報とメモリマップを保持する。

pub struct KProc {
  /// プロセスID
  pid : ProcId ,
  /// プロセス名
  command : String ,
  /// スレッド
  threads : HashMap  > ,
  /// 子プロセス
  children : HashMap < ProcId ,
    Rc < ProcRefCell < KProc > > > ,
  /// 有効かどうかのステータス
  status : ProcStatus ,
  /// 実行中/スリープ中/他
  state : ProcState ,
  /// 親
  parent : Option < Weak < ProcRefCell < KProc > > > ,
  /// ページディレクトリ
  pagedir : PageDir ,

  /// wait - waitpidのキュー
  wait : WQueue ,
 }

図3: Reenixでのプロセス構造体はWeenixのそれととてもよく似ているが、子プロセスやスレッドを保持する目的でWeenixの拘束リストの代わりにハッシュマップを利用している。

図3で見て取れるように、プロセス構造体の定義に少し注釈をいれた。もしそれを実装から離れたところにしたのなら、プロセスはまた、開かれたファイルとすべてのスレッドで共有されるカレントディレクトリ情報を保持している。スレッドはプロセスが現在何をしているかの情報を保持し、スケジューラの作業とともに、スタックの保有とブロックを行う。

2.4.1 コントロールと作成

Reenixは非常にシンプルなプロセスコントロールモデルを保有する。KProc::new関数によってプロセスをいつでも作成することができ、その第一スレッドによって呼び出される。この関数は新しいプロセスのユニークID番号か、なにか良くないことが起こった場合、エラー識別値を返す。一度この関数が呼ばれると、作成されたプロセスはスレッドが終了するか明示的に止められるまで実行し続ける。現在Reenixではマルチスレッドプロセスをサポートされていない。スレッドはプロセスの作成によって作られるためだ。この制限は現在における利便性のためにある。プロセスのデザインとスレッドは切り替えられるよう許可することは、マルチスレッドプロセスよりも簡単だ。Reenixにkill(2)に類似した機能はなく、スレッドを強制的に終了することはできず、停止している最中か停止に抵抗している場合に、どのスレッドが起動してもスレッドかプロセスをキャンセルできるようにして、スレッドは自らの意思で停止することができる。最後にwaitpid(2)のような関数をを通してどのプロセスも子プロセスが終了するのを待つことができる。プロセスはどの子プロセスでも、もしくは指定した一つのスレッドが終了するのを待つことができる。プロセスはスリープ中以外の既に終了したプロセスを終了するのを待つ。
このプロセス構造の所有権のシンプルな問題の実装における全てにおいて有名な試みである。もっとも明確な答えはプロセスはその子供の全てのプロセス構造を所有するべきであるということだ。これは我々が作成ししているwaitpidで作成しているプロセスツリーに反映され、よりシンプルな実装となっている。これを行う場合、事実にもとづいてプロセスは親のトラックを保持する必要性と、waitpidでスリープ中の親を通知する許可を対処する必要がある。さらに、現在のスレッド変数が常に通じていることが必要としないことは利便性において重要であり、したがって任意のプロセスIDをキャンセルか問い合わせ可能なプロセス構造の中に戻すことのいくつかの方法を必要とする。これらが我々が保持するプロセス構造体が最も参照カウントポインタである Rc<KProc> を全ての参照を所持しない Weak<KProc> という弱参照を通して使用することを全て許可する。これは我々にまだまだ提供されていてかつ完全な参照の取得のチェックなしで Weak<T> によるアクセスではないRustによる安全なアクセスをさせている間親としてのプロセス構造の所有を離させる。
Rustを使用することの利点はこのスコープベースのデストラクタが幾分コードを単純にすることを許可された点にある。これらのデストラクタがいつでもスコープ外のオブジェクトコードをきれいにし、エラーを単純化してきれいにすることを認めた。例えば、一般的に新しいプロセスが何らかの理由で失敗した場合、全ての新しいプロセス構造を含めた一時的な変数が破棄され、エラーコードが変えされることは知ることができる。これは繰り返しアクションを複数の場所でクリーンアップする必要がなく、'goto error'ベースのクリーンアップシステムが必要ないことを意味する。

2.4.2 同期

Reenixは厳格なシングルコアOSなのでよりシンプルな同期に関することの計画をできた。Reenixにおける全ての同期は待機キューを基にしている。待機キュー状態変数と同様にシンプルな動機プリミティブである。どれでも待機キューでの待機を選ぶことができ、それらはほかのスレッドでキューへの通知があるまでスリープする。これらの関数はすでに使用されている場合に割り込みをマスキングの面倒を見、スレッドがスリープしてる間か起動したときに割り込みが発生したとき抵抗する。待機キューはKQueueと呼ばれる構造体で実装される。実装においてエントリーキューで一度だけ起動だけできるので、スレッド軌道のために呼ばれているようなスリープするのを選択することができる。これを使用するのは、全てのミューテックスや状態変数のように同期コンストラクタ要求することはかなり容易なことだ。これらを作るために構造体をさらにジェネリックな構造体はカプセル化されたこの振る舞いのトレイトのペア作らせた。
上記のプロセスコントロールと同様に、同期の形を所有権のトリッキーな質問につなげるようにこれを実装していく。これは最終的には待機キューがシンプルな現在停止中スレッドリストであるためだ。待機キューは明確に、感覚的にもスレッドの所有者であるべきではなく、単純に一時参照を保持する。残念ながらRustのライフタイムシステムは我々の全てのスレッドにまたがって一貫性のある参照を与えたやり方ではきれいはライフタイムではない。これはRustのライフタイムが常に現在実行しているコールスタックに関連付けられているためだ。Rustは前提として、どの与えられたスレッドすべてが(a)永続的にありつづけるまたは(b)なにかに(特に現在のコールスタックで呼ぶ関数で)作られてかつフレームを抜けるときに破棄されるかのどちらかである。これは感覚的に、非常にトリッキーに完全に分けられたスタック上で生きる参照とともに働く。各スレッドにの総合的なライフタイム分割でRustで参照を安全に使用できることを証明する方法は他になく、したがってこの方法でキューを実装するのは認められない。解決可能な方法として前節でプロセスに行ったように弱参照を使うものがあり、もしこのプロジェクトでもした場合、おそらくそのようにしただろう。代わりに、Rustの能力の素晴らしい機能のRustの安全チェックを明示的に無効かするという悪用を選んだ。したがってスレッドをシンプルなポインタとしてキューにして持つようにし、そのポインタがなくなった場合にスレッドを元に戻すようにした。この解決方法は上記で開設した素朴な方法と同等に機能し、安全である。それはまた、弱参照による解決方法の極端に重い性質を便利に回避し、参照カウントポインタを守る必要性を回避するので、安心して作業することができる。

2.4.3 スケジュール

Reenix OSは単純なFirst-in-First-outスケジューラを用いる。それは単純な実行スレッドリストを保持する。各スレッドは特別なそのポインタ命令と他のレジスタに関する値を保持する構造体に状態を保持する。これは待機キューが所有権を戻すときに多くの点で自然に実行され、いくつかの方法でそれらを解決する。他の幾分奇妙な点として、私は多くの実行してすぐにスレッドがない場合にスケジューラ実行ループから離すコンパイラ最適化をしていた。それは他になにか変更できる方法がないリストのミュータブル参照のみを持っていることは確信できる。運が悪いことに、このリストはしばしば割り込みコンテキストによって変更され、随時確認されなければならない。そのようなエラーはCで発生する(そして最適化されて戻される)がRustで修正するのははるかに難しい。Cでは、使われている揮発性の変数は単純にマークされる必要があり、コンパイラはそのアクセスを最適化するのをやめる。同様のことをRustで行うには、変数の読み込みをマーカー関数を通して確実に行う必要がある。

2.5 ドライバ

次にReenixの有名な部分の基礎デバイスドライバの追加について記す。Weenixプロジェクトに従って、(エミュレートされた)ATAハードディスクドライバと基本的なTTY21ドライバスタックを付けることを選んだ。これは(少しばかりの)OS間相互作用をもつことを認め、テストと分析を簡単にし、そして将来的に持続性のがあるストレージを持つことを認める。さらに、これら二つのドライバはよりシンプルで理解しやすくてそしてドキュメント化しやすく、それらをゼロから作るプロセスを簡単にする。他のカーネルパーツでより簡単にドライバを利用するために、カプセル化する基本関数のために基本的なトレイトを作った。それらは図4でみることができる。それらはカーネル内ドライバのインプットとアウトプットをほど良く抽象化する。したがって、それらは何度も違う方引数による様々なデバイストレイトを再実装することを可能にするRustの良い機能を説明する。これは/dev/zeroのようなメモリドライバのような事例で非常に使いやすく、文字列から全メモリブロックを読み取るまたは文字列自体を読み取るのうちのどちらかをするようにするのが望ましい。
この節ではReenixで作った各ドライバの実装を掘り下げて説明する。また、私が製作中に直面した問題について確認できる。

2.5.1 TTYスタック

ReenixのTTYサブシステムは5つの分割されたパーツに分かれている。もっとも低レベルなのはキーボードドライバで、外界からの入力受信か外界に文字出力をさせるのを認める。ディスプレイドライバはシンプルなVGAモードで3テキストビデオインターフェースになっており、あらゆる全てを描画することを求められる実際のビデオドライバ作成のトラブルを回避する。キーボードドライバは標準PS/2キーボードで実装されており、標準キーの全てとメタキーのいくつかをサポートする。これら両方は完全にRustで実装されるが、WeenixではよりシンプルなCバージョンのパーツになる。
スクリーンドライバの利用において、シンプルな仮想ターミナルドライバを作った。仮想ターミナルは一般的なUNIX仮想スクリーンとキーボードを表す抽象的なものだ。これらの仮想ターミナルのスクロールと書くために次の位置を追跡するカーソルを実装する。これは他のCのより直接的なパーツをRustにしたものだ。最後に、上の仮想ターミナルとキーボードドライバについて、TTYドライバとライン制約を作った。これらは実際にTTYで実行されているドライバを使った。TTYドライバはキーボード割り込みを受信し、ライン制約過程を通して記録した後、スクリーンに出力する。TTYが表示可能な文字列に変換するためにライン制約を使用して書かれている時とそして仮想ターミナルに通す。TTYは全行を読まれている時に返すか、全行打たれるまで現在のスレッドはスリープ状態になる。
このパートのReenix設計に関することを与えられることを求めた試みはなかった。私ができなかった異なる仮想ターミナル間の切り替え方法を明確に見つけるという試みは難しかった。私はポインタ更新のためにどのTTYが入力送信をしたかわかるサブシステム割り込みを付けるのをやめた。しかしこれはまたCバージョンでも問題で、これは私が思いつく限りでこの問題を解決するための最善の方法だったことは確かで、以来Rustの評価が非常に下がった。実際のところ割り込みハンドラの切り替えは他の方法で解決できるが、特に異なる関数がほぼ同一であるという理由でやるのは信じられないほど大変だ。その上、割り込みを持っている間の割り込みハンドラの変更は苦痛で、そして、知っている限りで、これまでのところシステムは割り込みハンドラ変更操作をしないのだ。

/// ‘T‘単位で読み込み可能なデバイス。
pub trait RDevice  {
  /// buf.len ()のオフセットから始まるデバイスオブジェクトを読む。ストリームから読み込んだ
  /// オブジェクトの数か、失敗した場合はエラー番号を返す。
  fn read_from (& self , offset : usize , buf : & mut [ T ]) -> KResult ;
}

/// ‘T ‘単位で書き込み可能なデバイス。
pub trait WDevice  {
  /// デバイスのバッファを返し、デバイスの開始点を与えられたオフセットから開始する。
  /// 書き込んだバイト数を返すか、エラーが発生したらエラー番号を返す。
  fn write_to (& self , offset : usize , buf : &[ T ]) -> KResult ;
}

/// 読み込みと書き込みが可能なデバイス。
pub trait Device  : WDevice  + RDevice  + ’ static {}

/// バイト単位で読み込みと書き込み可能なデバイス。
pub type ByteDevice = Device ;

/// ブロック単位で読み込みと書き込み可能なデバイス。
pub trait BlockDevice : Device <[ u8; page :: SIZE ] > + MMObj {}

図4: 読み込みと確定サイズデータ書き込み可能なハードウェアデバイスカプセル化するデバイストレイト

2.5.2 ATAディスク

ReenixのATAディスクドライバはとても単純なデバイスドライバだ。ブロック単位ディスク読み取りと書き込みについて有用で、複数の連続した読み取りと書き込みを一度に行う。これを行うにはダイレクトメモリアクセス(DMA)を使用する。DMA使用中にドライバは指定したメモリのメモリバスがディスクに指示を送信するために使用する場所に書き込む。そしてディスクは指定したオペレーションを実行し、結果を(ユーザが指定した)メモリロケーションに結果を読み込む。さらにCPUに割り込ませる。これの全体プロセスはかなり正確でCS169のCバージョンと私のRustバージョンではわずかな違いしかない。主な違いは、RustはDMAデバイスコントロール用の秘伝マクロのいくつかを取り去れていることだ。

その他の思いつくものとしては、Cコードではそれを複製するために行っていたが、Rustでは相対的なアライメントの欠如が主な問題となった。DMAを使用するために、一つPhysical Region Descriptor Table(PRD)と呼ばれるデータ構造体を与える必要性がある。このテーブルはDMAが使われる際にPCIバスに伝えられる。なぜならこのテーブルはかなり密かにハードウェアにリンクされており、32バイト境界アライメントでなくてはならないという非常に厳しいアライメント要求がされる。Cでは大きな問題ではなく、単純なアライメント属性のテーブルを静的アロケートできるか、実行時にバッファをアライメントごとに区切られたアロケータかアライメント取得を保障できる十分なスペースで動的アロケートする。Rustではいくつかの理由によりはるかに挑戦的な問題となった。第一に強制的にデータアライメントする方法がRustにはなく、現在の勧告ではそれをエミュレートするために、SIMDタイプのゼロ配列長の配列を配置する。第二にReenixのアロケータは有効なアライメントを保障することができないため、とにかく良いアライメントを取得すことができなかったことが問題となった。これらを静的にアロケートできたが、Rustでこの問題を回避しようと試みたので、簡単にできなかった。さらに、CPUによりSIMDタイプによるラックサポートのためにコンパイルしているが、いい結果になったかどうか自身がない。最後にいつでもPRDテーブルを使えるようオーバーアロケートとにマニュアルデータアライメントを選択したが、むしろ面倒でエラーのもとになり、複雑になった。

2.6 KShell

最初に開始したもののうちのTTYドライバを実装し終えると、私はKShellと呼ぶ単純なコマンドシェルを実装を開始した。これは以前は不可能だったOS相互作用を認めるもので、キーボードを使って動的にタスクを選択できる。KShellのおかげで、テストとOSの試験をより簡単にすることができた。それがどれほど有用か過大にテストするのは難しかったが、俄然再コンパイルなしでコマンドシーケンスの差分を検証することを簡単にすることができた。さらに、KShellなしでTTYドライバのテストを行うにはより困難になった。最後に、ユーザスペースの形づくるか実装していなかったことから、カーネルスペースシェルは対話式コンソールシステムでできたただ一つの方法だった。多くのコマンドシェルのように、KShellは簡単なread-evaluate-printループを基にしている。KShellが使用可能であるコマンドの番号があり、平行に自身のスレッドで別のコマンドを実行するためのコマンドを含む。各コマンドはコマンド実行と成功したかどうかの値を返すRust関数によって書かれている。
最も興味深いことの一つは、KShellがRustで書かれたジェネリックREPLシェルとどのように似ているかということだ。様々な点において、通常のRustと同等の方法で実装されているのは確かだ。型が同じ、ループが同じ、などなど。これはReenixの変更なしでどれだけ多くのRust標準ライブラリが使えるか証明したということであり、興味深い。より高レベルな言語では、カーネルモードで実行中の実際のランタイムのラックは一度書いた場所からのコードのソートにひどく制約を受ける。さらに、KShellを書いている間、それがCで同じことをやるよりもRustでしたほうがどれだけ簡単に書けて私を驚かせた。Rustで書くことによって、私は簡単に使えるリストと型マップ及びRustの高レベルなstringルーチンを使うことができるという機能を活用することができた。これらはCで提供されているコマンドシェルを動作させるのを作りこむ上でもっとやっかいな側面のいくつかを取りされた。

2.7 仮想ファイルシステム

ドライバの実装を終えると、私は次にReenixの仮想ファイルシステムの製作にとりかかった。仮想ファイルシステム(VFS)はUnixライクなOSで最初に作られた有名な抽象化で、1980年代中ごろにSunによって広められた。VFSは様々なディスクとネットワークファイルシステムの差分を吸収するために設計された。VFSは有名な主要なファイルシステム全ての操作インターフェースと、ディレクトリ検索とファイル作成・削除・オープン・読み取り・書き込みなどのルーチン対話形式を定義する。また、一般的にファイルシステム配下によるブロックデバイス読み取りのキャッシュを認めるためにデータブロックキャッシュを定義する。WeenixとすべてのUnixライクOSのどちらも今日においてVFSライクなインターフェースを、ファイルシステムの仕様を容易にするために利用する。不幸なことに、時間的な制約の為ReenixではVFSの実装には手が届かなかった。コンポーネントのインクリメンタルテストを許可するため、私はWeenixの先例に従い、よりシンプルに RamFS とよばれるインメモリファイルシステムを実装することをS5ファイルシステムに完全に実装を移す前にVFSの機能のいくつかをテストするために決定した。しかしながら、ブロックデバイスとS5ファイルシステムのディスクアクセスレイヤーキャッシングの一部を実装できた。そして合理的にRamFSの製作を完了した。
この節は少なくとも完了時に開始することができた各VFSのパーツを調べる。我々はまず VNode トレイトの設計とそれに入った決定について焦点を当てる。次にRamFSテストファイルシステムのどれがもっとも完成することができたか調べる。最後に、ページフレーム、メモリオブジェクト、そしてそれらに付随する唯一細々と開始することができたキャッシングシステムについて調べる。

2.7.1 VNode

仮想ファイルシステムにおける主要なデータ型はVNodeだ。VnodeはVFSにおけるファイルまたはファイルシステムオブジェクトの表現になることができる。VNodeによる主要なファイルシステム操作に関する多くの関数が主に使われる。最も多く定義されていたのがその関数の実装であることにより、Reenixにおいて私はVNodeをトレイトで作った。VNodeは一般的に同時にファイルデータを利用する所有者に複数保有される。Weenixでは最も違う点としてVNodeの参照カウントをマニュアルで行う形で実装されており、その部分はエラーが発生しやすかった。
Reenixの一部であるVNodeトレイトは図5で見ることができる。これで記載しておいたこととしてはVNodeを直接与えずに代わりにハンドル参照カウントを与えるという事実を表現するためにRustの型システムを利用しているということだ。関数の結果の型はいくつかのVNodeの型のように使いやすく作られなければならないと主張している。これは各VNodeを実装するファイルシステムオブジェクトの分割型を持つこととこれらのファイルシステムオブジェクトのなにか標準的なenumを戻すことを許可する。この結果の型がコピー可能(クローントレイト)であることにより、VNode間で共有しているいくつかのオブジェクトを参照しているバッキングファイルを隠し、場面の後ろで参照カウントを認める。単純に参照されなくなった時にVNodeを解放する参照カウンタによってラップされた型を返すことができる。例えばRamFSにおける結果の型は Rc<RVNode> だ。これはWeenixにおけるVFSのトリッキーなパーツの一つであるエラークラス全体を防ぎ、マニュアル操作による参照カウントの必要性を取り去る。

pub trait VNode : fmt :: Debug {
  /// これは型システムが機能するようにここにしかない。HKTなしのb/cを必要とした。
  type Real : VNode ;
  /// Vnodeの型操作で取得/作成されるもの。これは複製でなくてはならない。
  /// 参照カウントを与えるラッパを持つことができるので、
  /// 借用(Borrow)と呼ぶのが望ましい。
  type Res : Borrow < Self :: Real > + Clone ;
  fn get_fs (& self ) -> & FileSystem < Real = Self :: Real , Node = Self :: Res >;
  fn get_mode (& self ) -> Mode ;
  fn get_number (& self ) -> InodeNum ;
  fn stat (& self ) -> KResult < Stat > { Err ( self . get_mode (). stat_err ()) }
  fn len (& self ) -> KResult  { Err ( self . get_mode (). len_err ()) }
  fn read (& self , _off : usize , _buf : & mut [u8 ]) -> KResult  {
  Err ( self . get_mode (). read_err ())
  }
  // ...
  fn create (& self , _name : &str) -> KResult < Self :: Res > { Err ( self . get_mode (). create_err ()) }
  fn lookup (& self , _name : &str) -> KResult < Self :: Res > { Err ( self . get_mode (). lookup_err ()) }
  fn link (& self , _from : & Self :: Res , _to : & str ) -> KResult <() > { Err ( self . get_mode (). link_err ()) }
  // ...
 }

図5: VNodeトレイト

2.7.2 RamFS

RamFSはテスト向けのWeenixとReenixでのVFS実装をテストするために使われたインメモリファイルシステムである。この"ファイルシステム"は実際のディスクなしでVFSに使われることを必要とするすべての呼び出しを実装する。WeenixでこのシステムはS5ファイルシステム製作との並行作業なしでVFSテストの有効なサポート実装の一つとして実装されている。実際その最終的なデザインができる前にRamFSを実装するいくつかを試みた。初期デザインはCからRustへの直接的な置き換えだった。私はVNode表現のような他の多くのWeenixサポートコードを信頼していたので、スタブバージョンを単純に使うことができなかった。これは想像していたものよりもより難しくさせた。
CのRamFSは最も一般的な実装となったが、ファイルNodeの割り当てられたトラック保持のためのシステムの容易性に欠き、ブロックメモリ確保のためのシステムがなかった(全てのファイルとディレクトリは正確に一つのブロックを取得する)。これは例えば、ディレクトリはディレクトリエントリの配列にキャストされているバイト配列で実装されていることを意味する。この方法でが非常に実ファイルシステムに酷似している間、Rustが生で、型付けされなく、Cのようにシンプルなストレージで働かないことが問題となった。Cでは配列の単純なキャストと(初期化なしの)ディレクトリエントリの配列の働きが、Rustで行うよりも面倒だった。この問題はS5FSを伴って(バイナリシリアライズライブラリの使用を通して)イベントハンドルする必要性があって、時間の関係で私はよりシンプルな道を選ぶことを決めた。
RamFSの第二のデザインはできる限りシンプルなファイルシステムのモックを作ることだった。実装において実ファイルシステムと同様に作るというあらゆる概念を棄てた。ファイルシステムオブジェクトの各型は違う型にし、各ディレクトリは単純なファイル名からなるNodeのマップとし、参照カウントはただちに標準ライブラリの RC<T> を使用した。これは遥かに簡単に実装され、大部分を短い時間で手早く終えることができた。残念ながらこれを終えるときにはVFSに加えて何か残りの作業をする時間がほとんどなかった。この間私はVNodeについて同作業するか決める最後のデザインをした。

2.7.3 PFrameとMMObj

私がVFSの作業を開始した時には、当初RamFSファイルシステムを作ることは計画していなかったので、S5FSによって必要とされたサポートコードの多くを作成することで開始した。このサポートコードはRamFSで使うファイルシステムで最も必要とされたブロックキャッシュ実装を必要とする構造体を含んでいる。これらのシステムはしばしばオペレーティングシステムカーネルプロジェクトにより仮想ファイルサブシステムの一部を考察されていたが、WeenixではそれらはS5FSプロジェクトまでの間実装されていなかった。このシステムは(キャッシュ可能な)データページを提供可能なデータソースの抽象化をするMMObj、キャッシュと更新可能なMMObjからなるデータページのPFrameという二つの有名なコンポーネントで作られている。PFrameは概念的に元となるMMObjによって所有され、感覚的にはシンプルなMMObjのCurrent Viewとなる。実際には、PFrameはMMObjから分割してキャッシュされて、このMMObjは単純にPFrameを満たすことに最も責務があり、PFrameが破壊される時にデータを書き戻す。Reenixではこれらの部品は完成していないが、公開するインターフェースは完全に書かれている。
この基本となるMMObjトレイトは図6で見ることができる。一つ注目すべき興味深いことがあり、この設計はPFrameを差し置いてファイルシステムコードによって直接的に呼ばれることがある関数がない。ファイルシステムがMMObjによるメモリページを必要とするときに、この基本的な構造体呼び出しは与えられたMMObj上のページ番号のPFrameを要求するだろう。これはすでに存在するか確かめるために最初にグローバルPFrameキャッシュを検索し、もし存在するのであればそれを返す。そうでないのなら新しいPFrameを作成し、MMObjで使用するデータを入れる。呼び出し元はその後PFrameを利用するが、スコープから抜けるときに解放されるのを防ぐため、それが参照カウントされるのを求められる。PFrameがスコープ外に抜けるときにその参照としてか、そのMMObjからのどちらかがどこからでもアクセス可能か確認する。もしそれができない(またはメモリがほとんど残っていない)場合、変更されているかどうかとMMObjが書き戻すかを確認する。
最も重要なのは構造体呼び出しが望んだ通りに実現できるかという問題がこれを自身で納得して作っている間に直面したということだ。初回呼び出しの多くの場合においては、実装を隠したトレイトにしており、判断するのを難しくさせることが発生される。他の私が抱えている問題としてはこのシステムが独立したテストを行うことが非常に難しいということだ。

pub trait MMObj : fmt :: Debug {
    /// このプロジェクトのMMObjIdを返す。
    fn get_id (& self ) -> MMObjId ;

    /// 入るべきデータを与えられたページフレームに入力する。
    fn fill_page (& self , pf : & mut pframe :: PFrame ) -> KResult <() >;

    /// フック。リクエストが非ダーティページに書き込む時に呼ばれる。
    /// 与えられたページへの書き込みを可能にしなけらばならない
    /// いずれかの必要なアクションを実行する。 これはブロックされる可能性がある。
    fn dirty_page (& self , pf : & pframe :: PFrame ) -> KResult <() >;

    /// pfアドレスで始まるページフレームの内容
    /// を書き戻す。pfで識別されるページにページ数をつける。
    /// これはブロックされる可能性がある。
    fn clean_page (& self , pf : & pframe :: PFrame ) -> KResult <() >;
}

図6: MMObjトレイト

2.8 他の問題

私がReenixの作業する上で、多くの問題とプロジェクトの任意の部品の一部を作ることを決めなければいけないことがあった。これらの試みはRustのスタックオーバーフロー調査の実装、スレッドローカルストレージ実装とシンプルなプロジェクト全ビルドの取得に含まれていた。これらと必要とされている全てを行う上で困難だったのは、RustとCの間の差異が主な理由だ。

2.8.1 スタックオーバーフロー検出

Cのような、ほかの(有名な)コンパイル言語より多くの良い機能を持っているRustはスタックオーバーフロー検出組み込みがサポートされている。これはメモリ保護可能ではないハードウェアの場合にスタックオーバーフロー検出ができることを意味する。RustはLLVM24が提供する、全てのサブルーチンがスタック終了地点を保持するスレッドローカル変数のチェックを保持しており、これを実行するメソッドを使用する。
各サブルーチン呼び出しにおいて、関数は保有するローカル変数スタックするための十分なスペースが存在するかどうか確かめるためにこれを使用する。特にx86システムで %gs セグメント内のオフセット0x30の値を確認する。この機能は(暗黙的に)スレッドローカル変数を利用するため、ランタイムサポートの指定とセットアップ作業を要求する。

c0048f10 < kproc :: KProc :: waitpid >:
# 使用済みスタックとスタックの終わりを比較する
c0048f10 : lea -0 x10c (% esp ) ,% ecx
c0048f17 : cmp %gs :0 x30 ,% ecx
# 十分なスペースがある場合持続する
c0048f1e : ja c0048f30
< kproc :: KProc :: waitpid +0 x20 >
# スタックのメタデータを保存する
c0048f20 : push $0xc
c0048f25 : push $0x10c
# __morestackは現在のプロセスを中断する。
# 名前はRustがセグメントスタックをサポートした時からずっと
# こうなっている。
c0048f2a : call c000975c < __morestack >
c0048f2f : ret
# 標準的なx86関数の前処理
c0048f30 : push %ebp
c0048f31 : mov %esp ,%ebp
c0048f33 : push %ebx
c0048f34 : push %edi
c0048f35 : push %esi
c0048f36 : sub $0xfc ,% esp

図7: KProc::waitpid から分解された関数前処理

Rustの標準x86関数前処理のコピーは図7で見れる。3行目で、関数は最初に使うスタックから最も遠い地点を計算するのを見ることができる。その後、関数は %gs セグメントからスタックの終わりまでの読み込みと、それと要求されたスタックスペースの比較を4行目で行う。最後に、6行目で実際の関数か、__morestack関数を呼び出してスレッドを中断処理のどちらかにジャンプする。
不運にもWeenixはこのスタックチェック形式をサポートしていなかったので、そのスタックチェック形式を望んで使う場合は自身の手で実装する必要があった。最初は、Weenixはこの機能なしで何一つ問題なく動いていたので、無効にすることができたと考えていた。だがさらに不運なことに、私がこのプロジェクトを開始した時には、全てにおいて関数からこのスタックチェックコードを取り除く方法がなかったのだ。これは自分で書いたコードを実行するために、LLVMを使ってスタックチェックメソッドのサポートをしなければならないことを意味していた。これを行うことは実際には恐れていたものよりもやや難しくなかった。Weenixによって提供された、グローバル記述子テーブル(GDT・Global Descriptor Table)操作ルーチンの使用で、保存するべき適切なオフセットのスタックエンドポイントのデータ構造を書くことができた。このデータの構造体は図8で見れる。

#[ cfg ( target_arch ="x86")]
#[ repr (C , packed )]
pub struct TSDInfo {
  vlow : [u8; 0 x30 ] ,
  stack_high : u32 , // オフセット0x30である
  /// 他のスレッド特有のデータ。
  data : VecMap < Box < Any > > ,
}

図8: TLSデータ構造体

これらのことから、私にはまだ少量のやらなければならないことがあった。私が次に必要としたのは、 実際幾分トリッキーな%gs GDTセグメントの準備を成功するまでの間、スタックチェック有効な呼び出しがないことを確認することだった。次に、プロセス切り替え中に我々が%gs 記述子の値の正しい変更を確実にすることと、先述と同様になるが、プロセス変更に成功するまでの間、スタックチェック有効な呼び出しなしにすることを必要とした。一度、これはドキュメント欠如の関係で最も作るのが難しいエラー報告関数に接続するために残された全てを達成された。皮肉なことに、これの全ての作業スタッフを揃えてから一週間もたたないうちに、Rustの開発者は完全にスタックチェックを無効にさせるスイッチを追加した。それはまた、コード中でいくつかの無限再帰呼び出しを見つけるという形で正当に残りのプロジェクトの作業の助けになった。

2.8.2 スレッドローカルストレージ

一度Rustでのスタックオーバーフロー検知作業を終えると、スレッドローカルシステムのほとんどを実現した。ほんの少しだけやることが残っていたので、私はその残りを実装することに決めた。標準的なWeenixでのシステムに匹敵はしないが。これを行うのに私は単純にスタックデータを含む構造体の最後にある VecMap を追加した。Weenixでスタティック変数に含まれたいくつかの現在のプロセスとスレッドを特定する情報を含んだこのスレッドローカルストレージを使用することを選択した。この方法でこのデータを保存することにより、それはまたマルチプロセッサマシン上で使用可能なReenix作成の障害を取り除くが、これを行うには沢山の問題がある。このスレッドローカルデータストレージ構造体は図8で見ることができる。それがスレッドローカルデータを VecMap<Box<Any>> 内に保持していることに注目してほしい。この Box<Any> 型は動的キャストを確認するための制限付き実行時型情報を使用する特殊な型だ。これは先述したデータのユーザがまだそれらが受けるデータが正しい型か確認に確認を重ねている間、正確にはなにか知る必要なく、このマップで任意のスレッドローカルデータを保存させる。例として、現在実行中のプロセスがこの構造体で使用ことを保存される。これが発生させることができるにもかかわらず、実際にはTLD構造体はプロセス構造体という概念を持たない。現在のプロセスが回収される時に、返された値、具体的にはプロセスの、どんなトレイトでも使用してユーザはマニュアルで確認しなければならない。

2.8.3 ビルドシステム

最後の一つは、Reenixを書いている間は全てビルドされるようになるという予期していなかった問題についてだ。標準的な方法は、Cargoのようなカスタムビルドツールを使用することでビルドする。このツールは各インクルード面で、外部ライブラリリンクや他のビルドツール呼び出しといった、素晴らしいサポートにより、多くの一般的なRustプロジェクトには完全に十分であった。不幸なことに、このツールは複雑なリンクやビルド操作を実行するが必要な場合に使用するのには非常に困難であるのだ。それにより、Rustでも動作できるmakefileの作成方法を探さなければならなかった。これはrustcが標準ライブラリと、他の自分でビルドしたライブラリのバージョンを使用するだけであること確認する必要があったので、幾分困難であることを明らかにした。加えてcratesにおける依存はCの依存に比べてはるかに複雑で、他のcratesで作成されたライブラリに依存する。さらにReenixで使用しているWeenixの命名規則とRustの標準的なライブラリの名前規則の二つはやや矛盾している。名前比較のためのrustc診断関数を使用することにより、私が作らなければならなかったより複雑で大きなルールを動的に作るmakefileマクロセット全てを終わらせた。

2.9 将来的な作業

Reenixが完成したと呼べる地点に至るまでには、いまだ多くのやるべき作業がある。最も明確に手を付けなければならないのは残された3つのVFS、S5FSそしてVMというWeenixの部品だ。これを行うにはいくつかのReenix設計の疑問点の解決をする必要がある。いくつかの主要な公開している疑問点としてはどのようにReenixはファイルオープンのトラックを維持するべきか、どのようにS5ファイルシステムと直列化されたバイナリデータ構造体を関係づけるか、そしてどのようにメモリマッピングのトラックを維持するべきか、などがある。さらには、Rustで書き直すべきWeenixがサポートするコードの大きな部品がまだある。ファイルシステムテストスイート、実際のシステムコールハンドラルーチン、ELFファイルローダとタイムスライスシステムなどが主要な部品である。これらOSカーネルの部品はそれぞれみんな重要ではあるが、最終的にはよりシンプルになり、カーネル全体の特殊な構造となって、これらの部品のWeenixバージョンを簡単に使用するのを防ぐ。
最後に、更なるソリューションの永続化のためには、Rustのメモリアロケーション失敗の問題を敬意をもって発見されるべきである。この問題は小節3.3で深く掘り下げて議論される。これなくしてシステムが本当にステーラブルになって使えるようになるのはうまくいったとしても困難だし最悪の場合不可能だ。これらは達成されているそれが追加可能なネットワーク、SMPまたは64bitサポートのようなさらに興味深い方向のReenixの実験を継続するために可能にするべきだ。

<<前 次>>

RustでOS作成したという論文を訳してみた その1

ここの翻訳になります
原文は2015年に書かれたものなので現在のRustの仕様と異なることが書いてある場合があります
本文章はGithubで管理されています
この翻訳は7割方間違っているので信用しないでください

次:その2

Reenix: RustでのUnixライクオペレーティングシステムの実装

Alex Light(alexander_light@brown.edu)
Advisor: Tom Doeppner
Reader: Shriram Krishnamurthi
Brown University, Department of Computer Science

要旨

この論文はRustでのUnixライクなOSカーネルの実装でわかった成功と失敗の体験を説明するものである。
CS167/9のために書かれたWeenix OSの基礎デザインと大量の低レベルな処理をサポートするコードを使って、協調的にスケジュールされた複数のカーネルプロセスをサポートする基本的なカーネル、基本的なデバイスのドライバーと仮想ファイルシステムの端緒を作ることができた。
Rust言語とその方安全システムで助けになったことといくつかの妨げ、このカーネルのRustとCの実装とのパフォーマンス比較について書き残しておく。
また、Rust言語とWeenixプロジェクトの概要も含める。

1 はじめに

1971年に初めて作られて以来UNIX OSはソフトウェア開発に定着した。世界のOS開発と一般的なソフトェア開発への貢献の一つはそれを書くために作られたC言語だ。リリースされてから数十年、Cは相対的に小さくしかし最先端のプログラミング言語設計とチェックを変えられ、大きく進歩した。unixのほぼすべてのOSカーネルがCまたはC++のような言語で書かれていたという成功に感謝する。これはC以降の言語設計の進歩がさらに表現豊かで検証可能な領域の大部分が成功することを意味している。
このプロジェクトのゴールはRustを用いてunixライクなOSを作るのを試みることにある。私の知る限りでは、これは本気で試みられたことはないか、現在までに誰も成果を上げていないプロジェクトだ。これにより実現可能性とRustのような高水準言語でカーネルを作ることの利便性を見出すことができた。同様に目的に合わせて言語の改善点を見つけることができる。
さらにC以外の言語を通して調べたunixの基礎デザインがどれだけ保持しているか評価することが認められることになるだろう。最後に、私たちは完成されたカーネル検証のタスクを扱ったRustの洗練された型安全なシステムをさらに体験することになるだろう。カーネル高速化のさらに高水準なパーツをやりはじめるのを認めてもらうために、私はCS169で実装されたWeenix OSでの成果をベースにした。これでOS開発に特化したものではない、メモリアロケートといったたくさんの低レベルのカーネル部分を実装しなくてよくなった。

1.1 Weenix OS

Weenix OSはx86ベースの教育用OSとして1998年にブラウンのCS167 OSコースで作られた。
現在CS169選択コースの学生たちはCS167に多くのWeenixによるunix-like OS由来の高レベルな部分で実装している。
学生たちはOSブートと実行のCコードを取得するために必要なコード、及びメモリ管理、デバッグプリントシステム、そして基礎的なlibc実装をこのプロジェクトで開始している。これを元にCS169の学生たちはかなり完成されたunix OSの実装を続けている。 プロジェクトと、それをサポートするコードはほとんどがCで、pythonシェルスクリプトをOSの実行とテストのために書いた。
このプロジェクトは複数に分割されていて、いわゆるプロセス、ドライバー、VFS、S5FS、そしてVMからなる。プロセスはUnixライクなプロセスモデルな親子関係プロセスと初期化プロセスをシンプルなスケジューラと原始的な同期により実装されている。ドライバはTTYドライバ、ユーザ入力とハードディスクを使用する(最低限の)ATA相互接続ドライバに大きく分割してされている。VFSは、抽象型仮想ファイルシステムで、テストのためにRamFSと呼ばれるram-backedファイルシステムの提供を採用している。S5FSはS5ファイルシステムと呼ばれているsysv-fsファイルシステムのバージョンの一つで、非揮発性ストレージを提供するために実装されている。最後にVM仮想メモリとユーザースペースにより実装されている。これらはまた多くの最終的なOSをテストできるユーザースペースを提供してきた。

1.2 Rust言語

Rust言語は比較的新しいシステムプログラミング言語で、Mozilla Foundationにより作られている。Rustは一般的に小さく高いパフォーマンスの低レベル層や組込プログラミング環境としてのCを使いやすく置き換えるために設計された。Mozilla FoundationはRustを使用してRustコンパイラ(Rustc)やServoと呼ばれる実験的なウェブブラウザといったいくつかの公式プロジェクトにRustを使用している。それはまた、Mozillaの有名なFirefoxブラウザをRust実装し始めることを近い将来に計画している。Rustは現在Github上で開発している。プロジェクトは非常に人気で開かれており、千人のコントリビュータがいるがそのほとんどはMozillaと関係がない。
Rust自身は手続き指向型プログラミング言語でCのようなシンタックスになっている。それは包括的な型システムや、データ"ライフタイム"システムとメモリ確保のための小さなランタイムとコンパイル時のスレッド安全性を採用している。具体的にRustはデータが他のオブジェクトによって使用されたときに予想外の変更されないことを保障するために所有権とライフタイム追跡システムを使用している。加えて、ライフタイム追跡システムはダングリングポインタができない言語を保障するために使用している。Rustのランタイムは分離可能な多くのパーツで構成されている。それはシンプルなアウトオブメモリもしくは致命的なエラーを回復する、求められた(そして最も基礎的な)機能だ。その最たる例の内にReenixが含まれていて、ヒープメモリの確保のためのインターフェースを提供する。ランタイムの他のすべての機能はOSの実行、ディスク入出力、プロセス間通信そしてスレッド作成、その他もろもろを一貫性のあるインターフェースとして提供するためにある。

1.2.1 シンタックスとセマンティクス

Rustのシンタックスも同様だが、ほかのCライクなプログラミング言語とは少し違う。言語機能を説明するために図を使用して説明しよう、図1にはRustでの基礎的なクイックソート実装が含まれている。Rustのシンタックスとセマンティクスのすべての詳細はオンライン上のdoc.rust-lang.orgで見つけることができる。
最も顕著な違いはRustには式と文にいくつかの違いがみられるということだ。Rustの式ではいくつかのコード片がyieldな値であることだ。構文上、ほかの手によって値が作られることはない。

/// 基本的なクイックソート実装  
/// 型ジェネリッククイックソート。 ‘T‘はソート対象の型で全体を順序付けなければならない。
/// (‘Ord‘ traitを実装すること) listで渡され、要素をソートしたソート済みlistを返す。 
/// この過程のlistはmutableで変更可能であると言える。  
pub fn quicksort (mut lst: Vec) -> Vec {   
  // ピボットから要素を取り出し、listが空なら何も返さない(そしてelse句へ行く)。   
  // そうでなければlistから最初の要素を取り除き、返す。  
  if let Some(pivot) = lst.pop() {   
    // ピボット周辺のlistを分ける。listを反復する(into_iter function)そして二つのlistに分ける。   
    // パーティション関数は二つのlistを返す。1番目のlistは全ての要素が状態がすべてtrueのもの、2番目の  
    // listは全てfalseのものである。     
    let (less, more): (Vec<_>, Vec<_>) = lst.into_iter().partition(|x| x < &pivot);   
    // ピボットより小さいlistの半分を再帰的にソートする。これはいずれ返されるlistとなる。  
    let mut res = quicksort(less);   
    // ソート済みのlistの半分の小さい方の最後の要素をピボットにする。これは'res'listにピボットを追加することである。
    res.push(pivot);   
    // 半分に分けた大きい方のlistをソートし、ソート済みの小さい方のlistとピボットに追加する。     
    // extendは'res'listに 与えられたlistの全てを追加する。
    res.extend(quicksort(more));   
    // 現在のソート済みlistを返す。ここではreturnが必要ないことを注意すること。   
    // 単純にこの行は'res'と書くだけでいい (’;’が必要なことは注意する)   
    // 関数が最後の式の値を返すのは(このif-elseのように)分岐の最後の値(Vec)を取ることと同等だろう。
    return res;   
  } else {   
    // lst.pop()により、listが返されなかった場合emptyでなくてはならないのでempty list をここに記述する。
    // returnは必要ではなくこれはブロック内の最後の式とこのブロックがfunction内で最後の式であることを注意すること。   
    // vec!は標準的なマクロで、Vecを作成する。   
    vec![]   
  }   
}
  
   
fn main() {   
  // ソートするlistを作成する。vec!はマクロでリストされた要素のvecを作成する。     
  let lst = vec![3,1,5,9,2,8,4,2,0,3,12,4,9,0,11];   
  println!("unsorted: {:?}", lst);   
  // quicksortを呼び出す。これはlstの所有権を放棄する。   
  println!("sorted: {:?}", quicksort(lst));   
}

図1:Rustでのquick sort

/// トレイト。構造体と列挙体はこれを実装できる。   
pub trait Id {   
  /// 要求される関数。 全ての実装するものはこの関数について定義を提供しなければならない。でなければ型チェックで失敗する。  
  /// ’staticは文字列が静的に確保されていることを要求されているという意味だ。  
  fn username(&self) -> &’static str;   
  /// 既定実装付きの関数。返される文字列は少なくともIdが有効な間使用可能でなくてはならない。  
  /// ’aは返されるstrが少なくとも’self’が使える間使えなくてはならないということを意味する。  
  /// 型チェックはこれが正しいことを保障する。
  fn screenname <’a>(&’a self, _board: &str) -> &’a str { self.username() }   
}

  
/// 構造体。 deriveは与えられたトレイトの規定実装を提供する。   
/// 特定のトレイトはこの方法で実装されるだろう。    
#[derive(Debug, Eq, PartialEq)]   
pub struct Account { name: &’static str, msgs: Vec, }

  
// Id taritの実装。 
//既定の実装では’screenname’が実装する必要ではないことを注意すること。
impl Id for Account {   
    fn username(&self) -> &’static str { self.name }   
}

  
// Accountに関連する関数を直接実装  
impl Account {   
  pub fn get_messages(&self) -> &[u64] { &self.msgs[..] }   
}

  
#[derive(Debug, Eq, PartialEq)]   
pub enum Commenter {   
  /// データ付き列挙体の値 
  User(Account),   
  /// データなしの列挙体の値  
  Anon,   
}

  
/// Idトレイトの実装   
impl Id for Commenter {   
  fn username(&self) -> &’static str {   
    // 値別のアクションを選択する。  
    match *self {   
      Commenter::User(ref a) => a.username(),   
      Commenter::Anon => "Anon",   
    }   
  }   
}

図2:Rustのtraitとtype

全ての関数内は図1の14,17行目と40行目のような、(1)let変数を除いて変更可能である、ということを除けば一般的な式で、(2)ループ構造、(3)式または構文のあとにセミコロンが置かれる。カーリー括弧に分割されたブロック({})は最後の式の値を使える式であることに注意してほしい。if-elseやmatchブロックは同じような式だ。例として図1でのif-elseブロックは9行目から開始されている Vec<T> 型の式だ。Rustはブロックで開始し、関数内の最上位の最後の式の値を暗黙的にreturnされている式にすることを採用した(図1内の8行目から開始されるif-elseだ)。が、図1のうちの一つは未だに ’return <value>;’ の形式をとっている。これは図2の45-47行目のようにmatchの戻り値が関数である場合にも見て取れる。さらに、これは図1の23行目を単純に’res’とプログラムを同じ意味で変えることを意味している。

他に顕著なCとの違いとしては、Rustが既定で完全なimmutableであることだ。オブジェクトをmutableにするには図1の14行目のようにmutで宣言しなければならない。これと同じように関数の引数も図1の5行目の第一引数のようにmutを記述しなければならない。この延長でポインタも既定でimmutableになっていて、mutableに使用するには &mut で宣言されなければならない。

RustはCと同様に構造体と列挙体の宣言構文を持っている。主な違いとして、列挙体はデータと関連付けることができるということがある。図2の33行目にあるように列挙体の定義はそのうちの一つのようにAccountタイプに関連付けられてデータを持っている。このデータは図2の45行目のようにmatch式で使われることができる。Rustはまたトレイトもあり、Javaのインターフェースと同等で、規定の関数定義と関連づけることができる。トレイトを使うことによってCよりも容易にジェネリック関数を作ることができる。例えば、図1のクイックソート実装は、オブジェクトは並び変えられるOrdトレイトだけを実装している必要があり、全て並び替えられる。図2の2-10行目のIdトレイトは16行目のAccountタイプと33行目のCommenterタイプで実装されているのがわかる。列挙体と構造体はトレイトを通してか、直接メソッドを持つことができる。Commenterトレイトは図2の28行目で実装されたget messages関数を持っている。Rustはオブジェクトハンドルをトレイトのポインタとして割り当てられた場合に仮想メソッドテーブル(vtables)を通して透過的な関数再帰呼び出しができる。これはまたジェネリックコードを容易に書くことができ、C言語よりも簡単に実装を隠すことができる。

Rustも匿名関数を宣言することがサポートされている。匿名関数はパイプ(|)によって囲まれた引数リストに続く式で宣言される。型アノテーションも同様にこれらの通常関数がオプションで認められる。戻り値と引数の型は書かれていない場合、推論される。図1の12行目で匿名関数が使われている。この行ではアイテムがpivotよりも小さいかどうかを見分けるために使われているので、partition関数はアイテムリストを二つのリストに分けることができる。
図1もRustマクロシステムを利用している。Rustマクロはコンパイル時間に構文抽象化ツリー(AST:abstract syntax tree)のコード片に変形されるのではなく、ソースコードのテキストにCマクロが行うことをしている。
マクロはおそらくDSL(Domain Specific Language)かrustcのコンパイラプラグインで実装されている。
二つのシステムは名前衝突と使用されているコンテキストから独立している場合にのみ衛生的なマクロの生成を許可している。マクロDSLコンパイル時計算を超えてのパターンマッチを許可せず、明確なquasiquote operatorを持たない。しかしながら、コンパイラプラグインはこれらのことを行う。
マクロは名前の終わりにエクスクラメーションマーク(!)で識別され、構文かネストされた式どちらかに展開されるだろう。図1の28行目と30行目で引数で与えられた Vec<T> フィールドを作成するvec![…]マクロを利用した。

Rustもかなり厚くパターンマッチングをサポートしている。図1の8行目と12行目にあるlet構文はオブジェクトとタプル構成部品の中で"破壊"可能なものの一つだ。12行目でpartition関数によって返された二つのタプルを破壊し、二つのリストにした。同行でも Vec<> をpartition関数のvariantを使ってコンパイラに伝えるために特定する必要がある。これについて、enumでも同様にできるが、図1の8行目ifかletどちらか使わなければならないか、図2の45-48行目のusernameようにmatch構文で全てを異なるものにできる。

1.2.2 所有権

もうひとつRustの主要な機能として所有権があげられる。一般的に、すべてのオブジェクトはスコープを抜けるときに破棄の責任がある。所有権は(ポインタの使用なしで)オブジェクトが"値として"他の関数に渡されるか、関数から返り値として戻される時に移譲されることができる。この方法で移譲される場合オブジェクトは新しいオーナーに移動されるということだ(ただし、実際のメモリは移動するかもしれないし、移動しないかもしれない)。一度所有権が移譲されると、オブジェクトの元のオーナーは直接的に移動されたオブジェクトを使うことはできず、何かしたければ新しいオーナーのポインタを入手しなければならない。所有権の移譲は図1の38行目でlstの所有権が変数quicksort関数に渡される時に見ることができる。もしこの行の後でlst変数を利用を試みた場合、コンパイラはlst変数は移動されたと言って防ぐだろう。quicksort関数で自身の変数の参照所有権は28行目でreturnする時に移譲されている。オブジェクトのフィールドは所有しているオブジェクトに所有されている。この所有権の木を生成し、現在のスレッドのスタックフレームか、静的に確保されたオブジェクトがルートになる。
当然、参照カウントポインタ、弱参照そしてミューテックスはシステム上の例外扱いとなる。
これらの型は全てunsafeに振舞うよう実装されており、Rustの型チェックと型安全システムを無視するように許可されている。スレッド間でデータを共有するのはオーナーなしでは難しい。さらに、このようにデータ共有する場合、全ての参照を最低でも使用されている間有効な状態を保つ必要が明確に必要になってくる。この問題については小節2.4について議論する。

1.2.3 型と借用のチェッカー

Rustの型チェッカーかなり標準的な型インターフェースを備えた静的型付け言語チェッカーだ。一つ興味深いのはRustの型チェッカーの機能は複数の方向性がある型インターフェースであるということだ。それは関数の別型は引数と(宣言された)戻り値の型二つに基づかれて使うことを選ぶ。図1での例として、lessとmoreは二つともVecかそうでなければ型チェッカーはpartition関数を使うために決定できないので、指定する必要がある。図1の12行目でしたように、型システムが型を提供しなければならない場所で、アンダースコア(_)を使うことができる。図1の14行目にあるように、これは常にlet構文で型が提供されていない場合の既定の動作だ。
他のRustの主要な機能として、データのライフタイムに考慮するRustチェッキングシステムがある。Rustはいつでもライフタイムにおいて、オブジェクトはチェッキングシステムによって自動的に作る。オブジェクトのライフタイムは生成されてから破壊されるまでを指す。オブジェクトのライフタイムは値によって移動する場合に変更することができるそうでなければ定数だ。ライフタイムには図2で見せたようにほかのジェネリックのような名前が与えられるか、特別な’staticライフタイムの一つとして使う。ライフタイムの名前は常に、対になっていないシングルクォート(’)でマークされる。Rustの借用チェッカーRustプログラム中の全てのオブジェクトライフタイムが一貫していることを確認する。オブジェクトのために作られ、いつでも同じオブジェクトに向けられたライフタイムを保持するポインタによって働く。Rustはポインタなしで最低でも(所有権を移譲する)移動できないオブジェクトのポインタが生きてる間、対象オブジェクトのライフタイムが生き残るのを確実にする。例えば、図2の9行目で返された文字列のライフタイムがscreenname関数で呼ばれたオブジェクトのライフタイムと同一であることを指定している。コンパイラはどんなときでも作ったオブジェクトが破壊された後、この関数から返された文字列を使用するのを防ぐだろう。ライフタイムはまた型に組み込まれており、構造体と列挙体の内部のポインタで可能にする。これらすべてのチェックは純粋にコンパイル時に行われ、実行時にオーバーヘッドがかからない。
Rustは必要ならこれらのチェックを回避することが可能だ。それを実施するにはunsafeな環境で使う。unsafeな環境である間、いくつかのつうじょうではRustの型安全システムで禁じられたことを可能にする。それらには生メモリの逆参照や、型キャストを検査しないで行うなどがある。

Xamarin.Formsでツイクラ作ってたけどモチベが下がってしまった

なんとも後ろ向きなタイトルですが、最近モチベが下がってしまってツイクラ開発が止まってしまています。
理由としてはこんな感じ
・公式のツイッタークライアントの出来が思っていた以上によかった。
・DM、投票機能など、公式クライアントにしか実装できない機能がある。

1番目の理由が結構大きくて、Xamarin.Formsで作ろうとするとものすごく高い壁を感じてます。公式でいいじゃんってなる。
やっぱりクロスプラットフォームツールキットはトレードオフなものだなあと。

それでまあ、今やることなくなって手持無沙汰になっててどうしたもんかなーってなってます。
一時はXamarin.FormsのLinuxディストリビューションへの対応でもやろうかと思ったんですが、
Linuxディストロってデスクトップ環境を好きに選べるからいろいろあってXamarin.Formsの方針であるそのOSのネイティブに合わせるっていう方針とイマイチマッチしていない感じがしてならないです。
Xamarin.Formsの方針に従うならとりあえずターゲットをUbuntuだけにしぼってGnome Sharpでするのが妥当なのかなとか、
妥協してGTK Sharpで実装するのが楽だし、幅広く提供できるよなとか考えて答えが見つからず、
そのうえLinux界隈の人がXamarinはMSのものだから使わないと言ってるのも観測してしまって、
作っても誰にも使われず徒労に終わるんじゃないかと考えてしまって最初の一歩を踏み出せない状況。一応興味はあるのですが・・・

なんにせよ何かやろうかとは思うんですがほかに作りたいアプリを思いつかないしどうしたもんかなって感じ。困った困った。

【Xamarin.Forms】XAML Compilerを使用してパフォーマンスを向上させる

Xamarin.FormsだとXAMLコンパイルは実行時におこなわれます。

これをC#コンパイル時に行ってパフォーマンスを向上させようというのがXAML Compilerです。XAMLCとも。

使い方は簡単でコンパイルしたいXAMLがあるプロジェクトの名前空間の上に以下のようなコードを追加します。

using Xamarin.Forms.Xaml;
...
[assembly: XamlCompilation (XamlCompilationOptions.Compile)]
namespace PhotoApp
{
  ...
}

詳しい内容はここを参照してください。
これでXAMLC#コンパイル時にコンパイルされます。

実行時のパフォーマンス向上以外にも、XAMLの解析がコンパイル時に行われるので自分のXAML書き間違いがコンパイル時にわかったり、XAMLが事前にコンパイルされるため最終的なアセンブリのサイズが小さくなったりとメリットがあるみたいです。

この機能デメリットなさそうだし、デフォで有効にしといてくれないかなあ。何で無効になってんだろ。('A`)

【2016/6/13 追記】この機能、どうもまだβ版のようです。

TypeScriptで疑似的な文字列enumを作る

※2015/09/24追記 やはりいろいろ問題があるようなのでこの方法は使わないほうが良いです。

知っての通り、TypeScriptのenumには文字列を設定することはできない。
下記のようにすればenumのメンバ名の文字列を取得することはできる。(TypeScript1.6を使用)

export enum Fruits {
    BANANA=1,
    APPLE=4,
    ORANGE=5
}


export function showFluits(fruits:Fruits){
    //これで文字列をとりだせる
    let fruitsStr = Fruits[fruits];
    console.log(fruitsStr);
}


showFluits(Fruits.APPLE);//APPLEと表示される

しかし文字列を定数として扱うことのみを考えた場合、TypeScriptのenumではやや冗長すぎるJavaScriptが生成される。

生成されたJavaScript

export var Fruits;
(function (Fruits) {
    Fruits[Fruits["BANANA"] = 1] = "BANANA";
    Fruits[Fruits["APPLE"] = 4] = "APPLE";
    Fruits[Fruits["ORANGE"] = 5] = "ORANGE";
})(Fruits || (Fruits = {}));
export function showFluits(fruits) {
    //これで文字列をとりだせる
    let fruitsStr = Fruits[fruits];
    console.log(fruitsStr);
}
showFluits(Fruits.APPLE); //APPLEと表示される


const変数やオブジェクトリテラルを使用してもよいのだが、それだとせっかくのTypeScriptの型の恩恵を受けることができない。
showFluits関数の引数fruitsがstring型になった場合を考えてみてほしい。その関数を使うユーザは何の文字列を引数にしてよいか戸惑ってしまうだろう。
それで型の恩恵を受けつつ文字列定数を定義する方法はないか考えた結果以下のようになった。

export interface Fruits extends String{}

//インターフェースと同じ名前の名前空間
export namespace Fruits{
	export const BANANA:Fruits="BANANA";
	export const APPLE:Fruits = "APPLE";
	export const ORANGE:Fruits = "ORANGE";
}

export function showFruits(fruits:Fruits){
	console.log(fruits);
}

showFruits(Fruits.APPLE);//APPLEと表示される

生成されたJavaScript

export var Fruits;
(function (Fruits) {
    Fruits.BANANA = "BANANA";
    Fruits.APPLE = "APPLE";
    Fruits.ORANGE = "ORANGE";
})(Fruits || (Fruits = {}));
export function showFruits(fruits) {
    console.log(fruits);
}
showFruits(Fruits.APPLE); //APPLEと表示される

まあ若干裏技感があるんだけどね。

C# for Systems Programming Part2 fail fast

C# for Systems Programmingの情報がJoe duffy氏のブログで新しいのが書かれていたので和訳してみます。例によってGoogle翻訳を使っての拙い訳です。

-----------------------------ここから和訳-----------------------------

If you’re going to fail, do it fast

私たちの言語開発チームで検討した技術の一つは"fail-fast"と呼ばれるもので、プログラムが可能な限り迅速にバグが検出された後に失敗するというアイデアだ。このアイデアは本当にフィクションなどではなく、現実的なアプリケーションなものだ。多分ね。

下記のものは私たちが開発中のシステムでfail-fastとなるものである。

・契約違反(Contract violation)

・実行時宣言違反(Runtime assertion violation)

・Null参照先のデータ取得(Null dereference)

・メモリ不足(Out of memory)

・スタックオーバーフロー(Stack overflow)

・ゼロ除算(Divide by zero)

・算術オーバーフロー(Arithmetic overflow)

もしあなたが.NETで投げられた例外の内の90%を見て、それらが上記の状況によるものである。これは面白い。

これは例で、開発中のシステムに似たものだ:

これは最終的に以下のようになる。

私の経験では開発者はふつう、以下の二つの失敗状態の反応により、一つを行うことで終了する。

1.キャッチして続行する。一般的にはバグを隠ぺいし、それが難しく後で検出するために行う。

2.プロセスをクラッシュさせる。その後スタックは戻されて実行が終了する。潜在的に重要なデバッグ状態は失われる。

私は合法的キャッチして例外を処理する3番目があると仮定したが、それは珍しいものでリストに上げたくはない。それらは通常プログラミングのエラーであり、キャッチされるべきでまた容易だ。"キャッチして無視"の規律は莫大な壊れたウィンドウとなる。

例外はその場所を持っている。しかし、入出力、データ、の残り10%のシナリオについては再スタート可能だ。(例:パーサなど)

私たちが既存のコードベースにfail-fastを適用したときには案の定多くのバグを見つけることができた。これは例外ベースの事故にはふくまれていないが、常に返値ベースのものだ。私たちが移植したのは音声サーバのプログラムだった。それは数年前からHRESULTが内包されていたルーチンで実装されていたが誰も気づいてなかった。悲しいことにこれが台湾の顧客は80%のエラーに見舞われた。fail-fastはそのエラーを私たちに対面させた。

このカテゴリに算術オーバーフロー(Arithmetic overflow)をいれることにあなたは疑問を抱くだろう。その通りで、私たちはデフォルトで計算チェックを使用している。興味深いことに、これは私たちが遭遇した障害のなかで共通して最もストレスの元となったものだった。(おかげで大部分がfail-fastとなるだけでなく、競合状態やデッドロックを排除して、安全な実行モデルとなる。・・・しかし私は脱線する)。なんて腹立たしいことだと言うかもしれない?まさか! ほとんどの時間は開発者が実際のところオーバーフローが可能であるなんて期待していなかったし、無言のラップアラウンドは偽の結果をもたらすだろう。事実、共通のセキュリティ開発の元は今日、整数オーバーフローをトリガーすることにより得られる。最善は開発者にそれをスローして対面させること、そして、その開発者にチェックをはずすかどうか決めさせることだ。

メモリ不足(Out of memory)は別のケースだから横に置いておこう。現代のアーキテクチャは障害をさける方法をとるよりかは、むしろ、障害を容認する傾向がある(例:再スタートできること、状態を書き込めることなどなど)。そう、オブジェクト指向モデリング硬化が時間とともにどんどん珍しくなる傾向になっている。それゆえ、fail-fastのデフォルトは実際ほとんどのコードにおいて正しいものだ。しかし、いくつかのサービスに--カーネル自身のような--は不適切なのかもしれない。私たちがどのようにこれに取り組んだかはブログ投稿を見てほしい。

.NETは既にfail-fastを選択的に使用している。このように"corrupted state exceptions”

私たちは積極的により広く、C#と.NETへのfail-fastの規律を適用し、調査している。だから、期待してほしい。しかしながら、幅広いプラットフォームのサポートがない場合にはその規律があなたのコードベースで簡単に採用できるのだ。

-----------------------------ここまで和訳-----------------------------

方針としてはやって当たり前なチェック処理は既定でやるようにして、必要に応じてそのチェックを外せるようにするみたいですね。このあたりがC++とは違うのでしょうか。 まあ、算術オーバーフローの項のuncheckedはC#にすでにあったと思いますが。

例にでてたrequires構文?はD言語のin契約に似ている印象(D言語まともにやったことないけど)。というかこれC#にも欲しい機能です簡潔に引数チェックできそうですし。

気になっていたメモリ管理についてはノータッチだったのでこの言語の評価はまだ何とも言えないですね。少し前の記事でクラス実装終わったみたいなこと書いてたんでぼちぼち情報が出てくるんじゃないでしょうか。とりあえずお蔵入りになってなくてよかったです。