今日は、「Center CLR Try!開発 #3」に参加ました。
クリエイトベース金山さんのスペースでやりました。冬場だからか、自分も含めて体調良くない人が居るようですね…
私はIL2Cのオブジェクト参照の追跡が足りない部分の修正と本を読む、MatsuokaさんはnanoFrameworkとMbed-Cli、neco3csさんはAngularの勉強、中村さんはNGK2018BのLT資料作成、加藤さんは非公開案件、という目標でした。
NGK2018B、今年はLT登壇エントリーしなかったので、気が楽ですわ :)
IL2C、昨日の時点で、 classとvalue typeのメソッドオーバーライド・オーバーロード・interfaceの暗黙実装・明示実装とそれぞれが複雑に絡んだパターンについてテストを書きまくって網羅した ので、長らく放置してた value type内のobjrefが追跡されていないので勝手にGCに回収されてしまう問題 を対処する、その方策を考えました。
今日中に対処できれば良いんですが、そう簡単でも無いことは分かっていたので、今日は算段をつけるという感じ。
例えば以下のようなコード: (ちょっとC#とIL混ぜちゃってますが、適当に読んでください)
public struct ObjRefInsideValueTypeType { public string Value; // <-- ここに動的に生成された文字列"ABCDEF"が保持される public ObjRefInsideValueTypeType(string value) => this.Value = value; } .class public IL2C.RuntimeSystems.ValueTypes { .method public static string ObjRefInsideValueType() cil managed { .maxstack 3 .locals init ( [0] valuetype IL2C.RuntimeSystems.ObjRefInsideValueTypeType // <-- IL2Cはこの構造体から上のValueフィールドを追跡できなければならない(がしていない) ) ldloca.s 0 ldstr "ABC" ldstr "DEF" call string [mscorlib]System.String::Concat(string, string) call instance void IL2C.RuntimeSystems.ObjRefInsideValueTypeType::.ctor(string) // Release concat string from the evaluation stack ldstr "dummy1" ldstr "dummy2" // <-- IL2Cはevaluation stackをCのローカル変数として確保するので、そこに参照が残っていると追跡されてしまうことから pop // 別の参照を上書かせて追跡されないようにしてテストしている。 pop // 本来なら、こうしたとしても、value typeのフィールドは別の手段で追跡されなければならない(が今はダメ) call void [mscorlib]System.GC::Collect() ldloc.0 ldfld string IL2C.RuntimeSystems.ObjRefInsideValueTypeType::Value ret } }
で、ObjRefInsideValueType()を呼ぶとこういったケースのテストを行うのですが、構造体ObjRefInsideValueTypeType内のValueフィールドにセットされた文字列(System.String.Concatによって動的に生成されてヒープに配置された文字列のインスタンス)が、GC.Collect()によって回収されてしまうという問題です。
(文字列を結合しているのは、単なるリテラル文字列だとstatic constに配置されてしまってGCから無視されてしまうので、動的に生成させています。まあboxingされたインスタンスとか使っても良かったんですが)
とりあえずこのテストを書いて実行すれば、(IL2Cの問題によって) 文字列はGCに回収されてしまい、メソッドから返された文字列への参照 (IL2C上はポインタ) は無効な値を示していて、その後のアサートで刺さるからテストに失敗する、というシナリオです。
なんですが… 何故かテストが成功する…
で、VC++で実際にデバッグしてみると、問題なく無効ポインタで刺さったことが検出されます。IL2CのランタイムがこれをNullReferenceExceptionとしてスローする所に問題があって、想像してなかった死に方をしましたが(Unhandled exceptionはTODOで放置してたんだった…)
IL2CのテストはNUnitで書いていますが、実際には:
- テストコードのC#は、Roslynによって普通にアセンブリ(IL)になる
- アセンブリを普通に実行して、正しい結果が得られることをNUnitでアサートする
- アセンブリをIL2C.Coreに食わせてCソースコードを生成する
- Cソースコードをコンパイル(MinGW gcc4 32bitを使用)してネイティブの実行コードを生成する(今の所Windowsでやってるので、test.exe的なものが生成される)
- 実行コードを実際に実行する。内部ではテスト結果が同じようにアサートされる(この実行には.NET CLRは一切関与しない)。成功時は”Success”とstdoutに出しているので、それを確認
という感じで、テスト結果が完全に一致することを自動的に確認するようにしています。なので、今度はVC++とgccで結果が異なるという可能性がありえたので、VSCodeでデバッグ(C++ extensionでGDBを使ってデバッグできる)してみたのですが、こちらも正しく無効ポインタを踏んで死んでました。
んー何故なのか… と調べてるところで時間切れ。嫌な感じだな…