获取带有多个枚举数组的属性构造函数参数时发生异常

本文关键字:参数 构造函数 异常 属性 数组 枚举 获取 | 更新日期: 2023-09-27 18:07:04

当我发现一个奇怪的情况时,我正在玩属性和反射。当我试图获取自定义属性的构造函数参数时,下面的代码在运行时给了我一个异常。

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属性构造函数的参数。如果其中一个为空,ConstructorArguments抛出异常。异常为ArgumentException,异常消息为name

下面是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()

如果我为每个参数设置一个非空值,则没有例外。它似乎只发生在enum数组中。如果我添加另一个参数,如string并将它们设置为null,则没有问题。

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

获取带有多个枚举数组的属性构造函数参数时发生异常

这与指定自定义属性的blob的结构有关。

数组值以整数开头,表示元素个数在数组中,然后将项值连接在一起。

null数组的长度为-1。

枚举实参用字节0x55 后跟表示指定名称的字符串和enum类型的汇编。

不幸的是,如果将枚举数组作为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));

问题在于enum的值只是用底层值(基本上是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虚拟机仅在大小与-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上找到一个建议的修复,将这个问题重新设计为CoreCLR。

在我之前的回答中,我追踪了Enum名称如何被mscorlib的当前标准.Net代码丢失…那么这个异常的原因

现在我只想显示构造函数参数的具体自定义重构,基于您特定的Test enum定义(因此以下内容不足以作为实际改进而提出,但它只是解释的补充部分)

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[]参数列表。

我怀疑这是一个。net bug!

但是如果你需要一个解决方法,你可以复制构造函数的参数到成员和访问像method.GetCustomAttribute<TestAttribute>().valuesOne