背景:
我想在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代码作为字节数组,我想创建一个程序集,然后创建其中的模块,然后创建类型,然后创建方法——所有这些都是动态的。在创建动态创建的方法的方法体时,我使用了在上面的代码中使用反射得到的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());
}
但是,它没有在输出窗口上打印98780
,而是抛出一个异常,称
System.Reflection.TargetInvocationException:调用的目标引发了异常。--->System.TypeLoadException:无法从程序集"MyDLL,版本=0.0.0.0,区域性=中性,PublicKeyToken=null"加载类型"Invalid_Token.0x0100001E">
nbsp 在MyType.MyMethod(Int32值1,Int32值2([…]
请帮助我找出错误的原因以及如何修复。
在任意程序集上运行ildasm.exe。使用View+Show标记值并查看一些反汇编的代码。您将看到IL通过一个数字包含对其他方法和变量的引用。数字是程序集元数据表的索引。
也许您现在看到了问题,您无法将IL的块从一个程序集移植到另一个程序集中,除非目标程序集具有相同的元数据。或者,除非将IL中的元数据标记值替换为与目标程序集的元数据匹配的值。当然,这是非常不切实际的,您最终会复制程序集。不妨复制一份程序集。或者,也可以使用原始程序集中现有的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()
:之后使用反射设置一个包含局部变量的MethodBuilder
的私有字段
typeof(MethodBuilder)
.GetField("m_localSignature", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(mb, module.ResolveSignature(methodBody.LocalSignatureMetadataToken));
关于原始错误:来自其他程序集(如System.Console
和System.Console.WriteLine
(的类型和方法是使用标记引用的。这些标记因程序集而异。这意味着,如果查看指令字节,在一个程序集中调用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,你需要进一步研究——但如果你只需要验证其余的代码是否有效,这可能会有所帮助。
您可能会发现有趣的一件事是,如果从Experiment
中取出Console.WriteLine
调用,则原始代码中的错误会发生变化。它变为InvalidProgramException
。我不知道为什么。。。
如果我很理解你的问题,而你只想动态生成一些.NET代码,并在远程客户端上执行,maeby可以看看IronPython。您只需要用脚本创建字符串,然后将其发送到客户端,客户端就可以在运行时执行它,并访问所有.NET Framework,甚至在运行时干扰您的客户端应用程序。
有一种很难让"复制"方法发挥作用的方法,需要一段时间。
看看ILSpy,这个应用程序用于查看和分析现有代码,并且是开源的。您可以从用于分析IL-ASM代码的项目中提取代码,并使用它来复制方法。
这个答案有点正交——更多的是关于问题,而不是所涉及的技术。
你可以使用表达式树——它们很好用,并且有VS语法糖。
对于序列化,您需要这个库(您也需要编译它(:http://expressiontree.codeplex.com/(这显然也适用于Silverlight 4。(
表达式树的局限性在于它们只支持Lambda表达式,即不支持块。
这并不是一个真正的限制,因为您仍然可以在lambda中定义其他lambda方法,并将它们传递给函数编程助手,如Linqneneneba 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<int> to(this int start, int end)
{
for (; start <= end; start++)
yield return start;
}
}
class Program
{
private static Expression<Func<int, int, int>> 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<Func<int, int, int>> deserializedSumRange =
serializer.Deserialize<Func<int, int, int>>(
XElement.Load(fileName)
);
Func<int, int, int> funcSumRange =
deserializedSumRange.Compile();
Console.WriteLine(
"Deserialized func returned: {0}",
funcSumRange(1, 4)
);
Console.ReadKey();
}
}
}