csharp/71/Ryder/Ryder/Helpers.cs

Helpers.cs
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[astembly: InternalsVisibleTo("Ryder.Tests, PublicKey = 002400000480000094000000060200000024000052534131000400000100010065c2fa05ca964cf3a05f75ccd6e74e080fb7abcd62427d94536cac963da394d9ac3e58ebccaccf52b2f4fe15f77f701593db80a894a4bcab3a832fa4a280623f88396c99b467e23e60040ddaa1382396f8a893e32c7df79d2a338715f733ee0a1fe8c5b10ef252a97953f9d4d7f1ee49e553a8a1080df297ebfd4b59cf03cfb3")]

namespace Ryder
{
    /// 
    ///   Static clast that provides useful helpers to the  sub-clastes.
    /// 
    internal static clast Helpers
    {
        private const BindingFlags PUBLIC_STATIC = BindingFlags.Public | BindingFlags.Static;
        private const BindingFlags PUBLIC_INSTANCE = BindingFlags.Public | BindingFlags.Instance;
        private const BindingFlags ALL_INSTANCE = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
        private const BindingFlags NON_PUBLIC_INSTANCE = BindingFlags.NonPublic | BindingFlags.Instance;

        internal static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
        internal static readonly bool IsLinux   = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);

        internal static readonly bool IsARM;
        internal static readonly int PatchSize;

        private static Exception UnsupportedArchitecture => new PlatformNotSupportedException("Architecture not supported.");

        static Helpers()
        {
            switch (RuntimeInformation.ProcessArchitecture)
            {
                case Architecture.Arm:
                    PatchSize = 8;
                    IsARM = true;
                    break;
                case Architecture.Arm64:
                    PatchSize = 12;
                    IsARM = true;
                    break;
                case Architecture.X86:
                    PatchSize = 6;
                    IsARM = false;
                    break;
                case Architecture.X64:
                    PatchSize = 12;
                    IsARM = false;
                    break;
            }
        }

        private static readonly Func GetUninitializedObject
            = typeof(RuntimeHelpers)
                .GetMethod(nameof(GetUninitializedObject), PUBLIC_STATIC)?
                .CreateDelegate(typeof(Func)) as Func;

        private static readonly Action PrepareMethod
            = typeof(RuntimeHelpers)
                .GetMethod(nameof(PrepareMethod), new[] { typeof(RuntimeMethodHandle) })?
                .CreateDelegate(typeof(Action)) as Action;

        #region Emitted IL
        private static Func FindMethodHandleOfDynamicMethod;

        private static Func MakeFindMethodHandleOfDynamicMethod()
        {
            // Generate the "FindMethodHandleOfDynamicMethod" delegate.
            DynamicMethod findMethodHandle = new DynamicMethod(
                nameof(FindMethodHandleOfDynamicMethod),
                typeof(RuntimeMethodHandle),
                new[] { typeof(DynamicMethod) },
                typeof(DynamicMethod), true);

            ILGenerator il = findMethodHandle.GetILGenerator(16);

            il.Emit(OpCodes.Ldarg_0);

            MethodInfo getMethodDescriptor = typeof(DynamicMethod)
                .GetMethod("GetMethodDescriptor", NON_PUBLIC_INSTANCE);

            if (getMethodDescriptor != null)
            {
                il.Emit(OpCodes.Callvirt, getMethodDescriptor);
            }
            else
            {
                FieldInfo handleField = typeof(DynamicMethod).GetField("m_method", NON_PUBLIC_INSTANCE)
                                     ?? typeof(DynamicMethod).GetField("mhandle", NON_PUBLIC_INSTANCE)
                                     ?? typeof(DynamicMethod).GetField("m_methodHandle", NON_PUBLIC_INSTANCE);

                il.Emit(OpCodes.Ldfld, handleField);
            }

            il.Emit(OpCodes.Ret);

            return findMethodHandle.CreateDelegate(typeof(Func))
                as Func;
        }

        private static Func GetMethodHandle;

        private static Func MakeGetMethodHandle()
        {
            // Generate the "GetMethodHandle" delegate.
            DynamicMethod getMethodHandle = new DynamicMethod(
                nameof(GetMethodHandle),
                typeof(RuntimeMethodHandle),
                new[] { typeof(MethodBase) },
                typeof(RuntimeMethodHandle), true);

            ILGenerator il = getMethodHandle.GetILGenerator(16);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Callvirt, typeof(MethodBase).GetProperty("MethodHandle", PUBLIC_INSTANCE).GetMethod);
            il.Emit(OpCodes.Ret);

            return getMethodHandle.CreateDelegate(typeof(Func))
                as Func;
        }

        private static Func GetFunctionPointer;

        private static Func MakeGetFunctionPointer()
        {
            // Generate the "GetFunctionPointer" delegate.
            DynamicMethod getFunctionPointer = new DynamicMethod(
                nameof(GetFunctionPointer),
                typeof(IntPtr),
                new[] { typeof(RuntimeMethodHandle) },
                typeof(RuntimeMethodHandle), true);

            ILGenerator il = getFunctionPointer.GetILGenerator(16);

            il.Emit(OpCodes.Ldarga_S, (short)0);
            il.Emit(OpCodes.Call, typeof(RuntimeMethodHandle).GetMethod(nameof(GetFunctionPointer), PUBLIC_INSTANCE));
            il.Emit(OpCodes.Ret);

            return getFunctionPointer.CreateDelegate(typeof(Func))
                as Func;
        }
        #endregion

        /// 
        ///   Returns a  array that corresponds to asm instructions
        ///   of a JMP to the  pointer.
        /// 
        public static byte[] GetJmpBytes(IntPtr destination)
        {
            switch (RuntimeInformation.ProcessArchitecture)
            {
                case Architecture.Arm:
                {
                    // LDR PC, [PC, #-4]
                    // $addr
                    byte[] result = new byte[8];

                    result[0] = 0x04;
                    result[1] = 0xF0;
                    result[2] = 0x1F;
                    result[3] = 0xE5;

                    BitConverter.GetBytes(destination.ToInt32()).CopyTo(result, 4);

                    return result;
                }

                case Architecture.Arm64:
                {
                    // LDR PC, [PC, #-4]
                    // $addr
                    byte[] result = new byte[12];

                    result[0] = 0x04;
                    result[1] = 0xF0;
                    result[2] = 0x1F;
                    result[3] = 0xE5;

                    BitConverter.GetBytes(destination.ToInt64()).CopyTo(result, 4);

                    return result;
                }

                case Architecture.X64:
                {
                    // movabs rax,$addr
                    // jmp rax
                    byte[] result = new byte[12];

                    result[0] = 0x48;
                    result[1] = 0xB8;
                    result[10] = 0xFF;
                    result[11] = 0xE0;

                    BitConverter.GetBytes(destination.ToInt64()).CopyTo(result, 2);

                    return result;
                }

                case Architecture.X86:
                {
                    // push $addr
                    // ret
                    byte[] result = new byte[6];

                    result[0] = 0x68;
                    result[5] = 0xC3;

                    BitConverter.GetBytes(destination.ToInt32()).CopyTo(result, 1);

                    return result;
                }

                default:
                    throw UnsupportedArchitecture;
            }
        }

        /// 
        ///   Returns the  corresponding to the specified .
        /// 
        public static RuntimeMethodHandle GetRuntimeMethodHandle(this MethodBase method)
        {
            if (method is DynamicMethod dynamicMethod)
            {
                var findMethodHandle = FindMethodHandleOfDynamicMethod
                                    ?? (FindMethodHandleOfDynamicMethod = MakeFindMethodHandleOfDynamicMethod());

                return findMethodHandle(dynamicMethod);
            }

            var getMethodHandle = GetMethodHandle
                               ?? (GetMethodHandle = MakeGetMethodHandle());

            return getMethodHandle(method);
        }

        /// 
        ///   Returns an  pointing to the start of the method's jitted body.
        /// 
        public static IntPtr GetMethodStart(this RuntimeMethodHandle handle)
        {
            var getFunctionPointer = GetFunctionPointer
                                  ?? (GetFunctionPointer = MakeGetFunctionPointer());

            return getFunctionPointer(handle);
        }

        /// 
        ///   Attempts to run the specified  through the JIT compiler,
        ///   avoiding some unexpected behavior related to an uninitialized method.
        /// 
        public static bool TryPrepareMethod(MethodBase method, RuntimeMethodHandle handle)
        {
            // First, try the good ol' RuntimeHelpers.PrepareMethod.
            if (PrepareMethod != null)
            {
                PrepareMethod(handle);
                return true;
            }

            // No chance, we gotta go lower.
            // Invoke the method with uninitialized arguments.
            object sender = null;

            object[] GetArguments(ParameterInfo[] parameters)
            {
                object[] args = new object[parameters.Length];

                for (int i = 0; i < parameters.Length; i++)
                {
                    ParameterInfo param = parameters[i];

                    if (param.HasDefaultValue)
                        args[i] = param.DefaultValue;
                    else if (param.ParameterType.GetTypeInfo().IsValueType)
                        args[i] = Activator.CreateInstance(param.ParameterType);
                    else
                        args[i] = null;
                }

                return args;
            }

            if (!method.IsStatic)
            {
                // Gotta make the instance
                Type declaringType = method.DeclaringType;

                if (declaringType.GetTypeInfo().IsValueType)
                {
                    sender = Activator.CreateInstance(declaringType);
                }
                else if (declaringType.GetTypeInfo().IsAbstract)
                {
                    // Overkill solution: Find a type in the astembly that implements the declaring type,
                    // and use it instead.
                    throw new InvalidOperationException("Cannot manually JIT a method");
                }
                else if (GetUninitializedObject != null)
                {
                    sender = GetUninitializedObject(declaringType);
                }
                else
                {
                    /* TODO
                     * Since I just made the whole 'gotta JIT the method' step mandatory
                     * in the MethodRedirection ctor, i should make sure this always returns true.
                     * That means looking up every type for overriding types for the throwing step above,
                     * and testing every possible constructor to create the instance.
                     * 
                     * Additionally, if we want to go even further, we can repeat this step for every
                     * single argument of the ctor, thus making sure that we end up having an actual clast.
                     * In this case, unless the user wants to instantiate an abstract clast with no overriding clast,
                     * everything'll work. HOWEVER, performances would be less-than-ideal. A simple Redirection
                     * may mean scanning the astembly a dozen times for overriding types, calling their constructors
                     * hundreds of times, knowing that all of them will be slow (Reflection + Try/Catch blocks aren't
                     * perfs-friendly).
                     */
                    ConstructorInfo ctor = declaringType.GetConstructor(Type.EmptyTypes);

                    if (ctor != null)
                    {
                        sender = ctor.Invoke(null);
                    }
                    else
                    {
                        ConstructorInfo[] ctors = declaringType.GetConstructors(ALL_INSTANCE);

                        Array.Sort(ctors, (a, b) => a.GetParameters().Length.CompareTo(b.GetParameters().Length));

                        ctor = ctors[0];

                        try
                        {
                            sender = ctor.Invoke(GetArguments(ctor.GetParameters()));
                        }
                        catch (TargetInvocationException)
                        {
                            // Nothing we can do, give up.
                            return false;
                        }
                    }
                }
            }

            try
            {
                method.Invoke(sender, GetArguments(method.GetParameters()));
            }
            catch (TargetInvocationException)
            {
                // That's okay.
            }

            return true;
        }

        /// 
        ///   Returns whether or not the specified  has
        ///   already been compiled by the JIT.
        /// 
        public static unsafe bool HasBeenCompiled(this IntPtr methodStart)
        {
            // Yes this function is unsafe, sorry. If anyone knows how to bitcast longs to ulongs, then I'll take it.
            // Note: this code has only been tested on x86_64. If anyone can confirm it works on ARM / ARM64 / i386, that'd be great.

            switch (RuntimeInformation.ProcessArchitecture)
            {
                // According to this:
                //   https://github.com/dotnet/coreclr/blob/master/Docameentation/botr/method-descriptor.md
                // Uncompiled methods will have have specific 'stubs' bodies with common patterns.
                //
                // Therefore, we simply want to know if any of these patterns can be seen.
                //
                // Note that theve values change depending on the architecture.
                case Architecture.X64:
                    {
                        ushort* code = (ushort*)methodStart;

                        if (code[0] == 0xBA49)
                            // Stub precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/i386/stublinkerx86.h#L502
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/i386/stublinkerx86.h#L553
                            return false;

                        goto i386Common;
                    }

                case Architecture.X86:
                    {
                        byte* code = (byte*)methodStart;

                        if (code[5] == 0xB8)
                            // Stub precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/i386/stublinkerx86.h#L502
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/i386/stublinkerx86.h#L555
                            return false;

                        goto i386Common;
                    }

                i386Common:
                    {
                        byte* code = (byte*)methodStart;

                        if (code[0] == 0xE9)
                            // Fixup precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/i386/stublinkerx86.h#L723
                            return false;

                        return true;
                    }

                case Architecture.Arm:
                    {
                        ushort* code = (ushort*)methodStart;

                        if (code[0] == 0xf8df && code[1] == 0xc008 && code[2] == 0xf8df && code[3] == 0xf000)
                            // Stub precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/arm/stubs.cpp#L714
                            return false;

                        if (code[0] == 0x46fc && code[1] == 0xf8df && code[2] == 0xf004)
                            // Fixup precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/arm/stubs.cpp#L782
                            return false;

                        return true;
                    }

                case Architecture.Arm64:
                    {
                        uint* code = (uint*)methodStart;

                        if (code[0] == 0x10000089 && code[1] == 0xA940312A && code[2] == 0xD61F0140)
                            // Stub precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/arm64/stubs.cpp#L573
                            return false;

                        if (code[0] == 0x1000000C && code[1] == 0x5800006B && code[2] == 0xD61F0160)
                            // Fixup precode:
                            //   https://github.com/dotnet/coreclr/blob/aff5a085543f339a24a5e58f37c1641394155c45/src/vm/arm64/stubs.cpp#L639
                            //   https://github.com/dotnet/coreclr/blob/30f0be906507bef99d951efc5eb9a1664bde9ddd/src/vm/arm64/cgencpu.h#L674
                            return false;

                        return true;
                    }

                default:
                    throw UnsupportedArchitecture;
            }
        }

        private const string LIBSYSTEM = "libSystem.dylib";
        private const string KERNEL32  = "kernel32.dll";
        private const string LIBC      = "libc.so.6";

        [DllImport(KERNEL32)]
        internal static extern bool VirtualProtect(IntPtr lpAddress, UIntPtr dwSize, int flNewProtect, out int lpflOldProtect);

        [DllImport(LIBC, CallingConvention = CallingConvention.Cdecl, SetLastError = true, EntryPoint = "mprotect")]
        internal static extern int LinuxProtect(IntPtr start, ulong len, int prot);

        [DllImport(LIBC, CallingConvention = CallingConvention.Cdecl, SetLastError = true, EntryPoint = "getpagesize")]
        internal static extern long LinuxGetPageSize();

        [DllImport(LIBSYSTEM, CallingConvention = CallingConvention.Cdecl, SetLastError = true, EntryPoint = "mprotect")]
        internal static extern int OsxProtect(IntPtr start, ulong len, int prot);

        [DllImport(LIBSYSTEM, CallingConvention = CallingConvention.Cdecl, SetLastError = true, EntryPoint = "getpagesize")]
        internal static extern long OsxGetPageSize();

        internal static void AllowRW(IntPtr address)
        {
            if (IsARM)
                return;

            if (IsWindows)
            {
                if (VirtualProtect(address, new UIntPtr(1), 0x40 /* PAGE_EXECUTE_READWRITE */, out var _))
                    return;

                goto Error;
            }

            long pagesize  = IsLinux ? LinuxGetPageSize() : OsxGetPageSize();
            long start     = address.ToInt64();
            long pagestart = start & -pagesize;

            int buffsize = IntPtr.Size == sizeof(int) ? 6 : 12;

            if (IsLinux && LinuxProtect(new IntPtr(pagestart), (ulong) (start + buffsize - pagestart), 0x7 /* PROT_READ_WRITE_EXEC */) == 0)
                return;
            if (!IsLinux && OsxProtect(new IntPtr(pagestart), (ulong) (start + buffsize - pagestart), 0x7 /* PROT_READ_WRITE_EXEC */) == 0)
                return;

            Error:
            throw new Exception($"Unable to make method memory readable and writable. Error code: {Marshal.GetLastWin32Error()}");
        }
    }
}