Skip to content

[API Proposal] Add reflection support to byref-like types #10057

@ahsonkhan

Description

@ahsonkhan

This addresses long-standing issues with reflection around support for byref-like types. For additional detail, see

API

Note that these are all new types so the diff format was not used for readability.

namespace System.Reflection
{
    public ref struct MethodInvoker
    {
        // This type is designed for supporting a variable-length number of arguments allocated by the caller
        public unsafe MethodInvoker(ArgumentValue* argumentStorage, int argCount)
        // Dispose needs to be called to unregister GC tracking
        public void Dispose()

        // Target
        public object? GetTarget()
        public ref T GetTarget<T>()
        public void SetTarget(object value)
        public unsafe void SetTarget(void* value, Type type)
        public void SetTarget<T>(ref T value)

        // Arguments
        public object? GetArgument(int index)
        public ref T GetArgument<T>(int index)
        public void SetArgument(int index, object? value)
        public unsafe void SetArgument(int index, void* value, Type type)
        public void SetArgument<T>(int index, ref T value)

        // Return
        public object? GetReturn()
        public ref T GetReturn<T>()
        public void SetReturn(object value)
        public unsafe void SetReturn(void* value, Type type)
        public void SetReturn<T>(ref T value)
       
        // Unsafe versions; no conversions or validation
        public unsafe void InvokeDirectUnsafe(MethodBase method)
        // Faster for fixed parameter count (object-only) and no ref\out. Any extra args are ignored.
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target)
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, ReadOnlySpan<object?> args)
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1)
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2)
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2, object? arg3)
        public static unsafe object? InvokeDirectUnsafe(MethodBase method, object? target, object? arg1, object? arg2, object? arg3, object? arg4)

        // Safe versions; validation and conversions as in reflection today
        public void Invoke(MethodBase method)
        public static object? Invoke(MethodBase method, object? target)
        public static object? Invoke(MethodBase method, object? target, ReadOnlySpan<object?> args)
        public static object? Invoke(MethodBase method, object? target, object? arg1)
        public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2)
        public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2, object? arg3)
        public static object? Invoke(MethodBase method, object? target, object? arg1, object? arg2, object? arg3, object? arg4)
     }

    // Used to define the correct storage requirements for the MethodInvoker constructor.
    public struct ArgumentValue { }
}

Samples

unsafe
{
    using (MethodInvoker invoker = new MethodInvoker(argCount: 3))
    {
        invoker.SetArgument(0, new MyClass());
        invoker.SetArgument(1, null);
        invoker.SetArgument(2, 42);
        invoker.SetArgument(3, "Hello");
        invoker.InvokeDirectUnsafe(method);
    }
}

Avoiding boxing

Value types can be references to avoid boxing.

int i = 42;
int ret = 0;
using (MethodInvoker invoker = new MethodInvoker(argCount: 3))
{
    invoker.SetArgument(0, new MyClass());
    invoker.SetArgument(1, null);
    invoker.SetArgument<int>(2, ref i); // No boxing (argument not required to be byref)
    invoker.SetArgument(3, "Hello");
    invoker.SetReturn<int>(ref ret); // No boxing; 'ret' variable updated automatically
    unsafe
    {
        invoker.InvokeDirectUnsafe(method);
    }
}

Pass a Span<T> to a method

Span<int> span = new int[] { 42, 43 };
using (MethodInvoker invoker = new MethodInvoker(argCount: 1))
{
    unsafe
    {
          MethodInvoker invoker = new MethodInvoker(ref args);
#pragma warning disable CS8500    
          void* ptr = (void*)new IntPtr(&span);
#pragma warning restore CS8500    
          // Ideally in the future we can use __makeref(span) instead of the above.
          invoker.SetArgument(0, ptr, typeof(Span<int>));
          invoker.InvokeDirectUnsafe(method);
    }
}

Future

For perf, we may add fixed-length parameter storage to MethodInvoker:

        // Fixed length (say up to 8)
        public MethodInvoker(ref ArgumentValuesFixed values)

along with the supporting type:

    // Used for fastest perf for the MethodInvoker ctor above where the arguments are of a known small count.
    public ref struct ArgumentValuesFixed
    {
        public const int MaxArgumentCount; // 8 shown here (pending perf measurements to find optimal value) 
        
        // Used for the general case instead of the ctors below that only take 'object'.
        public ArgumentValuesFixed(int argCount)

        public ArgumentValuesFixed(object? arg1)
        public ArgumentValuesFixed(object? arg1, object? arg2)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3, object? arg4)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3, object? arg4, object? arg5)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3, object? arg4, object? arg5, object? arg6)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3, object? arg4, object? arg5, object? arg6, object? arg7)
        public ArgumentValuesFixed(object? arg1, object? arg2, object? arg3, object? arg4, object? arg5, object? arg6, object? arg7, object? arg8)
    }

with samples:

Fixed-length arguments

MethodInfo method = ... // Some method to call
ArgumentValuesFixed values = new(4); // 4 parameters
MethodInvoker= new MethodInvoker(ref values);
invoker.SetArgument(0, new MyClass());
invoker.SetArgument(1, null);
invoker.SetArgument(2, 42);
invoker.SetArgument(3, "Hello");
// Can inspect before or after invoke:
object o0 = invoker.GetArgument(0);
object o1 = invoker.GetArgument(1);
object o2 = invoker.GetArgument(2);
object o3 = invoker.GetArgument(3);
invoker.InvokeDirect(method);
int ret = (int)invoker.GetReturn();

Fixed-length object arguments (faster)

ArgumentValuesFixed args = new(new MyClass(), null, 42, "Hello");
MethodInvoker invoker = new MethodInvoker(ref args);
invoker.InvokeDirect(method);

Original issue text from @ahsonkhan:

From https://github.com/dotnet/coreclr/issues/5851#issuecomment-337356969

It is about calling methods on Span or that take Span arguments via reflection:

  • It is not possible to do it via existing reflection methods. We should have test to verify that e.g. typeof(SpanExtensions).GetMethod("AsReadOnlySpan", new Type[] { typeof(string) }).Invoke(null, new object[] { "Hello" }); fails gracefully.
  • We may want to look into adding new reflection APIs that allow calling these methods via reflection.

cc @jkotas, @atsushikan, @RussKeldorph

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-System.Reflection

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions