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

时间:2019-06-12 14:49:18

标签: c# pass-by-reference reflection.emit typebuilder

在开始之前,这是我关于SO的第一个问题。因此,可能存在任何故障或缺少有关该问题的信息。请让我知道是否需要纠正。谢谢。


我正在构建一个实现接口的类,该接口包含带有TypeBuilder的方法。用ILGenerator实现该方法后,然后我调用TypeBuilder.CreateType(),在正常情况下一切正常。 但是,如果该方法包含带有in修饰符的任何参数(对于值类型也称为只读引用),则TypeBuilder.CreateType()会引发 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 = namedPropArgs.Select(arg => (FieldInfo)arg.MemberInfo).ToArray();
                var namedFieldValues = namedPropArgs.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.2C# 7.2 and 7.3上对此进行了测试。所有环境都给了我同样的例外。我在Windows上使用Visual Studio 2017。

有什么我想念的东西或解决方法吗?

谢谢您的帮助。

1 个答案:

答案 0 :(得分:1)

在写完上面的问题之后,我被调查了几天的IL和CoreCLR源代码的示例代码的二进制文件,现在我发现了问题和解决方法。

简而言之,返回类型和每种参数类型的必需和可选的定制修饰符像每种类型一样都具有方法签名的一部分,必须手动复制。我认为可以通过将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()  // *
    );

带有// *的标记行是新添加的,用于复制返回/参数类型的自定义修饰符。

或者,我们可以通过在没有任何类型和自定义修饰符参数的情况下调用MethodBuilder.SetSignature之后调用DefineMethod方法来做到这一点。如果我们决定分别调用SetSignature,则需要在属性{{1}的获取者{{1},DefineParameterSetCustomAttributeEquals(Object)之前调用它}和其他许多调用内部方法SetImplementationFlags的方法,这些方法缓存表示方法签名的字节。

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