ILGenerator imposes artificial limitations on otherwise valid IL
#118,238 opened on 2025年7月31日
説明
Description
Currently, if you attempt to use the ILGenerator.Emit(OpCode, MethodInfo) overload on an ILGenerator instance obtained from DynamicMethod.GetILGenerator() with OpCodes.Ldftn or OpCodes.Ldtoken and another DynamicMethod, you will encounter an ArgumentException due to the following "safeguard":
Mono does the same thing in solidarity with CoreCLR:
However, it's not entirely clear why these checks exist in the first place. Both ldftn and ldtoken are perfectly valid operations on a DynamicMethod within the context of another DynamicMethod, i.e., this limitation does not exist at the runtime level, and it is only imposed on the consumer by the refemit library.
My guess is that there was a time, perhaps years ago, when the CLR did not support these operations on DynamicMethods, and this check is a leftover artifact from that period, which appears to have been continuously ported through successive versions: from .NET Framework, to .NET Core, and finally to modern .NET - without anyone questioning why this restriction is still in place.
Reproduction Steps
using System.Reflection;
using System.Reflection.Emit;
DynamicMethod addMethod = new("Add", typeof(int), [typeof(int), typeof(int)], true);
ILGenerator addIl = addMethod.GetILGenerator();
addIl.Emit(OpCodes.Ldarg_0);
addIl.Emit(OpCodes.Ldarg_1);
addIl.Emit(OpCodes.Add);
addIl.Emit(OpCodes.Ret);
DynamicMethod getAddMethodFunctionPointerMethod = new("", typeof(nint), [], true);
ILGenerator getPointerIl = getAddMethodFunctionPointerMethod.GetILGenerator();
getPointerIl.Emit(OpCodes.Ldftn, addMethod);
getPointerIl.Emit(OpCodes.Ret);
Func<nint> getAddMethodFunctionPointer = getAddMethodFunctionPointerMethod.CreateDelegate<Func<nint>>();
Console.WriteLine(getAddMethodFunctionPointer());
Expected behavior
Should print Add's function pointer.
Actual behavior
Throws an ArgumentException on line 13:
getPointerIl.Emit(OpCodes.Ldftn, addMethod);
Regression?
No, this operation has always been prohibited. However, at the very least in the latest versions of CLR, CoreCLR, and Mono, it is actually supported by the runtime, so there's just no good reason for this check to continue to exist.
Known Workarounds
It is possible to register a DynamicMethod within the scope of another DynamicMethod through alternative means (e.g., via reflection or by inserting an unreachable call to the target method somewhere in the generated method body) and then infer its resulting metadata token for use with the ldftn and ldtoken instructions. However, this approach is non-portable and it's highly dependent on the current runtime and its internal implementation details. So, it would be far more preferable for ILGenerator to simply stop discarding valid code.
Configuration
This bug affects every version of .NET Framework, .NET Core, and .NET released to date.
Other information
Here's a complete example of what I'm talking about:
using System.Reflection;
using System.Reflection.Emit;
DynamicMethod addMethod = new("Add", typeof(int), [typeof(int), typeof(int)], true);
ILGenerator addIl = addMethod.GetILGenerator();
addIl.Emit(OpCodes.Ldarg_0);
addIl.Emit(OpCodes.Ldarg_1);
addIl.Emit(OpCodes.Add);
addIl.Emit(OpCodes.Ret);
DynamicMethod getMethodPointerAndHandleMethod = new("", typeof(void), [typeof(nint).MakeByRefType(), typeof(RuntimeMethodHandle).MakeByRefType()]);
ILGenerator il = getMethodPointerAndHandleMethod.GetILGenerator();
Label actualMethodBody = il.DefineLabel();
// The method "call" is here solely to register the "Add" method within the dynamic method's scope,
// avoiding the need for extensive use of reflection. However, Mono and .NET (Fx, Core, etc.)
// create metadata tokens in slightly different manners - this is why we need the ILGenerator APIs
// to function correctly, so we don't have to manufacture these tokens through some cursed means.
il.Emit(OpCodes.Br_S, actualMethodBody);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Call, addMethod);
il.Emit(OpCodes.Pop);
#if MONO
int addMethodMetadataToken = 1;
#else
int addMethodMetadataToken = 0x6000002;
#endif
il.MarkLabel(actualMethodBody);
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldftn, addMethodMetadataToken);
il.Emit(OpCodes.Stind_I);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldtoken, addMethodMetadataToken);
il.Emit(OpCodes.Stobj, typeof(RuntimeMethodHandle));
il.Emit(OpCodes.Ret);
MethodFunc getMethodPointerAndHandle = (MethodFunc)getMethodPointerAndHandleMethod.CreateDelegate(typeof(MethodFunc));
getMethodPointerAndHandle(out nint addFunctionPointer, out RuntimeMethodHandle addHandle);
// Mono 6.12, which is installed on my machine, lacks support for `calli`, so it just
// coredumps whenever it encounters this instruction. If you're using Microsoft's
// fork (any version released after 2021 or so), this won't be a problem, so you
// can safely remove this check.
#if !MONO
unsafe
{
var add1 = (delegate*<int, int, int>)addFunctionPointer;
Console.WriteLine("0x{0:X8}: 40 + 1 == {1}", (nint)add1, add1(40, 1)); // 0x________: 40 + 1 == 41
}
#endif
var add2 = (Func<int, int, int>)Activator.CreateInstance(typeof(Func<int, int, int>), null, addFunctionPointer)!;
Console.WriteLine("{0}: 40 + 2 == {1}", add2.Method, add2(40, 2)); // Int32 Add(Int32, Int32): 40 + 2 == 42
var add3 = (Func<int, int, int>)((MethodInfo)MethodBase.GetMethodFromHandle(addHandle)!).CreateDelegate(typeof(Func<int, int, int>));
Console.WriteLine("{0}: 40 + 3 == {1}", add3.Method, add3(40, 3)); // Int32 Add(Int32, Int32): 40 + 3 == 43
file delegate void MethodFunc(out nint method, out RuntimeMethodHandle handle);
If you run this code on .NET Framework, .NET Core, .NET, Mono (original), Mono (WineHQ), or Mono (Microsoft), you should see something like this:
0x7FC5E8B7B690: 40 + 1 == 41
Int32 Add(Int32, Int32): 40 + 2 == 42
Int32 Add(Int32, Int32): 40 + 3 == 43
This demonstrates that the runtimes associated with the specified core libraries do support ldftn and ldtoken on DynamicMethod instances, and they return valid values in response to those instructions. Therefore, the checks currently present in DynamicILGenerator on CoreCLR and RuntimeILGenerator on Mono are verifiably incorrect and should be removed.