动态创建方法并执行它
背景:
我想在 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.TargetInitationException:调用目标已引发异常。 ---> System.TypeLoadException:无法从程序集“MyDLL,Version=0.0.0.0,Culture=neutral,PublicKeyToken=null”加载类型“Invalid_Token.0x0100001E”。
在 MyType.MyMethod(Int32 value1, Int32 value2) [...]
请帮我找出错误的原因以及如何修复它。
Background:
I want to define few static
methods in C# , and generate IL code as byte array, from one of these methods, selected at runtime (on client), and send the byte array over network to another machine (server) where it should be executed after re-generating the IL code from the byte array.
My Attempt: (POC)
public static class Experiment
{
public static int Multiply(int a, int b)
{
Console.WriteLine("Arguments ({0}, {1})", a, b);
return a * b;
}
}
And then I get the IL code of the method body, as:
BindingFlags flags = BindingFlags.Public | BindingFlags.Static;
MethodInfo meth = typeof(Experiment).GetMethod("Multiply", flags);
byte[] il = meth.GetMethodBody().GetILAsByteArray();
So far I didn't create anything dynamically. But I've IL code as byte array, and I want to create an assembly, then a module in it, then a type, then a method - all dynamically. When creating the method body of the dynamically created method, I use the IL code which I got using reflection in the above code.
The code-generation code is as follows:
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());
}
But instead of printing 98780
on the output window, it throws an exception saying,
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation. ---> System.TypeLoadException: Could not load type 'Invalid_Token.0x0100001E' from assembly 'MyDLL, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null'.
at MyType.MyMethod(Int32 value1, Int32 value2)
[...]
Please help me figuring out the cause of the error, and how to fix it.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
在任意程序集上运行 ildasm.exe。使用“查看+显示标记值”并查看一些反汇编代码。您将看到 IL 包含通过数字对其他方法和变量的引用。该数字是程序集元数据表的索引。
也许您现在看到了问题,您无法将一大块 IL 从一个程序集移植到另一个程序集,除非目标程序集具有相同的元数据。或者除非您将 IL 中的元数据标记值替换为与目标程序集的元数据匹配的值。当然,这是非常不切实际的,您实际上最终会复制程序集。不妨复制一份程序集。或者就此而言,不妨使用原始程序集中现有的 IL。
您需要仔细考虑一下,目前还不清楚您实际上想要完成什么。 System.CodeDom 和 Reflection.Emit 命名空间可用于动态生成代码。
Run ildasm.exe on an arbitrary assembly. Use View + Show token values and look at some disassembled code. You'll see that the IL contains references to other methods and variables through a number. The number is an index into the metadata tables for an assembly.
Perhaps you now see the problem, you cannot transplant a chunk of IL from one assembly to another unless that target assembly has the same metadata. Or unless you replace the metadata token values in the IL with values that match the metadata of the target assembly. This is wildly impractical of course, you essentially end up duplicating the assembly. Might as well make a copy of the assembly. Or for that matter, might as well use the existing IL in the original assembly.
You need to think this through a bit, it is pretty unclear what you actually try to accomplish. The System.CodeDom and Reflection.Emit namespaces are available to dynamically generate code.
如果我采用以下方法:
在发布模式下编译它并在代码中使用它,则一切正常。如果我检查 il 数组,它包含 4 个字节,与 Jon 的答案中的四个指令(ldarg.0、ldarg.1、mul、ret)完全对应。
如果我在调试模式下编译它,代码是 9 个字节。在 Reflector 中,它看起来像这样:
有问题的部分是局部变量。如果您只是复制指令字节,则会发出使用局部变量的指令,但您从未声明它。
如果您使用的是
ILGenerator
,则可以使用DeclareLocal()
。看来您将能够使用但我还没有找到在 .Net 4 中执行此操作的方法,除了在调用
CreateMethodBody()
之后使用反射设置包含局部变量的MethodBuilder
私有字段:错误:使用标记引用其他程序集(例如 System.Console 和 System.Console.WriteLine)中的类型和方法。这些标记在不同的程序集中是不同的。这意味着如果您查看指令字节,在一个程序集中调用
Console.WriteLine()
的代码将不同于在另一个程序集中调用相同方法的代码。这意味着您必须真正理解 IL 字节的含义,并将引用类型、方法等的所有标记替换为您正在构建的程序集中有效的标记。
If I take the following method:
compile it in Release mode and use it in your code, everything works fine. And if I inspect the
il
array, it contains 4 bytes that correspond exactly to the four instructions from Jon's answer (ldarg.0, ldarg.1, mul, ret).If I compile it in debug mode, the code is 9 bytes. And in Reflector, it looks like this:
The problematic part is the local variable. If you just copy the instruction bytes, you emit instruction that uses a local variable, but you never declare it.
If you were using
ILGenerator
, you could useDeclareLocal()
. It seems you will be able to set local variables usingMethodBuilder.SetMethodBody()
in .Net 4.5 (the following works for me in VS 11 DP):But I haven't found a way to do that in .Net 4, except by using reflection to set a private field of
MethodBuilder
, that contains the local variables, after callingCreateMethodBody()
:Regarding the original error: Types and methods from other assemblies (like
System.Console
andSystem.Console.WriteLine
) are referenced using tokens. And those tokens are different from assembly to assembly. That means code to callConsole.WriteLine()
in one assembly will be different from code to call the same method in another assembly, if you look at the instruction bytes.What that means is that you would have to actually understand what the IL bytes mean and replace all tokens that reference types, methods, etc. to ones that are valid in the assembly you're building.
我认为问题在于在另一种类型/程序集中使用 IL。如果将 this: 替换
为 this:
,那么它将正确执行该方法(没有
Console.WriteLine
,但它返回正确的值)。如果您确实需要能够从现有方法中获取 IL,则需要进一步查看 - 但如果您只是需要验证其余代码是否正常工作,这可能会有所帮助。
您可能会发现有趣的一件事是,如果您从
Experiment
中取出Console.WriteLine
调用,原始代码中的错误会发生变化。相反,它变成了InvalidProgramException
。我不知道为什么...I think the problem is to do with using IL from one type/assembly in an another. If you replace this:
with this:
then it will execute the method correctly (no
Console.WriteLine
, but it returns the right value).If you really need to be able to slurp IL from an existing method, you'll need to look further - but if you just needed validation that the rest of the code was working, this may help.
One thing that you may find interesting is that the error changes in your original code if you take out the
Console.WriteLine
call fromExperiment
. It becomes anInvalidProgramException
instead. I've no idea why...如果我很好地理解你的问题,并且你只想动态生成一些 .NET 代码,并在远程客户端上执行它,那么请看一下 IronPython。您只需要使用脚本创建字符串,然后将其发送到客户端,客户端就可以在运行时执行它并访问所有.NET Framework,甚至在运行时干扰您的客户端应用程序。
If I good understand your problem, and you just want generate dynamically some .NET code, and execute it on remote client, maeby take a look at IronPython. You just need to create string with script then just send it to client, and client can execute it at runtime with access to all .NET Framework, even interfere with your client application in runtime.
要使“复制”方法发挥作用,有一种困难的方法,并且需要时间。
看看ILSpy,这个应用程序用于查看和分析现有代码,并且是开源的。您可以从用于分析 IL-ASM 代码的项目中提取代码并使用它来复制方法。
There is one hard way to get the "copy" method to work and will take a time.
Take a look at ILSpy, this application is used to view and analyse existing code and is open-source. You could extract the code from the project which is used to analyse IL-ASM code and use it to copy the method.
这个答案有点正交——更多的是关于问题而不是所涉及的技术。
您可以使用表达式树 - 它们很好用并且具有 VS 语法糖。
对于序列化,您需要这个库(您也需要编译它):
http://expressiontree.codeplex.com/
(显然,这也适用于 Silverlight 4。)
表达式树的限制是它们仅支持 Lambda 表达式 - 即没有块。
这实际上并不是一个限制,因为您仍然可以在 lambda 中定义其他 lambda 方法,并将它们传递给函数编程助手,例如 Linq API。
我提供了一个简单的扩展方法,向您展示如何为功能性内容添加其他帮助器方法 - 关键点是您需要在序列化器中包含包含扩展的程序集。
这段代码的工作原理:
This answer is a wee bit orthogonal - more about the problem than the technology involved.
You could use expression trees - they're nice to work with and have VS syntactic sugar.
For serialization you need this library (you'll need to compile it too):
http://expressiontree.codeplex.com/
(This works with Silverlight 4 too, apparently.)
The limitation of expression trees is that they only support Lambda expressions - i.e. no blocks.
This isn't really a limitation because you can still define other lambda methods within a lambda and pass them to functional-programming helpers such as the Linq API.
I've included a simple extension method to show you how to go about adding other helper methods for functional stuff - the key point is that you need to include the assemblies containing the extensions in your serializer.
This code works: