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

1

6

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?

Knowles answered 8/4, 2015 at 3:47 Comment(4)
What exactly is the problem? You have the IL, you know about Reflection.Emit, so just use that.Alfy
Also, before using Reflection.Emit, I would probably try using Castle DynamicProxy, it's likely going to be simpler.Alfy
svick, I did some try and wrote some code as above, please see the updated question, can you help me find what's the problem?Knowles
Did you try to save generated assembly to disk, and run PEVerify on it?Agnail
A
2

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.

Alfy answered 9/4, 2015 at 16:27 Comment(3)
svick, thanks for your answer. I corrected the code according to your comments, there is an exception("An unhandled exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll") when execute Activator.CreateInstance.Knowles
@jasonbie Assuming that's not the problem #2 I mentioned, you should probably ask a new question about that, with your code and the full exception message (including inner exception).Alfy
svick, I asked another question: #29534519, can you see what is the problem?Knowles

© 2022 - 2024 — McMap. All rights reserved.