从 CIL 的编织看 C# 泛型

前言

CIL(Common Intermediate Language) 作为 C# 语言跨平台上重要的一环,通过对其进行“改造”往往实现一类自定的需求。

C# 泛型代码的 CIL 样貌

定义泛型方法,编译后查看生成的 CIL 代码:

1
public void GenericMethod_T<T>(T input) { }
1
2
3
4
5
6
.method public hidebysig
instance void GenericMethod_T<T> (!!T input) cil managed
{
.maxstack 8
// CIL code...
}

对于方法的定义 CIL 代码和 C# 有很多相似的地方,CIL 代码中包含对于这个方法更完整的信息(比如这是一个实例方法在 CIL 中就使用了 instance 标记)。在 CIL 代码中泛型方法的泛型参数依旧是泛型的。

对于泛型类型约束,来看看 new()class 的区别:

1
2
3
public void GenericMethod_New<T>(T input) where T : new() { }
public void GenericMethod_Class<T>(T input) where T : class { }

限定了泛型类型的方法,其 CIL 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
.method public hidebysig
instance void GenericMethod_New<.ctor T> (!!T input) cil managed
{
.maxstack 8
// CIL code...
}
.method public hidebysig
instance void GenericMethod_Class<class T> (!!T input) cil managed
{
.maxstack 8
// CIL code...
}

泛型类型的约束在 CIL 中也体现了,即 where T : 被编译成 <constraint T>

接下来看看对 GenericMethod_T 泛型方法调用的部分:

1
GenericMethod_T<int>(1);

对应 CIL 调用指令:

1
2
3
IL_0009: ldarg.0
IL_000a: ldc.i4.1
IL_000b: call instance void CILGeneric::GenericMethod_T<int32>(!!0)

GenericMethod_T 方法的调用被编译为 3 条指令,具体为:

  • IL_0009: ldarg.0 - 加载方法第一个参数,这里是当前实例 this

  • IL_000a: ldc.i4.1 - 加载 int32 类型数值 1

  • IL_000b: call instance void CILGeneric::GenericMethod_T<int32>(!!0) - 调用实例的 GenericMethod_T 方法,输入参数为 1

从 CIL 代码中可以看出,调用泛型方法时泛型类型已经被具体类型替换(这里被替换为 int32),方法参数列表中第一个泛型类型参数类型对应为方法中的第一个泛型类型。

简单看了 CIL 中泛型的样貌,下面具体来看看如何来编织泛型 CIL 代码。

编织 & 插桩

在 Unity 中常使用 Mono.Cecil 来编织自定的 CIL 代码,实现一些“插桩”。

编织「泛型方法」

编织泛型方法和编织普通方法类似,但是对于泛型需要做一些特殊的处理。

  1. 首先使用 Mono.Cecil 编织一个普通方法,如下所示:
1
2
3
MethodDefinition method = new MethodDefinition("GenericMethod_T",
MethodAttributes.Public | MethodAttributes.HideBySig,
moduleDefinition.TypeSystem.Void);
  1. 定义一个泛型类型并给上面编织的方法添加这个泛型类型:
1
2
3
4
// Define generic type param
GenericParameter genericParameter = new GenericParameter("T", method);
// Add generic param for method
method.GenericParameters.Add(genericParameter);
  1. 最后编织泛型输入参数:
1
2
3
4
5
6
// Define param
ParameterDefinition param = new ParameterDefinition("t",
ParameterAttributes.None, genericParameter);
// Add param for method
method.Parameters.Add(param);

通过上面几步就定义了泛型方法 public void GenericMethod_T<T>(T input) 的 CIL 实现。

「泛型方法调用」编织

  1. 再来看看如何编织泛型方法的调用过程。调用实例方法需要实例 this 的指针地址,因此需要加载当前实例:
1
ILProcessor.Emit(OpCodes.Ldarg, 0);

上面的编织就对应 IL_0009: ldarg.0 指令。

  1. 上一步中编织的泛型方法需要传入一个参数,但是在传入这个参数之前我们必须指定泛型的具体类型:
1
2
3
4
5
// New 'GenericInstanceMethod' for call, the 'method' param is the
// method definition defined above
GenericInstanceMethod genericMethod = new GenericInstanceMethod(method);
// Specify type for generic param
genericMethod.GenericArguments.Add(ModuleDefinition.ImportReference(typeof(int)));

上面的编织代码对应 instance void CILGeneric::GenericMethod_T<int32>(!!0),这里指定了泛型的具体类型为 int

  1. 压入传递具体参数
1
ILProcessor.Emit(OpCodes.Ldc_I4, 1);

上面编织对应 IL_000a: ldc.i4.1 指令,将 1 传入 GenericMethod_T 方法。

  1. 调用泛型方法
1
ILProcessor.Emit(OpCodes.Call, genericMethod);

上面编织对应 IL_000b: call instance void CILGeneric::GenericMethod_T<int32>(!!0) 指令。

通过上面的编织,就能成功的调用泛型方法 void GenericMethod_T<T>(T input)。在编织泛型方法的具体 CIL 调用指令时,一定要注意指定正确泛型的具体类型,这里泛型类型约束没有编译器负责检查。

「初始化泛型的类成员变量」

通过 Mono.Cecil 可以方便的获取到类的成员变量(以 FieldDefinition 存在)。现在尝试通过 CIL 自动初始化编织实现初始化成员变量。这里以常用的 List<T> 为例,假设有成员变量 List<string> _list,初始化代码应当如何编织了?

  1. 首先成员变量属于当前实例,因此需要加载实例:
1
ILProcessor.Emit(OpCodes.Ldarg, 0);
  1. 初始化一个类型对应调用其某个构造方法执行,因此可以获取 List<T> 的一个构造方法然后执行来达到目的。
1
2
MethodReference listCtor = ModuleDefinition.ImportReference(
typeof(List<string>).GetConstructor(new Type[0]));

获取到构造方法后,使用 Mono.Cecil 的 Newobj 初始化:

1
ILProcessor.Emit(OpCodes.Newobj, listCtor);

上面的编织过程就可以初始化 List<string> _list

更通用的「初始化泛型的类成员变量」编织过程

以上情况指明了 List 的具体泛型类型为 string,但是在 CIL 编织中这个“具体泛型类型”往往是未知的,比如现在想对当前类中的所有 List<T> 类型的成员变量初始化,可能包含 List<string>、List<int>、List<float> 等等,这种情况如何来编织通用的 CIL 了?

既然“具体泛型类型”未知,那么在获取 List<T> 构造方法时也就不用具体指定泛型类型了,如下:

1
2
MethodReference listCtor = ModuleDefinition.ImportReference(
typeof(List<>).GetConstructor(new Type[0]));

上面获取到 List<T> 的一个泛型构造方法,要是能为其指定泛型类型就完整了。那这个具体泛型类型怎么获取了?答案就是从源头获取,因为不管是泛型成员变量定义还是泛型方法调用都会指定具体泛型类型。

下面看看成员变量 List<string> _list,如何获取其泛型类型 string。假设已经通过 Mono.Cecil 获取到了 _list 的 FieldDefinition 为 listFieldDefinition:

1
2
3
4
5
6
// Get 'List<string>' generic type
GenericInstanceType fieldGeneric =
listFieldDefinition.FieldType as GenericInstanceType;
// Get 'List<string>' the first generic argument
TypeReference listGenericType = fieldGeneric.GenericArguments[0];

通过上面的代码就能获取到 _list 成员变量泛型类型 string 的 TypeReference。然后将这个具体泛型类型指定给前面获取到的泛型构造方法的泛型参数:

1
2
3
GenericInstanceType listCtorDeclaringGeneric =
listCtor.DeclaringType as GenericInstanceType;
listCtorDeclaringGeneric.GenericArguments[0] = listGenericType;

执行初始化:

1
ILProcessor.Emit(OpCodes.Newobj, listCtor);

通过上面的编织,List<T> 任意的泛型类型都能满足初始化要求。