8

I was playing with attributes and reflection when I found a strange case. The following code gave me an exception at runtime when I try to get constructor arguments of custom attributes.

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)
    {
    }
}

The problem seems to be the parameters passed to the Test attribute constructor. If one of them is null, ConstructorArguments throw an exception. The exception is ArgumentException with name as exception message.

Here is the stack trace from ConstructorArguments call:

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()

If I set a non null value to each parameters there is no exception. It seems to only happen with enum array. If I add another parameter such as string and set them to null, there is no problem.

A solution could be to always pass a value such as an empty array but here I would like to keep the ability to pass null value because it has a special meaning in my case.

2
  • Definitely looks like a bug to me. Unclear at the moment whether it's a CLR bug or a Roslyn bug. Will raise the bat signal to Roslyn devs... Commented Sep 15, 2016 at 12:56
  • compiled on linux with dmcs and executed with mono, it works without throwing any exception Commented Sep 17, 2016 at 22:59

3 Answers 3

3

This has to do with the structure of the blob where the custom attribute is specified.

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.

Unfortunately, what happens if you pass an array of enum as null is that enum name is lost.

In terms of native debugging, this is the relevant source code

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

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

And this is how the c.tor parameter is instantiated

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

The problem is that the an enum value is simply represented using the underlying value (basically an int): the CLR implementation (RuntimeType) has to look at the attribute constructor signature to interpret it, but custom attribute signatures differ quite a bit from other types of signatures encoded in a .NET assembly.

More specifically, without a defined encodedArrayType (from GetElementType), the following if becomes false (and enumName remains 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

You can find the .custom instance of the Main from ildasm

in case of

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

it is (notice the FF FF FF FF meaning an array size 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 ) 

while for

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

you see

.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 virtual machine

Finally, you have confirmation that the CLR virtual machine is reading the blob of a custom attribute into an array only when the size is different from -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;
}

In conclusion, in this case the contructor arguments are not instantiated inside C#, so they can be retrieved only from the constructor itself: in fact the custom attribute is created (via CreateCaObject) in the CLR virtual machine by calling its contructor with unsafe pointers (directly to the 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;
    }

Possible bug

The critical point for a possible bug is

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

implemented in

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

maybe the following could be reviewed...

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

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

Proposed Fix

You can find this issue restyled to CoreCLR with a suggested FIX from github.

Sign up to request clarification or add additional context in comments.

Comments

1

In my previous answer I tracked down how the Enum name is lost by the current standard .Net code of mscorlib... and so the cause of this exception

Now I want only show a concrete custom reengineering of the constructor arguments, based on your specific Test enum definition (so the following is not standard enough to be proposed as an actual improvement, but it is only a complementary part of the explanation)

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

    }
}

so results will contain a list of the Test[] arguments used in the constructor.

Comments

0

I suspect it is a .NET bug!

But if you need a workaround you can copy the constructor args to members and access like method.GetCustomAttribute<TestAttribute>().valuesOne etc.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.