dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.98k stars 4.66k forks source link

System.Reflection.Emit can't call a generic method having a function pointer parameter #100020

Open MrJul opened 6 months ago

MrJul commented 6 months ago

Description

It isn't possible to emit a dynamic assembly using System.Reflection.Emit that emits a call to a generic method having a function pointer as an argument. The code fails with a ArgumentNullException

Reproduction Steps

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

public static class Program
{
    public static void Main()
    {
        var assembly = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("test"), AssemblyBuilderAccess.Run);
        var module = assembly.DefineDynamicModule("test");
        var type = module.DefineType("TestType", TypeAttributes.Class | TypeAttributes.Public);
        var method = type.DefineMethod("TestMethod", MethodAttributes.Public | MethodAttributes.Static, typeof(void), null);

        var m1 = typeof(C).GetMethod("M1")!;
        var m2 = typeof(C).GetMethod("M2")!;

        // void TestMethod() => C.M1<string>(&C.M2);
        var il = method.GetILGenerator();
        il.Emit(OpCodes.Ldftn, m2);
        il.EmitCall(OpCodes.Call, m1.MakeGenericMethod(typeof(string)), null);
        il.Emit(OpCodes.Ret);

        var runtimeType = type.CreateType();
        var runtimeMethod = runtimeType.GetMethod("TestMethod")!;
        runtimeMethod.Invoke(null, null);
    }
}

public static class C
{
    public static unsafe void M1<T>(delegate*<void> action) => action();
    public static void M2() => Console.WriteLine("Called");
}

This program emits a method equivalent to the following C# code:

void TestMethod() => C.M1<string>(&C.M2);

Expected behavior

The method is correctly emitted and runs.

Actual behavior

The EmitCall() fails with a ArgumentNullException.

Stack trace:

System.ArgumentNullException: String reference not set to an instance of a String.
   at System.Reflection.Emit.RuntimeModuleBuilder.GetTypeRefNested(Type type, Module refedModule)
   at System.Reflection.Emit.RuntimeModuleBuilder.GetTypeTokenWorkerNoLock(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.RuntimeModuleBuilder.GetTypeTokenInternal(Type type, Boolean getGenericDefinition)
   at System.Reflection.Emit.SignatureHelper.AddOneArgTypeHelperWorker(Type clsArgument, Boolean lastWasGenericInst)
   at System.Reflection.Emit.SignatureHelper.AddArguments(Type[] arguments, Type[][] requiredCustomModifiers, Type[][] optionalCustomModifiers)
   at System.Reflection.Emit.SignatureHelper.GetMethodSigHelper(Module scope, CallingConventions callingConvention, Int32 cGenericParam, Type returnType, Type[] requiredReturnTypeCustomModifiers, Type[] optionalReturnTypeCustomModifiers, Type[] parameterTypes, Type[][] requiredParameterTypeCustomModifiers, Type[][] optionalParameterTypeCustomModifiers)
   at System.Reflection.Emit.RuntimeModuleBuilder.GetMemberRefSignature(MethodBase method, Int32 cGenericParameters)
   at System.Reflection.Emit.RuntimeModuleBuilder.GetMemberRefToken(MethodBase method, Type[] optionalParameterTypes)
   at System.Reflection.Emit.RuntimeModuleBuilder.GetMethodTokenInternal(MethodBase method, Type[] optionalParameterTypes, Boolean useMethodDef)
   at System.Reflection.Emit.RuntimeILGenerator.EmitCall(OpCode opcode, MethodInfo methodInfo, Type[] optionalParameterTypes)
   at Program.Main() in C:\Dev\EmitCallGenericFuncPtrArgBug\Program.cs:line 20

Regression?

Technically in .NET 7 the emit phase works, which now fails in .NET 8.

That doesn't really matter since the emitted code can't run in .NET 7: it searches for a C.M1(IntPtr) overload, which doesn't exist.

Known Workarounds

Using IntPtr instead of a function pointer parameter works.

Configuration

.NET 8.0.2 Windows 11 22631.3296 x64

Other information

Remarks:

The GetTypeRefNested method probably isn't meant to be called for function pointers, as it seems to be assuming that a proper type (not a function pointer) was passed, here:

https://github.com/dotnet/runtime/blob/ffe7e26ccc29f5ca1737dd7d84fa98b1b7c10d5f/src/coreclr/System.Private.CoreLib/src/System/Reflection/Emit/RuntimeModuleBuilder.cs#L208

FullName is null for a function pointer, causing the GetTypeRef to fail: https://github.com/dotnet/runtime/blob/ffe7e26ccc29f5ca1737dd7d84fa98b1b7c10d5f/src/coreclr/vm/commodule.cpp#L42

dotnet-policy-service[bot] commented 6 months ago

Tagging subscribers to this area: @dotnet/area-system-reflection-emit See info in area-owners.md if you want to be subscribed.

steveharter commented 1 month ago

The method

public static unsafe void M1<T>(delegate*<void> action) => action();

uses an unnecessary generic.

Changing that to

public static unsafe void M1(delegate*<void> action) => action();

and

il.EmitCall(OpCodes.Call, m1.MakeGenericMethod(typeof(string)), null);

to

il.EmitCall(OpCodes.Call, m1, null);

works for me.

tannergooding commented 1 month ago

@steveharter, I think the principle here is that the code should work regardless of the generic being unnecessary.

A more complex example that uses the generic could be created and the same issue would still exist, that the API cannot be called.

Consider for example:

public static unsafe T M1<T>(delegate*<T> func) => func();
steveharter commented 1 month ago

This is indeed an issue, but moving to v10.

The workaround is to use IntPtr instead of the strongly-typed function pointer:

    public static unsafe void M1<T>(IntPtr action) => ((delegate*<void>)action)();

The issue is the code in SignatureHelper.AddOneArgTypeHelperWorker doesn't know how to add a signature, only type tokens and a function pointer needs to be a signature every time it is used (there is no token). My WIP for this is at https://github.com/steveharter/runtime/tree/Issue100020. Somewhat unrelated, that branch also adds support for Ldftn+Call since we now can obtain the return type and parameter types from a function pointer, although we need to verify we want to support that. Normally, one would use Ldftn+CallI where il.EmitCallI() requires the signature be specified instead of obtaining from a MethodInfo.

steveharter commented 1 month ago

See also https://github.com/dotnet/runtime/issues/75348 for other remaining function pointer work around Invoke.