Проблема MethodBuilder.CreateMethodBody() при создании динамического типа

В качестве эксперимента я пытаюсь прочитать тело метода (используя GetILasByteArray()) из исходного типа и добавляя его к новому типу (используя CreateMethodBody()).

Мой исходный класс просто это

public class FullClass
{
    public string Test(string data)
    {
        return data;
    }
    public string Test2(string data)
    {
        return data;
    }
    public string Test5(string data, string data1)
    {
        return data + data1;
    }
}

IL, сгенерированный для этого кода (снятый с использованием рефлектора)

.method public hidebysig instance string Test(string data) cil managed
{
    .maxstack 1
    .locals init (
        [0] string CS$1$0000)
    L_0000: nop 
    L_0001: ldarg.1 
    L_0002: stloc.0 
    L_0003: br.s L_0005
    L_0005: ldloc.0 
    L_0006: ret 
}

Но IL, сгенерированный из моего нового типа, выглядит так

.method public hidebysig virtual instance string Test(string) cil managed
{
    .maxstack 0
    L_0000: nop 
    L_0001: ldarg.1 
    L_0002: stloc.0 
    L_0003: br.s L_0005
    L_0005: ldloc.0 
    L_0006: ret 
}

Различия заключаются в значении maxstack и директиве .locals. Я не понимаю, почему мой фактический класс генерирует локальные переменные, хотя у него нет локальных переменных ??

И почему различия в значении .maxstack, поскольку я использую один и тот же IL из источника для создания нового типа?

Из-за этого я получаю сообщение об ошибке "Common Language Runtime обнаружил недопустимую программу" при вызове метода.

Мой код, создающий динамический тип, выглядит так

public static class Mixin<Target>
    {

       public static Target compose<TSource>()
        {
            Type newType = null;

            AppDomain currentDom = Thread.GetDomain();

            AssemblyName DAssembly = new AssemblyName();
            DAssembly.Name = "DynamicTypesAssembly";

            AssemblyBuilder DAssemblyBldr = currentDom.DefineDynamicAssembly(
                               DAssembly,
                               AssemblyBuilderAccess.RunAndSave);



            ModuleBuilder DModuleBldr = DAssemblyBldr.DefineDynamicModule(DAssembly.Name, DAssembly.Name + ".dll", false);
         //   var DInterface = EmitInterface(DModuleBldr);
            TypeBuilder TypeBldr = DModuleBldr.DefineType("WorkOut.DType",
                    TypeAttributes.Public | TypeAttributes.BeforeFieldInit | TypeAttributes.Serializable
                    ,typeof(object), new[] { typeof(Target) });

            //TypeBldr.AddInterfaceImplementation(typeof(DInterface));

            var methodCol = typeof(Target).GetMethods(BindingFlags.Public| BindingFlags.Instance);

            foreach (var ms in methodCol)
            {
                var paramCol = ms.GetParameters();
                var paramTypeArray = paramCol.Select(x => x.ParameterType).ToArray();
                var paramNameArray = paramCol.Select(x=>x.Name).ToArray();
                MethodBuilder MthdBldr = TypeBldr.DefineMethod(ms.Name,
                                  MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
                                  ms.ReturnType,
                                  paramTypeArray);

                for(int i=0;i<paramCol.Count();i++)
                {
                    MthdBldr.DefineParameter(i+1, ParameterAttributes.None, paramNameArray[i]);
                }


                MethodInfo[] methodInfos = typeof(TSource).GetMethods(BindingFlags.Public | BindingFlags.NonPublic |
                             BindingFlags.Static | BindingFlags.Instance);

                for (int i = 0; i < methodInfos.Count(); i++)
                {
                    var paramSrc = methodInfos[i].GetParameters();  
                    var paramSrcTypeArray = paramSrc.Select(x => x.ParameterType).ToArray();

                    if (methodInfos[i].Name == ms.Name && methodInfos[i].ReturnType == ms.ReturnType && paramSrc.Count() == paramCol.Count() && paramTypeArray.SequenceEqual(paramSrcTypeArray))
                     {
                        var ILcodes = methodInfos[i].GetMethodBody().GetILAsByteArray();
                        var ilGen = MthdBldr.GetILGenerator();
                        //ilGen.Emit(OpCodes.Ldarg_0); //Load the 'this' reference onto the evaluation stack
                        //ilGen.Emit(OpCodes.Initobj);
                        MthdBldr.CreateMethodBody(ILcodes, ILcodes.Length);
                        //ilGen.Emit(OpCodes.Ret);
                        break;
                    }
                }

            }
            newType = TypeBldr.CreateType();
            DAssemblyBldr.Save("a.dll");
            return (Target)Activator.CreateInstance(newType);  
        }

И код для вызова этого

     var resMix = Mixin<ITest>.compose<FullClass>();
     var returned1 = resMix.Test("sam");

Изменить: И интерфейс ITest (Target)

public interface ITest
{
     string Test(string data);     
}

ИЗМЕНИТЬ:

при комментировании этой строки

  //var ilGen = MthdBldr.GetILGenerator();

maxstack становится .maxstack 16

Я проверил новую dll с помощью инструмента PEverify, это дает следующую ошибку

WorkOut.DType::Test][offset 0x00000002] Неизвестный номер локальной переменной.

Любая помощь действительно ценится .... :)


person RameshVel    schedule 16.12.2010    source источник
comment
Привет, Рамеш, не могли бы вы запросить дополнительную информацию или принять ответ?   -  person Jb Evain    schedule 07.01.2011
comment
@Jb, я использую только вашу читалку IL ... забыл обновить .. изменю ее ..   -  person RameshVel    schedule 07.01.2011


Ответы (3)


Как говорится на странице MSDN о CreateMethodBody , это не полностью поддерживается.

Очень вероятно, что реализация не анализирует IL массив байтов, поэтому она устанавливает maxstack равным 16 ни с того ни с сего.

Если вы создадите ILGenerator для метода, он установит метод maxstack равным нулю. ILGenerator будет увеличивать его, когда вы будете использовать различные перегрузки Emit. Поскольку вы этого не делаете и используете CreateMethodBody, он остается обнуленным. Это объясняет разницу.

CreateMethodBody определенно проблематичен для сценариев, которые включают в себя что угодно, кроме простого кода. Каждый код операции, который принимает токен метаданных, не будет использоваться, поскольку вы не знаете конечный токен в области модуля при создании массива байтов. И это не позволяет вам создавать обработчики исключений.

Короче говоря, CreateMethodBody как таковой не имеет смысла.

Если вы хотите продолжить эксперимент, я предлагаю вам использовать мой IL-считыватель отражения, чтобы получить представление Instruction методов, затем используйте ILGenerator для воспроизведения тела метода внутри построителя методов.

person Jb Evain    schedule 16.12.2010
comment
Спасибо, Jb, за указание на ограничение в CreateMethodBody, я попробую считыватель отражения IL. :) - person RameshVel; 16.12.2010

Что ж, вы можете обойти ошибку «неопознанный номер локальной переменной», выполнив что-то вроде этого:

var ilGen = MthdBldr.GetILGenerator();
foreach (var localVariable in methodInfos[i].GetMethodBody().LocalVariables)
{
    ilGen.DeclareLocal(localVariable.LocalType, localVariable.IsPinned);
}

На самом деле я могу запустить программу в .NET 3.5/VS2008, хотя в .NET 4.0/VS2010 она по-прежнему падает, вероятно, потому, что maxstack неверен. Если вы посмотрите на TypeBuilder.CreateTypeNoLock в Reflector, он извлекает maxStackSize из ilGenerator, если он есть, и использует 16, если его нет, поэтому вы можете застрять.

Более серьезная проблема, с которой вы столкнетесь, заключается в том, что вы копируете токены метаданных байт в байт. Из MSDN:

Токены метаданных определяются в пределах области действия. Например, маркер метаданных со значением N полностью идентифицирует в заданной области запись, содержащую сведения об определении типа. Однако в другой области маркер метаданных с тем же значением N может указывать совершенно другую запись.

Как только вы обработаете метод, который считывает поле или вызывает другой метод, вы получите загадочную ошибку вроде «MissingFieldException: Field not found: WorkOut.DType.».

Если вы действительно хотите скопировать метод, вам потребуется проанализировать IL, используйте API Reflection на Модуль, например Module.ResolveMember для преобразования токенов метаданных в объекты MemberInfo, а затем используйте ILGenerator.Emit перегружает их для преобразования в новые маркеры метаданных в вашей динамической сборке.

Эта статья CodeProject, Анализ IL тела метода, покажет вам один из способов для разбора ИЛ. Он использует тип OpCodes для построения сопоставления от кода к структуре OpCode. Вы можете прочитать инструкции одну за другой и использовать OperandType, чтобы определить, как читать и переводить аргумент.

(Обратите внимание, что я бы не рекомендовал делать что-либо из этого в рабочем коде, но вы говорите «для эксперимента», и это определенно будет интересно.)

person Quartermeister    schedule 16.12.2010
comment
Спасибо друг. как вы сказали, проблема с локальной переменной решена. Но все же ошибка размера стека не устранена. :( - person RameshVel; 16.12.2010

Вам нужно повторно объявить аргументы в стеке, чтобы иметь возможность работать с ним. Код копирования байтов IL должен быть таким:

var ILcodes = methodInfos[i].GetMethodBody().GetILAsByteArray();
ILGenerator ILGen = MthdBldr.GetILGenerator();
foreach (ParameterInfo parameter in paramSrc)
{
    ILGen.DeclareLocal(parameter.ParameterType);
}
MthdBldr.CreateMethodBody(ILcodes, ILcodes.Length);
person Simon Mourier    schedule 16.12.2010
comment
Спасибо за информацию. Теперь я получаю .locals. Но все же .maxstack равен 0. И возникает ошибка переполнения стека [offset 0x00000001] в PEverify.exe. :( - person RameshVel; 16.12.2010
comment
Это ответ на ваш вопрос. Если вам нужно более общее решение, вам придется переписать все тело функции, инструкция за инструкцией, и вам потребуется лучшее понимание IL в целом :-) - person Simon Mourier; 16.12.2010