Miista / pose

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

`Enum.IsDefined` cannot be called from within `PoseContext.Isolate` #26

Closed Miista closed 8 months ago

Miista commented 8 months ago

Please refer to tonerdo/pose#26

Miista commented 8 months ago

As of right now, no, we cannot call Enum.IsDefined from within PoseContext.Isolate. I get the following stacktrace:

System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.NotSupportedException: Cannot create boxed ByRef-like values.
   at System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type type)
   at stub_newobj_System.ReadOnlySpan`1[System.Char]_.ctor(Char[])
   at impl_System.ReadOnlySpan`1[System.Char]_op_Implicit(Char[])
   at stub_call_System.ReadOnlySpan`1[System.Char]_op_Implicit(Char[])
   at impl_System.IO.StreamWriter_Write(StreamWriter, Char[])
   at stub_callvirt_System.IO.TextWriter_Write(TextWriter, Char[])
   at impl_System.IO.TextWriter_WriteLine(TextWriter)
   at stub_callvirt_System.IO.TextWriter_WriteLine(TextWriter)
   at impl_System.IO.TextWriter_WriteLine(TextWriter, Boolean)
   at stub_callvirt_System.IO.TextWriter_WriteLine(TextWriter, Boolean)
   at impl_System.IO.TextWriter+SyncTextWriter_WriteLine(SyncTextWriter, Boolean)
   at stub_callvirt_System.IO.TextWriter_WriteLine(TextWriter, Boolean)
   at impl_System.Console_WriteLine(Boolean)
   at stub_call_System.Console_WriteLine(Boolean)
   at impl_Pose.Sandbox.Program+<>c_<Main>b__0_2(<>c)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   --- End of inner exception stack trace ---
   at System.Reflection.MethodBaseInvoker.InvokeDirectByRefWithFewArgs(Object obj, Span`1 copyOfArgs, BindingFlags invokeAttr)
   at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Delegate.DynamicInvokeImpl(Object[] args)
   at Pose.PoseContext.Isolate(Action entryPoint, Shim[] shims) in C:\Rider\pose\src\Pose\PoseContext.cs:line 31
   at Pose.Sandbox.Program.Main(String[] args) in C:\Rider\pose\src\Sandbox\Program.cs:line 26

Specifically the following line hints at the culprit:

System.NotSupportedException: Cannot create boxed ByRef-like values.
   at System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(Type type)
Miista commented 8 months ago

My investigation shows that this is caused by attempting to treat Span<T> as a reference type.

The first line in GenerateStubForObjectInitialization calls MakeByRefType if the incoming type is a value type. This causes the remaining code to treat the type as a reference type. Unfortunately, this also causes the wrong IL to be emitted.

If I change the constructor stub generation to emit code as if Span<T> was a value type (as it is!) everything works.

Concern: Does this impact all value type constructors?

No, it doesn't seem like it impacts all value type constructors. I attempted to call new DateTime(2004, 1, 1) from within Isolate. It compiled and executed without issues. So it would seem that this issue is solely caused by the call to new Span<T>. This doesn't change the fact that we need to handle this as there is no guarantee that we won't encounter similar issues for other types in the future.

Miista commented 8 months ago

I found the issue. It turns out that it was completely self-inflicted. During my clean up of Stubs.cs I accidently swapped constructor.DeclaringType.IsValueType for thisType.IsValueType.

TL;DR; Change thisType.IsValueType to constructor.IsValueType.

Detailed example

In the following, the line if (thisType.IsValueType) is what's causing the exception.

The cleaned up code

public static DynamicMethod GenerateStubForObjectInitialization(ConstructorInfo constructor)
{
    var thisType = constructor.DeclaringType ?? throw new Exception($"Method {constructor.Name} does not have a {nameof(MethodBase.DeclaringType)}");

    if (thisType.IsValueType)
    {
        thisType = thisType.MakeByRefType();
    }

    if (thisType.IsValueType) // <-- THIS LINE IS CAUSING THE EXCEPTION
    {
    }
}

In the example, the line just above the offending line transforms the value type into a by-ref value type. This means that we never enter the if statement. It is literally dead code.

The original code, however, used the incoming constructor which does not change into a by-ref value type, thus entering the if statement.

The original code

public static DynamicMethod GenerateStubForObjectInitialization(ConstructorInfo constructor)
{
    var thisType = constructor.DeclaringType ?? throw new Exception($"Method {constructor.Name} does not have a {nameof(MethodBase.DeclaringType)}");

    if (thisType.IsValueType)
    {
        thisType = thisType.MakeByRefType();
    }

    if (constructor.IsValueType) // <-- THIS LINE MAKES EVERYTHING WORK AGAIN!
    {
    }
}
Miista commented 8 months ago

I'm including this in a separate patch release. This means that it will go into v2.0.1.