Метод вызова через DynamicMethod - Reflection.Emit

У меня есть немного измененный класс этот ответ для динамического вызова TryParse различных типов (char, int, long ).

public delegate TRet DynamicMethodDelegate<TRet>(object target, params object[] args);
public delegate void DynamicMethodDelegate(object target, params object[] args);

public class DynamicMethodDelegateFactory
{
    public static TDelegate CreateMethodCaller<TDelegate>(MethodInfo method)
        where TDelegate : class
    {
        ParameterInfo[] parameters = method.GetParameters();
        Type[] args = { typeof(object), typeof(object[]) };

        DynamicMethod dynam =
            new DynamicMethod
                (
                    method.Name
                    , method.ReturnType
                    , args
                    , typeof(DynamicMethodDelegateFactory)
                    , true
                );

        //Add parmeter attributes to the new method from the existing method
        for (int i = 0; i < parameters.Length; i++)
        {
            dynam.DefineParameter
            (
                i,
                parameters[i].Attributes,
                parameters[i].Name
            );
        }

        ILGenerator il = dynam.GetILGenerator();

        // If method isn't static push target instance on top of stack.
        if (!method.IsStatic)
        {
            // Argument 0 of dynamic method is target instance.
            il.Emit(OpCodes.Ldarg_0);
        }

        // Lay out args array onto stack.    
        LocalBuilder[] locals = new LocalBuilder[parameters.Length];
        List<LocalBuilder> outOrRefLocals = new List<LocalBuilder>();
        for (int i = 0; i < parameters.Length; i++)
        {
            //Push args array reference onto the stack, followed
            //by the current argument index (i). The Ldelem_Ref opcode
            //will resolve them to args[i].
            if (!parameters[i].IsOut)
            {
                // Argument 1 of dynamic method is argument array.
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Ldc_I4, i);
                il.Emit(OpCodes.Ldelem_Ref);
            }

            // If parameter [i] is a value type perform an unboxing.
            Type parameterType = parameters[i].ParameterType;
            if (parameterType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, parameterType);
            }
        }

        //Create locals for out parameters
        for (int i = 0; i < parameters.Length; i++)
        {
            if (parameters[i].IsOut)
            {
                locals[i] = il.DeclareLocal(parameters[i].ParameterType.GetElementType());
                il.Emit(OpCodes.Ldloca, locals[locals.Length - 1]);
            }
        }

        if (method.IsFinal || !method.IsVirtual)
        {
            il.Emit(OpCodes.Call, method);
        }
        else
        {
            il.Emit(OpCodes.Callvirt, method);
        }

        for (int idx = 0; idx < parameters.Length; ++idx)
        {
            if (parameters[idx].IsOut || parameters[idx].ParameterType.IsByRef)
            {
                il.Emit(OpCodes.Ldarg_1);
                il.Emit(OpCodes.Ldc_I4, idx);
                il.Emit(OpCodes.Ldloc, locals[idx].LocalIndex);

                if (parameters[idx].ParameterType.GetElementType().IsValueType)
                    il.Emit(OpCodes.Box, parameters[idx].ParameterType.GetElementType());

                il.Emit(OpCodes.Stelem_Ref);
            }
        }

        if (method.ReturnType != typeof(void))
        {
            // If result is of value type it needs to be boxed
            if (method.ReturnType.IsValueType)
            {
                il.Emit(OpCodes.Box, method.ReturnType);
            }
        }
        else
        {
            il.Emit(OpCodes.Ldnull);
        }

        il.Emit(OpCodes.Ret);

        return dynam.CreateDelegate(typeof(TDelegate)) as TDelegate;
    }
}

К сожалению, это выдает AccessViolationException, и после более подробной проверки кода я все еще не уверен, почему.

Этот код также «работает» в другом проекте, где кажется, что возвращаемое значение не соответствует. Иногда он просто возвращает false, когда фактический синтаксический анализ через TryParse успешен. Это звучит как неопределенное поведение, но я не могу найти проблему (ы).

Вот пример кода AccessViolationException И неопределенного поведения (удалить значение с плавающей запятой из массива для UB).

static void Main(string[] args)
{
    var arr = new object[] { 'A', (byte)1, (short)2, 3, 4L, 5M, 6.0, 7.0F, "8" };

    for (int i = 0; i < 100000; i++)
    {
        foreach (var item in arr)
            ParseTest(item);
    }

    int a = 1;
    int b = a;
}

static Type StringType = typeof(string);
static bool ParseTest(object data)
{
    var type = data.GetType();
    if (type == StringType)
        return true;
    else
    {
        var mi = type.GetMethod(nameof(int.TryParse), new[] { StringType, type.MakeByRefType() });
        var dmd = DynamicMethodDelegateFactory.CreateMethodCaller<DynamicMethodDelegate<bool> >(mi);

        dynamic dummy = null;
        var args = new object[] { data, dummy };

        var ok = dmd(null, args);

        return ok;
    }
}
  • В чем проблема УБ?
  • Почему AccessViolationException ? Другая проблема или связанная с UB?

person Blacktempel    schedule 12.12.2018    source источник
comment
Это действительно необходимо? Convert.ChangeType не самая быстрая вещь под солнцем, и вам нужно перехватывать исключения (поэтому это медленно, если вы ожидаете, что данные будут содержать много неразборчивых вещей), но синтаксический анализ в первую очередь не быстрый. В противном случае количество типов, фактически реализующих .TryParse, достаточно мало, чтобы вы могли иметь switch на TypeCode с некоторыми предварительно созданными делегатами. Генерация кода путем отражения удобна, если упаковка и преобразование имеют значительные накладные расходы, но опять же, если вы анализируете текст, это, вероятно, не так.   -  person Jeroen Mostert    schedule 12.12.2018
comment
Кажется, что если возвращаемый тип недействителен, вы возвращаете ноль. Я не думаю, что это вызывает AV, но это все еще ошибка. Также нет УБ. Скорее, вы должны генерировать неверный IL, и JIT не перехватывает ошибку, а создает неверный код.; Почему вы загружаете ссылки на элементы массива (il.Emit(OpCodes.Ldelem_Ref)), а не значения?   -  person usr    schedule 12.12.2018
comment
@JeroenMostert В данном случае существует больше типов, чем реализующих это типов .NET. Думаю, я вернусь к switch для этого фрагмента кода. К сожалению, проблема в других случаях использования остается.   -  person Blacktempel    schedule 13.12.2018
comment
@usr Ты прав, есть Ldnull, я удалю этот кусок. Создание недопустимого IL, скорее всего, верно... У меня пока нет большого опыта в этом, поэтому я не уверен, в чем проблема. Что касается ссылок - что мне делать здесь вместо этого?   -  person Blacktempel    schedule 13.12.2018
comment
Для отладки генерации кода я рекомендую следующий подход: перейти от DynamicMethod к полным AssemblyBuilder и MethodBuilder. Затем, после создания методов, используйте AssemblyBuilder.Save(), чтобы вывести результат на диск, и используйте peverify, чтобы выявить ошибки IL. Поднимайтесь и повторяйте, пока не получите правильное поколение.   -  person thehennyy    schedule 13.12.2018
comment
Другой подход заключается в использовании деревьев выражений. Намного проще. Или напишите код C# и посмотрите, что сделал бы компилятор C#. Затем пройдитесь по коду и тщательно проверьте, что вы испускаете одно и то же.   -  person usr    schedule 13.12.2018