动态创建方法并执行它

Naw*_*waz 27 c# reflection il cil reflection.emit

背景:

我想static在C#中定义几个方法,并从运行时(在客户端)选择的这些方法之一生成IL代码作为字节数组,并通过网络将字节数组发送到另一台机器(服务器),之后应该执行它从字节数组重新生成IL代码.

我的尝试:(POC)

public static class Experiment
{
    public static int Multiply(int a, int b)
    {
        Console.WriteLine("Arguments ({0}, {1})", a, b);
        return a * b;
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我得到方法体的IL代码,如:

BindingFlags flags = BindingFlags.Public | BindingFlags.Static;
MethodInfo meth = typeof(Experiment).GetMethod("Multiply", flags);
byte[] il = meth.GetMethodBody().GetILAsByteArray();
Run Code Online (Sandbox Code Playgroud)

到目前为止,我没有动态创建任何东西.但是我把IL代码作为字节数组,我想创建一个程序集,然后是一个模块,然后是一个类型,然后是一个方法 - 所有这些都是动态的.在创建动态创建方法的方法体时,我使用上面代码中使用反射得到的IL代码.

代码生成代码如下:

AppDomain domain = AppDomain.CurrentDomain;
AssemblyName aname = new AssemblyName("MyDLL");
AssemblyBuilder assemBuilder = domain.DefineDynamicAssembly(
                                               aname, 
                                               AssemblyBuilderAccess.Run);

ModuleBuilder modBuilder = assemBuilder.DefineDynamicModule("MainModule");

TypeBuilder tb = modBuilder.DefineType("MyType", 
                            TypeAttributes.Public | TypeAttributes.Class);

MethodBuilder mb = tb.DefineMethod("MyMethod", 
     MethodAttributes.Static | MethodAttributes.Public, 
     CallingConventions.Standard,
     typeof(int),                          // Return type
     new[] { typeof(int), typeof(int) });  // Parameter types

mb.DefineParameter(1, ParameterAttributes.None, "value1");  // Assign name 
mb.DefineParameter(2, ParameterAttributes.None, "value2");  // Assign name 

//using the IL code to generate the method body
mb.CreateMethodBody(il, il.Count()); 

Type realType = tb.CreateType();

var meth = realType.GetMethod("MyMethod");
try
{
    object result = meth.Invoke(null, new object[] { 10, 9878 });
    Console.WriteLine(result);  //should print 98780 (i.e 10 * 9878)
}
catch (Exception e)
{
    Console.WriteLine(e.ToString());
}
Run Code Online (Sandbox Code Playgroud)

但它没有98780在输出窗口上打印,而是抛出异常说,

System.Reflection.TargetInvocationException:调用目标抛出了异常.---> System.TypeLoadException:无法从程序集"MyDLL,Version = 0.0.0.0,Culture = neutral,PublicKeyToken = null"加载类型"Invalid_Token.0x0100001E".
     在MyType.MyMethod(Int32 value1,Int32 value2)[...]

请帮我弄清楚错误的原因,以及如何解决它.

Han*_*ant 18

在任意程序集上运行ildasm.exe.使用View + Show标记值并查看一些反汇编代码.您将看到IL通过数字包含对其他方法和变量的引用.该数字是程序集元数据表的索引.

也许你现在看到了这个问题,你不能将一大块IL从一个程序集移植到另一个程序集,除非该目标程序集具有相同的元数据.或者,除非使用与目标程序集的元数据匹配的值替换IL中的元数据标记值.这当然是非常不切实际的,你基本上最终会复制程序集.不妨制作一个装配的副本.或者就此而言,不妨使用原始程序集中的现有IL.

你需要稍微考虑一下,目前还不清楚你真正想要实现的目标.System.CodeDom和Reflection.Emit命名空间可用于动态生成代码.


svi*_*ick 11

如果我采取以下方法:

public static int Multiply(int a, int b)
{
    return a * b;
}
Run Code Online (Sandbox Code Playgroud)

在发布模式下编译并在代码中使用它,一切正常.如果我检查il数组,它包含4个字节,与Jon的答案(ldarg.0,ldarg.1,mul,ret)中的四个指令完全对应.

如果我在调试模式下编译它,代码是9个字节.在Reflector中,它看起来像这样:

.method public hidebysig static int32 Multiply(int32 a, int32 b) cil managed
{
    .maxstack 2
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.0 
    L_0002: ldarg.1 
    L_0003: mul 
    L_0004: stloc.0 
    L_0005: br.s L_0007
    L_0007: ldloc.0 
    L_0008: ret 
}
Run Code Online (Sandbox Code Playgroud)

有问题的部分是局部变量.如果只是复制指令字节,则发出使用局部变量的指令,但是你从不声明它.

如果你正在使用ILGenerator,你可以使用DeclareLocal().看来你可以MethodBuilder.SetMethodBody()在.Net 4.5中设置局部变量(以下在VS 11 DP中对我有用):

var module = typeof(Experiment).Module;
mb.SetMethodBody(il, methodBody.MaxStackSize,
                 module.ResolveSignature(methodBody.LocalSignatureMetadataToken),
                 null, null);
Run Code Online (Sandbox Code Playgroud)

但是我没有找到办法在.Net 4中做到这一点,除非通过使用反射来设置MethodBuilder包含局部变量的私有字段,在调用之后CreateMethodBody():

typeof(MethodBuilder)
    .GetField("m_localSignature", BindingFlags.NonPublic | BindingFlags.Instance)
    .SetValue(mb, module.ResolveSignature(methodBody.LocalSignatureMetadataToken));
Run Code Online (Sandbox Code Playgroud)

关于原始错误:使用标记引用其他程序集(如System.ConsoleSystem.Console.WriteLine)中的类型和方法.这些令牌不同于装配到装配.这意味着Console.WriteLine()在一个程序集中调用的代码将与在另一个程序集中调用相同方法的代码不同,如果查看指令字节.

这意味着你必须真正理解IL字节的含义,并将引用类型,方法等的所有标记替换为在你正在构建的程序集中有效的标记.


Jon*_*eet 8

我认为问题是在另一个类型/汇编中使用IL.如果你替换这个:

mb.CreateMethodBody(il, il.Count());
Run Code Online (Sandbox Code Playgroud)

有了这个:

ILGenerator generator = mb.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Mul);
generator.Emit(OpCodes.Ret);
Run Code Online (Sandbox Code Playgroud)

然后它将正确执行该方法(不Console.WriteLine,但它返回正确的值).

如果你真的需要能够从现有方法中剔除IL,那么你需要进一步研究 - 但如果你只是需要验证其余代码是否正常工作,这可能会有所帮助.

您可能会感兴趣的一件事是,如果您从中取出Console.WriteLine呼叫,则原始代码中的错误会发生变化Experiment.它变成了一个InvalidProgramException代替.我不知道为什么......