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

Direct Call Invoke #172

Open
ds5678 opened this issue Sep 15, 2024 · 2 comments
Open

Direct Call Invoke #172

ds5678 opened this issue Sep 15, 2024 · 2 comments
Assignees
Labels
generation Related to assembly generation
Milestone

Comments

@ds5678
Copy link
Collaborator

ds5678 commented Sep 15, 2024

Directly invoking method calls would enable use to avoid the performance overhead of using il2cpp_runtime_invoke. However, this comes at the cost of having to catch native exceptions ourselves.

@ds5678 ds5678 added the generation Related to assembly generation label Sep 15, 2024
@ds5678 ds5678 added this to the 2.0.0 milestone Sep 15, 2024
@BadRyuner
Copy link

Native exceptions on Windows can be caught without cost.
VectoredExceptionHandler is called when C++ code calls _CxxThrowException
This trick won't work on Linux.

It works, I've tried throwing my Exception in C++ code this way, intercepting like this and throwing my Managed Exception wrapper.
I would try this with some Il2Cpp game, but BepInEx crashes if you use the latest version of Il2CppInterop.Generator, because BepInEx uses AssemblyDefiniotion from Mono.Cecil, not AsmResolver. But in theory it should work with Il2cpp as well.
Example handler:

static partial class ExceptionHandler
{
    [MethodImpl(MethodImplOptions.NoInlining)]
    private static unsafe void InitializeWinExceptionHandler()
    {
        Win32.AddVectoredExceptionHandler(1, &Win32.Win32VehHandler);
    }

    static unsafe class Win32
    {
        internal static uint Win32VehHandler(ref ExceptionPointers exceptionPointers)
        {
            var exceptionInformation = exceptionPointers.ExceptionRecord->ExceptionInformation;
            if (exceptionInformation.Length >= 3 && exceptionInformation[0] == 0x19930520) // win c++ magic number
            {
                // exceptionInformation[1] ExceptionObject. Should be Il2CppSystem.Il2CppException
                // exceptionInformation[2] ThrowInfo. Contains ptr to ExceptionObject RTTI, attributes, etc.
                // exceptionInformation[3] on x64 (maybe): ModuleHandle. Used for rva_to_va conversion
                var exception = exceptionInformation[1];

                if (exception == 0)
                    return 0; // continue search

                try
                {   // somehow check is exception is Il2CppObject or random c++ exception
                    var result = IL2CPP.il2cpp_object_get_class(exception);
                    if (result == IntPtr.Zero) // it`s not a Il2CppException
                        return 0; // continue search

                    // it`s a Il2CppException, so...
                    throw new Il2CppInterop.Runtime.Il2CppException(exception);
                    // will quickly pass control to catch(Il2CppException) in managed code.
                }
                // it`s not a Il2CppException
                catch
                {
                    return 0; // continue search
                }
            }

            return 0; // continue search
        }

        [DllImport("Kernel32.dll", SetLastError = true)]
        internal static extern int AddVectoredExceptionHandler(uint first, delegate* <ref ExceptionPointers, uint> address);

        [StructLayout(LayoutKind.Sequential)]
        internal struct ExceptionPointers
        {
            public ExceptionRecord* ExceptionRecord;
            public byte* ContextRecord; // Registers
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct ExceptionRecord
        {
            public int ExceptionCode;
            public int ExceptionFlags;
            public ExceptionRecord* InnerExceptionRecord;
            public void* ExceptionAddress;
            public nint NumberParameters;
            private nint _exceptionInformation;

            public Span<nint> ExceptionInformation
            {
                get
                {
                    if (NumberParameters == 0)
                        return Array.Empty<nint>();
                    return new Span<nint>(Unsafe.AsPointer(ref _exceptionInformation), (int)NumberParameters);
                }
            }
        }
    }
}

So we can now call Il2cpp methods directly via calli and handle exceptions via c# try catch.

@js6pak js6pak self-assigned this Jan 12, 2025
@js6pak
Copy link
Member

js6pak commented Jan 12, 2025

@BadRyuner I've already had a working windows POC for a while:

internal static partial class Il2CppExceptionHelper
{
    [SupportedOSPlatform("windows")]
    public static unsafe partial class Windows
    {
        private static ReadOnlySpan<byte> ExceptionTypeName => ".?AUIl2CppExceptionWrapper@@"u8;

        public static bool Filter(Exception exception)
        {
            if (exception is not SEHException) return false;

            var exceptionRecord = GetExceptionRecord();

            if (exceptionRecord->ExceptionCode != EXCEPTION_MSVC || exceptionRecord->NumberParameters < 3)
            {
                return false;
            }

            // Based on wine's _is_exception_typeof, licensed under LGPL v2.1+
            var @base = exceptionRecord->NumberParameters >= 4 ? exceptionRecord->ExceptionInformation[3] : 0;

            var et = (cxx_exception_type*)exceptionRecord->ExceptionInformation[2];
            var tit = (cxx_type_info_table*)(@base + et->type_info_table);

            for (var i = 0; i < tit->count; i++)
            {
                var cti = (cxx_type_info*)(@base + tit->info[i]);
                var tti = (type_info*)(@base + cti->type_info);

                var mangled = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(&tti->mangled);

                if (mangled.SequenceEqual(ExceptionTypeName))
                {
                    return true;
                }
            }

            return false;
        }

        [DoesNotReturn]
        public static void ReThrowWrapped()
        {
            var exceptionRecord = GetExceptionRecord();

            Debug.Assert(exceptionRecord->ExceptionCode == EXCEPTION_MSVC);
            Debug.Assert(exceptionRecord->NumberParameters >= 3);

            var exception = (Il2CppExceptionWrapper*)exceptionRecord->ExceptionInformation[1];
            var il2CppException = exception->Exception;

            throw new WrappedIl2CppException(il2CppException);
        }

        private static EXCEPTION_RECORD* GetExceptionRecord()
        {
            var exceptionPointers = (EXCEPTION_POINTERS*)Marshal.GetExceptionPointers();
            return exceptionPointers->ExceptionRecord;
        }
    }
}
try
{
    var thunk = (delegate * unmanaged<T1, T2, T3, TReturn>)ptr;
    return thunk(a, b, c);
}
catch (SEHException exception) when (Il2CppExceptionHelper.Windows.Filter(exception))
{
    Il2CppExceptionHelper.Windows.ReThrowWrapped();
    return default;
}

My approach is somewhat different in that I used C#'s builtin ability to catch SEH exceptions.

I also have a somewhat working implementation for linux/osx using MonoMod's NativeExceptionHelper

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

No branches or pull requests

3 participants