Sigilは共通中間言語(CIL)を生成するライブラリである。ILGeneratorを扱いやすい粒度のインターフェースでラップしたもので、 多少の最適化を自動で行い、生成されるILの検証機構を提供する。 InfoQはILGeneratorとSigilの理解を深めるため、Sigilの作者であるKevin Montrose(StackOverflowのチームリーダー)に話を聞いた。
InfoQ: InfoQ: ILを生成するのに最適なシナリオは。リフレクションを使ったコードをILGeneratorやSigilで置き換えられるものなのか。
Kevin Montrose: 典型的にはリフレクションの高速な代替としてILが生成される。したがって(デ)シリアライゼーションとか、型マッピングとか、モック作成といったタスクに適している。リフレクションがよく用いられるところでSigilやILGeneratorを使うメリットがある。
Sigilがサポートしない(そしてILGeneratorなら可能という)ユースケースはごく少ない。例外フォールトブロックや、ジェネリックな型(たとえば、List<int>ではなくList<T>)を使うコード生成などだ。例外フォールトブロックは実際にはC#には存在せず、DynamicMethodでは合法でない。未バインドのジェネリック型は検証と生成を複雑にする*し、典型的なケースではSigilを使うときにすべての型が既知であるので、それらをサポートすることへの需要は少ない。
* ジェネリックな型は、制約を持たない場合、構造体と参照型の両方があり得るが、どちらであるかによって生成するILや検証の方法を変えなければならない。
InfoQ: 実行時にILを生成することの課題とは。
KM: ILGeneratorを使う場合、2つの大きな障害がある。
一つ目は、提供されているインターフェースだ。ILGeneratorの命令コードのほとんどはEmitのオーバーロードのどれかを呼び出すことで生成される。問題は、命令が実際に意味を成すかどうかについてはEmit(...)は何もチェックしてくれないことだ。
例を挙げると、
1: var dyn = new DynamicMethod("foo", typeof(void), new Type[0]); 2: var il = dyn.GetILGenerator(); 3: il.Emit(OpCodes.Ldc_I4_0); 4: il.Emit(OpCodes.Add, typeof(object)); 5: il.Emit(OpCodes.Pop); 6: il.Emit(OpCodes.Ret); 7: var del = (Action)dyn.CreateDelegate(typeof(Action)); 8: del();
これはコンパイルに失敗こそしないが、4行目は非合法である(Add命令は即値を受け取らない)。
ILGeneratorの二つ目の問題は、出力されるILの正しさが検証されないことだ。ILが正しかったかどうかはJITが受け取った時にしかチェックされない。それはつまりデリゲートやメソッドが最初に呼び出された時(上の例における8行目)ということになるため、実際に誤りを犯したところからは大きく離れてしまっている。上げられる例外からは詳細が失われていて、単に「CLRは不正なプログラムを検出しました。」とか「命令によってランタイムが不安定になる可能性がありました。」と言うのが関の山である。
そこまで重要ではない別の問題として、理想的なILを生成するのが難しいというのがある。可能な時には短い形式を使うとか(BrではなくBr_Sを使うとか、Ldc_I4 & 0ではなくLdc_I4_0を使うといったこと)、ローカル変数を再利用するとか(スタック上の余計な領域を使わないため)。
InfoQ: SigilでIL生成が楽になる仕組みは。
KM: Sigilはエラーの少ないインターフェースを提供しており、(デフォルトでは)出力されるIL列を検証する。
前の例は次のように表現されるだろう。
1: var e = Emit<Action>.NewDynamicMethod("foo"); 2: e.LoadConstant(0); 3: e.Add(typeof(object)); 4: e.Pop(); 5: e.Return(); 6: var del = e.CreateDelegate(); 7: del();
これはコンパイルに失敗する。Add()は引数を取らないからだ。次のように“修正”すれば、
1: var e = Emit<Action>.NewDynamicMethod("foo"); 2: e.LoadConstant(0); 3: e.Add(); 4: e.Pop(); 5: e.Return(); 6: var del = e.CreateDelegate(); 7: del();
コンパイルには通るようになるが、実行時に3行目でSigilVerificationException「Addはスタック上に2つの値を必要とする」を投げる。
それを“修正”し、次のようにした場合は、
1: var e = Emit<Action>.NewDynamicMethod("foo"); 2: e.LoadConstant(0); 3: e.LoadConstant(typeof(object)); 4: e.Add(); 5: e.Pop(); 6: e.Return(); 7: var del = e.CreateDelegate(); 8: del();
実行時にまた別のSigilVerificationExceptionが出るだろう。今回は4行目で「Addは参照値、double、float、int、long、native int、ポインター値を必要とするが、System.RuntimeTypeHandleが見つかった」となる。
IL列が正しいかどうか、まさにエラーを含んだ行そのものの時点ではSigilが判断できないようなケースもある。通常は分岐にかかわるケースだ。それでもSigilは可能な限り速やかに(常にJITが動作するよりも前に)失敗する。また、ILGeneratorよりも多くの情報を提供する。
基本的に、Sigilではコンパイル時のありがちなミスを起こすのは難しくなっていて、実行時にミスが起きてしまったとしてもより良いフィードバックを返す。Sigilはオペコード選択の自動化もする。可能な限り短い形式を使ってくれる。
Sigilではローカル変数の再利用も簡単だ。Sigilでは、ローカル変数(スタック上の変数)はIDisposableを実装するLocalクラスで表現される。
例を挙げると、
var e = Emit<Action>.NewDynamicMethod("foo"); using(var a = e.DeclareLocal<int>()) { e.LoadConstant(20); e.StoreLocal(a); } using(var b = e.DeclareLocal<int>()) { e.LoadConstant(30); e.StoreLocal(b); } e.Return(); var del = e.CreateDelegate(); del();
これは次のものを生成する。
ldc.i4.s 20 stloc.0 ldc.i4.0 stloc.0 ldc.i4.s 30 stloc.0 ret
このコードはスタックの先頭スロットを再利用している(stloc.0で指定されている)。
InfoQ: NETのIL Generatorと比べたときのSigilのパフォーマンスコストは。生成されるコード出力のサイズが明らかに増加することはあるか。
KM: SigilはILの _出力_ 時に遅くなる。比較するとより多くのことを実行しているからだ。検証もまた、IL出力中にメモリー使用量が無視できない程度増加することがある。両者とも、可能な限り早い段階で検証に失敗することによる結果であり、出力されるコードの分岐数に比例して増加する傾向がある。
Sigilでは検証をオフにすることができる。ILGeneratorとの境界をよりよくしたいと思ったときは、Emitのインスタンスを生成するときに"doVerify:false"を渡せばよい。
Sigilが生成するILと、ILGeneratorが生成するILに違いはない(Sigilがやっているちょっとした最適化をすべて手で行ったならば)。よって生成されるコードサイズも同じだ。
InfoQ: 最近Sigilが.NET Coreに移植された。かなり大変だったのでは。
KM: 実際には、ほとんどは同僚のMarc Gravellによるプルリクエストでなされた。私の理解では、困難だったことのほとんどが、型情報を得るための適切なメソッドやクラスを見つけることと、今回対応した.NET Coreのビルドでは不可能なことすべてを取り除くことにあった。
InfoQ: SigilはJilから使われている。これはあなたがStack Overflowで作った、スピードを重視したJSONシリアライザーだ。他にSigilを使っているプロジェクトを知っているか。
KM: 有名でオープンソースなものとしては他に知らないが、Stack Overflowの内部プロジェクトで少し使われている。GitHubを掘ってみればほかにもいくつかのプロジェクトが見つかるのではないか。
InhoQ: Sigilに追加したい新機能はあるか。
KM: fault句とジェネリック型のサポートを試している。何よりも完成のために。
非常に便利ではあるが実装が難しいような機能リクエストとしては、生成された型やデリゲートをDLLに保存して、あとから使用するというものがある。この機能があればビルドの一環としてSigilを利用できる――そうすれば、実行時のコード生成をサポートしていないプラットフォーム(例えばXamarin)や、「本番環境で」Sigilを使うオーバーヘッドを許容できないようなアプリケーションにとってはありがたいだろう。
Sigilはオープンソースプロジェクトで、GitHubから入手できる。ライブラリ参照であれば、Nugetパッケージが.NET 2.0から4.5まで、およびCoreCLRをサポートしている。