動的なilの生成と編集
DESCRIPTION
.NET基礎勉強会(http://connpass.com/event/2441/)での発表資料のバグ修正版ですTRANSCRIPT
技術領域
• Haxe/JavaScriptとかWindows Store Appsとか WPFとかSilverlightとかできます。
• Haxe/JavaScriptは「なごやまつり」で話す予定です。
• ギョーム系DataGridの実装に定評があります。
• たぶんシステムアーキテクチャ設計が本業です。
• 最近はF#でWebSocketサーバを試作してました。
コミュニティ活動 - DSTokaiとNGK
• DSTokai : 東海地方のメタコミュニティ
• IT系イベントカレンダーとコミュニティ連絡用ML
• http://go.dstokai.info/
• NGK : なごや ごうどう こんしんかい
• 毎年忘年会を開催しています
• 一昨年は100人、去年は120人
• ちなみに今年は12/7に開催予定
• 花見はここ数年サボってます
ILと私
• IronPythonとSilverlight2でR&D(3年前)
• Silverlight上でScalaアプリを実行(2年前)
• Silverlightアプリを動的生成(1年半前)
• Store AppsからDesktop用EXE生成(1年前)
• その他、.NETのお仕事で仕方なく…
ILとは
• .NETのアセンブリ言語
• IL = Intermediate Language = 中間言語
.assembly extern mscorlib { .publickeytoken = (b7 7a 5c 56 19 34 e0 89) .ver 4:0:0:0 } .assembly Test { .hash algorithm 0x00008004 .ver 1:0:0:0 } .module Test.exe .class auto ansi Test.Program extends [mscorlib]System.Object { .method static void Main (string[] args) cil managed { .maxstack 8 .entrypoint IL_0000: ldstr "Hello World" IL_0005: call void [mscorlib]System.Console::WriteLine(string) IL_000a: ret } }
.NETとILの関係図
Compiler
ilasm
ildasm
CLR
icons: http://www.gentleface.com/, CC BY-NC 3.0
C#, VB, F#, ...
IL
Assembly Native 実行
Assembly(PE:Portable Executable) .NETの実行バイナリ(EXE, DLL) ilasm ILアセンブラ .NET Frameworkに標準付属 ildasm IL逆アセンブラ .NET Frameworkに標準付属
動的なILの生成
.NETにはILを動的(アプリ実行時)に生成するAPIが標準で備わっています。
• 静的な型やメソッドの作成
• 生成したILの即時実行
• Assembly(EXE, DLL)の生成・保存
• 既存APIの編集(Monkey Patching)は不可能
• 別の手段を使えば編集することも可能
IL生成の利用例
• 処理の高速化
• 動的メソッドコール(Reflection)
• シリアライザ(動的に静的型を生成)
• 透過プロキシやMockの内部実装
• アスペクト指向/Weaving
• 自作コンパイラのバックエンド
本当にIL生成が必要か?
• IL生成は黒魔術
• 利用例で紹介した通り、必要な個所は限定される
• 問題点も多い
• 低レベルAPI
• 可読性やメンテナンス性の低下
• プログラミング言語の型制約の無視できる
• 型安全ではなくなり、品質保証にコストがかかる
IL生成の前に検討すべきもの
• デザインパターンやIoC Container
• コードの書き方で逃げられないか?
• T4テンプレート(自動コード生成)
• コンパイル前にどうにかならないか?
• System.Dynamic(C# 4.0のdynamic型)
• 動的プロパティや動的メソッドコール等で利用
• 内部的には実行時に自動でILを生成している
• 下手に手書きでILを生成するよりも高速
• F#(高度な型システム, TypeProvider)
• C#やVBの型システムが貧弱だからなのでは??
逆アセンブリツール
• ILSpy http://ilspy.net/
• 無償で使えておすすめ
• IL以外にもC#やVBへ逆アセンブリ可能
• ildasm
• .NET SDK標準ツール
• C:¥Program Files¥Microsoft SDKs¥Windows¥... に入っている
• .NET Reflector
• 有償($95)
API
• 標準API
• System.Reflectoin.Emit(.NET 2.0/1.1~)
• Sysytem.CodeDom(.NET 2.0/1.1~)
• System.Linq.Expressions(実質.NET 4.0~)
• 外部ライブラリ
• Mono.Cecil
• IKVM.Reflection
• CLR Profiling API
System.Reflection.Emit(.NET 2.0/1.1~)
• IL生成の基本的なAPI
• 動的に生成したILコードは即座に実行可能
• 低レベルAPIなのでプログラムが煩雑になる
• OpCodeを1つずつ埋め込むことが必要
• .NET 3.5までの環境ではこれを使うことになる
System.Reflection.Emitのコード例
Hello Worldを表示するだけのEXEを生成 var appDomain = AppDomain.CurrentDomain; var assemblyBuilder = appDomain.DefineDynamicAssembly( new AssemblyName("Test"), AssemblyBuilderAccess.RunAndSave); var moduleBuilder = assemblyBuilder.DefineDynamicModule("Test.exe"); var typeBuilder = moduleBuilder.DefineType("Program", TypeAttributes.Class); var methodBuilder = typeBuilder.DefineMethod("Main", MethodAttributes.Static); methodBuilder.SetParameters(typeof(string[])); var il = methodBuilder.GetILGenerator(); var write = typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }); il.Emit(OpCodes.Ldstr, "Hello World"); il.Emit(OpCodes.Call, write); il.Emit(OpCodes.Ret); typeBuilder.CreateType(); assemblyBuilder.SetEntryPoint(methodBuilder); assemblyBuilder.Save("Test.exe");
System.CodeDom(.NET 2.0/1.1~)
• C#やVBのコンパイラAPI
• 即時実行可能(スクリプト的に利用できる)
• Assemblyの出力可能
• System.Reflection.Emitの置き換えにはらなない
• コンパイル時にオーバーヘッドが発生する
• 生成するILを動的に変更したい場合、文字列として ソースコードを組み立てる必要がある
System.Linq.Expressions(実質.NET 4.0~)
• Expression Tree(式木)
• 式木とはいうものの、構文木(if文, for文など)もカバー
• System.Reflection.Emitよりも簡単
• ラムダ式からExpression Treeを構築できる
• Expression Treeを走査したり改変もできる
• .NET 4.0以降でIL生成といえばこれ
Expression<Action> exp = () => { Console.WriteLine("Hello World"); }
System.Linq.Expressionsのコード例
Hello Worldを表示するだけのEXEを生成 var appDomain = AppDomain.CurrentDomain; var assemblyBuilder = appDomain.DefineDynamicAssembly( new AssemblyName("Test"), AssemblyBuilderAccess.RunAndSave); var moduleBuilder = assemblyBuilder.DefineDynamicModule("Test.exe"); var typeBuilder = moduleBuilder.DefineType("Program", TypeAttributes.Class); var methodBuilder = typeBuilder.DefineMethod("Main", MethodAttributes.Static); methodBuilder.SetParameters(typeof(string[])); Expression<Action> exp = () => Console.WriteLine("Hello World"); exp.CompileToMethod(methodBuilder); typeBuilder.CreateType(); assemblyBuilder.SetEntryPoint(methodBuilder); assemblyBuilder.Save("Test.exe");
System.Linq.Expressionsのコード例
ラムダの書き換え(メッセージ出力を追加)
class SampleVisitor : ExpressionVisitor { protected override Expression VisitLambda<T>(Expression<T> node) { Expression<Action> post = () => Console.WriteLine("post"); return Expression.Lambda<Action>(Expression.Block(new Expression[] { Expression.Invoke(node), Expression.Invoke(post) })); } }
var visitor = new SampleVisitor(); var result = visitor.Visit(exp); var func = ((Expression<Action>)result).Compile(); func.Invoke();
Mono.Cecil
• Assembly(EXE, DLL)のRead/Write
• 生成時にデバッグシンボルの出力も可能
• Assemblyの解析、処理の挿入や改変
• リソースの挿入、差し替え
• 生成したILの即時実行は不可能
• Assembly保存→DLLとして読み込みなら可能
• コンパイラ, AOP, IL to JavaScript, ...
Mono.Cecilのコード例
Hello Worldを表示するだけのEXEを生成
var assembly = AssemblyDefinition.CreateAssembly( new AssemblyNameDefinition("Test", new Version()), "Test.exe", ModuleKind.Console); var module = assembly.MainModule; var type = module.Types.First(); var method = new MethodDefinition( "Main", MethodAttributes.Static, module.Import(typeof(void))); var il = method.Body.GetILProcessor(); var write = typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }); il.Emit(OpCodes.Ldstr, "HelloWorld"); il.Emit(OpCodes.Call, module.Import(write)); il.Emit(OpCodes.Ret); type.Methods.Add(method); assembly.EntryPoint = method; assembly.Write("Test.exe");
Mono.Cecilのコード例
全メソッドの末尾(retの前)にメッセージ出力を追加 var assembly = AssemblyDefinition.ReadAssembly("Test.exe"); var write = typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }); assembly.Modules .SelectMany(x => x.Types) .SelectMany(x => x.Methods) .ToList() .ForEach(method => { var il = method.Body.GetILProcessor(); var ldstr = il.Create(OpCodes.Ldstr, "Finish"); var call = il.Create(OpCodes.Call, method.Module.Import(write)); method.Body.Instructions .Where(x => x.OpCode == OpCodes.Ret) .ToList() .ForEach(x => { il.InsertBefore(x, ldstr); il.InsertBefore(x, call); }); }); assembly.Write("Test-Mod.exe");
Assembly内の全メソッドを取得
Console.WriteLine("Finish");を 挿入
IKVM.Reflection
• Assembly(EXE, DLL)のRead/Write
• 生成時にデバッグシンボルの出力も可能
• System.Reflectionに似たAPI
• 生成したILの即時実行は不可能
• Assembly保存→DLLとして読み込みなら可能
• Mono C#コンパイラのバックエンドで採用
CLR Profiling API
• .NET界でも最上級レベルの黒魔術
• 実行中に全APIをフックして処理の差し替えが可能
• 私は使ったことはありません
• 参考資料
• C# 動的メソッド入れ替え - Apply a monkey patch to any static languages on CLR - http://urasandesu.blogspot.jp/2011/10/c-apply-monkey-patch-to-any-static.html
.NET for Windows Store Apps
同じ.NETでも、Windows Store AppsとDesktopの APIは大きく異なる
• 名前空間の整理、スリム化
• 非同期前提
• セキュリティ的に危険なAPIを徹底削除
IL関連のAPI
• System.Linq.Expressionsのみが存在
• System.Reflection.Emitは削除
• Assemblyを生成/ロードするAPIは削除
• ILを生成してもローカルキャッシュできない
• Assemblyは所詮バイト列なのでどうにかなる
• ロードできないので、生成しても自分で使えない
• Windows RTで自作.NETアプリが動作すれば…
どうしてもIL生成したいのなら
• とりあえずSystem.Linq.Expressions
• 自由度が高く、APIもわかりやすい
• Mono.Cecilを使えばAssemblyの改変が捗る
• 採用しているOSSも多いので参考にしやすい