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

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

Open
MrJul opened this issue Mar 20, 2024 · 5 comments

Comments

@MrJul
Copy link

MrJul commented Mar 20, 2024

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:

  • This works if the called method isn't generic.
  • This works with a DynamicMethod instead of a dynamic assembly.

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:

FullName is null for a function pointer, causing the GetTypeRef to fail:

COMPlusThrow(kArgumentNullException, W("ArgumentNull_String"));

Copy link
Contributor

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

@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Mar 20, 2024
@steveharter steveharter self-assigned this Mar 21, 2024
@steveharter steveharter added bug and removed untriaged New issue has not been triaged by the area owner labels Mar 21, 2024
@steveharter steveharter added this to the 9.0.0 milestone Mar 21, 2024
@steveharter
Copy link
Member

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.

@steveharter steveharter closed this as not planned Won't fix, can't repro, duplicate, stale Aug 2, 2024
@tannergooding
Copy link
Member

@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 steveharter reopened this Aug 2, 2024
@steveharter
Copy link
Member

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 steveharter modified the milestones: 9.0.0, 10.0.0 Aug 6, 2024
@steveharter
Copy link
Member

See also #75348 for other remaining function pointer work around Invoke.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants