.net-core.net-assemblyreflection.emitdynamic-assemblies

How to copy a method from an existing assembly to a dynamic assembly in .NET Core?


I want to somehow add a method from an on disk assembly to an assembly I am generating, I am creating the assembly via the System.Reflection.Emit and saving it to a file using the Lokad.ILPack nuget package and loading it with AssemblyLoadContext since this is .NET 7 Core, the on disk assembly is also generated.

I would like to avoid using externals libraries, but I understand that it may not be plausible using the standard library, and I can't use something like Pinvoke because the assembly might not even exist when the method call is needed, also if the answer requires copying the type containing the method then that's fine.

Example of how I am creating the assembly:

public static void CreateMethod(string destDllPath)
{
    AssemblyName asmName = new AssemblyName("CAssembly");

    AssemblyBuilder asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.RunAndCollect);
    //Its RunAndCollect because if the AssemblyLoadContext fails to unload it will do so automatically

    ModuleBuilder modBuilder = asmBuilder.DefineDynamicModule("CModule");

    TypeBuilder typeBuilder = modBuilder.DefineType("CType", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed);
    //Abstract | Sealed means that the class is static

    MethodBuilder methodBuilder = typeBuilder.DefineMethod("CMethod",
        MethodAttributes.Static | MethodAttributes.Public,
        CallingConventions.Standard,
        typeof(bool),
        new[] { typeof(bool) });


    ILGenerator ILG = methodBuilder.GetILGenerator();

    ILG.Emit(OpCodes.Ldarg_0);
    //Push the first argument onto the evaluation stack
    ILG.Emit(OpCodes.Ret);
    //Return the first element from the evaluation stack

    _ = typeBuilder.CreateType();

    AssemblyGenerator asmGenerator = new AssemblyGenerator();
    asmGenerator.GenerateAssembly(asmBuilder, destDllPath);
}

Then using the method generated above

public static void CopyMethod(AssemblyBuilder toAssembly, string fromDllPath)
{
    string typeName = "CType";
    string methodName = "CMethod";

    Assembly fromAssembly = Assembly.LoadFile(fromDllPath);
    //note that the assembly at fromDllPath is created via MethodBuilder ILGenerator and Lokad.ILPack

    Type type = fromAssembly.GetType(typeName)!;

    MethodInfo method = type.GetMethod(methodName)!;

    //to test that the generated assembly is valid
    //bool testTesult = (bool)method.Invoke(null, new object[] { true });

    //somehow add method to toAssembly?
}

This is where I run into the problem of not knowing how to add the method to the assembly

I have spend a solid few days trying the find a solution to this problem, but there doesn't seem to be al ot of information on dynamic assembly creation in .NET Core 5 through 7.


Solution

  • After looking at different reflection libraries (Mono.Reflection, Mono.Cecil, Lokad.ILPack and a lot of code samples) have I found out that for my project I will be simply converting each opcode and operand manually. This only works because my project is guaranteed to generate simple assemblies that don't use any module defined tokens (or switch).

    For all 0 people following or looking at this answer this may not work for you so you're going to have to define these operations yourself, if the method and types you need is defined in the same module as the target method then you can get it by using the module.resolve methods to get the appropriate tokens.

    I found the MetadataBuilder class in System.Reflection.Metadata.Ecma335 (supports all version of .Net) and it seems to the best way to generate assemblies on an CIL level, but I don't have time to use it over the Emit API so here is my solution.

    Solution:

    public static void CopyMethod(this TypeBuilder destType, MethodInfo fromMethod)
    {
        MethodBuilder destMethod = destType.DefineMethod(
            fromMethod.Name, fromMethod.Attributes, 
            fromMethod.CallingConvention, 
            fromMethod.ReturnType, 
            fromMethod.GetParameters()
                .Select((p) => p.ParameterType)
                .ToArray());
    
        ILGenerator generator = destMethod.GetILGenerator();
    
        MethodBody fromBody = fromMethod.GetMethodBody()!;
        byte[] il = fromBody.GetILAsByteArray()!;
    
        foreach(LocalVariableInfo localVar in fromBody.LocalVariables)
        {
            generator.DeclareLocal(localVar.LocalType);
        }
    
        OpCode code;
        for (int i = 0; i < il.Length;)
        {
            if (il[i] == 254)
            {
                //the opcode is 2 bytes if the first byte is 0xfe
                byte[] rev = BitConverter.IsLittleEndian ? il.Take(i..(i + 2)).Reverse().ToArray() : il.Take(i..(i + 2)).ToArray();
                //if your machine uses littleEndian reverse the order of the 2 bytes
                short numc = BitConverter.ToInt16(rev, 0);
                code = (OpCode)typeof(OpCodes).GetFields().First(t => ((OpCode)t.GetValue(null)!).Value == numc).GetValue(null)!;
            }
            else
            {
                //the opcode is 1 byte
                code = (OpCode)typeof(OpCodes).GetFields().First(t => ((OpCode)t.GetValue(null)!).Value == il[i]).GetValue(null)!;
                //lookes at all fields defined in the class OpCodes and findes the corrosponding OpCode (i know this is a bad way to do it)
            }
            
            i += code.Size;
            //advance 1 or 2 bytes depending on the size of the code itself (not including the operand yet)
            
            switch (code.OperandType)
            {
                case OperandType.InlineBrTarget:
                case OperandType.InlineI:
                    generator.Emit(code, BitConverter.ToInt32(il, i));
                    i += 4;
                    break;
    
                case OperandType.InlineI8:
                    generator.Emit(code, BitConverter.ToInt64(il, i));
                    i += 8;
                    break;
                    
                case OperandType.InlineField:
                    //Todo: copy field to this type and permute metadatatoken from fromMethods module to this destTypes module
                    FieldInfo fieldInfo = fromMethod.Module.ResolveField(BitConverter.ToInt32(il, i))!;
                    generator.Emit(code, fieldInfo);
                    break;
                
                case OperandType.InlineMethod:
                    //Todo: copy method to this type if it dosnt exist in this module and permute metadatatoken from fromMethods module to this destTypes module
                    MethodInfo method = (MethodInfo)fromMethod.Module.ResolveMethod(BitConverter.ToInt32(il, i))!;
                    generator.Emit(code, method);
                    i += 4;
                    break;
    
                case OperandType.InlineNone:
                    generator.Emit(code);
                    break;
    
                //Note: OperandType.InlinePhi is deprecated
    
                case OperandType.InlineR:
                    generator.Emit(code, BitConverter.ToDouble(il, i));
                    i += 8;
                    break;
    
                case OperandType.InlineType:
                    //Todo: copy type to this module if it doesnt exist in it and permute metadatatoken from fromMethods module to this destTypes module
                    Type type = fromMethod.Module.ResolveType(BitConverter.ToInt32(il, i), fromMethod.GetType().GenericTypeArguments, fromMethod.GetGenericArguments());
                    generator.Emit(code, type);
                    i += 4;
                    break;
    
                case OperandType.ShortInlineBrTarget:
                    generator.Emit(code, il[i]);
                    i += 1;
                    break;
    
                case OperandType.ShortInlineI:
                    generator.Emit(code, il[i]);
                    i += 1;
                    break;
    
                case OperandType.ShortInlineR:
                    generator.Emit(code, BitConverter.ToDouble(il, i));
                    i += 8;
                    break;
    
                default:
                    throw new NotImplementedException($"Havnt added { code.OperandType } because it uses tokens or emit api donst support it ¯\\_(ツ)_/¯");
            }
        }
    }
    

    This solution seems to generate the same IL as show in ildasm, but it's not ideal. Also sorry for my poor writing skills and rambling