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

问题描述 投票:0回答:1

使用

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

不像

TypeLoadException
的通常情况,它实现了与接口中声明的签名相同的方法不存在,只有当方法包含
in
参数甚至签名是相同。当我删除或更改
in
修饰符为
ref
out
时,
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.InAttribute
CompilerServices.IsReadOnlyAttribute
。但是当我生成一个没有实现接口的方法时(这通常会成功,因为不需要签名匹配),生成方法的
in
参数只有一个自定义属性
InAttribute
。所以我从界面复制了参数的所有自定义属性,但仍然引发了 TypeLoadException。

我已经在

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

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

提前感谢您的帮助。

c# pass-by-reference reflection.emit typebuilder
1个回答
5
投票

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

简而言之,返回类型和每个参数类型的必需和可选自定义修饰符像每个类型一样占用方法签名的一部分,并且必须手动复制。我以为这将通过将

ParameterAttributes.In
传递给
MethodBuilder.DefineParameter
并复制自定义属性
InAttribute
来完成,但这是错误的。

并且,在

in
ref
out
修饰符中,只有
in
向指定参数发出所需的自定义修饰符。相反,
ref
out
只用它们的类型本身表示。这就是为什么只有
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
,我们需要在任何
DefineParameter
,
SetCustomAttribute
,
Equals(Object)
,
SetImplementationFlags
, getter of property
Signature
和许多其他调用内部方法
MethodBuilder.GetMethodSignature()的方法之前调用它
表示方法签名的缓存字节。

© www.soinside.com 2019 - 2024. All rights reserved.