注意
初めて書く記事ですのでお見苦しい点があると思います。
そのため温かい目でご覧ください。
C#でメタプログラミングをする方法
- Refrection.Emit(Emitter)
- Expression.Tree(式木)
- CodeDOM
- T4(Text Template Transformation Toolkit)
自分が認識しているのは標準ライブラリはこんな感じです
今回は 1 のRefrection.Emitを使います
Refrection.Emitについて
- メリット
- .netの仕組みを知れ、理解が深まる
- C#で使えない機能を扱える
- 他の動的コード生成の方法に比べ高速
- .NET Framework 2.0といった古いバージョンでも利用が可能
- デメリット
- CIL(MSIL)の知識が必要(今後回解説する予定です)
- 情報が少ない
このようにRefrection.Emitでは古いバージョンで動かしたい!.netの技術を深めたい!自作の言語を作りたい!コンパイラを作りたい!といった方にお勧めです。
しかしながら、簡単にコード生成をしたい!速度はどうでもいいからコードを生成を手っ取り早くやりたい!といった方にはCodeDOM、Expression.Treeをおすすめします。
##実際にコードを生成してみる##
今回は初回ですので、HelloWorldをしてみます
今回生成するファイルは以下の構文と同じ意味になります
class Program
{
static void Main()
{
letter = "Hello World";
System.Console.WriteLine(letter);
}
}
C#の構文
環境はVS2019 .NET Freamwork4.7.2で、コンソールアプリケーション(.NET Freamwork)です
.net coreについては一部動かないため方法が見つかるか、機能が追加されるまでおまちください
using System;
using System.Reflection;
using System.Reflection.Emit;
class Program
{
static void Main()
{
//アセンブリの設定
string projectName = "Hello World";
AssemblyName assemblyName = new AssemblyName() { Name = projectName };//アセンブリの名前を設定する
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save);//アセンブリを生成する
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(projectName, projectName + ".exe", true);//アセンブリのモデルを生成する
//クラスを宣言する
TypeBuilder typeBuilder = moduleBuilder.DefineType("Program", TypeAttributes.Class);//クラスを設定
MethodBuilder mainMethodBuilder = typeBuilder.DefineMethod("Main", MethodAttributes.Public | MethodAttributes.Static, typeof(void), Type.EmptyTypes);//メソッドの設定
ILGenerator ilMain = mainMethodBuilder.GetILGenerator();//ILを指定する
LocalBuilder msgLocal = ilMain.DeclareLocal(typeof(string));//stringの変数を宣言する
msgLocal.SetLocalSymInfo("letter");//変数名を決める
ilMain.Emit(OpCodes.Ldstr, "Hello World");//スタックにHello Worldをプッシュする(文字列の時)
ilMain.Emit(OpCodes.Stloc, msgLocal);//スタックの一番上から現在の値をポップし変数に格納する
ilMain.Emit(OpCodes.Ldloc, msgLocal);//スタックに変数からプッシュする
ilMain.EmitCall(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) }), null);//呼び出す
ilMain.Emit(OpCodes.Ret);
typeBuilder.CreateType();
assemblyBuilder.SetEntryPoint(mainMethodBuilder);
assemblyBuilder.Save(projectName+".exe");
}
}
これでHello Worldを出力するexeファイルを出力します
出力先は実行ファイルを置いたパスに出力されます
8行目AssemblyBuilderを宣言するとき、引数にパスを追加することで出力先を指定できます。
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Save,任意のパス);
当たり前のようにプッシュとかポップとかって言ってますが、.netはレジスタではなくスタックで実装されています。またILはそのまま変数から呼び出すことができず、スタックに入れてから使う必要があります。
pushではスタックに上から入れます
popではスタックの値を上から取り出します
イメージでは本を積んで下から取り出せないようになっている感じです。
また、CILの命令はOpCodesというClassに含まれているフィールドで構成されています。それに、CILではスタックにポップ、プッシュするとき型によって変わります。
また、実行命令については以下のサイトをご参照ください
OpCodes クラス
IL Dasmでデコンパイルしたものを見てみましょう
Developer Command Promptで
ildasm
を実行することでIL Dasmが開かれます
そこに実行ファイルをドラッグアンドドロップで置くことでCILを見ることができます
CIL構文
.method public static void Main() cil managed
{
.entrypoint
// コード サイズ 13 (0xd)
.maxstack 1
.locals init ([0] string letter)
IL_0000: ldstr "Hello"
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: call void [mscorlib]System.Console::WriteLine(string)
IL_000c: ret
} // end of method Program::Main
こうなってたらちゃんとできてます。
では出力ファイルを実行してみましょう
今回はConsole.ReadLine()を追加していないので、結果を見ることができないので
コマンドプロンプトで実行する必要があります。
実行したら以下のように表示されます。
これが表示されたら成功です。
Hello World
最後に
最後まで読んでいただきありがとうございます。
今回は初回ですが気が向いたら第二回以降も書こうと思います。
次回をするとしたら、staticメソッドの生成と、呼び出しについてやります。
また、番外として命令表をわかりやすくまとめたいと思います。