前回は、CIL の命令とスタック動作を読みました。
今回は、さらに一歩進んで、実行時にアセンブリや型を生成する方法を見ていきます。
.NET には System.Reflection.Emit という名前空間があり、CIL をプログラムから生成できます。
日常のアプリケーションで頻繁に使う API ではありません。
しかし、動的プロキシ、モック生成、シリアライザー、ORM、DI コンテナーなど、フレームワーク的な仕組みの裏側を理解する上で役立ちます。
動的アセンブリとは
動的アセンブリとは、実行時にプログラムから生成されるアセンブリです。
通常の流れは次の通りです。
C# ソースコード
↓ ビルド
DLL または EXE
↓ 実行
動的生成では、実行中のプログラムが型やメソッドを組み立てます。
実行中のプログラム
↓
アセンブリを作る
↓
型を作る
↓
メソッドの CIL を生成する
↓
その場で使う
この仕組みを使うと、事前にソースコードとして存在しない型を作れます。
System.Reflection.Emit の主な型
動的生成でよく使う型は次の通りです。
AssemblyBuilder
動的アセンブリを表します。
ModuleBuilder
アセンブリ内のモジュールを表します。
TypeBuilder
実行時に作成する型を表します。
FieldBuilder
動的型に追加するフィールドを表します。
ConstructorBuilder
動的型に追加するコンストラクターを表します。
MethodBuilder
動的型に追加するメソッドを表します。
ILGenerator
メソッド本体の CIL 命令を出力するための型です。
これらを組み合わせて、「アセンブリ」「モジュール」「型」「メンバー」「命令」を順に組み立てます。
文字列を返すだけのメソッドを生成する
まず、もっとも小さな例として、SayHello() メソッドを持つ型を生成します。
using System.Reflection;
using System.Reflection.Emit;
AssemblyName assemblyName = new AssemblyName("DynamicHelloAssembly");
AssemblyBuilder assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder =
assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder typeBuilder =
moduleBuilder.DefineType(
"HelloClass",
TypeAttributes.Public);
MethodBuilder methodBuilder =
typeBuilder.DefineMethod(
"SayHello",
MethodAttributes.Public,
typeof(string),
Type.EmptyTypes);
ILGenerator il = methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello from dynamic type!");
il.Emit(OpCodes.Ret);
Type helloType = typeBuilder.CreateType()!;
object instance = Activator.CreateInstance(helloType)!;
MethodInfo method = helloType.GetMethod("SayHello")!;
string result = (string)method.Invoke(instance, null)!;
Console.WriteLine(result);
このコードでは、ソースコード上に存在しない HelloClass を実行時に生成し、SayHello() を呼び出しています。
アセンブリとモジュールを作る
最初に作っているのは、動的アセンブリとモジュールです。
AssemblyName assemblyName = new AssemblyName("DynamicHelloAssembly");
AssemblyBuilder assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(
assemblyName,
AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder =
assemblyBuilder.DefineDynamicModule("MainModule");
AssemblyBuilderAccess.Run は、生成したアセンブリを現在のプロセス内で実行するための指定です。
現代の .NET では、動的生成したアセンブリをファイルとして保存する用途は限定的です。
多くの場合、その場で生成してその場で使います。
型を作る
型は TypeBuilder で作ります。
TypeBuilder typeBuilder =
moduleBuilder.DefineType(
"HelloClass",
TypeAttributes.Public);
ここでは public な HelloClass という型を定義しています。
ただし、この時点ではまだ実際に使える Type ではありません。
最後に CreateType() を呼ぶことで型が完成します。
Type helloType = typeBuilder.CreateType()!;
CreateType() 後は、通常の型と同じようにリフレクションで扱えます。
メソッドを作る
メソッドは DefineMethod() で定義します。
MethodBuilder methodBuilder =
typeBuilder.DefineMethod(
"SayHello",
MethodAttributes.Public,
typeof(string),
Type.EmptyTypes);
この例では、次のようなメソッドを作っています。
public string SayHello()
メソッド本体は ILGenerator で出力します。
ILGenerator il = methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldstr, "Hello from dynamic type!");
il.Emit(OpCodes.Ret);
Ldstr は文字列を評価スタックへ積みます。
Ret はスタック上の値を戻り値として返します。
フィールドとコンストラクターを作る
次に、フィールドを持つ型を生成します。
作りたい C# のイメージです。
public class HelloClass
{
private readonly string message;
public HelloClass(string message)
{
this.message = message;
}
public string SayHello()
{
return message;
}
}
動的生成コードです。
using System.Reflection;
using System.Reflection.Emit;
AssemblyBuilder assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName("DynamicMessageAssembly"),
AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder =
assemblyBuilder.DefineDynamicModule("MainModule");
TypeBuilder typeBuilder =
moduleBuilder.DefineType(
"HelloClass",
TypeAttributes.Public);
FieldBuilder messageField =
typeBuilder.DefineField(
"message",
typeof(string),
FieldAttributes.Private | FieldAttributes.InitOnly);
ConstructorBuilder constructorBuilder =
typeBuilder.DefineConstructor(
MethodAttributes.Public,
CallingConventions.Standard,
new[] { typeof(string) });
ILGenerator ctor = constructorBuilder.GetILGenerator();
ctor.Emit(OpCodes.Ldarg_0);
ctor.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes)!);
ctor.Emit(OpCodes.Ldarg_0);
ctor.Emit(OpCodes.Ldarg_1);
ctor.Emit(OpCodes.Stfld, messageField);
ctor.Emit(OpCodes.Ret);
MethodBuilder sayHelloBuilder =
typeBuilder.DefineMethod(
"SayHello",
MethodAttributes.Public,
typeof(string),
Type.EmptyTypes);
ILGenerator sayHello = sayHelloBuilder.GetILGenerator();
sayHello.Emit(OpCodes.Ldarg_0);
sayHello.Emit(OpCodes.Ldfld, messageField);
sayHello.Emit(OpCodes.Ret);
Type helloType = typeBuilder.CreateType()!;
object instance = Activator.CreateInstance(
helloType,
"Hello from field!")!;
string result = (string)helloType
.GetMethod("SayHello")!
.Invoke(instance, null)!;
Console.WriteLine(result);
コンストラクターでは、まず object のコンストラクターを呼び出し、その後 message フィールドへ引数を保存しています。
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ldarg.0
ldarg.1
stfld string HelloClass::message
ret
前回までに見た CIL の読み方が、そのまま動的生成にもつながります。
生成した型を使う
生成した型は、通常のリフレクションと同じように扱えます。
Console.WriteLine(helloType.FullName);
foreach (MethodInfo method in helloType.GetMethods())
{
Console.WriteLine(method.Name);
}
共通インターフェイスを実装する動的型を生成すれば、リフレクションではなくインターフェイス経由で呼び出すこともできます。
public interface IMessageProvider
{
string GetMessage();
}
動的型にこのインターフェイスを実装させれば、最終的に次のように扱えます。
IMessageProvider provider = (IMessageProvider)instance;
Console.WriteLine(provider.GetMessage());
このような設計にすると、動的生成の複雑さを境界の内側に閉じ込められます。
どんな場面で使うのか
System.Reflection.Emit は強力ですが、読み書きは難しくなりがちです。
日常の業務コードで直接使う場面は多くありません。
使われやすい領域は次のようなものです。
- 動的プロキシ生成
- モックフレームワーク
- 高速なシリアライザー
- ORM のアクセサー最適化
- DI コンテナーの高速化
- スクリプトやルールから実行コードを生成する仕組み
つまり、アプリケーションコードというより、フレームワークや基盤ライブラリの内側で役立つ技術です。
代替手段も考える
実行時にコードを作りたい場合、必ず Reflection.Emit を使う必要はありません。
目的によっては、次の選択肢の方が扱いやすい場合があります。
- リフレクション
- 式木
- ソースジェネレーター
- Roslyn によるコンパイル
- 共通インターフェイスとプラグイン設計
特に現代の .NET では、ビルド時にコードを生成するソースジェネレーターも重要です。
実行時生成が必要なのか、ビルド時生成で足りるのかを考えると、設計が安定します。
まとめ
System.Reflection.Emit を使うと、実行時にアセンブリ、モジュール、型、フィールド、コンストラクター、メソッドを生成できます。
その中心にあるのが ILGenerator です。
ILGenerator は、CIL 命令をプログラムから出力するための API です。
CIL を読む知識があると、動的生成のコードも理解しやすくなります。
この連載全体のテーマは、「C# の裏側にある中間言語を理解し、必要なら実行時にコードを作る力を持つ」ことだと言えます。