// This temporary utility fixes an esoteric issue in XNA Framework where deserialization depends on // the order of fields returned by Type.GetFields, but that order changes after Harmony/MonoMod use // reflection to access the fields due to an issue in .NET Framework. // https://twitter.com/0x0ade/status/1414992316964687873 // // This will be removed when Harmony/MonoMod are updated to incorporate the fix. // // Special thanks to 0x0ade for submitting this worokaround! Copy/pasted and adapted from MonoMod. using System; using System.Reflection; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using HarmonyLib; using System.Reflection.Emit; // ReSharper disable once CheckNamespace -- Temporary hotfix submitted by the MonoMod author. namespace MonoMod.Utils { [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Temporary hotfix submitted by the MonoMod author.")] [SuppressMessage("ReSharper", "PossibleNullReferenceException", Justification = "Temporary hotfix submitted by the MonoMod author.")] static class MiniMonoModHotfix { // .NET Framework can break member ordering if using Module.Resolve* on certain members. private static object[] _NoArgs = new object[0]; private static object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null }; private static Type t_RuntimeModule = typeof(Module).Assembly .GetType("System.Reflection.RuntimeModule"); private static PropertyInfo p_RuntimeModule_RuntimeType = typeof(Module).Assembly .GetType("System.Reflection.RuntimeModule") ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); private static Type t_RuntimeType = typeof(Type).Assembly .GetType("System.RuntimeType"); private static PropertyInfo p_RuntimeType_Cache = typeof(Type).Assembly .GetType("System.RuntimeType") ?.GetProperty("Cache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); private static MethodInfo m_RuntimeTypeCache_GetFieldList = typeof(Type).Assembly .GetType("System.RuntimeType+RuntimeTypeCache") ?.GetMethod("GetFieldList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); private static MethodInfo m_RuntimeTypeCache_GetPropertyList = typeof(Type).Assembly .GetType("System.RuntimeType+RuntimeTypeCache") ?.GetMethod("GetPropertyList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); private static readonly ConditionalWeakTable _CacheFixed = new ConditionalWeakTable(); public static void Apply() { var harmony = new Harmony("MiniMonoModHotfix"); harmony.Patch( original: typeof(Harmony).Assembly .GetType("HarmonyLib.MethodBodyReader") .GetMethod("ReadOperand", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix)) ); harmony.Patch( original: typeof(MonoMod.Utils.ReflectionHelper).Assembly .GetType("MonoMod.Utils.DynamicMethodDefinition+<>c__DisplayClass3_0") .GetMethod("<_CopyMethodToDefinition>g__ResolveTokenAs|1", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance), transpiler: new HarmonyMethod(typeof(MiniMonoModHotfix), nameof(ResolveTokenFix)) ); } private static IEnumerable ResolveTokenFix(IEnumerable instrs) { MethodInfo getdecl = typeof(MiniMonoModHotfix).GetMethod(nameof(GetRealDeclaringType)); MethodInfo fixup = typeof(MiniMonoModHotfix).GetMethod(nameof(FixReflectionCache)); foreach (CodeInstruction instr in instrs) { yield return instr; if (instr.operand is MethodInfo called) { switch (called.Name) { case "ResolveType": // type.FixReflectionCache(); yield return new CodeInstruction(OpCodes.Dup); yield return new CodeInstruction(OpCodes.Call, fixup); break; case "ResolveMember": case "ResolveMethod": case "ResolveField": // member.GetRealDeclaringType().FixReflectionCache(); yield return new CodeInstruction(OpCodes.Dup); yield return new CodeInstruction(OpCodes.Call, getdecl); yield return new CodeInstruction(OpCodes.Call, fixup); break; } } } } public static Type GetModuleType(this Module module) { // Sadly we can't blindly resolve type 0x02000001 as the runtime throws ArgumentException. if (module == null || t_RuntimeModule == null || !t_RuntimeModule.IsInstanceOfType(module)) return null; // .NET if (p_RuntimeModule_RuntimeType != null) return (Type)p_RuntimeModule_RuntimeType.GetValue(module, _NoArgs); // The hotfix doesn't apply to Mono anyway, thus that's not copied over. return null; } public static Type GetRealDeclaringType(this MemberInfo member) => member.DeclaringType ?? member.Module?.GetModuleType(); public static void FixReflectionCache(this Type type) { if (t_RuntimeType == null || p_RuntimeType_Cache == null || m_RuntimeTypeCache_GetFieldList == null || m_RuntimeTypeCache_GetPropertyList == null) return; for (; type != null; type = type.DeclaringType) { // All types SHOULD inherit RuntimeType, including those built at runtime. // One might never know what awaits us in the depths of reflection hell though. if (!t_RuntimeType.IsInstanceOfType(type)) continue; CacheFixEntry entry = _CacheFixed.GetValue(type, rt => { CacheFixEntry entryNew = new CacheFixEntry(); object cache; Array properties, fields; // All RuntimeTypes MUST have a cache, the getter is non-virtual, it creates on demand and asserts non-null. entryNew.Cache = cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs); entryNew.Properties = properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList); entryNew.Fields = fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList); _FixReflectionCacheOrder(properties); _FixReflectionCacheOrder(fields); entryNew.NeedsVerify = false; return entryNew; }); if (entry.NeedsVerify && !_Verify(entry, type)) { lock (entry) { _FixReflectionCacheOrder(entry.Properties); _FixReflectionCacheOrder(entry.Fields); } } entry.NeedsVerify = true; } } private static bool _Verify(CacheFixEntry entry, Type type) { object cache; Array properties, fields; // The cache can sometimes be invalidated. // TODO: Figure out if only the arrays get replaced or if the entire cache object gets replaced! if (entry.Cache != (cache = p_RuntimeType_Cache.GetValue(type, _NoArgs))) { entry.Cache = cache; entry.Properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList); entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList); return false; } else if (entry.Properties != (properties = _GetArray(cache, m_RuntimeTypeCache_GetPropertyList))) { entry.Properties = properties; entry.Fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList); return false; } else if (entry.Fields != (fields = _GetArray(cache, m_RuntimeTypeCache_GetFieldList))) { entry.Fields = fields; return false; } else { // Cache should still be the same, no re-fix necessary. return true; } } private static Array _GetArray(object cache, MethodInfo getter) { // Get and discard once, otherwise we might not be getting the actual backing array. getter.Invoke(cache, _CacheGetterArgs); return (Array)getter.Invoke(cache, _CacheGetterArgs); } private static void _FixReflectionCacheOrder(Array orig) where T : MemberInfo { // Sort using a short-lived list. List list = new List(orig.Length); for (int i = 0; i < orig.Length; i++) list.Add((T)orig.GetValue(i)); list.Sort((a, b) => a.MetadataToken - b.MetadataToken); for (int i = orig.Length - 1; i >= 0; --i) orig.SetValue(list[i], i); } private class CacheFixEntry { public object Cache; public Array Properties; public Array Fields; public bool NeedsVerify; } } }