使用多个枚举数组获取属性构造函数参数时出现异常

Exception when getting attribute constructor arguments with multiple enum arrays

我在玩属性和反射的时候发现了一个奇怪的案例。当我尝试获取自定义属性的构造函数参数时,以下代码在运行时给了我一个异常。

using System;
using System.Reflection;

class Program
{
    [Test(new[] { Test.Foo }, null)]
    static void Main(string[] args)
    {
        var type = typeof(Program);
        var method = type.GetMethod("Main", BindingFlags.Static | BindingFlags.NonPublic);
        var attribute = method.GetCustomAttributesData()[0].ConstructorArguments;

        Console.ReadKey();
    }
}

public enum Test
{
    Foo,
    Bar
}

[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class TestAttribute : Attribute
{
    public TestAttribute(Test[] valuesOne, Test[] valuesTwo)
    {
    }
}

问题似乎是传递给 Test 属性构造函数的参数。如果其中之一为 null,则 ConstructorArguments 抛出异常。异常是 ArgumentExceptionname 作为异常消息。

这是来自 ConstructorArguments 调用的堆栈跟踪:

System.RuntimeTypeHandle.GetTypeByNameUsingCARules(String name, RuntimeModule scope)
System.Reflection.CustomAttributeTypedArgument.ResolveType(RuntimeModule scope, String typeName)
System.Reflection.CustomAttributeTypedArgument..ctor(RuntimeModule scope, CustomAttributeEncodedArgument encodedArg)
System.Reflection.CustomAttributeData.get_ConstructorArguments()

如果我为每个参数设置一个非空值也不例外。它似乎只发生在枚举数组中。如果我再添加一个参数比如string,然后设置为null,就没有问题了。

一个解决方案可能是始终传递一个值,例如空数组,但在这里我想保留传递空值的能力,因为它对我的情况有特殊意义。

我怀疑这是一个 .NET 错误!

但是如果您需要解决方法,您可以将构造函数参数复制到成员并像 method.GetCustomAttribute<TestAttribute>().valuesOne 等那样访问

这与指定的 structure of the blob where the custom attribute 有关。

Array values start with an integer indicating the number of elements in the array, then the item values concatentated together.

A null array is represented using a length of -1.

An enum argument is represented using the byte 0x55 followed by a string specifying the name and assembly of the enum type.

不幸的是,如果将枚举数组作为 null 传递,会发生什么情况,即枚举名称丢失。

原生调试方面,这是相关源码

    else if (encodedType == CustomAttributeEncoding.Array)
    {                
        encodedType = encodedArg.CustomAttributeType.EncodedArrayType;
        Type elementType;

        if (encodedType == CustomAttributeEncoding.Enum)
        {
            elementType = ResolveType(scope, encodedArg.CustomAttributeType.EnumName);
        }

这就是 c.tor 参数的实例化方式

        for (int i = 0; i < parameters.Length; i++)
            m_ctorParams[i] = new CustomAttributeCtorParameter(InitCustomAttributeType((RuntimeType)parameters[i].ParameterType));

问题是枚举值只是使用基础值(基本上是一个 int)来表示: CLR 实现 (RuntimeType) 必须查看 属性构造函数签名 来解释它,但是自定义 属性签名 有很大不同来自 .NET 程序集中编码的其他类型的签名。

更具体地说,没有定义的encodedArrayType(来自GetElementType),下面的if变为false(并且enumName保持为null)

        if (encodedType == CustomAttributeEncoding.Array)
        {
            parameterType = (RuntimeType)parameterType.GetElementType();
            encodedArrayType = CustomAttributeData.TypeToCustomAttributeEncoding(parameterType);
        }

        if (encodedType == CustomAttributeEncoding.Enum || encodedArrayType == CustomAttributeEncoding.Enum)
        {
            encodedEnumType = TypeToCustomAttributeEncoding((RuntimeType)Enum.GetUnderlyingType(parameterType));
            enumName = parameterType.AssemblyQualifiedName;
        }

ILDASM

您可以从 ildasm 中找到 Main 的 .custom 实例

的情况下
[Test(new[] { Test.Bar }, null)]
static void Main(string[] args)

它是(注意 FF FF FF FF 表示 数组大小 -1

.custom instance void TestAttribute::.ctor(valuetype Test[],
                                         valuetype Test[]) = 
( 01 00 01 00 00 00 01 00 00 00 FF FF FF FF 00 00 ) 

而对于

[Test(new[] { Test.Bar }, new Test[] { })]
static void Main(string[] args)

你看

.custom instance void TestAttribute::.ctor(valuetype Test[],
                                          valuetype Test[]) = 
( 01 00 01 00 00 00 01 00 00 00 00 00 00 00 00 00 ) 

CLR 虚拟机

最后,您确认 CLR virtual machine 仅在大小不同于 -1

时才将自定义属性的 blob 读取到数组中
case SERIALIZATION_TYPE_SZARRAY:      
typeArray:
{
    // read size
    BOOL isObject = FALSE;
    int size = (int)GetDataFromBlob(pCtorAssembly, SERIALIZATION_TYPE_I4, nullTH, pBlob, endBlob, pModule, &isObject);
    _ASSERTE(!isObject);

    if (size != -1) {
        CorSerializationType arrayType;
        if (th.IsEnum()) 
            arrayType = SERIALIZATION_TYPE_ENUM;
        else
            arrayType = (CorSerializationType)th.GetInternalCorElementType();

        BASEARRAYREF array = NULL;
        GCPROTECT_BEGIN(array);
        ReadArray(pCtorAssembly, arrayType, size, th, pBlob, endBlob, pModule, &array);
        retValue = ObjToArgSlot(array);
        GCPROTECT_END();
    }
    *bObjectCreated = TRUE;
    break;
}

总而言之,在这种情况下,构造函数参数未在 C# 中实例化,因此只能从构造函数本身检索它们:实际上自定义属性已创建(通过CreateCaObject) 在 CLR 虚拟机中通过使用 不安全指针 调用其构造函数(直接指向 blob

    [MethodImplAttribute(MethodImplOptions.InternalCall)]
    private static unsafe extern Object _CreateCaObject(RuntimeModule pModule, IRuntimeMethodInfo pCtor, byte** ppBlob, byte* pEndBlob, int* pcNamedArgs);
    [System.Security.SecurityCritical]  // auto-generated
    private static unsafe Object CreateCaObject(RuntimeModule module, IRuntimeMethodInfo ctor, ref IntPtr blob, IntPtr blobEnd, out int namedArgs)
    {
        byte* pBlob = (byte*)blob;
        byte* pBlobEnd = (byte*)blobEnd;
        int cNamedArgs; 
        object ca = _CreateCaObject(module, ctor, &pBlob, pBlobEnd, &cNamedArgs);
        blob = (IntPtr)pBlob;
        namedArgs = cNamedArgs;
        return ca;
    }

可能存在错误

可能出现错误的临界点是

            unsafe
            {
                ParseAttributeArguments(
                    attributeBlob.Signature,
                    (int)attributeBlob.Length,
                    ref customAttributeCtorParameters,
                    ref customAttributeNamedParameters,
                    (RuntimeAssembly)customAttributeModule.Assembly);
            }

实施于

FCIMPL5(VOID, Attribute::ParseAttributeArguments, void* pCa, INT32 cCa,
        CaArgArrayREF* ppCustomAttributeArguments,
        CaNamedArgArrayREF* ppCustomAttributeNamedArguments,
        AssemblyBaseObject* pAssemblyUNSAFE)

也许可以审查以下...

    cArgs = (*ppCustomAttributeArguments)->GetNumComponents();

    if (cArgs)
    {        
        gc.pArgs = (*ppCustomAttributeArguments)->GetDirectPointerToNonObjectElements();

建议修复

您可以通过 github.

中的建议 FIX 发现此问题已重新设计为 CoreCLR

在我之前的回答中,我追查了枚举名称是如何被 mscorlib 的当前标准 .Net 代码丢失的...以及此异常的原因

现在我只想根据您的特定测试枚举定义展示构造函数参数的具体自定义重新设计(因此以下内容不够标准,无法作为实际改进提出,但它只是补充部分解释)

var dataCust = method.GetCustomAttributesData()[0];
var ctorParams = dataCust.GetType().GetField("m_ctorParams", BindingFlags.Instance | BindingFlags.NonPublic);
var reflParams = ctorParams.GetValue(dataCust);

var results = new List<Test[]>();
bool a = reflParams.GetType().IsArray;
if (a)
{
    var mya = reflParams as Array;
    for (int i = 0; i < mya.Length; i++)
    {
        object o = mya.GetValue(i);
        ctorParams = o.GetType().GetField("m_encodedArgument", BindingFlags.Instance | BindingFlags.NonPublic);
        reflParams = ctorParams.GetValue(o);
        var array = reflParams.GetType().GetProperty("ArrayValue", BindingFlags.Instance | BindingFlags.Public);
        reflParams = array.GetValue(reflParams);

        if (reflParams != null)
        {
            var internal_array = reflParams as Array;
            var resultTest = new List<Test>();
            foreach (object item in internal_array)
            {
                ctorParams = item.GetType().GetField("m_primitiveValue", BindingFlags.Instance | BindingFlags.NonPublic);
                reflParams = ctorParams.GetValue(item);
                resultTest.Add((Test)long.Parse(reflParams.ToString()));
            }
            results.Add(resultTest.ToArray());
        } else
        {
            results.Add(null);
        } 

    }
}

所以 results 将包含构造函数中使用的 Test[] 个参数的列表。