summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs')
-rw-r--r--src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs239
1 files changed, 239 insertions, 0 deletions
diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
new file mode 100644
index 00000000..9d63ab2c
--- /dev/null
+++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
@@ -0,0 +1,239 @@
+// 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<Type, CacheFixEntry> _CacheFixed = new ConditionalWeakTable<Type, CacheFixEntry>();
+
+ 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<CodeInstruction> ResolveTokenFix(IEnumerable<CodeInstruction> 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<PropertyInfo>(properties);
+ _FixReflectionCacheOrder<FieldInfo>(fields);
+
+ entryNew.NeedsVerify = false;
+ return entryNew;
+ });
+
+ if (entry.NeedsVerify && !_Verify(entry, type))
+ {
+ lock (entry)
+ {
+ _FixReflectionCacheOrder<PropertyInfo>(entry.Properties);
+ _FixReflectionCacheOrder<FieldInfo>(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<T>(Array orig) where T : MemberInfo
+ {
+ // Sort using a short-lived list.
+ List<T> list = new List<T>(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;
+ }
+ }
+}