可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答2:
If I take the following method:
public static int Multiply(int a, int b)
{
return a * b;
}
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:
.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
}
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 use DeclareLocal()
. It seems you will be able to set local variables using MethodBuilder.SetMethodBody()
in .Net 4.5 (the following works for me in VS 11 DP):
var module = typeof(Experiment).Module;
mb.SetMethodBody(il, methodBody.MaxStackSize,
module.ResolveSignature(methodBody.LocalSignatureMetadataToken),
null, null);
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 calling CreateMethodBody()
:
typeof(MethodBuilder)
.GetField("m_localSignature", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(mb, module.ResolveSignature(methodBody.LocalSignatureMetadataToken));
Regarding the original error: Types and methods from other assemblies (like System.Console
and System.Console.WriteLine
) are referenced using tokens. And those tokens are different from assembly to assembly. That means code to call Console.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.
回答3:
I think the problem is to do with using IL from one type/assembly in an another. If you replace this:
mb.CreateMethodBody(il, il.Count());
with this:
ILGenerator generator = mb.GetILGenerator();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldarg_1);
generator.Emit(OpCodes.Mul);
generator.Emit(OpCodes.Ret);
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 from Experiment
. It becomes an InvalidProgramException
instead. I've no idea why...
回答4:
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.
回答5:
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.
回答6:
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:
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();
}
}
}