Feature image

用C# ILGenerator在运行时动态生成proxy

问题描述

C#中经常会遇到通过单一入口动态调用对象或服务的情况,形如:

1
2
3
4
public abstract class ProxyBase
{
protected abstract object Invoke(object someMethodRelatedInfo, object[] arguments);
}

比如Reflection,远程服务,Host动态脚本引擎时从C#调用引擎context内的方法等等情况都可以归类于这样的模型。

一种较好的工程实现就是把这些服务方法用接口定义,获得强类型的校验,避免出现不必要的bug,并便于维护。如:

1
2
3
4
5
public interface IFooService
{
void MethodWithNoReturn();
int MethodTakeParameterAndReturn(int a, int b);
}

对于不同的后端,需要有具体的调用实现:

1
2
3
4
5
6
7
8
9
public class FooProxyBase : ProxyBase 
{
protected override object Invoke(object someMethodRelatedInfo, object[] arguments)
{

// Pack to JSON and send via http
// Or adapte and call other classes
// Or whatever
}
}

最终的Proxy类通过继承调用实现类,同时实现服务约定接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FooService : FooProxyBase, IFooService
{
#region Implement IFooService
public void MethodWithNoReturn()
{
Invoke("MethodWithNoReturn", new object[0]);
}

public int MethodTakeParameterAndReturn(int a, int b)
{
return Invoke("MethodTakeParameterAndReturn", new object[] { a, b });
}
#endregion
}

这样一来有一个显然的问题,Proxy类包含大量重复的代码,方法越多实现起来越费劲。这个问题的point of interest就在于Proxy类的动态生成,实现以后只需要一行代码就能替代人肉实现一个巨大的Proxy类:

1
IFooService proxy = ProxyEmitter.CreateProxy<FooProxyBase, IFooService>(/*Constructor parameters are supported*/);

要动态生成Proxy类有很多种方法(如生成源代码然后编译),这里采用在运行时通过Reflection获取服务接口的方法,动态生成Proxy类,最后用ILGenerator.Emit用.Net IL实现代码逻辑。

实现要点

如何动态创建Assembly, Module, Type的框架性代码MSDN有详尽的walkthrough,不在本文讨论重点,具体实现可参考源代码。

这一节记录在实现这个项目中几处逻辑的IL代码生成,有几点是必须要知道的:

  • .Net CLR是基于栈的虚拟机
  • .Net CLR(在生成C#类时)是强类型的
  • 参数顺序入栈
  • 非static method的第一个参数总是this指针

1. 有参数的constructor

在C#中很多涉及自动生成的情况(如serialization)都要求无参数的constructor,在有的情况下很让人忧桑,其实要支持有参数的constructor也是可行的。

如果父类只有一个有参数的constructor,子类的constructor实现必须用足够的参数构造:

1
2
3
4
class Derived: Base
{
public Derived(int may, string para, object[] meters): base(may, para, meters) {}
}

用IL实现上述代码,需要将参数重新压栈,然后call base的ctor指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private static void EmitCtor(TypeBuilder tBuilder, ConstructorInfo ctor)
{

var pTypes = ctor.GetParameters().Select(p => p.ParameterType).ToArray();
var builder = Emitter.GetConstructor(
tBuilder,
MethodAttributes.Public |
MethodAttributes.HideBySig |
MethodAttributes.SpecialName |
MethodAttributes.RTSpecialName,
pTypes
);
var ilGen = builder.GetILGenerator();

// No locals

// Load all args, note arg 0 is this pointer, so must emit one more
for (int i = 0; i <= pTypes.Length; i++)
{
DoEmit(ilGen, OpCodes.Ldarg_S, i);
}
// Call base ctor
DoEmit(ilGen, OpCodes.Call, ctor);

// Return
DoEmit(ilGen, OpCodes.Ret);
}

生成的IL形如:

1
2
3
4
5
6
IL_0000: ldarg.0
IL_0001: ldarg.1
IL_0002: ldarg.2
IL_0003: ldarg.3
IL_0004: call instance void Base::.ctor(int32, string, object)
IL_0009: ret

2. Array的初始化
由于Invoke的长相,决定了这个生成器中需要大量的生成object[]对象,并把参数装进去。
创建一个local variable,首先需要declare:

1
ilGen.DeclareLocal(typeof(object[]))

每个method的运行环境里维护了一个local列表,IL代码通过index把local入栈和出栈。
创建Array对象,并设置到local:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// Initialize array
// IL_0006: ldc.i4.x
DoEmit(ilGen, OpCodes.Ldc_I4_S, pTypes.Length);
// IL_0007: newarr [mscorlib]System.Object
DoEmit(ilGen, OpCodes.Newarr, typeof(Object));
// IL_000c: stloc.1
DoEmit(ilGen, OpCodes.Stloc_0);
```
对Array元素的逐条赋值由4~5条机器指令完成:

* ldloc.?将array入栈
* ldc_i4_?将当前元素的index入栈
* 将需要赋给元素的值入栈(本例中为参数用ldarg_s,注意参数0this指针)
* 如果是value type需要box
* stelem.ref指令完成赋值

```cs
// Now fill the array
for (int i = 0; i < pTypes.Length; i++)
{
// Load the array first
// IL_000X + 00: ldloc.0
DoEmit(ilGen, OpCodes.Ldloc_0);

// Push the index
// IL_000X + 01: ldc_i4_x
DoEmit(ilGen, OpCodes.Ldc_I4_S, i);
// Load argument i + 1 (note that argument 0 is this pointer(?))
// IL_000X + 02: ldarg_X
DoEmit(ilGen, OpCodes.Ldarg_S, i + 1);
// Box value type
if (pTypes[i].IsValueType)
{
// IL_000X + 03: box pTypes[i]
DoEmit(ilGen, OpCodes.Box, pTypes[i]);
}
// Set arrary element
// IL_00X + ??: stelem.ref
DoEmit(ilGen, OpCodes.Stelem_Ref);
}

源代码及使用方法

GitHub