动态创建方法并执行它

背景:

我想在C#中定义一些static方法,并从运行时(在客户端上)选择的这些方法之一生成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; } } 

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

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

到目前为止,我没有动态创建任何东西。 但我把IL代码作为字节数组,我想创建一个程序集,然后是一个模块,然后是一个类型,然后是一个方法 – 所有这些都是动态的。 在创建动态创建方法的方法体时,我使用上面代码中使用reflection得到的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 (ie 10 * 9878) } catch (Exception e) { Console.WriteLine(e.ToString()); } 

但是,它不是在输出窗口上打印98780 ,而是抛出exception说,

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

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

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

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

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

如果我采取以下方法:

 public static int Multiply(int a, int b) { return a * b; } 

在发布模式下编译并在代码中使用它,一切正常。 如果我检查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 } 

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

如果您使用的是ILGenerator ,则可以使用DeclareLocal() 。 看来你可以在.Net 4.5中使用MethodBuilder.SetMethodBody()设置局部变量(以下在VS 11 DP中适用于我):

 var module = typeof(Experiment).Module; mb.SetMethodBody(il, methodBody.MaxStackSize, module.ResolveSignature(methodBody.LocalSignatureMetadataToken), null, null); 

但是我没有找到在.Net 4中这样做的方法,除了在调用CreateMethodBody()之后使用reflection来设置MethodBuilder的私有字段,其中包含局部变量:

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

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

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

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

 mb.CreateMethodBody(il, il.Count()); 

有了这个:

 ILGenerator generator = mb.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Mul); generator.Emit(OpCodes.Ret); 

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

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

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

如果我很好理解您的问题,并且您只想动态生成一些.NET代码,并在远程客户端上执行它,那么maeby会看一下IronPython。 您只需要使用脚本创建字符串然后将其发送到客户端,客户端可以在运行时执行它以访问所有.NET Framework,甚至在运行时干扰您的客户端应用程序。

有一种难以让“复制”方法起作用并且需要一段时间。

看看ILSpy ,这个应用程序用于查看和分析现有代码,是开源的。 您可以从项目中提取代码,该代码用于分析IL-ASM代码并使用它来复制方法。

这个答案有点正交 – 比所涉及的技术更多地涉及问题。

你可以使用表达式树 – 它们很适合使用并具有VS语法糖。

对于序列化,你需要这个库(你也需要编译它): http : //expressiontree.codeplex.com/ (这显然适用于Silverlight 4。)

表达式树的局限性在于它们只支持Lambda表达式 – 即没有块。

这并不是一个限制,因为您仍然可以在lambda中定义其他lambda方法,并将它们传递给函数式编程助手,例如Linq API。

我已经包含了一个简单的扩展方法来向您展示如何为函数添加其他辅助方法 – 关键是您需要在序列化程序中包含包含扩展的程序集。

此代码有效:

 using System; using System.Linq; using System.Linq.Expressions; using ExpressionSerialization; using System.Xml.Linq; using System.Collections.Generic; using System.Reflection; namespace ExpressionSerializationTest { public static class FunctionalExtensions { public static IEnumerable to(this int start, int end) { for (; start <= end; start++) yield return start; } } class Program { private static Expression> sumRange = (x, y) => x.to(y).Sum(); static void Main(string[] args) { const string fileName = "sumRange.bin"; ExpressionSerializer serializer = new ExpressionSerializer( new TypeResolver(new[] { Assembly.GetExecutingAssembly() }) ); serializer.Serialize(sumRange).Save(fileName); Expression> deserializedSumRange = serializer.Deserialize>( XElement.Load(fileName) ); Func funcSumRange = deserializedSumRange.Compile(); Console.WriteLine( "Deserialized func returned: {0}", funcSumRange(1, 4) ); Console.ReadKey(); } } }