Miista / pose

Replace any .NET method (including static and non-virtual) with a delegate
MIT License
27 stars 3 forks source link

Shim throws TypeInitializationException in Unity Engine #37

Open sam-ln opened 10 months ago

sam-ln commented 10 months ago

When trying to shim a static method, the execution of PoseContext.Isolate fails with a TypeInitializationException.

The class whose method I'm trying to shim:

public static class SimpleStaticClass
{
    public static double SimpleStaticMethod()
    {
        return 5;
    }
}

My test-class:

public class SimplePoseTest 
{
   [Test]
   public void SimpleTest()
   {
      Shim methodShim = Shim.Replace(() => SimpleStaticClass.SimpleStaticMethod()).With(
         () => (double)20);
      PoseContext.Isolate( () => 
      {
        //doing nothing yet
      }, methodShim);
   }
}

The stack-trace:

System.TypeInitializationException : The type initializer for 'Pose.Helpers.StubHelper' threw an exception.
  ----> System.Exception : Cannot get method GetMethodDescriptor from type DynamicMethod
---
at Pose.IL.MethodRewriter.Rewrite () [0x000b4] in <fdbdd2421b614a2cbd32b6336046a6a9>:0 
  at Pose.PoseContext.Isolate (System.Action entryPoint, Pose.Shim[] shims) [0x00058] in <fdbdd2421b614a2cbd32b6336046a6a9>:0 
  at SimplePoseTest.SimpleTest () [0x0004f] in E:\Unity Projects\Worlds\Misc\PoserUnitTesting\Assets\Tests\SimplePoseTest.cs:12 
  at (wrapper managed-to-native) System.Reflection.RuntimeMethodInfo.InternalInvoke(System.Reflection.RuntimeMethodInfo,object,object[],System.Exception&)
  at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x0006a] in <eef08f56e2e042f1b3027eca477293d9>:0 
--Exception
  at Pose.Helpers.StubHelper..cctor () [0x0001a] in <fdbdd2421b614a2cbd32b6336046a6a9>:0

More details:

The project is using the net48 Pose dll. It is run from within Unity Engine Issue appears in both 2.0 and 2.1 alpha

BTW: @Miista Thanks a lot for reviving this project! 👍

sam-ln commented 10 months ago

This might occur due to a misconfiguration in my project, leading to the dependency System.Reflection.Emit.Lightweight missing. Will investigate further.

Miista commented 10 months ago

I will test it out in the sandbox just to be sure.

UPDATE: So far the below sample executes without exception in the Sandbox.

Sample with target framework net48

namespace Pose.Sandbox
{
    public static class SimpleStaticClass
    {
        public static double SimpleStaticMethod()
        {
            return 5;
        }
    }

    public class Program
    {
        public static async Task<int> GetIntAsync()
        {
            Console.WriteLine("Here");
            return await Task.FromResult(1);
        }

        public static void Main(string[] args)
        {
            Shim methodShim = Shim.Replace(() => SimpleStaticClass.SimpleStaticMethod()).With(
                () => (double)20);
            PoseContext.Isolate( () => 
            {
                //doing nothing yet
            }, methodShim);
        }
    }
}
Miista commented 10 months ago

@sam-ln: Let me know what you find out. Perhaps there is some way we can guard against this.

sam-ln commented 10 months ago

So, the issue doesn't seem to be related to a missing dependency. I can use System.Reflection.Emit.Lightweight.DynamicMethod in the project just fine. The Expection System.Exception : Cannot get method GetMethodDescriptor from type DynamicMethod made me take a look at the DynamicMethod Reference and there is in fact no method GetMethodDescriptor in DynamicMethod. Any thoughts? I also added the info that my project is running from within Unity Engine to this issue as it might be relevant.

sam-ln commented 10 months ago

Oh, I see, it's a private method. https://github.com/Miista/pose/blob/91ac854bdc69f2eef12d3ee877a6703a38213006/src/Pose/Helpers/StubHelper.cs#L10-L14 In this static initializer you check whether DynamicMethod has a method GetMethodDescriptor and there it throws an Exception because for some reason it can't access it.

sam-ln commented 10 months ago

It seems that GetMethodDescriptor in DynamicMethod does have the [SecurityCritical]-Attribute. And beginning with .NET Framework 4, methods with this attribute cannot be retrieved by reflection in a lot of environments (transparent or partially-trusted-code).

Miista commented 10 months ago

@sam-ln That's weird because we have a test for specifically this scenario. Please see StubHelperTests.Can_get_method_pointer.

That said, I can see that the method is actually missing on .NET 5. Never mind. I found it.

@sam-ln You mentioned that methods cannot be retrieved in a lot of environments. Could it be that this is a Unity specific issue? If we could find some way to run the test suite against Unity, we could try it out.

sam-ln commented 10 months ago

I think this only applies for [SecurityCritical] methods. The article I linked says

Transparent code cannot use reflection to access security-critical members, even if the code is fully trusted.

I'm not entirely sure what "transparent" refers to in this context, but I think that this might be the issue.

The .NET environment in Unity (running on Mono) seems to be affected by this restriction, but I'd be surprised if this problem is only Unity-specific.

sam-ln commented 10 months ago

@Miista Sure, I could set up a unity project with your test suite in it if that would help. I guess the easiest way to check for that restriction would be to execute

var methods = typeof(DynamicMethod).GetMethods(BindingFlags.Instance | BindingFlags.NonPublic);

If I run it in a .NET Core solution it shows all methods including GetMethodDescriptor, but if I run it in the Unity environment it only returns the private/internal methods which are not labelled as [SecurityCritical]

Miista commented 10 months ago

@sam-ln What should we do if the method does not exist (as is the case for Unity)? Do we provide a better exception message?

Scripting restrictions for Unity: https://docs.unity3d.com/Manual/ScriptingRestrictions.html

I'm reading through the above link and I spot this:

Unity supports reflection on AOT platforms.

On the same page I can see that Mono does not support ahead-of-time. Could this be the reason why it fails?

That said, I can see that GetMethodDescriptor is missing in the Mono implementation.

Mono source: https://github.com/mono/mono/blob/main/mcs/class/corlib/System.Reflection.Emit/DynamicMethod.cs

The method does exist in the reference source: https://github.com/mono/mono/blob/38b0227c1ce0c53058a5d78d080923435132773a/mcs/class/referencesource/mscorlib/system/reflection/emit/dynamicmethod.cs#L580

We could consider copying the implementation verbatim and provide it ourselves if we find that there is none provided by the platform. Although I'm not really keen on writing this ourselves.

sam-ln commented 10 months ago

@Miista Mono is not used for the AOT platforms (They use IL2CCP for that), so that shouldn't be the issue. It is odd that the function doesn't exist in the Mono implementation, although I can see it in the Rider/ReShaper disassembly(?) Yea, it would be wonderful if we found a workaround for those cases! Still would be great to figure out what exactly the problem is (missing mono implementation? insufficient trust level?, security policy violation?), so we can give a proper Exception as well.

Miista commented 10 months ago

@sam-ln Have you had a look at managed code stripping? Link: https://docs.unity3d.com/Manual/ManagedCodeStripping.html It's referenced from the scripting restrictions as "if [the] compiler can’t infer that the code is used via reflection the code might not exist at runtime."

It looks like it can be disabled by declaring a Link.xml. Ref: https://docs.unity3d.com/Manual/ManagedCodeStripping.html#LinkXMLAnnotation

I'll be honest with you. I haven't worked with Unity before.

Miista commented 9 months ago

@sam-ln Have you had a chance to look more into this?

sam-ln commented 9 months ago

@Miista Good idea regarding the code stripping, this wasn't the issue though (it's disabled in my project).

sam-ln commented 9 months ago

@Miista

I'll be honest with you. I haven't worked with Unity before.

No worries! If you'd like to look into this more, I've set up a demo project for you with Poser imported and some Unit test cases which highlight the issue. Let me know if you need any assistance! https://github.com/sam-ln/poser-unity-issue

Right now, my best guess is that the problem is the missing implementation of GetMethodDescriptor in Mono (what you mentioned earlier)

Miista commented 5 months ago

@sam-ln Did you find a solution or a suitable workaround at least?

sam-ln commented 5 months ago

@Miista Unfortunately not, but I haven't tried since. I think where we left off is that we could write a GetMethodDescriptor replacement for it to work in Mono. Have you had a chance to open it up in Unity and review the problem yourself?