ページ

2012-08-19

C#でHelloWorldプログラムを作成する

C#を使い始めたので、ひとつ入門記事でも。Hello, Worldするプログラムを作るよ。

まずはソースから。


いつもの数行のコードだと思った?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

余談

実際のところ、「dynamic」もあるし、System.Reflection.EmitでILを書くことになるケースはほとんどないと思う。ダイナミックに関数を生成したいだけなら、Expression Treeという便利なものもあるし。ただ、type-safeなものをなんとかして作りたいなんてレアなときに有用かもしれない。

0 件のコメント:

コメントを投稿