今日は、「Center CLR Try!開発 #2」に参加ました。
クリエイトベース金山さんのスペースを借りることが出来ました。募集が遅かったので人の集まりが悪かったのですが、参加者3人ともいろいろ吸収出来たようで良かったです。
私はIL2Cの例外処理、Matsuokaさんは次週のプレゼン資料、ayumaxさんはUnityで年末忘年会用の出し物を作る、という目標でした。
IL2Cは例外処理に取り組んでいます。
例外のうち、単純なcatch・複数のcatchの呼び分け・ネストしたcatch・rethrowと、それらそれぞれについてlocal unwind・global unwindは動いていて、finallyを変換可能にするのが今日の目標でしたがダメでした。
この図はIL内にtry-catch-finallyブロックの定義があった場合に、どのようなCのソースコードに変換すれば実現できるかを検討していたものです。ざっくり言うと、sjlj方式でsetjmpの戻り値によって例外ブロックに分岐して処理させ、finallyはそれらがbreakでブロック外に遷移したときに面倒を見る、という感じです。
(sjljの効率が悪いことは承知。移植性重視で、この作業が終わってからWindowsについてはSEHを使えるように、その先ではlibunwindを使うことも考えています)
悩んで解決できなかった部分が、最近C# 6で追加された例外フィルタ条件のサポートです。これはVB.net 1.0から使える(つまりはIL的には最初から実現可能だった)ものです。以下にC# 6で書いた例を示します:
class Program { static string GetMessage(Exception ex, string banner) { Console.WriteLine(banner); return ex.Message; } static void Main(string[] args) { try { try { // C D B E F H throw new Exception("111"); // C D [Unhandled exception] //throw new Exception("333"); } catch (Exception ex) when (GetMessage(ex, "C") == "222") { Console.WriteLine("A"); } finally { Console.WriteLine("B"); } Console.WriteLine("G"); } catch (Exception ex) when (GetMessage(ex, "D") == "111") { Console.WriteLine("E"); } finally { Console.WriteLine("F"); } Console.WriteLine("H"); } }
C#クイズとかで出てきそうですが、例外フィルタ式(コード上 GetMessage()を呼び出しているwhen句)の呼び出し順序を見て、のけ反る人もいるかも知れません。コメントに書いておきました。
例外がスローされるとき、
- 現在のスタックフレームの直近に存在するcatchブロックのうち、指定された例外型にキャスト可能なものがあるかどうかを探索し、なければよりスタックの底に向かって探索し続ける。
- 上記が存在する場合、更にフィルタ条件式があればそれを実行し、結果が満たされるかどうかを確認する。
- 1又は2が満たされてから、そのcatchブロックに遷移する。
という順序で実行する必要があります。ここで苦しいのは、スタックの底まですべての条件を確認し終えるまでは、catchブロックに遷移してはならない、という点です。したがって、まず、catch句の条件とwhenのフィルタ条件をすべてチェックする必要があります(満たされるものが見つかった時点で打ち切ることは可能)。
問題なのは、例外の型のチェックだけなら、IL2Cが自分の都合の良い判定Cコードを生成すればいいのですが、フィルタ条件式はILで指定されるので、ILを実行してフィルタ条件を満たしているかどうかを確認しなければなりません。しかし、catchブロックやfinallyブロックは、そのブロックを実行することが判明した時点でそこまでスタックを巻き戻し(sjljであればlongjmpする)ても問題ないことが自明ですが、フィルタ条件式の実行中は、まだスタックを巻き戻すことは出来ません。巻き戻してしまったらもう元に戻せず、それまでのスタック上の情報はすべて失われます(移植性を考えると、失われたと見なす必要があります)。
(上記のサンプルコードはまだ良いのですが、メソッドのローカル変数を使ったフィルタ条件式を考えてみると、問題の大きさがわかります)
これは頭を抱えました。
そもそも、例外フィルタ条件式を真面目に処理することをやめて、以下のように評価するという代替え案も考えられます:
try { throw new Exception("111"); } catch (Exception ex) // when (GetMessage(ex, "C") == "222") { // Rethrow if additional condition is false if (!(GeMessage(ex, "C") == "222")) throw; Console.WriteLine("A"); }
しかし、この方法には2つの問題があります:
- 例外ハンドラに遷移してから、更にrethrowで遷移するので、コストが高い。
- 同じ型でcatchするブロックについて、一つのブロックで処理するように統合する必要がある。
11/11追記: これもやっぱり駄目ですね。スタック上位にfinallyブロックがある場合、このcatchに遷移した時点でfinallyを実行してしまいます。例外フィルタ式を使った場合と、実行順序が入れ替わってしまいます。
色々考えた結果、結局、以下の理由で、今その対処を行うことをやめました:
- 現在(メソッドの)ローカル変数群のうち、オブジェクト参照を追跡する必要のある変数(GCがトラッキングして、不要なインスタンスではないことを確認する)は、そのアドレス群を “EXECUTION_FRAME”という構造体に記録し、リンクリストに追加することでGCがトラッキングできるようにしていますが、ここの実行効率が悪いことがわかっています。
- 近い内にこれを改良する予定ですが、その際にこれらのローカル変数へのベースとなるポインタが用意に得られるようになるはずなので、フィルタ条件式のILを変換する場合には、このポインタ経由でローカル変数群にアクセス可能にすれば、スタックを一時的に巻き戻さなくとも式の評価が出来るはずです。
- そのため、手順として、EXECUTION_FRAMEの改良が終わってから、改めて考えても良いかも?
と考えました。
なので、本日の目標としては、フィルタ条件式に対応しないが例外は処理できる、という感じに決定しました(そして、完成しなかった… 8割ぐらい?家に戻ったらfinishしたい)
ちなみに、.NET Framework CLRや.NET Core、monoとかはどうやっているのかという興味が出てきますね。自力でネイティブコードを出力してるので、如何様にでも出来るといえば出来ますが…
——-
Try!開発は定期的にやる予定なので、興味があれば是非参加してください。