6

I would like to know what is the value of an out/ref parameter of a invoked method.

When the method is invoked without throws an exception, the value is received in the parameter, but I do not get the value when an exception throws in the invoked method. Invoking directly the method without Reflection, the value is received.

Am I doing something wrong or is this a .net limitation?

using System;
using System.Reflection;

class Program
{
    static void Main()
    {
        string[] arguments = new string[] { bool.FalseString, null }; 
        MethodInfo method = typeof(Program).GetMethod("SampleMethod");
        try
        {
            method.Invoke(null, arguments);
            Console.WriteLine(arguments[1]); // arguments[1] = "Hello", Prints Hello
            arguments = new string[] { bool.TrueString, null };
            method.Invoke(null, arguments);
        }
        catch (Exception)
        {
            Console.WriteLine(arguments[1]); // arguments[1] = null, Does not print
        }
        arguments[1] = null;
        try
        {
            SampleMethod(bool.TrueString, out arguments[1]);
        }
        catch (Exception)
        {
            Console.WriteLine(arguments[1]); // arguments[1] = "Hello"
        }
    }

    public static void SampleMethod(string throwsException, out string text)
    {
        text = "Hello";
        if (throwsException == bool.TrueString)
            throw new Exception("Test Exception");
    }
}

After search a litlle bit I found the solution below. Would be good to use it?

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class MethodInfoExtension
{
    public static object InvokeStrictly(this MethodInfo source, object obj, object[] parameters)
    {
        ParameterInfo[] paramInfos = source.GetParameters();
        if ((parameters == null) || (paramInfos.Length != parameters.Length))
        {
            throw new ArgumentException();
        }

        Type[] paramTypes = new[] { typeof(object[]) };
        DynamicMethod invokerBuilder = new DynamicMethod(string.Empty, typeof(object), paramTypes);

        ILGenerator ilGenerator = invokerBuilder.GetILGenerator();
        Label exBlockLabel = ilGenerator.BeginExceptionBlock();

        for (int i = 0; i < paramInfos.Length; i++)
        {
            var paramInfo = paramInfos[i];
            bool paramIsByRef = paramInfo.ParameterType.IsByRef;
            var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

            ilGenerator.DeclareLocal(paramType);

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldelem_Ref);
            Label label1 = ilGenerator.DefineLabel();
            ilGenerator.Emit(OpCodes.Brfalse, label1);

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldelem_Ref);
            ilGenerator.Emit(OpCodes.Unbox_Any, paramType);
            ilGenerator.Emit(OpCodes.Stloc_S, (byte)i);

            ilGenerator.MarkLabel(label1);

            if (paramIsByRef)
            {
                ilGenerator.Emit(OpCodes.Ldloca_S, (byte)i);
            }
            else
            {
                ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
            }
        }

        LocalBuilder resultLocal = ilGenerator.DeclareLocal(typeof(object), false);
        ilGenerator.Emit(OpCodes.Call, source);
        if (source.ReturnType == typeof(void))
        {
            ilGenerator.Emit(OpCodes.Ldnull);
        }
        ilGenerator.Emit(OpCodes.Stloc_S, resultLocal);
        ilGenerator.Emit(OpCodes.Leave, exBlockLabel);

        ilGenerator.BeginFinallyBlock();
        for (int i = 0; i < paramInfos.Length; i++)
        {
            var paramInfo = paramInfos[i];
            bool paramIsByRef = paramInfo.ParameterType.IsByRef;
            var paramType = paramIsByRef ? paramInfo.ParameterType.GetElementType() : paramInfo.ParameterType;

            ilGenerator.Emit(OpCodes.Ldarg_0);
            ilGenerator.Emit(OpCodes.Ldc_I4, i);
            ilGenerator.Emit(OpCodes.Ldloc_S, (byte)i);
            if (paramType.IsValueType)
            {
                ilGenerator.Emit(OpCodes.Box, paramType);
            }
            ilGenerator.Emit(OpCodes.Stelem, typeof(object));
        }
        ilGenerator.EndExceptionBlock();

        ilGenerator.Emit(OpCodes.Ldloc_S, resultLocal);
        ilGenerator.Emit(OpCodes.Ret);

        var invoker = (Func<object[], object>)invokerBuilder.CreateDelegate(typeof(Func<object[], object>));
        return invoker(parameters);
    }
}

public class Program
{
    static void Main()
    {
        object[] args = new object[1];
        try
        {
            MethodInfo targetMethod = typeof(Program).GetMethod("Method");
            targetMethod.InvokeStrictly(null, args);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            Console.WriteLine();
        }
        Console.WriteLine(args[0]);
        Console.ReadLine();
    }
    public static void Method(out string arg)
    {
        arg = "Hello";
        throw new Exception("Test Exception");
    }
}

1 Answers1

0

In short, you are not doing anything wrong. Its a limitation on the implementation on invoke.

When using ref in a direct call the reference of your local value gets passed into the method. With invoke, for security reasons, a copy is made and copied back into your local reference only if the call did not throw an exception.


For the long answer...

So, taken your example I created this fiddle to view the IL code. That gives us the following:

.method public hidebysig static void SampleMethod(string throwsException, [out] string& text) cil managed
 {
    // 
    .maxstack  2
    .locals init (bool V_0)
    IL_0000:  nop
    IL_0001:  ldarg.1             // Get argument 2
    IL_0002:  ldstr      "Hello"  // Get string literal
    IL_0007:  stind.ref           // store in reference address
    IL_0008:  ldarg.0
    IL_0009:  ldsfld     string [mscorlib]System.Boolean::TrueString
    IL_000e:  call       bool   [mscorlib]System.String::op_Equality(string, string)
    IL_0013:  ldc.i4.0
    IL_0014:  ceq
    IL_0016:  stloc.0
    IL_0017:  ldloc.0
    IL_0018:  brtrue.s   IL_0025

    IL_001a:  ldstr      "Test Exception"
    IL_001f:  newobj     instance void [mscorlib]System.Exception::.ctor(string)
    IL_0024:  throw

    IL_0025:  ret
} // end of method Program::SampleMethod

As expected the value of "Hello" gets set in the reference address of the second (output) parameter. Meaning that the thrown exception makes no difference to setting the value or not.

Now for using invoke there is no direct call. I didn't lookup the IL code for this part, but the source is enough to figure out whats going on. First up the Invoke method is called:

public override Object Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
{
    object[] arguments = InvokeArgumentsCheck(obj, invokeAttr, binder, parameters, culture);    
    // [Security Check omitted for readability]   
    return UnsafeInvokeInternal(obj, parameters, arguments);
}

Notice, that it calls the InvokeArgumentsCheck which returns an array of value called arguments. The method is implemented as follows:

internal Object[] CheckArguments(Object[] parameters, Binder binder, BindingFlags invokeAttr, CultureInfo culture, Signature sig)
{
    // copy the arguments in a different array so we detach from any user changes 
    Object[] copyOfParameters = new Object[parameters.Length];
    // [Code omitted for readability]
    for (int i = 0; i < parameters.Length; i++)
    {
        // [Code omitted for readability]
        copyOfParameters[i] = argRT.CheckValue(arg, binder, culture, invokeAttr);
    }

    return copyOfParameters;
}

The method basically creates an copy of the input parameters you specified (with various type checks in place). As you can see from the comment placed in the method, this is done to prevent any changes by the user from influencing the data will the method is being called.

As last we look into the UnsafeInvokeInternal. The method source looks as follows:

private object UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
{
    if (arguments == null || arguments.Length == 0)
        return RuntimeMethodHandle.InvokeMethod(obj, null, Signature, false);
    else
    {
        Object retValue = RuntimeMethodHandle.InvokeMethod(obj, arguments, Signature, false);

        // copy out. This should be made only if ByRef are present.
        for (int index = 0; index < arguments.Length; index++)
            parameters[index] = arguments[index];

        return retValue;
    }
}

As we have arguments, we can focus on the 'else' part. The method is invoked by passing the arguments which is, as we determined before, a copy of the provided parameters. After the call is complete, the argument values are pushed back into the source array 'Parameters'.

In the case of an exception that means that the code gets aborted before it can 'push back' the "Hello" to our output parameter. Most likely (but I've been unable to check) it does change the copied-value in the arguments array, we just cannot access it.

I'll let you decide if this was by design, oversight, or they just thought there shouldn't be a use-case for this anyway.

RMH
  • 817
  • 5
  • 13
  • Thanks @RMH. I edited the question putting the solution. Would be nice to use it? – Anderson Vasconcelos Pires Feb 15 '18 at 23:37
  • @AndersonVasconcelosPires Let me just point out first that I'm not that experienced with Emit (you could try your luck it on [codereview](https://codereview.stackexchange.com/)). A quick inspection does not show anything too weird in the code, just note that the example only works on static methods. All, in all, I would really look at your use-case first before using this implementation as it's an easy way to create cryptic bugs, however if you _have_ to use this construction it seems like a nice solution. Just make sure you test is extensively. – RMH Feb 16 '18 at 13:15