Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IL Emit support for MethodInfo.Invoke() and friends #67917

Merged
merged 23 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d4f6b51
Add IL Emit support for MethodInfo.Invoke()
steveharter Apr 11, 2022
023299c
Test updates
steveharter Apr 13, 2022
19959c0
Create true Nullable<T>s to simplify native and IL
steveharter Apr 19, 2022
40eb391
Update Emit code for new Nullability; still running non-emit for tests
steveharter Apr 19, 2022
a1f7a5c
Enable IL for testing; improve perf for ByRef param validation
steveharter Apr 20, 2022
50cd45b
Merge branch 'main' of https://github.com/steveharter/runtime into Em…
steveharter Apr 21, 2022
ba141ce
Fixup merge
steveharter Apr 21, 2022
79571d9
Ensure a copy is made for byref value types
steveharter Apr 22, 2022
c2ae6e4
Remove temps in IL
steveharter Apr 24, 2022
97dc203
Remove hard check for Type.GetType()
steveharter Apr 24, 2022
69d467d
Active issue some tests; misc other
steveharter Apr 25, 2022
259c4e0
Remove IL-only test hack; ready for review
steveharter Apr 27, 2022
9dab8f7
Add emit to DynamicMethod; other misc feedback
steveharter Apr 28, 2022
e7878ff
Fix Mono compile issue
steveharter Apr 28, 2022
13f913a
Fix shuffle thunk; other misc non-functional
steveharter Apr 29, 2022
1516fbf
Fix possible null method.DeclaringType; binding test failures with Ch…
steveharter May 2, 2022
54e0a77
Fix DynamicMethod.Invoke when using Emit-based invoke
steveharter May 3, 2022
89a0483
Disable some JIT Stress tests on arm32 linux
steveharter May 3, 2022
9e50960
renaming; native perf; other feedback
steveharter May 5, 2022
53a4167
Nit feedback
steveharter May 5, 2022
8efba3d
For perf, avoid calling ReboxFromNullable when not necessary
steveharter May 6, 2022
55a4f95
Merge branch 'main' into EmitInvoke
steveharter May 6, 2022
c7bee67
For perf, avoid calling ReboxFromNullable when not necessary part 2
steveharter May 6, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -368,25 +368,15 @@ public override MethodImplAttributes GetMethodImplementationFlags()
return retValue;
}

#pragma warning disable CA1822 // Mark members as static
internal bool SupportsNewInvoke => true;
#pragma warning restore CA1822 // Mark members as static

[DebuggerHidden]
[DebuggerStepThrough]
internal unsafe object? InvokeNonEmitUnsafe(object? obj, IntPtr* arguments, Span<object?> argsForTemporaryMonoSupport, BindingFlags invokeAttr)
{
if ((invokeAttr & BindingFlags.DoNotWrapExceptions) == 0)
{
try
{
return RuntimeMethodHandle.InvokeMethod(obj, (void**)arguments, Signature, isConstructor: false);
}
catch (Exception e)
{
throw new TargetInvocationException(e);
}
}
else
{
return RuntimeMethodHandle.InvokeMethod(obj, (void**)arguments, Signature, isConstructor: false);
}
return RuntimeMethodHandle.InvokeMethod(obj, (void**)arguments, Signature, isConstructor: false);
}

#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\MethodInfo.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\MethodInfo.Internal.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\MethodInvoker.cs" Condition="'$(TargetsCoreRT)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\InvokerEmitUtil.cs" Condition="'$(TargetsCoreRT)' != 'true'" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\Missing.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\Module.cs" />
<Compile Include="$(MSBuildThisFileDirectory)System\Reflection\ModuleResolveEventHandler.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,331 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;

namespace System.Reflection
{
internal static class InvokerEmitUtil
{
// This is an instance method to avoid overhead of shuffle thunk.
steveharter marked this conversation as resolved.
Show resolved Hide resolved
internal unsafe delegate object? InvokeFunc<T>(T obj, object? target, IntPtr* arguments);

// Avoid high arg count due to increased stack.
// This also allows the local variables to use that more smaller encoded _S opcode variants.
public const int MaxArgumentCount = 64;

public static unsafe InvokeFunc<T> CreateInvokeDelegate<T>(MethodBase method)
{
Debug.Assert(!method.ContainsGenericParameters);

ParameterInfo[] parameters = method.GetParametersNoCopy();
Debug.Assert(parameters.Length <= MaxArgumentCount);

Type[] delegateParameters = new Type[3] { typeof(T), typeof(object), typeof(IntPtr*) };

// We could use the overload with 'owner' in order to associate to this module.
var dm = new DynamicMethod(
"InvokeStub_" + method.DeclaringType!.Name + "." + method.Name,
returnType: typeof(object),
delegateParameters,
owner: typeof(T),
skipVisibility: true);

ILGenerator il = dm.GetILGenerator();

if (parameters.Length == 0)
{
HandleThisPointer(il, method);
Invoke(il, method);
}
else
{
HandleThisPointer(il, method);
PushArguments(il, parameters, out NullableRefInfo[]? byRefNullables, out int byRefNullableCount);
Invoke(il, method);

if (byRefNullableCount > 0)
{
Debug.Assert(byRefNullables != null); ;
UpdateNullables(il, parameters, byRefNullables, byRefNullableCount);
}
}

HandleReturn(il, method);

return (InvokeFunc<T>)dm.CreateDelegate(typeof(InvokeFunc<T>));
}

private static void HandleThisPointer(ILGenerator il, MethodBase method)
{
bool emitNew = method is RuntimeConstructorInfo;
bool hasThis = !(emitNew || method.IsStatic);

if (hasThis)
{
il.Emit(OpCodes.Ldarg_1);
if (method.DeclaringType!.IsValueType)
{
il.Emit(OpCodes.Unbox, method.DeclaringType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The boxing/unboxing of this and return value should be moved to the static code too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes per discussion the forcing factor will be supporting the new byref-like type APIs possibly using TypedReference. That will enable true by-ref returns (with aliasing) and ability to invoke a target value type (and eventually byref-like types) without boxing or making a copy of the target.

}
}
}

/// <summary>
/// Push each argument.
/// </summary>
private static void PushArguments(
ILGenerator il,
ParameterInfo[] parameters,
out NullableRefInfo[]? byRefNullables,
out int byRefNullablesCount
)
{
byRefNullables = null;
byRefNullablesCount = 0;

LocalBuilder? local_pArg = il.DeclareLocal(typeof(IntPtr*));
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Stloc_S, local_pArg);

for (int i = 0; i < parameters.Length; i++)
{
RuntimeType parameterType = (RuntimeType)parameters[i].ParameterType;

// Get the argument as a ref
il.Emit(OpCodes.Ldloc_S, local_pArg);
il.Emit(OpCodes.Ldind_Ref);

if (parameterType.IsByRef)
{
RuntimeType elementType = (RuntimeType)parameterType.GetElementType();

if (elementType.IsNullableOfT)
steveharter marked this conversation as resolved.
Show resolved Hide resolved
{
byRefNullables ??= new NullableRefInfo[MaxArgumentCount - i];

LocalBuilder tmp = il.DeclareLocal(typeof(object).MakeByRefType());
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloc_S, tmp);

// Get the raw pointer.
il.Emit(OpCodes.Ldobj, typeof(object));
il.Emit(OpCodes.Unbox, elementType);

// Copy the pointer to the temp variable and load as a ref.
LocalBuilder byRefPtr = il.DeclareLocal(elementType.MakePointerType());
il.Emit(OpCodes.Stloc_S, byRefPtr);
il.Emit(OpCodes.Ldloca_S, byRefPtr);
il.Emit(OpCodes.Ldind_Ref);

byRefNullables[byRefNullablesCount++] = new NullableRefInfo { ParameterIndex = i, LocalIndex = byRefPtr.LocalIndex };
}
else if (elementType.IsPointer)
{
LocalBuilder tmp = il.DeclareLocal(typeof(IntPtr).MakeByRefType());
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloc_S, tmp);
il.Emit(OpCodes.Ldobj, typeof(IntPtr).MakeByRefType());
}
else
{
LocalBuilder tmp = il.DeclareLocal(parameterType);
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloca_S, tmp);
il.Emit(OpCodes.Ldind_Ref); //keep this or remove and use ldloca?
}
}
else if (parameterType.IsNullableOfT)
{
// Nullable<T> is the only incoming value type that is boxed.
steveharter marked this conversation as resolved.
Show resolved Hide resolved
LocalBuilder tmp = il.DeclareLocal(typeof(object).MakeByRefType());
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloc_S, tmp);

il.Emit(OpCodes.Ldobj, typeof(object));
il.Emit(OpCodes.Unbox, parameterType);
il.Emit(OpCodes.Ldobj, parameterType);
}
else if (parameterType.IsPointer)
{
LocalBuilder tmp = il.DeclareLocal(typeof(IntPtr).MakeByRefType());
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloc_S, tmp);
il.Emit(OpCodes.Ldobj, typeof(IntPtr));
}
else
{
LocalBuilder tmp = il.DeclareLocal(parameterType.MakeByRefType());
il.Emit(OpCodes.Stloc_S, tmp);
il.Emit(OpCodes.Ldloc_S, tmp);
il.Emit(OpCodes.Ldobj, parameterType);
}

// Move to the next argument.
if (i < parameters.Length - 1)
{
il.Emit(OpCodes.Ldloc_S, local_pArg);
il.Emit(OpCodes.Sizeof, typeof(IntPtr));
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Stloc_S, local_pArg);
}
}
}

/// <summary>
/// Update any nullables that were passed by reference.
/// </summary>
private static void UpdateNullables(
ILGenerator il,
ParameterInfo[] parameters,
NullableRefInfo[] byRefNullables,
int byRefNullablesCount)
{
for (int i = 0; i < byRefNullablesCount; i++)
{
NullableRefInfo info = byRefNullables[i];

RuntimeType parameterType = (RuntimeType)parameters[info.ParameterIndex].ParameterType;
Debug.Assert(parameterType.IsByRef);

RuntimeType? elementType = (RuntimeType)parameterType.GetElementType()!;
Debug.Assert(elementType.IsNullableOfT);

// Get the original byref location.
il.Emit(OpCodes.Ldc_I4_S, info.ParameterIndex);
il.Emit(OpCodes.Conv_I);
il.Emit(OpCodes.Sizeof, typeof(IntPtr));
il.Emit(OpCodes.Mul);
il.Emit(OpCodes.Ldarg_2);
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ldind_I);

// Get the Nullable<T>& value and update the original.
il.Emit(OpCodes.Ldloc_S, info.LocalIndex);
il.Emit(OpCodes.Ldobj, elementType);
il.Emit(OpCodes.Box, elementType);
il.Emit(OpCodes.Stind_Ref);
}
}

private static void Invoke(ILGenerator il, MethodBase method)
{
// todo: once we return the value without boxing add support for OpCodes.Tailcall for perf.
steveharter marked this conversation as resolved.
Show resolved Hide resolved

bool emitNew = method is RuntimeConstructorInfo;
if (emitNew)
{
Debug.Assert(method!.IsStatic);
il.Emit(OpCodes.Newobj, (ConstructorInfo)method);
}
else if (method.IsStatic || method.DeclaringType!.IsValueType)
{
il.Emit(OpCodes.Call, (RuntimeMethodInfo)method);
}
else
{
il.Emit(OpCodes.Callvirt, (RuntimeMethodInfo)method);
}
}

private static void HandleReturn(ILGenerator il, MethodBase method)
{
bool emitNew = method is RuntimeConstructorInfo;
Type returnType = emitNew ? method.DeclaringType! : ((RuntimeMethodInfo)method).ReturnType;

if (returnType == typeof(void))
{
il.Emit(OpCodes.Ldnull);
il.Emit(OpCodes.Ret);
return;
}

if (returnType.IsByRef)
{
// Check for null ref return.
Type elementType = returnType.GetElementType()!;
Label retValueOk = il.DefineLabel();

il.Emit(OpCodes.Dup);
il.Emit(OpCodes.Brtrue_S, retValueOk);
il.Emit(OpCodes.Call, Methods.ThrowHelper_Throw_NullReference_InvokeNullRefReturned());
il.MarkLabel(retValueOk);

if (elementType.IsValueType)
{
LocalBuilder? local_return = il.DeclareLocal(typeof(object));
il.Emit(OpCodes.Ldobj, elementType);
il.Emit(OpCodes.Box, elementType);
il.Emit(OpCodes.Stloc_S, local_return);
il.Emit(OpCodes.Ldloc_S, local_return);
}
else if (elementType.IsPointer)
{
LocalBuilder? local_return = il.DeclareLocal(elementType);
il.Emit(OpCodes.Ldind_Ref);
il.Emit(OpCodes.Stloc_S, local_return);
il.Emit(OpCodes.Ldloc_S, local_return);
il.Emit(OpCodes.Ldtoken, elementType);
il.Emit(OpCodes.Call, Methods.Type_GetTypeFromHandle());
il.Emit(OpCodes.Call, Methods.Pointer_Box());
}
else
{
LocalBuilder? local_return = il.DeclareLocal(elementType);
il.Emit(OpCodes.Ldind_Ref);
il.Emit(OpCodes.Stloc_S, local_return);
il.Emit(OpCodes.Ldloc_S, local_return);
}
}
else if (returnType.IsPointer)
{
il.Emit(OpCodes.Ldtoken, returnType);
il.Emit(OpCodes.Call, Methods.Type_GetTypeFromHandle());
il.Emit(OpCodes.Call, Methods.Pointer_Box());
}
else if (returnType.IsValueType)
{
il.Emit(OpCodes.Box, returnType);
}

il.Emit(OpCodes.Ret);
}

private static class ThrowHelper
{
public static void Throw_NullReference_InvokeNullRefReturned()
{
throw new NullReferenceException(SR.NullReference_InvokeNullRefReturned);
}
}

private struct NullableRefInfo
{
public int ParameterIndex;
public int LocalIndex;
}

private static class Methods
{
private static MethodInfo? s_ThrowHelper_Throw_NullReference_InvokeNullRefReturned;
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(ThrowHelper))]
public static MethodInfo ThrowHelper_Throw_NullReference_InvokeNullRefReturned() =>
s_ThrowHelper_Throw_NullReference_InvokeNullRefReturned ??
(s_ThrowHelper_Throw_NullReference_InvokeNullRefReturned = typeof(ThrowHelper).GetMethod(nameof(ThrowHelper.Throw_NullReference_InvokeNullRefReturned))!);

private static MethodInfo? s_Pointer_Box;
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicMethods, typeof(Pointer))]
public static MethodInfo Pointer_Box() =>
s_Pointer_Box ??
(s_Pointer_Box = typeof(Pointer).GetMethod(nameof(Pointer.Box), new[] { typeof(void*), typeof(Type) })!);

private static MethodInfo? s_Type_GetTypeFromHandle;
public static MethodInfo Type_GetTypeFromHandle() =>
s_Type_GetTypeFromHandle ??
(s_Type_GetTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle), new[] { typeof(RuntimeTypeHandle) })!);
}
}
}
Loading