Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unwanted behaviour caused by name conflict of compiler generated members #73

Open
WEGFan opened this issue Feb 23, 2021 · 0 comments
Open
Labels

Comments

@WEGFan
Copy link

WEGFan commented Feb 23, 2021

Description

If a method contains lambdas, compiler will generate a lambda method in <>c.<[methodName]>b__[methodOrdinal]_[lambdaOrdinal], and a cache field in <>c.<>9__[methodOrdinal]_[lambdaOrdinal]. Since the name of the cache field is only based on ordinals, if patch_[className] has the same cache field name as the original class, lambdas may not work as expected after patching.

MonoMod version: 21.1.11.1

Example

The entire project can be downloaded from here, including source code and output files.

Suppose we have ConflictTest/ConflictTest.cs, which is the original class:

using System;

namespace ConflictTest {
    internal class ConflictTest {
        private static void Test1() {
            Action func = () => Console.WriteLine("Test1");
            func();
        }

        private static void Test2() {
            Func<int> func = () => {
                Console.WriteLine("Test2");
                return 0;
            };
            func();
        }

        private static void Test3() {
            Console.WriteLine("Test3");
        }

        private static void Test4() {
            Console.WriteLine("Test4");
        }

        public static void Main(string[] args) {
            Console.WriteLine("=== Test1() ===");
            Test1();

            Console.WriteLine("=== Test2() ===");
            Test2();

            Console.WriteLine("=== Test3() ===");
            Test3();

            Console.WriteLine("=== Test4() ===");
            Test4();
        }
    }
}

and ConflictTest.Mod.mm/ConflictTest.cs, which is the patch that we are going to apply:

using System;
using MonoMod;

namespace ConflictTest {
    internal class patch_ConflictTest {
        [MonoModReplace]
        private static void Test3() {
            Action func = () => Console.WriteLine("patched Test3");
            func();
        }

        [MonoModReplace]
        private static void Test4() {
            Action func = () => Console.WriteLine("patched Test4");
            func();
        }
    }
}

After patching, the program is supposed to output:

=== Test1() ===
Test1
=== Test2() ===
Test2
=== Test3() ===
patched Test3
=== Test4() ===
patched Test4

But actually it outputs:

=== Test1() ===
Test1
=== Test2() ===
Test2
=== Test3() ===
Test1
=== Test4() ===

Unhandled Exception: System.MissingFieldException: Field not found: '<>c.<>9__1_0'.
   at ConflictTest.ConflictTest.Test4()
   at ConflictTest.ConflictTest.Main(String[] args)

Test3() should outputs patched Test3 but it outputs Test1, and Test4() should outputs patched Test4 but the program crashes.

If we put the code of original class into SharpLab (link), we can find that the lambda in Test1() became <>c.<Test1>b__0_0(), and in Test1(), the code first check for the cache field <>c.<>9__0_0, if it is null, then assign <>c.<Test1>b__0_0() to it and execute it.

If we put patch_ConflictTest into SharpLab (link), we can find that the cache field of the lambda in Test3() is also <>c.<>9__0_0, so these two fields conflict and when running the patched program, the cache field is assigned to the method that outputs Test1 when executing Test1(), and when executing Test3(), the cached field is used so the output is same as Test1(). As for Test2() and Test4(), the type of cache field is Func<int> in the original class and Action in patch and patched class, so I guess the casting can't be done and it crashes.

Putting placeholder members before the actual patch methods can solve this because the ordinal of them increase and no longer conflict with original ones.

*** ConflictTest.Mod.mm/ConflictTest.cs
@@ -4,8 +4,12 @@
 namespace ConflictTest {
     internal class patch_ConflictTest {
+        public string PlaceHolder1;
+        public string PlaceHolder2;
+        public string PlaceHolder3;
+        public string PlaceHolder4;

         [MonoModReplace]
         private static void Test3() {
             Action func = () => Console.WriteLine("patched Test3");
             func();
         }

Reference: The pattern of compiler generated names: GeneratedNames.cs and GeneratedNameKind.cs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant