Skip to content

InvalidCastException in MonoCustomAttrs.GetCustomAttributes patch when caching zero-length attribute arrays #9

Description

@DarkByteZero

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions