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.