まずはソースから。
いつもの数行のコードだと思った?Hello, Worldプログラム自体を生成するプログラムでした。
プログラムの使い方はこう。第一引数にexeのファイル名を指定すると、Hello, World!を出力するexeファイルを作成する。
c:\temp> HelloWorld1.exe Hello.exe c:\temp> Hello.exe Hello, World!
スタックのイメージ
説明の前に、この絵がとても大事なので覚えてほしい。このイメージがあれば世のMSIL説明記事と戦える。.NETの世界では、ローカル変数、スタック、引数という3つの枠があり、それぞれ必要に応じて移動して使う。引数の枠から、値をスタックに移動してきて、なんらかの操作を実行、必要に応じてローカル変数に退避する、という感じかな。
ソースの説明
さて、実際にHello, World生成クラスを作ってみる。static void Main()でWriteLine()するだけで本当は十分なのだが、説明のためにインスタンスを生成してSay()というメソッドを呼び出すようになっている。なお、ソースの頭のところは、お決まりの引数処理だったり.NETの仕組みみたいな話なので省略して進める。
1. クラスの作成
TypeBuilder helloClassBuilder = moduleBuilder.DefineType("Hello", TypeAttributes.Class); ConstructorBuilder ctorBuilder = helloClassBuilder.DefineDefaultConstructor(MethodAttributes.Public);ModuleBuilderのDefineType()というメソッドを使い、クラスを作る。
あとでnewしたいので、デフォルトコンストラクタのConstructorBuilderをとっておく。
2. インスタンスメソッドSay()の作成
文字列をコンソールに出力するSay()を作成する。生成後をC#で書くとこんな感じ。public void Say(string value) { System.Console.WriteLine(value); }
生成するコードは以下。
MethodBuilder sayMethodBuilder = helloClassBuilder.DefineMethod("Say", MethodAttributes.Public, typeof(void), new Type[] { typeof(string) });TypeBuilderのDefineMethod()を使って、メソッドを作る。"Say"という名前で、public、戻り値はvoid、ひとつの引数stringを取るという感じ。
ILGenerator ilSay = sayMethodBuilder.GetILGenerator();MethodBuilderのGetILGenerator()というメソッドを呼び出すと、ILを生成していくためのILGeneratorオブジェクトを得られる。アセンブラコードを書いていくようなイメージ。このあと、Emit()やEmitなんとか()を利用して書き込んでいく。
// System.Console.WriteLine()にSayの第一引数を渡してコール ilSay.Emit(OpCodes.Ldarg_1); ilSay.EmitCall(OpCodes.Call, typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(String) }), null); // Sayを抜ける ilSay.Emit(OpCodes.Ret);
まずldarg_1を使って、関数の引数の1番目を評価スタックにロードする。なお、0はインスタンス。C#はthisはメソッド定義に書いてないけど、最初の引数がselfだったりthisだったりする言語あるよね。そんな感じ。
そして、callを使って、System.Console.WriteLineを呼び出す。
OpCodes.Callには、MethodInfoを渡してあげる必要があるが、これはリフレクションで取ってきてしまう。
上記の例では、stringという引数を持つWriteLineというメソッドを、System.Consoleのタイプから拾ってくる形である。
なぜCallのときはEmitではなく、EmitCallを使っているかというと、可変長引数を持つメソッド用の型をEmit()が受け取ることができないから。ここでは使ってないので、実はEmitでよい。
最後にreturnたるretを書いて、Sayの実装は終わり。
3. Mainの作成
エントリポイントとなるMain()を実装する。生成後のものをC#で書くとこんな感じ。private static void Main(string[] array) { string text = "Hello, World"; Hello hello = new Hello(); hello.Say(text); }
生成するコードは以下。
MethodInfo thisMainMethod = System.Reflection.Assembly.GetExecutingAssembly().EntryPoint; MethodBuilder mainMethodBuilder = helloClassBuilder.DefineMethod( thisMainMethod.Name, thisMainMethod.Attributes, thisMainMethod.ReturnType, (from p in thisMainMethod.GetParameters() select p.ParameterType).ToArray<Type>()); ILGenerator ilMain = mainMethodBuilder.GetILGenerator();Mainメソッドは、この生成プログラム自体の定義からもらってきた。実際にILGeneratorを使ってILを作っていくような場面では、生成側のオブジェクトの定義をリフレクションでもらってくるケースが多いと思う。
3.1. ローカル変数の作成
// ローカル変数を宣言してメッセージを格納 LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string)); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); ilMain.Emit(OpCodes.Stloc, msgLocal);試しにローカル変数を作ってみた例。DeclareLocal()を呼ぶと、ローカル変数が宣言できるので、評価スタックに値をロードしてから、stlocを使って、そこに保存する。
3.2. インスタンスの生成
// ローカル変数を宣言してHelloオブジェクトを生成・格納 LocalBuilder helloLocal = ilMain.DeclareLocal(helloClassBuilder); ilMain.Emit(OpCodes.Newobj, ctorBuilder); ilMain.Emit(OpCodes.Stloc, helloLocal);インスタンスを作成するときは、newobjにコンストラクタを渡す。ここに限った話じゃないけど、Emit達は、~Builderを受けてくれる。すごくよくできていると思う。
3.3. Sayメソッドを実行
// 作成したオブジェクトに対して、Sayメソッドを実行 ilMain.Emit(OpCodes.Ldloc, helloLocal); ilMain.Emit(OpCodes.Ldloc, msgLocal); ilMain.EmitCall(OpCodes.Call, sayMethodBuilder, null);インスタンス、引数の順にldlocを使ってスタックに詰めて、callでSayメソッドを呼び出す。
なお、お気づきかもしれないが、「3.2. インスタンスの生成」で、オブジェクトはスタックに生成されているので、いちいちローカル変数に入れなくてもいい。文字列も固定なので同様。なので、3.1.~は、以下のように書くこともできる。
ilMain.Emit(OpCodes.Newobj, ctorBuilder); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); ilMain.EmitCall(OpCodes.Call, sayMethodBuilder, null);
ilMain.Emit(OpCodes.Ret);retを呼んで終了。
あとは、エントリポイントを指定して、アセンブリをファイルにSaveするだけ。
ここまで読んだあなたは、C#を使って、プログラムを生成することができるようになっているはず。
フィールドとか、Genericとか、よく使いそうなものを全然解説していないけど、そこは「詳しくはWebで」という感じで。
Javaでも同じようなことがきっとできると思う。Cはどうだろう?LLVMとかの力を借りればいいのかな。
IL生成の罠
間違えると実行時のエラーがわかりにくいので、大変である。以下は例。
ret忘れちゃった
// Sayを抜ける // ilSay.Emit(OpCodes.Ret);
ハンドルされていない例外: System.InvalidProgramException: 共通言語ランタイムが無効なプログラムを検出しました。
場所 Hello.Say(String )
場所 Hello.Main(String[] )
スタックから取るの忘れた
// ローカル変数を宣言してメッセージを格納 LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string)); ilMain.Emit(OpCodes.Ldstr, "Hello, World"); // ilMain.Emit(OpCodes.Stloc, msgLocal);
ハンドルされていない例外: System.InvalidProgramException: JIT コンパイラで内部的な制限が発生しました。
場所 Hello.Main(String[] )
気をつけるべし。
参考
動的処理 « C#たんっ! http://csharptan.wordpress.com/2011/12/16/%E5%8B%95%E7%9A%84%E5%87%A6%E7%90%86/KEN's .NET IL入門 http://www5b.biglobe.ne.jp/~yone-ken/VBNET/IL/il01_FirstStep.html
ラッパークラスの生成 - 匣の向こう側 - あまりに.NETな http://d.hatena.ne.jp/akiramei/20040810/1345306640
0 件のコメント:
コメントを投稿