Exception on getting attribute constructor arguments with multiple enum arrays

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

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 is with the parameters passed to the Test attribute constructor. If one of them is null, ConstructorArguments throws an exception. The exception is an ArgumentException with name as the exception message.

Here is the stack trace from the 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-zero value for each parameter, there are no exceptions. This seems to only happen with the enum array. If I add another parameter, such as a string, and set them to null, no problem.

The solution may be to always pass a value, such as an empty array, but here I would like to leave the option to pass a null value, since it has a special meaning in my case.

+8
c # system.reflection
source share
3 answers

This is due to the structure of the blob structure, where user attribute e is specified .

The values โ€‹โ€‹of the array begin with an integer indicating the number of elements in the array, then the values โ€‹โ€‹of the element are combined together.

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

The enumeration argument is represented using byte 0x55 , followed by a string specifying the name and assembly of the enumeration type.

Unfortunately, what happens if you pass the enum array as null, this rename name will be lost.

In terms of native debugging, this is the corresponding source code.

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

And so the c.tor instance is created

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

The problem is that the enum value is simply represented using the base value (basically int): the CLR implementation ( RuntimeType ) needs to look at the signature of the attribute constructor to interpret it, but custom attribute signatures are slightly different from other types of signatures encoded in the assembly. NET

In particular, without a specific encodedArrayType (from GetElementType ), the following if it becomes false (and enumName remains zero)

  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 a .custom instance of Main from ildasm

when

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

this (note that FF FF FF FF means array size -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 ) 

and 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 reads the blob of a user attribute into an array only when the size is other than -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, constructor arguments are not created inside C #, therefore, they can only be obtained from the constructor itself: in fact, the user attribute is created (via CreateCaObject ) in the CLR virtual machine, 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 error

The critical point for a possible error 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) 

The following may be considered:

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

Proposed Fix

You can find this problem repeatedly in CoreCLR with the proposed FIX from github.

+2
source share

In my previous answer, I found out how the name Enum is lost according to the current standard of the .sc code mscorlib ... and therefore the reason for this exception

Now I only want to show a specific individual reengineering of the constructor arguments based on your specific definition of the test enumeration (so the following is not standard enough to be offered as an actual improvement, but this is only an additional 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); } } } 

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

+1
source share

I suspect this is a .NET error!

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

0
source share

All Articles