Summary
The MonoCustomAttrs.GetCustomAttributes patch in Source/PerformanceFish/System/ReflectionCaching.cs caches zero-length results as Array.Empty<object>(). Mono's System.Attribute.InternalGetCustomAttributes(PropertyInfo, Type, bool) later casts the returned array to the requested attribute type ((T[])result), which throws InvalidCastException because the cached array's runtime type is object[], not T[].
Location
Source/PerformanceFish/System/ReflectionCaching.cs around line 695:
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static object[] CopyCachedAttributes(object[] attributes)
=> attributes.Length != 0 ? (object[])attributes.Clone() : Array.Empty<object>();
Why it bites
Array.Empty<object>() returns a singleton of type object[]. When the calling code does (MyAttribute[])MonoCustomAttrs.GetCustomAttributes(...), the cast fails because typeof(object[]) != typeof(MyAttribute[]). Arrays in .NET are not covariant for this kind of cast.
The non-empty branch is fine because attributes.Clone() preserves the original runtime type.
Reproduction
Any reflection-heavy library that does propertyInfo.GetCustomAttribute<T>() on a property whose attribute set is empty will hit this once the cache returns an object[] for that key.
In my case it crashed Scriban's BuiltinFunctions static initializer (triggered first by RimTalk's prompt rendering), which then poisons that type for the rest of the AppDomain:
System.TypeInitializationException: The type initializer for 'Scriban.Functions.BuiltinFunctions' threw an exception.
---> System.InvalidCastException: Specified cast is not valid.
at System.Attribute.InternalGetCustomAttributes (PropertyInfo, Type, bool)
at System.Attribute.GetCustomAttributes (MemberInfo, Type, bool)
at System.Attribute.GetCustomAttribute (MemberInfo, Type, bool)
at System.Reflection.CustomAttributeExtensions.GetCustomAttribute (MemberInfo, Type)
at System.Reflection.CustomAttributeExtensions.GetCustomAttribute[T] (MemberInfo)
at Scriban.Runtime.ScriptObjectExtensions.Import (...)
at Scriban.Runtime.ScriptObject..ctor (...)
at Scriban.Functions.DateTimeFunctions..ctor ()
at Scriban.Functions.BuiltinFunctions+DefaultBuiltins..ctor ()
at Scriban.Functions.BuiltinFunctions..cctor ()
Disabling the "Caches attributes for reflection lookups" toggle is the workaround that resolves it.
Suggested fix
Return an array of the correct runtime type when the result is empty. The attributeType is already known at cache-store time via the cache key:
private static object[] CopyCachedAttributes(object[] attributes, Type attributeType)
=> attributes.Length != 0
? (object[])attributes.Clone()
: (object[])Array.CreateInstance(attributeType, 0);
Then thread attributeType from the cache key into the copy sites. Alternatively, store typed empty arrays in StoreCompleted/TryGetCachedAttributes instead of normalizing them at copy time.
Versions
- RimWorld 1.6
- Performance Fish (this fork): current Workshop / repo build
- Mono: shipping Unity Mono
Summary
The
MonoCustomAttrs.GetCustomAttributespatch inSource/PerformanceFish/System/ReflectionCaching.cscaches zero-length results asArray.Empty<object>(). Mono'sSystem.Attribute.InternalGetCustomAttributes(PropertyInfo, Type, bool)later casts the returned array to the requested attribute type ((T[])result), which throwsInvalidCastExceptionbecause the cached array's runtime type isobject[], notT[].Location
Source/PerformanceFish/System/ReflectionCaching.csaround line 695:Why it bites
Array.Empty<object>()returns a singleton of typeobject[]. When the calling code does(MyAttribute[])MonoCustomAttrs.GetCustomAttributes(...), the cast fails becausetypeof(object[]) != typeof(MyAttribute[]). Arrays in .NET are not covariant for this kind of cast.The non-empty branch is fine because
attributes.Clone()preserves the original runtime type.Reproduction
Any reflection-heavy library that does
propertyInfo.GetCustomAttribute<T>()on a property whose attribute set is empty will hit this once the cache returns anobject[]for that key.In my case it crashed Scriban's
BuiltinFunctionsstatic initializer (triggered first by RimTalk's prompt rendering), which then poisons that type for the rest of the AppDomain:Disabling the "Caches attributes for reflection lookups" toggle is the workaround that resolves it.
Suggested fix
Return an array of the correct runtime type when the result is empty. The
attributeTypeis already known at cache-store time via the cache key:Then thread
attributeTypefrom the cache key into the copy sites. Alternatively, store typed empty arrays inStoreCompleted/TryGetCachedAttributesinstead of normalizing them at copy time.Versions