2

I've got a slightly modified class of this answer in order to dynamically call TryParse of various types (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;
    }
}

Unfortunately this throws an AccessViolationException and after checking the code a bit more in detail I'm still unsure why.

This code also "runs" in another project where it seems like the return value is not consistent. Sometimes it just returns false when the actual parsing via TryParse is successful. It sounds like undefined behaviour, but I can't seem to find the issue(s).

Here's a sample code of the AccessViolationException AND the undefined behaviour (remove float value from array for 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;
    }
}
  • What is the problem of the UB ?
  • Why the AccessViolationException ? Another problem or related to UB ?
Blacktempel
  • 3,935
  • 3
  • 29
  • 53
  • Is this *really* necessary? `Convert.ChangeType` isn't the fastest thing under the sun and you need to catch exceptions (so it's slow if you expect the data to contain many unparseable things) but parsing isn't fast in the first place. Failing that, the number of types actually implementing `.TryParse` is small enough that you can have a `switch` on the `TypeCode` with some pre-made delegates. Emitting code by reflection is handy if boxing and conversion have significant overhead, but again, if you're parsing text that's probably not the case. – Jeroen Mostert Dec 12 '18 at 11:45
  • It seems in case the return type is void you return null. I don't think this causes the AV but it's still a bug. Also, there is no UB. Rather, you must be generating invalid IL and the JIT does not catch the error but creates bad code.; Why are you loading references to the array items (`il.Emit(OpCodes.Ldelem_Ref)`) rather than loading the values? – usr Dec 12 '18 at 12:17
  • @JeroenMostert There are more types than the .NET types implementing that, in this case. I guess I'll go back to the `switch` for this piece of code. Unfortunately the problem in other use-cases stays. – Blacktempel Dec 13 '18 at 05:24
  • @usr You're right there's a `Ldnull`, I'll remove that piece. Generating invalid IL is likely true... I haven't got a lot of experience regarding this yet, so I'm unsure where the problem lies. Regarding the references - what should I do here instead ? – Blacktempel Dec 13 '18 at 05:39
  • 1
    To debug code generation I recommend the following approach: Change from `DynamicMethod` to the full `AssemblyBuilder` and `MethodBuilder`. Then after creating your methods, use `AssemblyBuilder.Save()` to dump the result to the disk and use `peverify` to spot the IL mistakes. Rise and repeat until you got the generation right. – thehennyy Dec 13 '18 at 09:34
  • Another approach is to use expression trees. *Far* easier. Or, write C# code and see what the C# compiler would have done. Then, step through your code and validate carefully, that you are emitting the same thing. – usr Dec 13 '18 at 14:56

0 Answers0