summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/TemporaryHacks/MiniMonoModHotfix.cs
blob: 9f5819d71a944a55b1b604bc9683750b27ac47d4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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<Type, object> _CacheFixed = new ConditionalWeakTable<Type, object>();

        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;

                _CacheFixed.GetValue(type, rt =>
                {

                    object cache = p_RuntimeType_Cache.GetValue(rt, _NoArgs);
                    _FixReflectionCacheOrder<PropertyInfo>(cache, m_RuntimeTypeCache_GetPropertyList);
                    _FixReflectionCacheOrder<FieldInfo>(cache, m_RuntimeTypeCache_GetFieldList);

                    return new object();
                });
            }
        }

        private static void _FixReflectionCacheOrder<T>(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<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);
        }

    }
}