获取带有多个枚举数组的属性构造函数参数时发生异常
本文关键字:参数 构造函数 异常 属性 数组 枚举 获取 | 更新日期: 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
等