Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write a C# class with Reflection.Emit dynamically according to IL

Suppose we have an interface:

public interface ICalculator
{
    decimal Calculate(decimal x, decimal y);
}

the calculate logic is implemented in javascript (actually is TypeScript) code, we want to dynamically create the follow implementation using Reflection.Emit, so we can share the unit tests with the C# implementation:

public class Calculator : ICalculator
{

    private ScriptEngine ScriptEngine;

    public Calculator(ScriptEngine scriptEngine, string jsFileFullPath)
    {
        this.ScriptEngine = scriptEngine;
        var jsFileContent = File.ReadAllText(jsFileFullPath);
        this.ScriptEngine.Execute(jsFileContent);
    }

    public decimal Calculate(decimal x, decimal y)
    {

        string script = @"
                var rf1013 = new TotalTaxation.TaxformCalculation.RF1013({0},{1});
                rf1013.Calculate();
                var result = rf1013.RF1013Sum;
            ";

        this.ScriptEngine.Evaluate(string.Format(script, x, y));

        var result = this.ScriptEngine.Evaluate("result");
        return Convert.ToDecimal(result);

    }
}

we can get the IL from IL DASM:

.class public auto ansi beforefieldinit Calculator
       extends [mscorlib]System.Object
       implements ICalculator
{
} // end of class Calculator


.field private class [ClearScript]Microsoft.ClearScript.ScriptEngine ScriptEngine

.method public hidebysig specialname rtspecialname 
        instance void  .ctor(class [ClearScript]Microsoft.ClearScript.ScriptEngine scriptEngine,
                             string jsFileFullPath) cil managed
{
  // Code size       37 (0x25)
  .maxstack  2
  .locals init ([0] string jsFileContent)
  IL_0000:  ldarg.0
  IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
  IL_0006:  nop
  IL_0007:  nop
  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  stfld      class [ClearScript]Microsoft.ClearScript.ScriptEngine Calculator::ScriptEngine
  IL_000f:  ldarg.2
  IL_0010:  call       string [mscorlib]System.IO.File::ReadAllText(string)
  IL_0015:  stloc.0
  IL_0016:  ldarg.0
  IL_0017:  ldfld      class [ClearScript]Microsoft.ClearScript.ScriptEngine Calculator::ScriptEngine
  IL_001c:  ldloc.0
  IL_001d:  callvirt   instance void [ClearScript]Microsoft.ClearScript.ScriptEngine::Execute(string)
  IL_0022:  nop
  IL_0023:  nop
  IL_0024:  ret
} // end of method JsRF1013Wrapper::.ctor


.method public hidebysig newslot virtual final 
        instance valuetype [mscorlib]System.Decimal 
        Calculate(valuetype [mscorlib]System.Decimal x,
                  valuetype [mscorlib]System.Decimal y) cil managed
{
  // Code size       65 (0x41)
  .maxstack  4
  .locals init ([0] string script,
           [1] object result,
           [2] valuetype [mscorlib]System.Decimal CS$1$0000)
  IL_0000:  nop
  IL_0001:  ldstr      "\r\n                    var rf1013 = new TotalTaxati"
  + "on.TaxformCalculation.RF1013({0},{1});\r\n                    rf1013.Calc"
  + "ulate();\r\n                    var result = rf1013.RF1013Sum;\r\n         "
  + "       "
  IL_0006:  stloc.0
  IL_0007:  ldarg.0
  IL_0008:  ldfld      class [ClearScript]Microsoft.ClearScript.ScriptEngine Calculator::ScriptEngine
  IL_000d:  ldloc.0
  IL_000e:  ldarg.1
  IL_000f:  box        [mscorlib]System.Decimal
  IL_0014:  ldarg.2
  IL_0015:  box        [mscorlib]System.Decimal
  IL_001a:  call       string [mscorlib]System.String::Format(string,
                                                              object,
                                                              object)
  IL_001f:  callvirt   instance object [ClearScript]Microsoft.ClearScript.ScriptEngine::Evaluate(string)
  IL_0024:  pop
  IL_0025:  ldarg.0
  IL_0026:  ldfld      class [ClearScript]Microsoft.ClearScript.ScriptEngine Calculator::ScriptEngine
  IL_002b:  ldstr      "result"
  IL_0030:  callvirt   instance object [ClearScript]Microsoft.ClearScript.ScriptEngine::Evaluate(string)
  IL_0035:  stloc.1
  IL_0036:  ldloc.1
  IL_0037:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Convert::ToDecimal(object)
  IL_003c:  stloc.2
  IL_003d:  br.s       IL_003f
  IL_003f:  ldloc.2
  IL_0040:  ret
} // end of method Calculator::Calculate

We created the TypeCreator to do that:

namespace TypeCreator
{
    public interface ICalculator
    {
        decimal Calculate(decimal x, decimal y);
    }

    public class TypeCreator
    {
        private Type targetType;
        private ScriptEngine scriptEngine;
        private string jsFileFullPath;

        public TypeCreator(Type targetType, ScriptEngine scriptEngine, string jsFileFullPath)
        {
            this.targetType = targetType;
            this.scriptEngine = scriptEngine;
            this.jsFileFullPath = jsFileFullPath;
        }
        public Type build()
        {
            AppDomain currentAppDomain = AppDomain.CurrentDomain;
            AssemblyName assyName = new AssemblyName();
            assyName.Name = "MyAssyFor_" + targetType.Name;
            AssemblyBuilder assyBuilder = currentAppDomain.DefineDynamicAssembly(assyName, AssemblyBuilderAccess.Run);
            ModuleBuilder modBuilder = assyBuilder.DefineDynamicModule("MyModFor_" + targetType.Name);
            String newTypeName = "Imp_" + targetType.Name;
            TypeAttributes newTypeAttribute = TypeAttributes.Class | TypeAttributes.Public;

            Type[] ctorParams = new Type[] { typeof(ScriptEngine), typeof(string) };
        Type newTypeParent;
        Type[] newTypeInterfaces;
        if (targetType.IsInterface)
        {
            newTypeParent = null;
            newTypeInterfaces = new Type[] { targetType };
        }
        else
        {
            newTypeParent = targetType;
            newTypeInterfaces = new Type[0];
        }
        TypeBuilder typeBuilder = modBuilder.DefineType(newTypeName, newTypeAttribute, newTypeParent, newTypeInterfaces);

        FieldBuilder scriptEngineField = typeBuilder.DefineField("scriptEngine", typeof(ScriptEngine),
                                                           FieldAttributes.Public);
        FieldBuilder jsFileFullPathField = typeBuilder.DefineField("jsFileFullPath", typeof(string),
                                                           FieldAttributes.Public);
        Type objType = Type.GetType("System.Object");
        ConstructorInfo objCtor = objType.GetConstructor(new Type[0]);

        ConstructorBuilder wrapperCtor = typeBuilder.DefineConstructor(
                       MethodAttributes.Public,
                       CallingConventions.Standard,
                       ctorParams);
        ILGenerator ctorIL = wrapperCtor.GetILGenerator();
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Call, objCtor);
        ctorIL.Emit(OpCodes.Nop);
        ctorIL.Emit(OpCodes.Nop);
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Ldarg_1);
        ctorIL.Emit(OpCodes.Stfld, scriptEngineField);
        ctorIL.Emit(OpCodes.Ldarg_2);
        ctorIL.Emit(OpCodes.Call, typeof(File).GetMethod("ReadAllText", new Type[] { typeof(string) }));
        ctorIL.Emit(OpCodes.Stloc_0);
        ctorIL.Emit(OpCodes.Ldarg_0);
        ctorIL.Emit(OpCodes.Ldfld, scriptEngineField);
        ctorIL.Emit(OpCodes.Ldloc_0);
        ctorIL.Emit(OpCodes.Callvirt, typeof(ScriptEngine).GetMethod("Execute", new Type[] { typeof(string) }));
        ctorIL.Emit(OpCodes.Nop);
        ctorIL.Emit(OpCodes.Nop);
        //ctorIL.Emit(OpCodes.Stfld, jsFileFullPathField);
        ctorIL.Emit(OpCodes.Ret);

        string methodName = "Calculate";

        MethodBuilder getFieldMethod = typeBuilder.DefineMethod(methodName, MethodAttributes.Public, typeof(decimal), new Type[] { typeof(decimal), typeof(decimal) });
        ILGenerator methodIL = getFieldMethod.GetILGenerator();
        methodIL.Emit(OpCodes.Nop);
        methodIL.Emit(OpCodes.Ldstr, @"var rf1013 = new TotalTaxation.TaxformCalculation.RF1013({0},{1});
                rf1013.Calculate();
                var result = rf1013.RF1013Sum;");
        methodIL.Emit(OpCodes.Stloc_0);
        methodIL.Emit(OpCodes.Ldarg_0);
        methodIL.Emit(OpCodes.Ldfld, scriptEngineField);
        methodIL.Emit(OpCodes.Ldloc_0);
        methodIL.Emit(OpCodes.Ldarg_1);
        methodIL.Emit(OpCodes.Box, typeof(decimal));
        methodIL.Emit(OpCodes.Ldarg_2);
        methodIL.Emit(OpCodes.Call, typeof(String).GetMethod("Format", new Type[] { typeof(string), typeof(object), typeof(object) }));

        methodIL.Emit(OpCodes.Callvirt, typeof(ScriptEngine).GetMethod("Execute", new Type[] { typeof(string) }));
        methodIL.Emit(OpCodes.Pop);
        methodIL.Emit(OpCodes.Ldarg_0);
        methodIL.Emit(OpCodes.Ldfld, scriptEngineField);
        methodIL.Emit(OpCodes.Ldstr, "result");
        methodIL.Emit(OpCodes.Callvirt, typeof(ScriptEngine).GetMethod("Execute", new Type[] { typeof(string) }));
        methodIL.Emit(OpCodes.Stloc_0);
        methodIL.Emit(OpCodes.Ldloc_0);
        methodIL.Emit(OpCodes.Call, typeof(Convert).GetMethod("ToDecimal", new Type[] { typeof(object) }));
        methodIL.Emit(OpCodes.Stloc_2);
        methodIL.Emit(OpCodes.Br_S);
        methodIL.Emit(OpCodes.Ldloc_2);
        methodIL.Emit(OpCodes.Ret);

        return (typeBuilder.CreateType());
    }
}

Use it like this:

        var jsFileFullPath = "JsFiles\\Total.js";
        TypeCreator tc = new TypeCreator(typeof(ICalculator), new JScriptEngine(), jsFileFullPath);
        Type t = tc.build();

        // Prepares the parameters
        var scriptArgs = new System.Collections.ArrayList();
        scriptArgs.Add(new JScriptEngine());
        scriptArgs.Add(jsFileFullPath);

        ICalculator calculator = (ICalculator)Activator.CreateInstance(t, scriptArgs);
        var result = calculator.Calculate(3.0m, 5.0m);
        Console.Write(string.Format("calculator.Calculate(3.0m, 5.0m)={0}", result));
        Console.Read();

It throw an exception:

Method 'Calculate' in type 'Imp_ICalculator' from assembly 'MyAssyFor_ICalculator, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' does not have an implementation.

What's the problem?

like image 570
jason bie Avatar asked Apr 08 '15 03:04

jason bie


1 Answers

There are several problems in your code:

  1. The reported error is because methods that implement interfaces have to be virtual (see §II.12.2 Implementing virtual methods on interfaces of ECMA-335). This can also be seen from the disassembled IL (I believe the newslot and final modifiers are there so that the method doesn't behave like C# virtual method):

    .method public hidebysig newslot virtual final 
            instance valuetype [mscorlib]System.Decimal 
            Calculate(valuetype [mscorlib]System.Decimal x,
                      valuetype [mscorlib]System.Decimal y) cil managed
    

    To fix this, you need to add | MethodAttributes.Virtual to the DefineMethod() call.

  2. You're calling Activator.CreateInstance() with a single ArrayList parameter. If you want to call the constructor with two parameters, you need to either pass in a single object[] or use params:

    Activator.CreateInstance(t, new ScriptEngine(), jsFileFullPath)
    
  3. You're using local variables in IL, but you're not declaring them. Use DeclareLocal() to fix that.

This is where I stopped, so it's possible your code has other issues.

like image 136
svick Avatar answered Oct 06 '22 02:10

svick