前回は、CIL を読む理由と基本的なスタック動作を見ました。
今回は、CIL で型やメンバーがどのように表現されるかを確認します。
C# のクラス、インターフェイス、構造体、列挙型、ジェネリック型、フィールド、コンストラクター、プロパティが、CIL 上ではどのような宣言になるのかを見ていきます。
外部アセンブリの参照
CIL では、外部アセンブリへの参照を宣言できます。
.assembly extern System.Runtime
{
.ver 8:0:0:0
}
これは「このアセンブリは System.Runtime を参照している」という情報です。
C# のプロジェクト参照や NuGet パッケージ参照が、ビルド後にはアセンブリ参照としてメタデータに記録されます。
現在のアセンブリを定義する
現在作っているアセンブリ自身も宣言されます。
.assembly SampleLibrary
{
.ver 1:0:0:0
}
アセンブリ名やバージョンは、実行時の読み込みや依存関係解決に関わります。
リフレクションで見ると、次のような情報として取得できます。
using System.Reflection;
Assembly assembly = Assembly.GetExecutingAssembly();
AssemblyName name = assembly.GetName();
Console.WriteLine(name.Name);
Console.WriteLine(name.Version);
名前空間とクラス
C# では名前空間とクラスを次のように書きます。
namespace SampleLibrary;
public class Calculator
{
public int Add(int x, int y)
{
return x + y;
}
}
CIL では、名前空間を含んだ型名として表現されます。
.class public auto ansi beforefieldinit SampleLibrary.Calculator
extends [System.Runtime]System.Object
{
}
名前空間専用のブロックがあるというより、型の完全名に名前空間が含まれると考えると分かりやすいです。
インターフェイス
C# のインターフェイスです。
public interface ICalculator
{
int Add(int x, int y);
}
概念的な CIL では、次のようになります。
.class interface public abstract auto ansi ICalculator
{
.method public hidebysig newslot abstract virtual
instance int32 Add(int32 x, int32 y) cil managed
{
}
}
インターフェイスのメソッドは、実装を持たない抽象的な契約として表現されます。
クラスがインターフェイスを実装する場合は、型宣言に実装情報が含まれます。
.class public auto ansi beforefieldinit Calculator
extends [System.Runtime]System.Object
implements ICalculator
{
}
構造体
C# の構造体です。
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
CIL では、構造体は System.ValueType を継承する型として表現されます。
.class public sequential ansi sealed beforefieldinit Point
extends [System.Runtime]System.ValueType
{
}
C# では構造体がクラスと別の構文になっていますが、メタデータ上は値型として特別な基底型を持つ型です。
列挙型
C# の列挙型です。
public enum EngineState
{
Stopped,
Running,
Broken
}
CIL では、System.Enum を継承する型として表現されます。
.class public auto ansi sealed EngineState
extends [System.Runtime]System.Enum
{
.field public specialname rtspecialname int32 value__
.field public static literal valuetype EngineState Stopped = int32(0)
.field public static literal valuetype EngineState Running = int32(1)
.field public static literal valuetype EngineState Broken = int32(2)
}
列挙値は、静的なリテラルフィールドとして表現されます。
ジェネリック型
C# のジェネリック型です。
public class Box<T>
{
public T Value { get; set; } = default!;
}
概念的な CIL では、型引数を持つクラスとして表現されます。
.class public auto ansi beforefieldinit Box`1<T>
extends [System.Runtime]System.Object
{
.field private !T '<Value>k__BackingField'
}
Box の後ろに付く `1 は、型引数が 1 つあることを表します。
!T は、型引数 T を表します。
ジェネリックは C# の表面構文では読みやすく書けますが、CIL では型引数の個数や型パラメーターが明示されます。
フィールド
C# のフィールドです。
public class Counter
{
private int count;
}
CIL では次のように表現されます。
.field private int32 count
自動プロパティを使った場合も、内部的にはバッキングフィールドが生成されます。
public string Name { get; set; } = "";
概念的には次のようなフィールドが作られます。
.field private string '<Name>k__BackingField'
コンストラクター
C# のコンストラクターです。
public class Person
{
public string Name { get; }
public Person(string name)
{
Name = name;
}
}
CIL では、コンストラクターは .ctor という特別な名前のメソッドとして表現されます。
.method public hidebysig specialname rtspecialname
instance void .ctor(string name) cil managed
{
ldarg.0
call instance void [System.Runtime]System.Object::.ctor()
ldarg.0
ldarg.1
stfld string Person::'<Name>k__BackingField'
ret
}
ldarg.0 は、インスタンスメソッドでは this を表します。
コンストラクターでは、まず基底クラスのコンストラクターを呼び、その後フィールドへ値を設定します。
プロパティ
C# のプロパティです。
public string Name { get; set; } = "";
CIL 上では、プロパティ本体そのものというより、取得メソッドと設定メソッドが定義されます。
.property instance string Name()
{
.get instance string Person::get_Name()
.set instance void Person::set_Name(string)
}
取得メソッドと設定メソッドは、通常のメソッドとしても表現されます。
.method public hidebysig specialname
instance string get_Name() cil managed
{
ldarg.0
ldfld string Person::'<Name>k__BackingField'
ret
}
プロパティは C# ではフィールドのように見えますが、実際にはメソッド呼び出しとして表現される点が重要です。
引数情報
メソッド引数もメタデータとして記録されます。
public void Move(int x, int y)
{
}
概念的には、次のようなメソッドシグネチャになります。
.method public hidebysig instance void Move(int32 x, int32 y) cil managed
{
ret
}
引数名はデバッグ情報やメタデータに含まれる場合があります。
リフレクションでは次のように取得できます。
using System.Reflection;
MethodInfo method = typeof(Person).GetMethod("Move")!;
foreach (ParameterInfo parameter in method.GetParameters())
{
Console.WriteLine($"{parameter.ParameterType.Name} {parameter.Name}");
}
C# と CIL の対応を読む意義
CIL の型定義を読むと、C# の便利な構文がどのように展開されるかが見えてきます。
- プロパティは取得/設定メソッドとして表現される
- 自動プロパティにはバッキングフィールドが生成される
- コンストラクターは
.ctorという特別なメソッドになる - 構造体は
System.ValueTypeを継承する - 列挙型は
System.Enumを継承し、列挙値はフィールドになる - ジェネリック型は型引数の個数を含む名前で表現される
これらを知っておくと、リフレクションで見える情報の意味が理解しやすくなります。
まとめ
CIL では、C# の型やメンバーがメタデータとして明示的に表現されます。
クラス、インターフェイス、構造体、列挙型、ジェネリック型、フィールド、コンストラクター、プロパティは、それぞれ CIL 上の宣言に変換されます。
次回は、CIL の命令に注目し、ローカル変数、引数、this、ループ、ラベル、スタックサイズなどを読み解きます。