From 72b3c9d14314a3f69a0ca9d6ac9de9de1d31943d Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 14 Jul 2021 18:02:13 -0400 Subject: add workaround for Harmony 2.x breaking XNA content pipeline for some assets (#711, #722) --- .../Framework/TemporaryHacks/MiniMonoModHotfix.cs | 176 +++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs (limited to 'src/SMAPI/Framework/TemporaryHacks') diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs new file mode 100644 index 00000000..6814abea --- /dev/null +++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs @@ -0,0 +1,176 @@ +// 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.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.CompilerServices; +using HarmonyLib; + +// 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 readonly object[] _NoArgs = new object[0]; + private static readonly object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null }; + + private static readonly Type t_RuntimeModule = + typeof(Module).Assembly + .GetType("System.Reflection.RuntimeModule"); + + private static readonly PropertyInfo p_RuntimeModule_RuntimeType = + typeof(Module).Assembly + .GetType("System.Reflection.RuntimeModule") + ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + private static readonly Type t_RuntimeType = + typeof(Type).Assembly + .GetType("System.RuntimeType"); + + private static readonly PropertyInfo p_RuntimeType_Cache = + typeof(Type).Assembly + .GetType("System.RuntimeType") + ?.GetProperty("Cache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + private static readonly MethodInfo m_RuntimeTypeCache_GetFieldList = + typeof(Type).Assembly + .GetType("System.RuntimeType+RuntimeTypeCache") + ?.GetMethod("GetFieldList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + + private static readonly 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(Harmony).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; + + _CacheFixed.GetValue(type, rt => + { + + object cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs); + _FixReflectionCacheOrder(cache, m_RuntimeTypeCache_GetPropertyList); + _FixReflectionCacheOrder(cache, m_RuntimeTypeCache_GetFieldList); + + return new object(); + }); + } + } + + private static void _FixReflectionCacheOrder(object cache, MethodInfo getter) where T : MemberInfo + { + // Get and discard once, otherwise we might not be getting the actual backing array. + getter.Invoke(cache, _CacheGetterArgs); + Array orig = (Array)getter.Invoke(cache, _CacheGetterArgs); + + // 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); + } + + } +} -- cgit From 167d5831d12f0b7c58b3533b716f9041f6ae02e7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 23 Jul 2021 20:29:44 -0400 Subject: use unmerged Harmony assembly (#711) Harmony merges Mono.Cecil and MonoMod.Common into its DLL, and keeps some (but not all) of the merged types public. That causes type conflicts in SMAPI's code since it uses both Harmony and Mono.Cecil, and extern aliases break on Linux due to IDE/compiler limitations. This commit uses a custom build of Harmony without the assembly merging, so SMAPI can use and manage Mono.Cecil itself. --- build/0Harmony.dll | Bin 802816 -> 166912 bytes build/common.targets | 3 +++ build/prepare-install-package.targets | 3 +++ .../Framework/TemporaryHacks/MiniMonoModHotfix.cs | 2 +- src/SMAPI/SMAPI.csproj | 3 ++- 5 files changed, 9 insertions(+), 2 deletions(-) (limited to 'src/SMAPI/Framework/TemporaryHacks') diff --git a/build/0Harmony.dll b/build/0Harmony.dll index 1c5b9c09..bab3bb4d 100644 Binary files a/build/0Harmony.dll and b/build/0Harmony.dll differ diff --git a/build/common.targets b/build/common.targets index f5d01606..aed619da 100644 --- a/build/common.targets +++ b/build/common.targets @@ -40,6 +40,9 @@ + + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 0c1a6c1a..88e565f9 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -47,6 +47,9 @@ + + + diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs index 6814abea..9f5819d7 100644 --- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs +++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs @@ -69,7 +69,7 @@ namespace MonoMod.Utils ); harmony.Patch( - original: typeof(Harmony).Assembly + 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)) diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index d36d0d7a..d06e3364 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -20,9 +20,10 @@ - + MonoCecilPackage + -- cgit From e8ad5d0a2404720ec4f8cc75117e62bcc72cd068 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 28 Jul 2021 18:03:49 -0400 Subject: fix Data\Movies error regression when patching dictionary (#711) --- .../Framework/TemporaryHacks/MiniMonoModHotfix.cs | 101 +++++++++++++++++---- 1 file changed, 82 insertions(+), 19 deletions(-) (limited to 'src/SMAPI/Framework/TemporaryHacks') diff --git a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs index 9f5819d7..9d63ab2c 100644 --- a/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs +++ b/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs @@ -8,12 +8,12 @@ // 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.Reflection; -using System.Reflection.Emit; using System.Runtime.CompilerServices; using HarmonyLib; +using System.Reflection.Emit; // ReSharper disable once CheckNamespace -- Temporary hotfix submitted by the MonoMod author. namespace MonoMod.Utils @@ -24,38 +24,38 @@ namespace MonoMod.Utils { // .NET Framework can break member ordering if using Module.Resolve* on certain members. - private static readonly object[] _NoArgs = new object[0]; - private static readonly object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null }; + private static object[] _NoArgs = new object[0]; + private static object[] _CacheGetterArgs = { /* MemberListType.All */ 0, /* name apparently always null? */ null }; - private static readonly Type t_RuntimeModule = + private static Type t_RuntimeModule = typeof(Module).Assembly .GetType("System.Reflection.RuntimeModule"); - private static readonly PropertyInfo p_RuntimeModule_RuntimeType = + private static PropertyInfo p_RuntimeModule_RuntimeType = typeof(Module).Assembly .GetType("System.Reflection.RuntimeModule") ?.GetProperty("RuntimeType", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - private static readonly Type t_RuntimeType = + private static Type t_RuntimeType = typeof(Type).Assembly .GetType("System.RuntimeType"); - private static readonly PropertyInfo p_RuntimeType_Cache = + private static PropertyInfo p_RuntimeType_Cache = typeof(Type).Assembly .GetType("System.RuntimeType") ?.GetProperty("Cache", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - private static readonly MethodInfo m_RuntimeTypeCache_GetFieldList = + private static MethodInfo m_RuntimeTypeCache_GetFieldList = typeof(Type).Assembly .GetType("System.RuntimeType+RuntimeTypeCache") ?.GetMethod("GetFieldList", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - private static readonly MethodInfo m_RuntimeTypeCache_GetPropertyList = + 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(); + private static readonly ConditionalWeakTable _CacheFixed = new ConditionalWeakTable(); public static void Apply() { @@ -143,24 +143,80 @@ namespace MonoMod.Utils if (!t_RuntimeType.IsInstanceOfType(type)) continue; - _CacheFixed.GetValue(type, rt => - { + 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); - object cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs); - _FixReflectionCacheOrder(cache, m_RuntimeTypeCache_GetPropertyList); - _FixReflectionCacheOrder(cache, m_RuntimeTypeCache_GetFieldList); + _FixReflectionCacheOrder(properties); + _FixReflectionCacheOrder(fields); - return new object(); + entryNew.NeedsVerify = false; + return entryNew; }); + + if (entry.NeedsVerify && !_Verify(entry, type)) + { + lock (entry) + { + _FixReflectionCacheOrder(entry.Properties); + _FixReflectionCacheOrder(entry.Fields); + } + } + + entry.NeedsVerify = true; } } - private static void _FixReflectionCacheOrder(object cache, MethodInfo getter) where T : MemberInfo + 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); - Array orig = (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++) @@ -172,5 +228,12 @@ namespace MonoMod.Utils orig.SetValue(list[i], i); } + private class CacheFixEntry + { + public object Cache; + public Array Properties; + public Array Fields; + public bool NeedsVerify; + } } } -- cgit