当通过 TypeBuilder.CreateType 实现具有带有 'in' 参数的方法的接口时,抛出 TypeLoadException

When implementing an interface that has a method with 'in' parameter by TypeBuilder.CreateType, TypeLoadException is thrown

在开始之前,这是我关于 SO 的第一个问题。因此,可能存在错误或缺少有关该问题的信息。如果我需要更正某些内容,请告诉我。谢谢


使用 TypeBuilder,我正在构建一个 class 来实现一个包含方法的接口。在使用 ILGenerator 实现该方法后,我调用 TypeBuilder.CreateType() 并且在正常情况下一切顺利。 但是如果该方法包含任何带有 in 修饰符的参数,对于值类型也称为 readonly referenceTypeBuilder.CreateType() throws TypeLoadException("Method 'SomeMethod' ... does not have an implementation.").

TypeLoadException 的通常情况不同,不存在与接口中声明的方法具有相同签名的实现方法,只有当方法包含 in 参数连签名都是一样的。当我删除或将 in 修饰符更改为 refout 时,TypeBuilder.CreateType() 成功地将生成的方法识别为接口中声明的方法的实现,并构建了类型通常。

这是一个完全可编译的示例:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
 
namespace EmitMethodWithInParamTest
{
    public struct StructParam
    {
        public String Data;
    }
 
    public interface ISomeInterface
    {
        Int32 SomeMethod(in StructParam param);
    }
 
    static class EmitExtension
    {
        public static void ReplicateCustomAttributes(this ParameterBuilder paramBuilder, ParameterInfo paramInfo)
        {
            foreach (var attrData in paramInfo.GetCustomAttributesData())
            {
                var ctorArgs = attrData.ConstructorArguments.Select(arg => arg.Value).ToArray();
 
                // Handling variable arguments
                var ctorParamInfos = attrData.Constructor.GetParameters();
                if (ctorParamInfos.Length > 0 &&
                    ctorParamInfos.Last().IsDefined(typeof(ParamArrayAttribute)) &&
                    ctorArgs.Last() is IReadOnlyCollection<CustomAttributeTypedArgument> variableArgs)
                {
                    ctorArgs[ctorArgs.Length - 1] = variableArgs.Select(arg => arg.Value).ToArray();
                }
 
                var namedPropArgs = attrData.NamedArguments.Where(arg => !arg.IsField);
                var namedPropInfos = namedPropArgs.Select(arg => (PropertyInfo)arg.MemberInfo).ToArray();
                var namedPropValues = namedPropArgs.Select(arg => arg.TypedValue.Value).ToArray();
 
                var namedFieldArgs = attrData.NamedArguments.Where(arg => arg.IsField);
                var namedFieldInfos = namedFieldArgs.Select(arg => (FieldInfo)arg.MemberInfo).ToArray();
                var namedFieldValues = namedFieldArgs.Select(arg => arg.TypedValue.Value).ToArray();
 
                var attrBuilder = new CustomAttributeBuilder(attrData.Constructor,
                    ctorArgs, namedPropInfos, namedPropValues, namedFieldInfos, namedFieldValues);
                paramBuilder.SetCustomAttribute(attrBuilder);
            }
        }
    }
 
    class Program
    {
        static Program()
        {
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-us");
        }
 
        static void Main(String[] args)
        {
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            var typeBuilder = moduleBuilder.DefineType("SomeClass",
                TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
                null /*base class*/,
                new[] { typeof(ISomeInterface) });
 
            var methodInfoToImpl = typeof(ISomeInterface).GetMethod(nameof(ISomeInterface.SomeMethod));
            var paramInfos = methodInfoToImpl.GetParameters();
 
            var methodBuilder = typeBuilder.DefineMethod(methodInfoToImpl.Name,
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final,
                CallingConventions.HasThis,
                methodInfoToImpl.ReturnType,
                paramInfos.Select(pi => pi.ParameterType).ToArray());
 
            foreach (var paramInfo in paramInfos)
            {
                // paramInfo.Position is zero-based but DefineParameter requires 1-based index.
                var paramBuilder = methodBuilder.DefineParameter(paramInfo.Position + 1, paramInfo.Attributes, paramInfo.Name);
                if (paramInfo.Attributes.HasFlag(ParameterAttributes.HasDefault))
                {
                    paramBuilder.SetConstant(paramInfo.DefaultValue);
                }
                paramBuilder.ReplicateCustomAttributes(paramInfo);
            }
 
            // Dummy implementation for example. Always throws NotImplementedException.
            var ilGen = methodBuilder.GetILGenerator();
            ilGen.Emit(OpCodes.Newobj, typeof(NotImplementedException).GetConstructor(Type.EmptyTypes));
            ilGen.Emit(OpCodes.Throw);
 
            var builtType = typeBuilder.CreateType();               // <- TypeLoadException("Method 'SomeMethod' in type 'SomeClass' from assembly 'DynamicAssembly, ...' does not have an implementation.") is thrown.
            var generatedObj = (ISomeInterface)Activator.CreateInstance(builtType);
 
            var someParam = new StructParam() { Data = "SomeData" };
            var result = generatedObj.SomeMethod(in someParam);     // <- NotImplementedException expected by dummy implementation if executed.
 
            Console.WriteLine($"Result: {result}");
        }
    }
}

此代码也上传到Pastebin

在深入挖掘这个问题时,我发现in参数有两个自定义属性,InteropServices.InAttributeCompilerServices.IsReadOnlyAttribute。但是当我生成一个没有实现接口的方法时(这正常成功,因为不需要签名匹配),生成方法的in参数只有一个自定义属性,InAttribute.所以我从接口复制了参数的所有自定义属性,但仍然引发了 TypeLoadException。

我已经在 .NET Framework 4.6.1.NET Core 2.2 上用 C# 7.2 and 7.3 测试了这个。所有环境都给了我同样的例外。我在 Windows.

上使用 Visual Studio 2017

有什么我遗漏的或有任何解决方法吗?

提前感谢您的帮助。

写完上面的问题后,我研究了几天在IL中构建的示例代码和CoreCLR源代码的二进制文件,现在我找到了问题和解决方案。

简而言之,return类型的必需和可选自定义修饰符以及每个参数类型都像每个类型一样占用方法签名的一部分,并且必须手动复制。我以为这将通过将 ParameterAttributes.In 传递给 MethodBuilder.DefineParameter 并复制自定义属性 InAttribute 来完成,但这是错误的。

并且,在 inrefout 修饰符中,只有 in 向指定参数发出必需的自定义修饰符。相反,refout 仅用它们的类型本身表示。这就是为什么只有 in 没有按预期工作的原因。

要复制自定义修饰符,需要像这样修改对 TypeBuilder.DefineMethod 的调用:

var methodBuilder = typeBuilder.DefineMethod(methodInfoToImpl.Name,
    MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final,
    CallingConventions.HasThis,
    methodInfoToImpl.ReturnType,
    methodInfoToImpl.ReturnParameter.GetRequiredCustomModifiers(),      // *
    methodInfoToImpl.ReturnParameter.GetOptionalCustomModifiers(),      // *
    paramInfos.Select(pi => pi.ParameterType).ToArray(),
    paramInfos.Select(pi => pi.GetRequiredCustomModifiers()).ToArray(), // *
    paramInfos.Select(pi => pi.GetOptionalCustomModifiers()).ToArray()  // *
    );

新添加带有 // * 的标记行以复制 return/parameter 类型的自定义修饰符。

或者,我们可以通过在调用 DefineMethod 之后调用 MethodBuilder.SetSignature 方法来完成此操作,而无需任何类型和自定义修饰符参数。如果我们决定单独调用SetSignature,我们需要在DefineParameterSetCustomAttributeEquals(Object)SetImplementationFlags、getter之前调用属性 Signature 和许多其他调用内部方法的方法 MethodBuilder.GetMethodSignature() 缓存表示方法签名的字节。

感谢您阅读并给我建议。 :)