Исключение при получении аргументов конструктора атрибутов с несколькими массивами перечислений

я играл с атрибутами и отражением, когда нашел странный случай. Следующий код дал мне исключение во время выполнения, когда я пытаюсь получить аргументы конструктора пользовательских атрибутов.

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 бросать исключение. Исключение составляет 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, проблем не будет.

решением может быть всегда передавать значение, такое как пустой массив, но здесь я хотел бы сохранить возможность передавать значение null, потому что оно имеет особое значение в моем случае.

3 ответов


это имеет отношение к структура blob, где пользовательский атрибутe указывается.

значения массива начинаются с целого числа, указывающего количество элементов в массиве, затем значения элементов concatentated вместе.

нулевой массив представлен с длиной -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));

проблема в том, что значение перечисления просто представлено с использованием базового значения (в основном int): реализация среды CLR (RuntimeType) должен смотреть на подпись конструктора атрибутов интерпретировать его, но custom атрибут подписей сильно отличаются от других типов сигнатур, закодированных в сборке .NET.

более конкретно, без определена encodedArrayType (от GetElementType), следующее если становится ложным (и 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

вы можете найти .пользовательский экземпляр Main from ildasm

в случае

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

это (обратите внимание на FF FF FF FF означает размер массива of -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 считывает blob пользовательского атрибута в массив только тогда, когда размер отличается от -1

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;
}

в заключение, в этом случае аргументы contructor не создаются внутри C#, поэтому их можно получить только из самого конструктора: фактически создается пользовательский атрибут (через CreateCaObject) в виртуальной машине CLR, вызвав ее contructor с помощью небезопасные указатели (непосредственно 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();

Предложено Исправить

вы можете найти эту проблему рестайлинг в CoreCLR с предложенным исправить из github.


в моем предыдущем ответе я отслеживал, как имя перечисления теряется текущим стандартным .Net-кодом mscorlib... и вот причина этого исключения

Теперь я хочу показать только конкретную пользовательскую реинжиниринг аргументов конструктора, основанную на вашем конкретном определении перечисления тестов (поэтому следующее недостаточно стандартное, чтобы быть предложенным как фактическое улучшение, но это только дополнительная часть объяснения)

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!

но если вам нужно обходное решение, вы можете скопировать args конструктора в члены и получить доступ, как method.GetCustomAttribute<TestAttribute>().valuesOne etc.