diff --git a/Dependencies/Il2CppAssemblyGenerator/Core.cs b/Dependencies/Il2CppAssemblyGenerator/Core.cs index 88e940aee..f71fa8b2d 100644 --- a/Dependencies/Il2CppAssemblyGenerator/Core.cs +++ b/Dependencies/Il2CppAssemblyGenerator/Core.cs @@ -1,9 +1,13 @@ -using System.IO; -using System.Net.Http; -using MelonLoader.Il2CppAssemblyGenerator.Packages; +using MelonLoader.Il2CppAssemblyGenerator.Packages; using MelonLoader.Il2CppAssemblyGenerator.Packages.Models; using MelonLoader.Modules; using MelonLoader.Utils; +using System.IO; +using System.Linq; +using System.Net.Http; +#if ANDROID +using MelonLoader.Java; +#endif namespace MelonLoader.Il2CppAssemblyGenerator { @@ -31,7 +35,18 @@ public override void OnInitialize() { Logger = LoggerInstance; +#if ANDROID + // Android has problems with SSL, which prevents any connections. This is the workaround. + HttpClientHandler handler = new() + { + ClientCertificateOptions = ClientCertificateOption.Manual, + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + + webClient = new(handler); +#else webClient = new(); +#endif webClient.DefaultRequestHeaders.Add("User-Agent", $"{Properties.BuildInfo.Name} v{Properties.BuildInfo.Version}"); AssemblyGenerationNeeded = LoaderConfig.Current.UnityEngine.ForceRegeneration; @@ -46,6 +61,8 @@ public override void OnInitialize() #if OSX GameAssemblyPath = Path.Combine(MelonEnvironment.GameExecutablePath, "Contents", "Frameworks", gameAssemblyName); +#elif ANDROID + GameAssemblyPath = GetLibIl2CppPath(); #else GameAssemblyPath = Path.Combine(MelonEnvironment.GameRootDirectory, gameAssemblyName); #endif @@ -60,12 +77,17 @@ private static int Run() if (!LoaderConfig.Current.UnityEngine.ForceOfflineGeneration) RemoteAPI.Contact(); - Cpp2IL cpp2IL_netcore = new Cpp2IL(); + Packages.Cpp2IL cpp2IL_netcore = new(); if (MelonUtils.IsWindows - && (cpp2IL_netcore.VersionSem < Cpp2IL.NetCoreMinVersion)) + && (cpp2IL_netcore.VersionSem < Packages.Cpp2IL.NetCoreMinVersion)) cpp2il = new Cpp2IL_NetFramework(); else cpp2il = cpp2IL_netcore; + +#if ANDROID + cpp2il = new Cpp2IL_Android(); +#endif + cpp2il_scrs = new Cpp2IL_StrippedCodeRegSupport(cpp2il); il2cppinterop = new Packages.Il2CppInterop(); @@ -133,6 +155,44 @@ private static int Run() return 0; } +#if ANDROID + private string GetLibIl2CppPath() + { + // get player activity + using JClass unityClass = JNI.FindClass("com/unity3d/player/UnityPlayer"); + JFieldID activityFieldId = JNI.GetStaticFieldID(unityClass, "currentActivity", "Landroid/app/Activity;"); + using JObject currentActivityObj = JNI.GetStaticObjectField(unityClass, activityFieldId); + + // get applicationinfo + using JClass activityType = JNI.GetObjectClass(currentActivityObj); + JMethodID getAppInfoId = JNI.GetMethodID(activityType, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;"); + using JObject applicationInfoObj = JNI.CallObjectMethod(currentActivityObj, getAppInfoId); + + // get nativelibrarydir + JFieldID filesFieldId = JNI.GetFieldID(JNI.GetObjectClass(applicationInfoObj), "nativeLibraryDir", "Ljava/lang/String;"); + using JString pathJString = JNI.GetObjectField(applicationInfoObj, filesFieldId); + + if (pathJString == null || !pathJString.Valid()) + { + MelonLogger.Msg("Unable to get libil2cpp path."); + if (JNI.ExceptionCheck()) + { + var ex = JNI.ExceptionOccurred(); + JNI.ExceptionClear(); + MelonLogger.Msg(ex.GetMessage()); + } + + return ""; + } + + string nativePath = JNI.GetJStringString(pathJString); + string[] directoryLibs = Directory.GetFiles(nativePath, "*.so"); + + string libil2Path = directoryLibs.FirstOrDefault(s => s.EndsWith("libil2cpp.so")); + return libil2Path; + } +#endif + private static void OldFiles_Cleanup() { if (Config.Values.OldFiles.Count <= 0) diff --git a/Dependencies/Il2CppAssemblyGenerator/Il2CppAssemblyGenerator.csproj b/Dependencies/Il2CppAssemblyGenerator/Il2CppAssemblyGenerator.csproj index b117f5027..b6f113551 100644 --- a/Dependencies/Il2CppAssemblyGenerator/Il2CppAssemblyGenerator.csproj +++ b/Dependencies/Il2CppAssemblyGenerator/Il2CppAssemblyGenerator.csproj @@ -12,11 +12,15 @@ - + + + + + \ No newline at end of file diff --git a/Dependencies/Il2CppAssemblyGenerator/Packages/Cpp2IL_Android.cs b/Dependencies/Il2CppAssemblyGenerator/Packages/Cpp2IL_Android.cs new file mode 100644 index 000000000..d28581c4b --- /dev/null +++ b/Dependencies/Il2CppAssemblyGenerator/Packages/Cpp2IL_Android.cs @@ -0,0 +1,137 @@ +#if ANDROID + +using AssetRipper.Primitives; +using Cpp2IL.Core; +using Cpp2IL.Core.Api; +using Cpp2IL.Core.Extensions; +using Cpp2IL.Core.Logging; +using LibCpp2IL.Wasm; +using MelonLoader.InternalUtils; +using MelonLoader.Utils; +using System; +using System.IO; + +namespace MelonLoader.Il2CppAssemblyGenerator.Packages +{ + // Using a direct implementation of Cpp2IL as there isn't any good way of running the executable to my knowledge + internal class Cpp2IL_Android : Models.ExecutablePackage + { + internal Cpp2IL_Android() + { + Version = "2022.1.0-pre-release.20"; + + Name = nameof(Cpp2IL); + Destination = Path.Combine(Core.BasePath, Name); + OutputFolder = Path.Combine(Destination, "cpp2il_out"); + } + + internal override bool ShouldSetup() + => string.IsNullOrEmpty(Config.Values.DumperVersion) + || !Config.Values.DumperVersion.Equals(Version); + + internal override void Cleanup() { } + + internal override void Save() + => Save(ref Config.Values.DumperVersion); + + internal override bool Execute() + { + Logger.InfoLog += (l, s) => Core.Logger.Msg($"[{s}] {l.TrimEnd('\n')}"); + Logger.WarningLog += (l, s) => Core.Logger.Warning($"[{s}] {l.TrimEnd('\n')}"); + Logger.ErrorLog += (l, s) => Core.Logger.Error($"[{s}] {l.TrimEnd('\n')}"); + Logger.VerboseLog += (l, s) => Core.Logger.Msg($"[{s}] {l.TrimEnd('\n')}"); + + byte[] mdData = APKAssetManager.GetAssetBytes("bin/Data/Managed/Metadata/global-metadata.dat"); + string mdPath = Path.Combine(Core.BasePath, "global-metadata.dat"); + File.WriteAllBytes(mdPath, mdData); + + Cpp2IlApi.Init(); + Cpp2IlApi.ConfigureLib(false); + var result = new Cpp2IlRuntimeArgs() + { + PathToAssembly = Core.GameAssemblyPath, + PathToMetadata = mdPath, + UnityVersion = UnityVersion.Parse(UnityInformationHandler.EngineVersion.ToString()), // they use different versions of the same library but under different names, thanks ds5678 + Valid = true, + OutputRootDirectory = OutputFolder, + OutputFormat = OutputFormatRegistry.GetFormat("dummydll"), + ProcessingLayersToRun = [ProcessingLayerRegistry.GetById("attributeinjector")], + }; + + return RunCpp2IL(result); + } + + // mostly copied from https://github.com/SamboyCoding/Cpp2IL/blob/development/Cpp2IL/Program.cs + private bool RunCpp2IL(Cpp2IlRuntimeArgs runtimeArgs) + { + var executionStart = DateTime.Now; + + runtimeArgs.OutputFormat?.OnOutputFormatSelected(); + + WasmFile.RemappedDynCallFunctions = null; + + Cpp2IlApi.InitializeLibCpp2Il(runtimeArgs.PathToAssembly, runtimeArgs.PathToMetadata, runtimeArgs.UnityVersion); + + foreach (var (key, value) in runtimeArgs.ProcessingLayerConfigurationOptions) + Cpp2IlApi.CurrentAppContext.PutExtraData(key, value); + + //Pre-process processing layers, allowing them to stop others from running + Core.Logger.Msg("Pre-processing processing layers..."); + var layers = runtimeArgs.ProcessingLayersToRun.Clone(); + RunProcessingLayers(runtimeArgs, processingLayer => processingLayer.PreProcess(Cpp2IlApi.CurrentAppContext, layers)); + runtimeArgs.ProcessingLayersToRun = layers; + + //Run processing layers + Core.Logger.Msg("Invoking processing layers..."); + RunProcessingLayers(runtimeArgs, processingLayer => processingLayer.Process(Cpp2IlApi.CurrentAppContext)); + + var outputStart = DateTime.Now; + + if (runtimeArgs.OutputFormat != null) + { + Core.Logger.Msg($"Outputting as {runtimeArgs.OutputFormat.OutputFormatName} to {runtimeArgs.OutputRootDirectory}..."); + runtimeArgs.OutputFormat.DoOutput(Cpp2IlApi.CurrentAppContext, runtimeArgs.OutputRootDirectory); + Core.Logger.Msg($"Finished outputting in {(DateTime.Now - outputStart).TotalMilliseconds}ms"); + } + else + { + Core.Logger.Warning("No output format requested, so not outputting anything. The il2cpp game loaded properly though! (Hint: You probably want to specify an output format, try --output-as)"); + } + + Cpp2IlPluginManager.CallOnFinish(); + + File.Delete(runtimeArgs.PathToMetadata); // because we extracted it from the apk's assets folder; only purpose was this + + Core.Logger.Msg($"Done. Total execution time: {(DateTime.Now - executionStart).TotalMilliseconds}ms"); + return true; + } + + private static void RunProcessingLayers(Cpp2IlRuntimeArgs runtimeArgs, Action run) + { + foreach (var processingLayer in runtimeArgs.ProcessingLayersToRun) + { + var processorStart = DateTime.Now; + + Core.Logger.Msg($" {processingLayer.Name}..."); + +#if !DEBUG + try + { +#endif + run(processingLayer); +#if !DEBUG + } + catch (Exception e) + { + Logger.Error($"Processing layer {processingLayer.Id} threw an exception: {e}"); + Environment.Exit(1); + } +#endif + + Core.Logger.Msg($" {processingLayer.Name} finished in {(DateTime.Now - processorStart).TotalMilliseconds}ms"); + } + } + } +} + +#endif \ No newline at end of file diff --git a/Dependencies/SupportModules/Il2Cpp/Il2Cpp.csproj b/Dependencies/SupportModules/Il2Cpp/Il2Cpp.csproj index 6f713e514..67db03c03 100644 --- a/Dependencies/SupportModules/Il2Cpp/Il2Cpp.csproj +++ b/Dependencies/SupportModules/Il2Cpp/Il2Cpp.csproj @@ -8,7 +8,6 @@ embedded SM_Il2Cpp true - https://nuget.bepinex.dev/v3/index.json false diff --git a/Directory.Build.props b/Directory.Build.props index 1f6ce06e5..de8c39bcc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,6 +2,7 @@ 0.7.2 1.5.0-ci.625 + 1.5.0-ci.5-arm64 Lava Gang discord.gg/2Wn3N2P @@ -13,6 +14,9 @@ x86;x64 false false + + + $(ForcedRID) @@ -24,6 +28,18 @@ .a + + linux-bionic-arm64 + linux-bionic-arm64 + arm64 + $(DefineConstants);ANDROID + lib + .a + + + $(Il2CppInteropAndroidVersion) + + osx-x64 @@ -34,7 +50,8 @@ .a - + + win-x64;win-x86 win-$(Platform) $(DefineConstants);WINDOWS diff --git a/MelonLoader.Bootstrap/BionicNativeAot.targets b/MelonLoader.Bootstrap/BionicNativeAot.targets new file mode 100644 index 000000000..5c3e856ab --- /dev/null +++ b/MelonLoader.Bootstrap/BionicNativeAot.targets @@ -0,0 +1,81 @@ + + + + + $(RuntimeIdentifier.ToLower().StartsWith('android')) + $(RuntimeIdentifier.ToLower().StartsWith('linux-bionic')) + $([MSBuild]::IsOSPlatform('Windows')) + $([MSBuild]::IsOSPlatform('Linux')) + $([MSBuild]::IsOSPlatform('OSX')) + true + + + + + $(ANDROID_NDK_ROOT)\source.properties + windows-x86_64 + linux-x86_64 + darwin-x86_64 + $(ANDROID_NDK_ROOT)/toolchains/llvm/prebuilt/$(NdkHost)/sysroot + + $([System.IO.File]::ReadAllText('$(NdkPropertiesFile)')) + $([System.Text.RegularExpressions.Regex]::Match('$(BaseRevision)', 'Pkg.Revision\s*=\s*(\S+)').Groups[1].Value.Split('.')[0]) + 0 + + + + + $(ANDROID_NDK_ROOT)/toolchains/llvm/prebuilt/$(NdkHost)/bin/clang + $(ANDROID_NDK_ROOT)/toolchains/llvm/prebuilt/$(NdkHost)/bin/llvm-objcopy + + android_clang.cmd + android_llvm-objcopy.cmd + + + + + + + + + + + + + + + $([System.Int32]::Parse($([System.String]::Copy('$(NdkVersion)').Split('.')[0]))) + + + + + + + + + + $(ANDROID_NDK_ROOT)\toolchains\llvm\prebuilt\$(NdkHost)\bin\ + $(ANDROID_NDK_ROOT)/toolchains/llvm/prebuilt/$(NdkHost)/bin/ + + + + + + + + + + + + + + + + + + true + + + + diff --git a/MelonLoader.Bootstrap/Core.cs b/MelonLoader.Bootstrap/Core.cs index 1a3dd0fc1..76d8a9986 100644 --- a/MelonLoader.Bootstrap/Core.cs +++ b/MelonLoader.Bootstrap/Core.cs @@ -12,7 +12,7 @@ namespace MelonLoader.Bootstrap; public static class Core { -#if LINUX || OSX +#if LINUX || OSX || ANDROID [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] private delegate nint DlsymFn(nint handle, string symbol); private static readonly DlsymFn HookDlsymDelegate = HookDlsym; @@ -40,15 +40,29 @@ public static void Init(nint moduleHandle) var exePath = Environment.ProcessPath!; GameDir = Path.GetDirectoryName(exePath)!; -#if !OSX - DataDir = Path.Combine(GameDir, Path.GetFileNameWithoutExtension(exePath) + "_Data"); -#else +#if ANDROID + DataDir = Proxy.Android.AndroidBootstrap.GetDataDir(); + if (!Proxy.Android.AndroidBootstrap.EnsurePerms()) + throw new Exception("Permissions not granted! MelonLoader cannot continue without permissions."); + + MelonLoader.Utils.APKAssetManager.Initialize(); + Proxy.Android.AndroidProxy.Log("JNI initialized!"); + + Proxy.Android.AndroidBootstrap.CopyMelonLoaderData(Proxy.Android.AndroidBootstrap.GetApkModificationDate()); + Proxy.Android.AndroidProxy.Log("APK assets copied!"); +#elif OSX DataDir = Path.Combine(Path.GetDirectoryName(GameDir)!, "Resources", "Data"); +#else + DataDir = Path.Combine(GameDir, Path.GetFileNameWithoutExtension(exePath) + "_Data"); #endif if (!Directory.Exists(DataDir)) return; +#if !ANDROID LoaderConfig.Initialize(); +#else + LoaderConfig.Initialize(DataDir); // Android doesn't work with the GetCurrentProcess system so we use the cached DataDir +#endif if (LoaderConfig.Current.Loader.Disable) return; @@ -57,7 +71,7 @@ public static void Init(nint moduleHandle) if (!LoaderConfig.Current.Loader.CapturePlayerLogs) ConsoleHandler.NullHandles(); -#if LINUX || OSX +#if LINUX || OSX || ANDROID PltHook.InstallHooks ([ ("dlsym", Marshal.GetFunctionPointerForDelegate(HookDlsymDelegate)) @@ -90,7 +104,7 @@ private static nint RedirectSymbol(nint handle, string symbolName, nint original return redirect.detourPtr; } -#if LINUX || OSX +#if LINUX || OSX || ANDROID private static nint HookDlsym(nint handle, string symbol) { nint originalSymbolAddress = LibcNative.Dlsym(handle, symbol); diff --git a/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libdobby.a b/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libdobby.a new file mode 100644 index 000000000..ad084b2e6 Binary files /dev/null and b/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libdobby.a differ diff --git a/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libplthook.a b/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libplthook.a new file mode 100644 index 000000000..e676d82b6 Binary files /dev/null and b/MelonLoader.Bootstrap/Deps/linux-bionic-arm64/libplthook.a differ diff --git a/MelonLoader.Bootstrap/Exports.cs b/MelonLoader.Bootstrap/Exports.cs index 2270a6b80..44a88a6dc 100644 --- a/MelonLoader.Bootstrap/Exports.cs +++ b/MelonLoader.Bootstrap/Exports.cs @@ -212,7 +212,7 @@ public static unsafe void LogMsg(ColorARGB* msgColor, char* msg, int msgLength, return; } - var mMsg = new ReadOnlySpan(msg, msgLength); + var mMsg = PointerToSpan(msg, msgLength); if (sectionColor == null || section == null) { @@ -220,13 +220,15 @@ public static unsafe void LogMsg(ColorARGB* msgColor, char* msg, int msgLength, return; } - MelonLogger.Log(*msgColor, mMsg, *sectionColor, new(section, sectionLength)); + var mSect = PointerToSpan(section, sectionLength); + + MelonLogger.Log(*msgColor, mMsg, *sectionColor, mSect); } [UnmanagedCallersOnly(EntryPoint = "LogError")] public static unsafe void LogError(char* msg, int msgLength, char* section, int sectionLength, bool warning) { - var mMsg = new ReadOnlySpan(msg, msgLength); + var mMsg = PointerToSpan(msg, msgLength); if (section == null) { if (warning) @@ -237,10 +239,22 @@ public static unsafe void LogError(char* msg, int msgLength, char* section, int return; } + var mSect = PointerToSpan(section, sectionLength); + if (warning) - MelonLogger.LogWarning(mMsg, new(section, sectionLength)); + MelonLogger.LogWarning(mMsg, mSect); else - MelonLogger.LogError(mMsg, new(section, sectionLength)); + MelonLogger.LogError(mMsg, mSect); + } + + private static unsafe ReadOnlySpan PointerToSpan(char* c, int length) + { +#if !ANDROID + return new(c, length); +#else + // Required to prevent strings from being mangled when logged on Android. + return System.Text.Encoding.UTF8.GetString(new ReadOnlySpan(c, length)).TrimEnd('\0', '\n', '\r'); +#endif } [UnmanagedCallersOnly(EntryPoint = "LogMelonInfo")] @@ -279,4 +293,12 @@ public static unsafe void GetLoaderConfig(nint* pConfig) { Marshal.StructureToPtr(LoaderConfig.Current, *pConfig, false); } + +#if ANDROID + [UnmanagedCallersOnly(EntryPoint = "GetJavaVM")] + public static unsafe IntPtr GetJavaVM() + { + return (IntPtr)MelonLoader.Java.JNI.VM; + } +#endif } diff --git a/MelonLoader.Bootstrap/LibcNative.cs b/MelonLoader.Bootstrap/LibcNative.cs index 1ffff1df2..2edd251f4 100644 --- a/MelonLoader.Bootstrap/LibcNative.cs +++ b/MelonLoader.Bootstrap/LibcNative.cs @@ -1,4 +1,4 @@ -#if LINUX || OSX +#if LINUX || OSX || ANDROID using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -13,8 +13,17 @@ internal partial class LibcNative internal const int RtldLazy = 0x1; internal const int RtldNoLoad = 0x10; - - [LibraryImport("libc", EntryPoint = "__libc_start_main")] + + const string DL_LIB = +#if !ANDROID + "libc"; +#else + "libdl"; +#endif + + const string C_LIB = "libc"; + + [LibraryImport(C_LIB, EntryPoint = "__libc_start_main")] public static unsafe partial int LibCStartMain( delegate* unmanaged[Cdecl] main, int argc, @@ -24,51 +33,51 @@ public static unsafe partial int LibCStartMain( nint rtLdFini, nint stackEnd); - [LibraryImport("libc", EntryPoint = "dlopen", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(DL_LIB, EntryPoint = "dlopen", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial nint Dlopen(string handle, int flags); - [LibraryImport("libc", EntryPoint = "dlsym", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(DL_LIB, EntryPoint = "dlsym", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial nint Dlsym(nint handle, string symbol); - [LibraryImport("libc", EntryPoint = "setenv", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "setenv", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int Setenv(string name, string value,[MarshalAs(UnmanagedType.Bool)] bool overwrite); - [LibraryImport("libc", EntryPoint = "dup2")] + [LibraryImport(C_LIB, EntryPoint = "dup2")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int Dup2(int oldFd, int newFd); #if OSX - [LibraryImport("libc", EntryPoint = "fopen", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "fopen", StringMarshalling = StringMarshalling.Utf8)] #else - [LibraryImport("libc", EntryPoint = "fopen64", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "fopen64", StringMarshalling = StringMarshalling.Utf8)] #endif [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial nint Fopen64(string pathName, string modes); - [LibraryImport("libc", EntryPoint = "vfprintf", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "vfprintf", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int Vfprintf(nint stream, string format, nint vList); - [LibraryImport("libc", EntryPoint = "vsnprintf", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "vsnprintf", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static unsafe partial int Vsnprintf(byte* s, int maxLen, string format, nint arg); - [LibraryImport("libc", EntryPoint = "fseek", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "fseek", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int Fseek(nint stream, long offset, int whence); - [LibraryImport("libc", EntryPoint = "fwrite", StringMarshalling = StringMarshalling.Utf8)] + [LibraryImport(C_LIB, EntryPoint = "fwrite", StringMarshalling = StringMarshalling.Utf8)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static unsafe partial int Fwrite(byte* ptr, int size, int nItems, nint stream); - [LibraryImport("libc", EntryPoint = "fileno")] + [LibraryImport(C_LIB, EntryPoint = "fileno")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int Fileno(nint stream); - [LibraryImport("libc", EntryPoint = "fclose")] + [LibraryImport(C_LIB, EntryPoint = "fclose")] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] public static partial int FClose(nint stream); } diff --git a/MelonLoader.Bootstrap/Logging/MelonLogger.cs b/MelonLoader.Bootstrap/Logging/MelonLogger.cs index 54cb31e8e..07031bf94 100644 --- a/MelonLoader.Bootstrap/Logging/MelonLogger.cs +++ b/MelonLoader.Bootstrap/Logging/MelonLogger.cs @@ -163,7 +163,11 @@ public static void Log(ColorARGB msgColor, ReadOnlySpan msg) return; } +#if !ANDROID Console.WriteLine($"[{time.Pastel(timeColor)}] {msg.Pastel(msgColor)}"); +#else + Proxy.Android.AndroidProxy.Log($"[{time.Pastel(timeColor)}] {msg.Pastel(msgColor)}"); +#endif } public static void Log(ColorARGB msgColor, ReadOnlySpan msg, ColorARGB sectionColor, ReadOnlySpan sectionName) @@ -198,7 +202,11 @@ public static void Log(ColorARGB msgColor, ReadOnlySpan msg, ColorARGB sec return; } +#if !ANDROID Console.WriteLine($"[{time.Pastel(timeColor)}] [{sectionName.Pastel(sectionColor)}] {msg.Pastel(msgColor)}"); +#else + Proxy.Android.AndroidProxy.Log($"[{time.Pastel(timeColor)}] [{sectionName.Pastel(sectionColor)}] {msg.Pastel(msgColor)}"); +#endif } public static void LogWarning(ReadOnlySpan msg) @@ -244,7 +252,11 @@ public static void LogError(ReadOnlySpan msg) return; } +#if !ANDROID Console.WriteLine($"[{time}] {msg}".Pastel(ColorARGB.IndianRed)); +#else + Proxy.Android.AndroidProxy.Log($"[{time}] {msg}".Pastel(ColorARGB.IndianRed)); +#endif } public static void LogError(ReadOnlySpan msg, ReadOnlySpan sectionName) @@ -264,7 +276,11 @@ public static void LogError(ReadOnlySpan msg, ReadOnlySpan sectionNa return; } +#if !ANDROID Console.WriteLine($"[{time}] [{sectionName}] {msg}".Pastel(ColorARGB.IndianRed)); +#else + Proxy.Android.AndroidProxy.Log($"[{time}] [{sectionName}] {msg}".Pastel(ColorARGB.IndianRed)); +#endif } public static void LogMelonInfo(ColorARGB nameColor, ReadOnlySpan name, ReadOnlySpan info) @@ -297,7 +313,11 @@ public static void LogMelonInfo(ColorARGB nameColor, ReadOnlySpan name, Re return; } +#if !ANDROID Console.WriteLine($"[{time.Pastel(timeColor)}] {name.Pastel(nameColor)} {info}"); +#else + Proxy.Android.AndroidProxy.Log($"[{time.Pastel(timeColor)}] {name.Pastel(nameColor)} {info}"); +#endif } public static void LogSpacer() @@ -307,6 +327,10 @@ public static void LogSpacer() if (!ConsoleHandler.IsOpen) return; +#if !ANDROID Console.WriteLine(); +#else + Proxy.Android.AndroidProxy.Log(""); +#endif } } diff --git a/MelonLoader.Bootstrap/MelonLoader.Bootstrap.csproj b/MelonLoader.Bootstrap/MelonLoader.Bootstrap.csproj index f47a10763..74e36b711 100644 --- a/MelonLoader.Bootstrap/MelonLoader.Bootstrap.csproj +++ b/MelonLoader.Bootstrap/MelonLoader.Bootstrap.csproj @@ -14,6 +14,8 @@ $(DefineConstants);BOOTSTRAP + + linuxexports.def true @@ -24,6 +26,10 @@ Deps/$(RuntimeIdentifier)/osxentry.o true + + androidexports.def + true + @@ -65,6 +71,8 @@ + + @@ -87,9 +95,13 @@ Overwrite="true"/> - + + + + + diff --git a/MelonLoader.Bootstrap/PltHook.cs b/MelonLoader.Bootstrap/PltHook.cs index 9e7a75e32..5ec9a0500 100644 --- a/MelonLoader.Bootstrap/PltHook.cs +++ b/MelonLoader.Bootstrap/PltHook.cs @@ -35,8 +35,13 @@ internal static partial class PltHook [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] private static partial nint PlthookError(); +#if !ANDROID private static readonly string? PlayerFileName = Process.GetCurrentProcess().Modules.OfType() .FirstOrDefault(x => x.FileName.Contains("UnityPlayer"))?.FileName; +#else + private static readonly string? PlayerFileName = Process.GetCurrentProcess().Modules.OfType() + .FirstOrDefault(x => x.FileName.Contains("libunity.so"))?.FileName; +#endif internal static void InstallHooks(List<(string functionName, nint hookFunctionPtr)> hooks) { diff --git a/MelonLoader.Bootstrap/Proxy/Android/AndroidBootstrap.cs b/MelonLoader.Bootstrap/Proxy/Android/AndroidBootstrap.cs new file mode 100644 index 000000000..39b6e03bd --- /dev/null +++ b/MelonLoader.Bootstrap/Proxy/Android/AndroidBootstrap.cs @@ -0,0 +1,236 @@ +#if ANDROID +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using MelonLoader.Java; +using MelonLoader.Utils; + +namespace MelonLoader.Bootstrap.Proxy.Android; + +public static class AndroidBootstrap +{ + public static string? PackageName { get; private set; } + public static string? DotnetDir { get; private set; } + public static int ApiLevel { get; private set; } = 0; + + [RequiresDynamicCode("Calls Init then InitConfig ")] + public static unsafe int LoadBootstrap() + { + // linux-bionic .NET logs everything to stdout/err, this allows us to see these logs in logcat with our logs + StdRedirect.RedirectStdOut(); + StdRedirect.RedirectStdErr(); + + Core.Init(NativeLibrary.Load("libmain.so")); + return 1; + } + + public static void CopyMelonLoaderData(DateTimeOffset date) + { + CopyAssetFolder("MelonLoader", Core.DataDir, Core.DataDir, date); + CopyAssetFolder("dotnet", $"/data/data/{PackageName}/", $"/data/data/{PackageName}/", date); + + DotnetDir = Path.Combine($"/data/data/{PackageName}/dotnet"); + } + + private static void CopyAssetFolder(string assetName, string copyBase, string targetBase, DateTimeOffset cutoffDate) + { + string targetFolder = Path.Combine(targetBase, assetName); + string markerFile = Path.Combine(targetFolder, ".copy_complete"); + + bool needsCopy = true; + + if (Directory.Exists(targetFolder)) + { + if (File.Exists(markerFile)) + { + DateTime lastComplete = File.GetLastWriteTimeUtc(markerFile); + if (lastComplete > cutoffDate) + { + AndroidProxy.Log($"{assetName} folder is already up-to-date"); + needsCopy = false; + } + } + } + + if (needsCopy) + { + // TODO: put up toasts as initial copy can take awhile + AndroidProxy.Log($"Copying {assetName} assets..."); + if (Directory.Exists(targetFolder)) + Directory.Delete(targetFolder, true); + + APKAssetManager.SaveItemToDirectory(assetName, copyBase, includeInitial: true); + + Directory.CreateDirectory(targetFolder); + File.WriteAllText(markerFile, $"Copied at {DateTime.UtcNow:o}"); + } + } + + public static string GetDataDir() + { + JClass unityPlayer = JNI.FindClass("com/unity3d/player/UnityPlayer"); + JFieldID activityFieldId = JNI.GetStaticFieldID(unityPlayer, "currentActivity", "Landroid/app/Activity;"); + using JObject currentActivityObj = JNI.GetStaticObjectField(unityPlayer, activityFieldId); + using JString callObjectMethod = JNI.CallObjectMethod(currentActivityObj, JNI.GetMethodID(JNI.GetObjectClass(currentActivityObj), "getPackageName", "()Ljava/lang/String;")); + PackageName = callObjectMethod.GetString(); + + JClass environment = JNI.FindClass("android/os/Environment"); + using JObject getExtDir = JNI.CallStaticObjectMethod(environment, JNI.GetStaticMethodID(environment, "getExternalStorageDirectory", "()Ljava/io/File;")); + + JMethodID jMethodId = JNI.GetMethodID(JNI.GetObjectClass(getExtDir), "toString", "()Ljava/lang/String;"); + using JString objectMethod = JNI.CallObjectMethod(getExtDir, jMethodId); + + return Path.Combine(objectMethod.GetString(), "MelonLoader", PackageName); + } + + // TODO: test multiple android versions + public static bool EnsurePerms() + { + JClass versionClass = JNI.FindClass("android/os/Build$VERSION"); + JFieldID sdkIntField = JNI.GetStaticFieldID(versionClass, "SDK_INT", "I"); + ApiLevel = JNI.GetStaticField(versionClass, sdkIntField); + AndroidProxy.Log($"Android API Level: {ApiLevel}"); + + JClass threadClass = JNI.FindClass("android/app/ActivityThread"); + JMethodID currentActivityThreadId = JNI.GetStaticMethodID(threadClass, "currentActivityThread", "()Landroid/app/ActivityThread;"); + using JObject currentActivityThread = JNI.CallStaticObjectMethod(threadClass, currentActivityThreadId); + + JMethodID getApplicationMethodId = JNI.GetMethodID(JNI.GetObjectClass(currentActivityThread), "getApplication", "()Landroid/app/Application;"); + using JObject currentActivityObj = JNI.CallObjectMethod(currentActivityThread, getApplicationMethodId); + + if (!CheckManageAllFilesPermission(currentActivityObj)) + { + AndroidProxy.Log("Failed to get MANAGE_ALL_FILES permission."); + return false; + } + + if (!EnsurePermsWithUnity(currentActivityObj)) + { + AndroidProxy.Log("Failed to ensure permissions with Unity."); + return false; + } + + return true; + } + + private static bool CheckManageAllFilesPermission(JObject currentActivityObj) + { + if (ApiLevel < 30) + return true; // This part of the API does not exist on Android versions below 11 (API level 30) + + const int MAX_WAIT = 30000; // in milliseconds + + JClass environment = JNI.FindClass("android/os/Environment"); + JClass uri = JNI.FindClass("android/net/Uri"); + JClass intent = JNI.FindClass("android/content/Intent"); + + JMethodID isExternalStorageManagerMethodId = JNI.GetStaticMethodID(environment, "isExternalStorageManager", "()Z"); + bool isExternalStorageManager = JNI.CallStaticMethod(environment, isExternalStorageManagerMethodId); + if (JNI.ExceptionCheck()) + return false; + + if (isExternalStorageManager) + return true; + + using JString actionName = JNI.NewString("android.settings.MANAGE_APP_ALL_FILES_ACCESS_PERMISSION"); + + using JString packageName = JNI.NewString($"package:{PackageName}"); + + using JObject callStaticObjectMethod = JNI.CallStaticObjectMethod(uri, JNI.GetStaticMethodID(uri, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"), packageName); + + JMethodID intentConstructor = JNI.GetMethodID(intent, "", "(Ljava/lang/String;Landroid/net/Uri;)V"); + using JObject initialIntent = JNI.NewObject(intent, intentConstructor, actionName, callStaticObjectMethod); + + JMethodID addFlagsMethodId = JNI.GetMethodID(intent, "addFlags", "(I)Landroid/content/Intent;"); + int flag = 0x10000000; // FLAG_ACTIVITY_NEW_TASK + using JObject flaggedIntent = JNI.CallObjectMethod(initialIntent, addFlagsMethodId, new JValue(flag)); + + JClass activityClass = JNI.GetObjectClass(currentActivityObj); + JMethodID startActivityMethod = JNI.GetMethodID(activityClass, "startActivity", "(Landroid/content/Intent;)V"); + JNI.CallVoidMethod(currentActivityObj, startActivityMethod, new JValue(flaggedIntent)); + + JNI.CheckExceptionAndThrow(); + + // TODO: this shouldn't sleep in the main thread; not sure if there is a better method + int totalWaitTime = 0; + while (totalWaitTime < MAX_WAIT) { + isExternalStorageManager = JNI.CallStaticMethod(environment, isExternalStorageManagerMethodId); + if (JNI.ExceptionCheck()) + return false; + + if (isExternalStorageManager) + return true; + + System.Threading.Thread.Sleep(250); + totalWaitTime += 250; + } + + AndroidProxy.Log("Timed out waiting for MANAGE_ALL_FILES permission, final check..."); + isExternalStorageManager = JNI.CallStaticMethod(environment, isExternalStorageManagerMethodId); + if (JNI.ExceptionCheck()) + return false; + + if (isExternalStorageManager) + return true; + + AndroidProxy.Log("Failed to get MANAGE_ALL_FILES permission after waiting."); + return false; + } + + private static bool EnsurePermsWithUnity(JObject currentActivityObj) { + if (ApiLevel >= 30) + return true; // Not necessary on Android 11+ as you need MANAGE_ALL_FILES_ACCESS_PERMISSION instead. + + string[] permissions = new string[] + { + "android.permission.WRITE_EXTERNAL_STORAGE", + "android.permission.READ_EXTERNAL_STORAGE", + "android.permission.MANAGE_EXTERNAL_STORAGE" + }; + + JClass unityPermissions = JNI.FindClass("com/unity3d/player/UnityPermissions"); + JClass unityWaitPermissionsClass = JNI.FindClass("com/unity3d/player/UnityPermissions$ModalWaitForPermissionResponse"); + + using JObject waitPermission = JNI.NewObject(unityWaitPermissionsClass, JNI.GetMethodID(unityWaitPermissionsClass, "", "()V")); + + using JArray permissionArray = new JArray(permissions.Length); + for (int i = 0; i < permissions.Length; i++) + { + permissionArray[i] = JNI.NewString(permissions[i]); + } + + JMethodID requestUserPermissionsId = JNI.GetStaticMethodID(unityPermissions, "requestUserPermissions", "(Landroid/app/Activity;[Ljava/lang/String;Lcom/unity3d/player/IPermissionRequestCallbacks;)V"); + JNI.CallStaticVoidMethod(unityPermissions, requestUserPermissionsId, new JValue(currentActivityObj), new JValue(permissionArray), new JValue(waitPermission)); + + if (JNI.ExceptionCheck()) + { + AndroidProxy.Log("Failed to request permissions."); + return false; + } + + JMethodID waitForResponseId = JNI.GetMethodID(JNI.GetObjectClass(waitPermission), "waitForResponse", "()Z"); + JNI.CallVoidMethod(waitPermission, waitForResponseId); + + if (JNI.ExceptionCheck()) + { + AndroidProxy.Log("Failed to wait for permission response."); + return false; + } + + return true; + } + + public static DateTimeOffset GetApkModificationDate() + { + var assetBytes = APKAssetManager.GetAssetBytes("lemon_patch_date.txt"); + string assetContent = Encoding.UTF8.GetString(assetBytes); + + // Now parse the string content into an RFC 3339 DateTime + // RFC 3339 is essentially ISO 8601, so DateTime.Parse can handle it. + if (DateTimeOffset.TryParse(assetContent, out DateTimeOffset date)) + return date; + + return default; + } +} +#endif \ No newline at end of file diff --git a/MelonLoader.Bootstrap/Proxy/Android/AndroidProxy.cs b/MelonLoader.Bootstrap/Proxy/Android/AndroidProxy.cs new file mode 100644 index 000000000..51012f959 --- /dev/null +++ b/MelonLoader.Bootstrap/Proxy/Android/AndroidProxy.cs @@ -0,0 +1,85 @@ +#if ANDROID +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; +using MelonLoader.Java; +using MelonLoader.Bootstrap.Utils; + +namespace MelonLoader.Bootstrap.Proxy.Android; + +public static class AndroidProxy +{ + [DllImport("liblog", EntryPoint = "__android_log_print")] + private static extern int LogInternal(int prio, string tag, string text); + + public static void Log(string text) => LogInternal(4, "MelonLoader", text); + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + [RequiresDynamicCode("Calls MelonLoader.Bootstrap.Proxy.Android.AndroidBootstrap.LoadBootstrap()")] + private static unsafe byte Load(void* env, void* jobject, void* str) + { + LoadUnity(); + AndroidBootstrap.LoadBootstrap(); + return 1; + } + + public static unsafe void LoadUnity() + { + if (!NativeLibrary.TryLoad("libunity.so", out var libUnity)) + { + Log("Failed to load libunity.so"); + return; + } + + if (!NativeFunc.GetExport(libUnity, "JNI_OnLoad", out var jniOnLoad)) + { + Log("Can't load Export via JNI_OnLoad"); + return; + } + + jniOnLoad((IntPtr)JNI.VM, IntPtr.Zero); + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate nint JNI_OnLoadFunc(IntPtr vm, IntPtr reserved); + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static unsafe byte Unload(void* vm, void* reserved) + { + Log("Unload called"); + return 1; + } + + [UnmanagedCallersOnly(EntryPoint = "JNI_OnLoad")] + public static unsafe JNI.Version JNI_OnLoad(IntPtr vm, void* reserved) + { + JNI.Initialize(vm); + JClass nativeLoader = JNI.FindClass("com/unity3d/player/NativeLoader"); + if (!nativeLoader.Valid()) + { + Log("Cannot find NativeLoader class"); + return JNI.Version.V1_6; + } + + var methods = (JNINativeMethod*)NativeMemory.Alloc((nuint)(sizeof(JNINativeMethod) * 2)); + + methods[0] = new JNINativeMethod { Name = Utf8StringMarshaller.ConvertToUnmanaged("load"), Signature = Utf8StringMarshaller.ConvertToUnmanaged("(Ljava/lang/String;)Z"), FnPtr = (delegate* unmanaged[Cdecl])&Load }; + methods[1] = new JNINativeMethod { Name = Utf8StringMarshaller.ConvertToUnmanaged("unload"), Signature = Utf8StringMarshaller.ConvertToUnmanaged("()Z"), FnPtr = (delegate* unmanaged[Cdecl])&Unload }; + + var registerNatives = JNI.Env->Functions->RegisterNatives(JNI.Env, nativeLoader.Handle, (IntPtr)methods, 2); + if (registerNatives != 0) + { + Log("Failed to register native methods"); + } + return JNI.Version.V1_6; + } + + private unsafe struct JNINativeMethod + { + public byte* Name; + public byte* Signature; + public void* FnPtr; + } +} +#endif \ No newline at end of file diff --git a/MelonLoader.Bootstrap/Proxy/Android/StdRedirect.cs b/MelonLoader.Bootstrap/Proxy/Android/StdRedirect.cs new file mode 100644 index 000000000..ea956fb0e --- /dev/null +++ b/MelonLoader.Bootstrap/Proxy/Android/StdRedirect.cs @@ -0,0 +1,73 @@ +#if ANDROID +using System.Runtime.InteropServices; +using System.Text; + +namespace MelonLoader.Bootstrap.Proxy.Android; + +internal static class StdRedirect +{ + private const int STDERR_FILENO = 2; + private const int STDOUT_FILENO = 1; + + [DllImport("libc.so", SetLastError = true)] + private static extern int pipe(int[] pipefd); + + [DllImport("libc.so", SetLastError = true)] + private static extern int dup2(int oldfd, int newfd); + + [DllImport("libc.so", SetLastError = true)] + private static extern IntPtr fdopen(int fd, string mode); + + [DllImport("libc.so", SetLastError = true)] + private static extern IntPtr fgets(byte[] buffer, int size, IntPtr stream); + + [DllImport("liblog.so", SetLastError = true)] + private static extern int __android_log_write(int prio, string tag, string text); + + public static void RedirectStdErr() + { + Environment.SetEnvironmentVariable("COREHOST_TRACE", "1"); + Environment.SetEnvironmentVariable("COREHOST_TRACE_VERBOSITY", "3"); + + RedirectStream(STDERR_FILENO, "MelonLoader"); + } + + public static void RedirectStdOut() + { + Environment.SetEnvironmentVariable("COREHOST_TRACE", "1"); + Environment.SetEnvironmentVariable("COREHOST_TRACE_VERBOSITY", "3"); + + RedirectStream(STDOUT_FILENO, "MelonLoader"); + } + + private static void RedirectStream(int fileno, string tag) + { + int[] pipes = new int[2]; + if (pipe(pipes) != 0) + { + Console.WriteLine("Failed to create pipe"); + return; + } + + dup2(pipes[1], fileno); + IntPtr inputFile = fdopen(pipes[0], "r"); + + Thread logThread = new Thread(() => + { + byte[] buffer = new byte[512]; + while (true) + { + IntPtr result = fgets(buffer, buffer.Length, inputFile); + if (result == IntPtr.Zero) + break; + + string logMsg = Encoding.UTF8.GetString(buffer).TrimEnd('\0', '\n', '\r'); + __android_log_write(3, tag, logMsg); // debug + } + }); + + logThread.IsBackground = true; + logThread.Start(); + } +} +#endif diff --git a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/ClrMonoLib.cs b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/ClrMonoLib.cs new file mode 100644 index 000000000..bcdac663e --- /dev/null +++ b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/ClrMonoLib.cs @@ -0,0 +1,65 @@ +#if ANDROID +using MelonLoader.Bootstrap.Proxy.Android; +using MelonLoader.Bootstrap.Utils; +using System.Runtime.InteropServices; + +namespace MelonLoader.Bootstrap.RuntimeHandlers.Il2Cpp; + +// NOTE: parts of this are specific to a LemonLoader fork of dotnet/runtime +internal class ClrMonoLib +{ + public required nint Handle { get; init; } + + public required SetThreadCheckerDelegate SetThreadChecker { get; init; } + public required ThreadSuspendReloadDelegate ThreadSuspendReload { get; init; } + public required InstallUnhandledExceptionHookDelegate InstallUnhandledExceptionHook { get; init; } + public required PrintUnhandledExceptionDelegate PrintUnhandledException { get; init; } + public required SetStringDelegate SetLevelString { get; init; } + public required SetStringDelegate SetMaskString { get; init; } + + public static ClrMonoLib? TryLoad() + { + MelonDebug.Log("Loading CoreCLR Mono exports"); + + if (string.IsNullOrEmpty(AndroidBootstrap.DotnetDir)) + { + MelonDebug.Log("DotnetDir is not set, cannot load CoreCLR Mono exports."); + return null; + } + + if (!NativeLibrary.TryLoad(Path.Combine(AndroidBootstrap.DotnetDir, "shared", "Microsoft.NETCore.App", "8.0.6", "libcoreclr.so"), out var hRuntime) + || !NativeFunc.GetExport(hRuntime, "mono_melonloader_set_thread_checker", out var setThreadChecker) + || !NativeFunc.GetExport(hRuntime, "mono_melonloader_thread_suspend_reload", out var threadSuspendReload) + || !NativeFunc.GetExport(hRuntime, "mono_install_unhandled_exception_hook", out var installUnhandledExceptionHook) + || !NativeFunc.GetExport(hRuntime, "mono_print_unhandled_exception", out var printUnhandledException) + || !NativeFunc.GetExport(hRuntime, "mono_trace_set_level_string", out var setLevelString) + || !NativeFunc.GetExport(hRuntime, "mono_trace_set_mask_string", out var setMaskString)) + return null; + + return new() + { + Handle = hRuntime, + SetThreadChecker = setThreadChecker, + ThreadSuspendReload = threadSuspendReload, + InstallUnhandledExceptionHook = installUnhandledExceptionHook, + PrintUnhandledException = printUnhandledException, + SetLevelString = setLevelString, + SetMaskString = setMaskString + }; + } + + public delegate bool CheckThreadDelegate(ulong threadId); + public delegate void MonoUnhandledExceptionDelegate(IntPtr exc, IntPtr userData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SetThreadCheckerDelegate(CheckThreadDelegate checker); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void ThreadSuspendReloadDelegate(); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void InstallUnhandledExceptionHookDelegate(MonoUnhandledExceptionDelegate callback, IntPtr userData); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void PrintUnhandledExceptionDelegate(IntPtr exc); + [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] + public delegate void SetStringDelegate(string str); +} +#endif \ No newline at end of file diff --git a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Dotnet.cs b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Dotnet.cs index b3bbe1010..846c8bebb 100644 --- a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Dotnet.cs +++ b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Dotnet.cs @@ -18,6 +18,8 @@ internal static partial class Dotnet StringMarshalling.Utf8; #endif + private const string ANDROID_DOTNET_VERSION = "8.0.6"; + public static bool LoadHostfxr() { var path = GetHostfxrPath(); @@ -26,10 +28,20 @@ public static bool LoadHostfxr() private static string? GetHostfxrPath() { +#if ANDROID + if (string.IsNullOrEmpty(MelonLoader.Bootstrap.Proxy.Android.AndroidBootstrap.DotnetDir)) + { + MelonDebug.Log("DotnetDir is not set, cannot find hostfxr."); + return null; + } + + return Path.Combine(MelonLoader.Bootstrap.Proxy.Android.AndroidBootstrap.DotnetDir, "host", "fxr", ANDROID_DOTNET_VERSION, "libhostfxr.so"); +#else var buffer = new StringBuilder(1024); var bufferSize = (nint)buffer.Capacity; var result = get_hostfxr_path(buffer, ref bufferSize, 0); return result != 0 ? null : buffer.ToString(); +#endif } public static bool InitializeForRuntimeConfig(string runtimeConfigPath, out nint context) diff --git a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppHandler.cs b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppHandler.cs index 888919a3d..e2f1903b3 100644 --- a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppHandler.cs +++ b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppHandler.cs @@ -8,6 +8,9 @@ internal static class Il2CppHandler private static Action? startFunc; // Prevent GC private static Il2CppLib il2cpp = null!; +#if ANDROID + private static ClrMonoLib mono = null!; +#endif private static bool il2cppInitDone; private static bool invokeStarted; @@ -29,6 +32,16 @@ public static void Initialize(nint handle) } il2cpp = il2cppLib; + +#if ANDROID + var monoLib = ClrMonoLib.TryLoad(); + if (monoLib is null) + { + Core.Logger.Error("Could not load CoreCLR Mono"); + return; + } + mono = monoLib; +#endif } internal static nint InitDetour(nint a) @@ -65,6 +78,16 @@ private static void InitializeManaged() return; } +#if ANDROID + // Workaround so that we can use .NET 8 on Android without upgrading MelonLoader's .NET version + string runtimeConfig = File.ReadAllText(runtimeConfigPath); + if (runtimeConfig.Contains("net6")) + { + runtimeConfig = runtimeConfig.Replace("6.0", "8.0"); + File.WriteAllText(runtimeConfigPath, runtimeConfig); + } +#endif + MelonDebug.Log("Attempting to load hostfxr"); if (!Dotnet.LoadHostfxr()) { @@ -95,6 +118,10 @@ private static void InitializeManaged() return; } +#if ANDROID + ApplyMonoPatches(); +#endif + var startFuncPtr = Core.LibraryHandle; MelonDebug.Log("Invoking NativeHost entry"); @@ -123,6 +150,11 @@ internal static nint InvokeDetour(nint method, nint obj, nint args, nint exc) invokeStarted = true; MelonDebug.Log("Invoke hijacked"); +#if ANDROID + mono.ThreadSuspendReload(); + MelonDebug.Log("Mono thread reset"); +#endif + Start(); return result; @@ -143,6 +175,62 @@ private static unsafe void NativeHookDetachImpl(nint* target, nint detour) Dobby.HookDetach(*target); } +#if ANDROID + private static void ApplyMonoPatches() + { + MelonDebug.Log("Applying Mono runtime patches"); + + mono.SetLevelString("warning"); + mono.SetMaskString("all"); + + MelonDebug.Log("Enabled Mono logging"); + + mono.InstallUnhandledExceptionHook(MonoUnhandledExceptionHandler, IntPtr.Zero); + + MelonDebug.Log("Installed unhandled exception hook"); + + mono.SetThreadChecker(MonoCheckThread); + + MelonDebug.Log("Installed thread checker"); + } + + private static void MonoUnhandledExceptionHandler(IntPtr exc, IntPtr userData) + { + if (exc == IntPtr.Zero) + return; + + mono.PrintUnhandledException(exc); + } + + private static bool MonoCheckThread(ulong threadId) + { + UIntPtr size = 0; + + IntPtr threads = il2cpp.GetAllAttachedThreads(ref size); + IntPtr[] threadsSlice = new IntPtr[(int)size]; + Marshal.Copy(threads, threadsSlice, 0, threadsSlice.Length); + + for (int i = 0; i < threadsSlice.Length; i++) + { + Il2CppThread thread = Marshal.PtrToStructure(threadsSlice[i]); + IntPtr internalThreadPtr = thread.internal_thread; + + if (internalThreadPtr != IntPtr.Zero) + { + Il2CppInternalThread internalThread = Marshal.PtrToStructure(internalThreadPtr); + + if (internalThread.tid == threadId) + { + MelonDebug.Log($"Attached IL2CPP thread {internalThread.tid:X}"); + return false; + } + } + } + + return true; + } +#endif + // Requires the bootstrap handle to be passed first private delegate void InitializeFn(ref nint startFunc); } diff --git a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppLib.cs b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppLib.cs index b91ab1c2f..88b48f94a 100644 --- a/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppLib.cs +++ b/MelonLoader.Bootstrap/RuntimeHandlers/Il2Cpp/Il2CppLib.cs @@ -9,19 +9,22 @@ internal class Il2CppLib(Il2CppLib.MethodGetNameFn methodGetName) public required InitFn Init { get; init; } public required RuntimeInvokeFn RuntimeInvoke { get; init; } + public required GetAllAttachedThreadsDelegate GetAllAttachedThreads { get; init; } public static Il2CppLib? TryLoad(nint hRuntime) { if (!NativeFunc.GetExport(hRuntime, "il2cpp_init", out var init) || !NativeFunc.GetExport(hRuntime, "il2cpp_runtime_invoke", out var runtimeInvoke) - || !NativeFunc.GetExport(hRuntime, "il2cpp_method_get_name", out var methodGetName)) + || !NativeFunc.GetExport(hRuntime, "il2cpp_method_get_name", out var methodGetName) + || !NativeFunc.GetExport(hRuntime, "il2cpp_thread_get_all_attached_threads", out var getAllAttached)) return null; return new(methodGetName) { Handle = hRuntime, Init = init, - RuntimeInvoke = runtimeInvoke + RuntimeInvoke = runtimeInvoke, + GetAllAttachedThreads = getAllAttached }; } @@ -36,4 +39,66 @@ internal class Il2CppLib(Il2CppLib.MethodGetNameFn methodGetName) public delegate nint RuntimeInvokeFn(nint method, nint obj, nint args, nint exc); [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate nint MethodGetNameFn(nint method); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate IntPtr GetAllAttachedThreadsDelegate(ref UIntPtr size); } + +[StructLayout(LayoutKind.Sequential)] +public struct Il2CppObject +{ + private IntPtr data; + private IntPtr monitor; +} + +[StructLayout(LayoutKind.Sequential)] +public struct Il2CppInternalThread +{ + public Il2CppObject obj; + public int lock_thread_id; + public IntPtr handle; + public IntPtr native_handle; + public IntPtr cached_culture_info; + public IntPtr name; + public int name_len; + public uint state; + public IntPtr abort_exc; + public int abort_state_handle; + public ulong tid; + public IntPtr debugger_thread; + public IntPtr static_data; + public IntPtr runtime_thread_info; + public IntPtr current_appcontext; + public IntPtr root_domain_thread; + public IntPtr _serialized_principal; + public int _serialized_principal_version; + public IntPtr appdomain_refs; + public int interruption_requested; + public IntPtr synch_cs; + public byte threadpool_thread; + public byte thread_interrupt_requested; + public int stack_size; + public byte apartment_state; + public int critical_region_level; + public int managed_id; + public uint small_id; + public IntPtr manage_callback; + public IntPtr interrupt_on_stop; + public IntPtr flags; + public IntPtr thread_pinning_ref; + public IntPtr abort_protected_block_count; + public int priority; + public IntPtr owned_mutexes; + public IntPtr suspended; + public int self_suspended; + public ulong thread_state; + public ulong unused2; + public IntPtr last; +} + +[StructLayout(LayoutKind.Sequential)] +public struct Il2CppThread +{ + // Truncated just for the things needed + public Il2CppObject obj; + public IntPtr internal_thread; +} \ No newline at end of file diff --git a/MelonLoader.Bootstrap/RuntimeHandlers/Mono/MonoHandler.cs b/MelonLoader.Bootstrap/RuntimeHandlers/Mono/MonoHandler.cs index 54c85dafd..836301226 100644 --- a/MelonLoader.Bootstrap/RuntimeHandlers/Mono/MonoHandler.cs +++ b/MelonLoader.Bootstrap/RuntimeHandlers/Mono/MonoHandler.cs @@ -15,7 +15,7 @@ internal static class MonoHandler private const char MonoPathSeparator = #if WINDOWS ';'; -#elif LINUX || OSX +#elif LINUX || OSX || ANDROID ':'; #endif diff --git a/MelonLoader.Bootstrap/SharedDelegates.cs b/MelonLoader.Bootstrap/SharedDelegates.cs index b5060ef52..dbf999d98 100644 --- a/MelonLoader.Bootstrap/SharedDelegates.cs +++ b/MelonLoader.Bootstrap/SharedDelegates.cs @@ -30,3 +30,8 @@ namespace MelonLoader.Bootstrap; [UnmanagedFunctionPointer(CallingConvention.StdCall)] internal delegate void GetLoaderConfigFn(ref LoaderConfig config); + +#if ANDROID +[UnmanagedFunctionPointer(CallingConvention.StdCall)] +internal delegate System.IntPtr GetJavaVM(); +#endif \ No newline at end of file diff --git a/MelonLoader.Bootstrap/Utils/WineUtils.cs b/MelonLoader.Bootstrap/Utils/WineUtils.cs index b82a7dac7..e005efa2e 100644 --- a/MelonLoader.Bootstrap/Utils/WineUtils.cs +++ b/MelonLoader.Bootstrap/Utils/WineUtils.cs @@ -13,9 +13,8 @@ static WineUtils() #if WINDOWS if (NativeLibrary.TryLoad("ntdll.dll", out var ntdll) && NativeLibrary.TryGetExport(ntdll, "wine_get_version", out _)) IsWine = true; -#endif -#if LINUX - IsWine = false; +#else + IsWine = false; #endif } } diff --git a/MelonLoader.Bootstrap/androidexports.def b/MelonLoader.Bootstrap/androidexports.def new file mode 100644 index 000000000..2e3310eb2 --- /dev/null +++ b/MelonLoader.Bootstrap/androidexports.def @@ -0,0 +1,17 @@ +{ + global: + DotNetRuntimeDebugHeader; + NativeHookAttach; + NativeHookDetach; + LogMsg; + LogError; + LogMelonInfo; + MonoInstallHooks; + MonoGetDomainPtr; + MonoGetRuntimeHandle; + IsConsoleOpen; + GetLoaderConfig; + GetJavaVM; + JNI_OnLoad; + local: *; +}; diff --git a/MelonLoader/Core.cs b/MelonLoader/Core.cs index 510512a84..350f68e78 100644 --- a/MelonLoader/Core.cs +++ b/MelonLoader/Core.cs @@ -27,6 +27,11 @@ internal static class Core internal static int Initialize() { +#if ANDROID + Java.JNI.Initialize(BootstrapInterop.Library.GetJavaVM()); + APKAssetManager.Initialize(); +#endif + // The config should be set before running anything else due to static constructors depending on it // Don't ask me how this works, because I don't know either. -slxdy var config = new LoaderConfig(); @@ -126,7 +131,9 @@ internal static int Initialize() // if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) // NativeStackWalk.LogNativeStackTrace(); +#if !ANDROID Fixes.Dotnet.DotnetAssemblyLoadContextFix.Install(); +#endif Fixes.Dotnet.DotnetModHandlerRedirectionFix.Install(); #endif @@ -146,7 +153,9 @@ internal static int Initialize() #endif Fixes.Il2CppInterop.Il2CppInteropFixes.Install(); +#if !ANDROID Fixes.Il2CppInterop.Il2CppInteropGetFieldDefaultValueFix.Install(); +#endif Fixes.Il2CppInterop.Il2CppICallInjector.Install(); diff --git a/MelonLoader/Fixes/Il2CppInterop/Il2CppInteropFixes.cs b/MelonLoader/Fixes/Il2CppInterop/Il2CppInteropFixes.cs index 0707fb2f9..ddf289983 100644 --- a/MelonLoader/Fixes/Il2CppInterop/Il2CppInteropFixes.cs +++ b/MelonLoader/Fixes/Il2CppInterop/Il2CppInteropFixes.cs @@ -187,11 +187,13 @@ internal static void Install() new HarmonyMethod(_systemTypeFromIl2CppType_Transpiler)); */ +#if !ANDROID // On Android this patch causes MissingMethodExceptions for an unknown reason LogDebugMsg("Patching Il2CppInterop ClassInjector.RegisterTypeInIl2Cpp..."); Core.HarmonyInstance.Patch(_registerTypeInIl2Cpp, null, null, new HarmonyMethod(_registerTypeInIl2Cpp_Transpiler)); +#endif LogDebugMsg("Patching Il2CppInterop ClassInjector.IsTypeSupported..."); Core.HarmonyInstance.Patch(_isTypeSupported, diff --git a/MelonLoader/InternalUtils/BootstrapLibrary.cs b/MelonLoader/InternalUtils/BootstrapLibrary.cs index d7256bbfa..52e4d2469 100644 --- a/MelonLoader/InternalUtils/BootstrapLibrary.cs +++ b/MelonLoader/InternalUtils/BootstrapLibrary.cs @@ -14,4 +14,7 @@ internal class BootstrapLibrary internal PtrRetFn MonoGetRuntimeHandle { get; private set; } internal BoolRetFn IsConsoleOpen { get; private set; } internal GetLoaderConfigFn GetLoaderConfig { get; private set; } +#if ANDROID + internal GetJavaVM GetJavaVM { get; private set; } +#endif } diff --git a/MelonLoader/InternalUtils/UnityInformationHandler.cs b/MelonLoader/InternalUtils/UnityInformationHandler.cs index 5250cae11..9f24a0ace 100644 --- a/MelonLoader/InternalUtils/UnityInformationHandler.cs +++ b/MelonLoader/InternalUtils/UnityInformationHandler.cs @@ -49,9 +49,11 @@ internal static void Setup() ReadGameInfo(assetsManager, gameDataPath); assetsManager.UnloadAll(); +#if !ANDROID // app.info doesn't exist on Android if (string.IsNullOrEmpty(GameDeveloper) || string.IsNullOrEmpty(GameName)) ReadGameInfoFallback(); +#endif if (EngineVersion == UnityVersion.MinVersion) EngineVersion = ReadVersionFallback(gameDataPath); @@ -78,20 +80,33 @@ private static void ReadGameInfo(AssetsManager assetsManager, string gameDataPat try { string bundlePath = Path.Combine(gameDataPath, "globalgamemanagers"); - if (!File.Exists(bundlePath)) + if (!FileExists(bundlePath)) bundlePath = Path.Combine(gameDataPath, "mainData"); - if (!File.Exists(bundlePath)) + if (!FileExists(bundlePath)) { bundlePath = Path.Combine(gameDataPath, "data.unity3d"); - if (!File.Exists(bundlePath)) + if (!FileExists(bundlePath)) return; +#if !ANDROID BundleFileInstance bundleFile = assetsManager.LoadBundleFile(bundlePath); +#else + Stream bundleStream = APKAssetManager.GetAssetStream(bundlePath); + BundleFileInstance bundleFile = assetsManager.LoadBundleFile(bundleStream, bundlePath); +#endif instance = assetsManager.LoadAssetsFileFromBundle(bundleFile, "globalgamemanagers"); } else + { +#if !ANDROID instance = assetsManager.LoadAssetsFile(bundlePath, true); +#else + Stream bundleStream = APKAssetManager.GetAssetStream(bundlePath); + instance = assetsManager.LoadAssetsFile(bundleStream, bundlePath, true); +#endif + } + if (instance == null) return; @@ -165,6 +180,7 @@ private static void ReadGameInfoFallback() private static UnityVersion ReadVersionFallback(string gameDataPath) { +#if !ANDROID string unityPlayerPath = MelonEnvironment.UnityPlayerPath; if (!File.Exists(unityPlayerPath)) unityPlayerPath = MelonEnvironment.GameExecutablePath; @@ -174,12 +190,13 @@ private static UnityVersion ReadVersionFallback(string gameDataPath) var unityVer = FileVersionInfo.GetVersionInfo(unityPlayerPath); return TryParse(unityVer.FileVersion); } +#endif try { var globalgamemanagersPath = Path.Combine(gameDataPath, "globalgamemanagers"); - if (File.Exists(globalgamemanagersPath)) - return GetVersionFromGlobalGameManagers(File.ReadAllBytes(globalgamemanagersPath)); + if (FileExists(globalgamemanagersPath)) + return GetVersionFromGlobalGameManagers(FileReadAllBytes(globalgamemanagersPath)); } catch (Exception ex) { @@ -190,8 +207,8 @@ private static UnityVersion ReadVersionFallback(string gameDataPath) try { var dataPath = Path.Combine(gameDataPath, "data.unity3d"); - if (File.Exists(dataPath)) - return GetVersionFromDataUnity3D(File.OpenRead(dataPath)); + if (FileExists(dataPath)) + return GetVersionFromDataUnity3D(FileOpenRead(dataPath)); } catch (Exception ex) { @@ -252,5 +269,32 @@ private static UnityVersion GetVersionFromDataUnity3D(Stream fileStream) return TryParse(verString.ToString().Trim()); } + + private static bool FileExists(string path) + { +#if !ANDROID + return File.Exists(path); +#else + return APKAssetManager.DoesAssetExist(path); +#endif + } + + private static byte[] FileReadAllBytes(string path) + { +#if !ANDROID + return File.ReadAllBytes(path); +#else + return APKAssetManager.GetAssetBytes(path); +#endif + } + + private static Stream FileOpenRead(string path) + { +#if !ANDROID + return File.OpenRead(path); +#else + return APKAssetManager.GetAssetStream(path); +#endif + } } } diff --git a/MelonLoader/JNI/JArray.cs b/MelonLoader/JNI/JArray.cs new file mode 100644 index 000000000..94f677ad3 --- /dev/null +++ b/MelonLoader/JNI/JArray.cs @@ -0,0 +1,39 @@ +#if ANDROID +namespace MelonLoader.Java; + +using System.Collections; +using System.Collections.Generic; + +public class JArray : JObject, IEnumerable +{ + public JArray() : base() { } + + public JArray(int size) => JNI.NewArray(size); + + public int Length => JNI.GetArrayLength(this); + + public T this[int index] + { + get => JNI.GetArrayElement(this, index); + set => JNI.SetArrayElement(this, index, value); + } + + public T[] GetElements() + { + return JNI.GetArrayElements(this); + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < this.Length; i++) + { + yield return JNI.GetArrayElement(this, i); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } +} +#endif diff --git a/MelonLoader/JNI/JClass.cs b/MelonLoader/JNI/JClass.cs new file mode 100644 index 000000000..4e4481425 --- /dev/null +++ b/MelonLoader/JNI/JClass.cs @@ -0,0 +1,154 @@ +#if ANDROID +using System.Collections.Generic; + +namespace MelonLoader.Java; + +public class JClass : JObject +{ + private Dictionary FieldCache { get; set; } = new(); + + private Dictionary MethodCache { get; set; } = new(); + + public JClass() : base() { } + + public JFieldID GetFieldID(string name, string sig) + { + CacheItem key = new(name, sig); + + if (this.FieldCache.TryGetValue(key, out JFieldID found)) + { + return found; + } + else + { + JFieldID id = JNI.GetFieldID(this, name, sig); + this.FieldCache.Add(key, id); + return id; + } + } + + public JFieldID GetStaticFieldID(string name, string sig) + { + CacheItem key = new(name, sig); + + if (this.FieldCache.TryGetValue(key, out JFieldID found)) + { + return found; + } + else + { + JFieldID id = JNI.GetStaticFieldID(this, name, sig); + this.FieldCache.Add(key, id); + return id; + } + } + + public JMethodID GetMethodID(string name, string sig) + { + CacheItem key = new(name, sig); + + if (this.MethodCache.TryGetValue(key, out JMethodID found)) + { + return found; + } + else + { + JMethodID id = JNI.GetMethodID(this, name, sig); + this.MethodCache.Add(key, id); + return id; + } + } + + public JMethodID GetStaticMethodID(string name, string sig) + { + CacheItem key = new(name, sig); + + if (this.MethodCache.TryGetValue(key, out JMethodID found)) + { + return found; + } + else + { + JMethodID id = JNI.GetStaticMethodID(this, name, sig); + this.MethodCache.Add(key, id); + return id; + } + } + + public T GetStaticObjectField(string name, string sig) where T : JObject, new() + { + return JNI.GetStaticObjectField(this, this.GetStaticFieldID(name, sig)); + } + + public T GetStaticField(string name) + { + return JNI.GetStaticField(this, this.GetStaticFieldID(name, JNI.GetTypeSignature())); + } + + public void SetStaticField(string name, T value) + { + JNI.SetStaticField(this, this.GetStaticFieldID(name, JNI.GetTypeSignature()), value); + } + + public T GetObjectField(JObject obj, string name, string sig) where T : JObject, new() + { + return JNI.GetObjectField(obj, this.GetFieldID(name, sig)); + } + + public T GetField(JObject obj, string name) + { + return JNI.GetField(obj, this.GetFieldID(name, JNI.GetTypeSignature())); + } + + public void SetObjectField(JObject obj, string name, string sig, JObject value) + { + JNI.SetObjectField(obj, this.GetFieldID(name, sig), value); + } + + public void SetField(JObject obj, string name, T value) + { + JNI.SetField(obj, this.GetFieldID(name, JNI.GetTypeSignature()), value); + } + + public T CallStaticObjectMethod(string name, string sig, params JValue[] args) where T : JObject, new() + { + return JNI.CallStaticObjectMethod(this, this.GetStaticMethodID(name, sig), args); + } + + public T CallStaticMethod(string name, string sig, params JValue[] args) + { + return JNI.CallStaticMethod(this, this.GetStaticMethodID(name, sig), args); + } + + public void CallStaticVoidMethod(string name, string sig, params JValue[] args) + { + JNI.CallStaticVoidMethod(this, this.GetMethodID(name, sig), args); + } + + public T CallObjectMethod(JObject obj, string name, string sig, params JValue[] args) where T : JObject, new() + { + return JNI.CallObjectMethod(obj, this.GetMethodID(name, sig), args); + } + + public T CallMethod(JObject obj, string name, string sig, params JValue[] args) + { + return JNI.CallMethod(obj, this.GetMethodID(name, sig), args); + } + + public void CallVoidMethod(JObject obj, string name, string sig, params JValue[] args) + { + JNI.CallVoidMethod(obj, this.GetMethodID(name, sig), args); + } + + public T NewObject(string name, string sig, params JValue[] args) where T : JObject, new() + { + return JNI.NewObject(this, this.GetMethodID(name, sig), args); + } + + private class CacheItem(string name, string value) + { + public string Name { get; private set; } = name; + public string Value { get; private set; } = value; + } +} +#endif \ No newline at end of file diff --git a/MelonLoader/JNI/JFieldID.cs b/MelonLoader/JNI/JFieldID.cs new file mode 100644 index 000000000..ab63fd778 --- /dev/null +++ b/MelonLoader/JNI/JFieldID.cs @@ -0,0 +1,24 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public readonly struct JFieldID : IEquatable +{ + public readonly IntPtr Handle { get; } + + internal JFieldID(IntPtr handle) + { + this.Handle = handle; + } + + public static implicit operator IntPtr(JFieldID fieldID) => fieldID.Handle; + + public static implicit operator JFieldID(IntPtr pointer) => new(pointer); + + public bool Equals(JFieldID other) + { + return this.Handle == other.Handle; + } +} +#endif diff --git a/MelonLoader/JNI/JMethodID.cs b/MelonLoader/JNI/JMethodID.cs new file mode 100644 index 000000000..31fac16fa --- /dev/null +++ b/MelonLoader/JNI/JMethodID.cs @@ -0,0 +1,24 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public readonly struct JMethodID : IEquatable +{ + public readonly IntPtr Handle { get; } + + internal JMethodID(IntPtr handle) + { + this.Handle = handle; + } + + public static implicit operator IntPtr(JMethodID methodID) => methodID.Handle; + + public static implicit operator JMethodID(IntPtr pointer) => new(pointer); + + public bool Equals(JMethodID other) + { + return this.Handle == other.Handle; + } +} +#endif diff --git a/MelonLoader/JNI/JNI.cs b/MelonLoader/JNI/JNI.cs new file mode 100644 index 000000000..5be72a5d7 --- /dev/null +++ b/MelonLoader/JNI/JNI.cs @@ -0,0 +1,1447 @@ +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type +#pragma warning disable CS8605 // Unboxing a possibly null value. +#pragma warning disable CS8632 // The annotation for nullable reference types... +#pragma warning disable CS8602 // Dereference of a possibly null reference + +#if ANDROID +namespace MelonLoader.Java; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +/// +/// Represents the Java Native Interface +/// +public unsafe static partial class JNI +{ + internal static JavaVM* VM; + + [ThreadStatic] + private static JNIEnv* _env; + internal static JNIEnv* Env + { + get + { + if (_env == null) + { + if (VM == null) + throw new InvalidOperationException("JNI not initialized. Call JNI.Initialize() first."); + + Initialize(IntPtr.Zero); + } + + return _env; + } + } + + internal static Dictionary ClassCache { get; set; } = new(); + + public static void Initialize(IntPtr vmPtr) + { + if (VM == null && vmPtr != IntPtr.Zero) + VM = (JavaVM*)vmPtr; + else if (VM == null) + throw new InvalidOperationException("JavaVM not initialized. Call JNI.Initialize() with a VM pointer first."); + + unsafe + { + JavaVMAttachArgs args = new() + { + version = (int)Version.V1_6, + name = IntPtr.Zero, + group = IntPtr.Zero + }; + + IntPtr argsPtr = +#if NET35 + Marshal.AllocHGlobal(Marshal.SizeOf(typeof(JavaVMAttachArgs))); +#else + Marshal.AllocHGlobal(Marshal.SizeOf()); +#endif + + try + { + Marshal.StructureToPtr(args, argsPtr, false); + + // TODO: threads need detached at some point, not sure how this should be handled for things like the finalizer thread + Result res = VM->Functions->AttachCurrentThread(VM, out JNIEnv* localEnv, argsPtr); + _env = localEnv; + + if (res != Result.Ok) + throw new InvalidOperationException($"Failed to attach current thread to JVM. Result: {res}"); + } + finally + { + Marshal.FreeHGlobal(argsPtr); + } + } + } + + [StructLayout(LayoutKind.Sequential)] + struct JavaVMAttachArgs + { + public int version; + public IntPtr name; + public IntPtr group; + } + + public static int GetVersion() + { + unsafe + { + return Env->Functions->GetVersion(Env); + } + } + + public static JClass DefineClass(string name, JObject loader, sbyte[] bytes) + { + unsafe + { + IntPtr bytesPtr = Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0); + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + + IntPtr res = Env->Functions->DefineClass(Env, nameAnsi, loader.Handle, bytesPtr, bytes.Length); + + Marshal.FreeHGlobal(nameAnsi); + + using JClass local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static JClass FindClass(string name) + { + unsafe + { + if (ClassCache.TryGetValue(name, out JClass? found)) + { + return found; + } + else + { + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + IntPtr res = Env->Functions->FindClass(Env, nameAnsi); + + Marshal.FreeHGlobal(nameAnsi); + + using JClass local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + JClass global = NewGlobalRef(local); + ClassCache.Add(name, global); + return global; + } + } + } + + public static JMethodID FromReflectedMethod(JObject method) + { + unsafe + { + return Env->Functions->FromReflectedMethod(Env, method.Handle); + } + } + + public static JFieldID FromReflectedField(JObject field) + { + unsafe + { + return Env->Functions->FromReflectedField(Env, field.Handle); + } + } + + public static JObject ToReflectedMethod(JClass cls, JMethodID methodID, bool isStatic) + { + unsafe + { + IntPtr res = Env->Functions->ToReflectedMethod(Env, cls.Handle, methodID, Convert.ToByte(isStatic)); + + using JObject local = new() { Handle = res, ReferenceType = ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static JClass GetSuperClass(JClass sub) + { + unsafe + { + IntPtr res = Env->Functions->GetSuperClass(Env, sub.Handle); + + using JClass local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static bool IsAssignableFrom(JClass sub, JClass sup) + { + unsafe + { + return Convert.ToBoolean(Env->Functions->IsAssignableFrom(Env, sub.Handle, sup.Handle)); + } + } + + public static JObject ToReflectedField(JClass cls, JFieldID fieldID, bool isStatic) + { + throw new NotImplementedException(); + } + + public static void Throw(JThrowable throwable) + { + unsafe + { + int res = Env->Functions->Throw(Env, throwable.Handle); + } + } + + public static void ThrowNew(JClass cls, string message) + { + unsafe + { + IntPtr messageAnsi = Marshal.StringToHGlobalAnsi(message); + int res = Env->Functions->Throw(Env, messageAnsi); + Marshal.FreeHGlobal(messageAnsi); + } + } + + public static JThrowable ExceptionOccurred() + { + unsafe + { + IntPtr res = Env->Functions->ExceptionOccurred(Env); + + using JThrowable local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static void ExceptionDescribe() + { + unsafe + { + Env->Functions->ExceptionDescribe(Env); + } + } + + public static void ExceptionClear() + { + unsafe + { + Env->Functions->ExceptionClear(Env); + } + } + + public static void FatalError(string message) + { + throw new NotImplementedException(); + } + + public static int PushLocalFrame(int capacity) + { + throw new NotImplementedException(); + } + + public static JObject PopLocalFrame(JObject result) + { + throw new NotImplementedException(); + } + + public static T NewGlobalRef(JObject lobj) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->NewGlobalRef(Env, lobj.Handle); + return new T() { Handle = res, ReferenceType = JNI.ReferenceType.Global }; + } + } + + public static void DeleteGlobalRef(JObject gref) + { + unsafe + { + if (gref == null) + return; + + if (!gref.Valid()) + return; + + Env->Functions->DeleteGlobalRef(Env, gref.Handle); + } + } + + public static void CheckExceptionAndThrow() + { + if (ExceptionCheck()) + { + JThrowable throwable = ExceptionOccurred(); + ExceptionClear(); + throw new JThrowableException(throwable); + } + } + + public static void DeleteLocalRef(JObject lref) + { + unsafe + { + if (lref == null) + return; + + if (!lref.Valid()) + return; + + Env->Functions->DeleteLocalRef(Env, lref.Handle); + } + } + + public static bool IsSameObject(JObject obj1, JObject obj2) + { + unsafe + { + return Convert.ToBoolean(Env->Functions->IsSameObject(Env, obj1.Handle, obj2.Handle)); + } + } + + public static T NewLocalRef(JObject obj) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->NewLocalRef(Env, obj.Handle); + return new T() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + } + } + + public static int EnsureLocalCapacity(int capacity) + { + unsafe + { + return Env->Functions->EnsureLocalCapacity(Env, capacity); + } + } + + public static T AllocObject(JClass cls) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->AllocObject(Env, cls.Handle); + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static T NewObject(JClass cls, JMethodID methodID, params JValue[] args) where T : JObject, new() + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + IntPtr res = Env->Functions->NewObjectA(Env, cls.Handle, methodID, argsPtr); + JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static JClass GetObjectClass(JObject obj) + { + unsafe + { + IntPtr res = Env->Functions->GetObjectClass(Env, obj.Handle); + + using JClass local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static bool IsInstanceOf(JObject obj, JClass cls) + { + unsafe + { + return Convert.ToBoolean(Env->Functions->IsInstanceOf(Env, obj.Handle, cls.Handle)); + } + } + + public static JMethodID GetMethodID(JClass cls, string name, string sig) + { + unsafe + { + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + IntPtr sigAnsi = Marshal.StringToHGlobalAnsi(sig); + + JMethodID id = Env->Functions->GetMethodID(Env, cls.Handle, nameAnsi, sigAnsi); + + Marshal.FreeHGlobal(nameAnsi); + Marshal.FreeHGlobal(sigAnsi); + return id; + } + } + + public static T CallObjectMethod(JObject obj, JMethodID methodID, params JValue[] args) where T : JObject, new() + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + + fixed (JValue* v = args) + { + IntPtr res = Env->Functions->CallObjectMethodA(Env, obj.Handle, methodID, argsPtr); + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + } + + public static T CallMethod(JObject obj, JMethodID methodID, params JValue[] args) + { + unsafe + { + Type t = typeof(T); + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + + if (t == typeof(bool)) + { + return (T)(object)Convert.ToBoolean(Env->Functions->CallBooleanMethodA(Env, obj.Handle, methodID, argsPtr)); + } + else if (t == typeof(sbyte)) + { + return (T)(object)Env->Functions->CallByteMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(char)) + { + return (T)(object)Env->Functions->CallCharMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(short)) + { + return (T)(object)Env->Functions->CallShortMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(int)) + { + return (T)(object)Env->Functions->CallIntMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(long)) + { + return (T)(object)Env->Functions->CallLongMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(float)) + { + return (T)(object)Env->Functions->CallFloatMethodA(Env, obj.Handle, methodID, argsPtr); + } + else if (t == typeof(double)) + { + return (T)(object)Env->Functions->CallDoubleMethodA(Env, obj.Handle, methodID, argsPtr); + } + else + { + throw new ArgumentException($"CallMethod Type {t} not supported."); + } + } + } + + public static void CallVoidMethod(JObject obj, JMethodID methodID, params JValue[] args) + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + Env->Functions->CallVoidMethodA(Env, obj.Handle, methodID, argsPtr); + } + } + + public static T CallNonvirtualObjectMethod(JObject obj, JClass cls, JMethodID methodID, params JValue[] args) where T : JObject, new() + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + IntPtr res = Env->Functions->CallNonvirtualObjectMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static T CallNonvirtualMethod(JObject obj, JClass cls, JMethodID methodID, params JValue[] args) + { + unsafe + { + Type t = typeof(T); + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + + if (t == typeof(bool)) + { + return (T)(object)Convert.ToBoolean(Env->Functions->CallNonvirtualBooleanMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr)); + } + else if (t == typeof(sbyte)) + { + return (T)(object)Env->Functions->CallNonvirtualByteMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(char)) + { + return (T)(object)Env->Functions->CallNonvirtualCharMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(short)) + { + return (T)(object)Env->Functions->CallNonvirtualShortMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(int)) + { + return (T)(object)Env->Functions->CallNonvirtualIntMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(long)) + { + return (T)(object)Env->Functions->CallNonvirtualLongMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(float)) + { + return (T)(object)Env->Functions->CallNonvirtualFloatMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(double)) + { + return (T)(object)Env->Functions->CallNonvirtualDoubleMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + else + { + throw new ArgumentException($"CallNonvirtualMethod Type {t} not supported."); + } + } + } + + public static void CallNonvirtualVoidMethod(JObject obj, JClass cls, JMethodID methodID, params JValue[] args) + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + Env->Functions->CallNonvirtualVoidMethodA(Env, obj.Handle, cls.Handle, methodID, argsPtr); + } + } + + public static JFieldID GetFieldID(JClass cls, string name, string sig) + { + unsafe + { + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + IntPtr sigAnsi = Marshal.StringToHGlobalAnsi(sig); + + JFieldID id = Env->Functions->GetFieldID(Env, cls.Handle, nameAnsi, sigAnsi); + + Marshal.FreeHGlobal(nameAnsi); + Marshal.FreeHGlobal(sigAnsi); + return id; + } + } + + public static T GetObjectField(JObject obj, JFieldID fieldID) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->GetObjectField(Env, obj.Handle, fieldID); + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static T GetField(JObject obj, JFieldID fieldID) + { + unsafe + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + return (T)(object)Convert.ToBoolean(Env->Functions->GetBooleanField(Env, obj.Handle, fieldID)); + } + else if (t == typeof(sbyte)) + { + return (T)(object)Env->Functions->GetByteField(Env, obj.Handle, fieldID); + } + else if (t == typeof(char)) + { + return (T)(object)Env->Functions->GetCharField(Env, obj.Handle, fieldID); + } + else if (t == typeof(short)) + { + return (T)(object)Env->Functions->GetShortField(Env, obj.Handle, fieldID); + } + else if (t == typeof(int)) + { + return (T)(object)Env->Functions->GetIntField(Env, obj.Handle, fieldID); + } + else if (t == typeof(long)) + { + return (T)(object)Env->Functions->GetLongField(Env, obj.Handle, fieldID); + } + else if (t == typeof(float)) + { + return (T)(object)Env->Functions->GetFloatField(Env, obj.Handle, fieldID); + } + else if (t == typeof(double)) + { + return (T)(object)Env->Functions->GetDoubleField(Env, obj.Handle, fieldID); + } + else + { + throw new ArgumentException($"GetField Type {t} not supported."); + } + } + } + + public static void SetObjectField(JObject obj, JFieldID fieldID, JObject val) + { + unsafe + { + Env->Functions->SetObjectField(Env, obj.Handle, fieldID, val.Handle); + } + } + + public static void SetField(JObject obj, JFieldID fieldID, T value) + { + unsafe + { + switch (value) + { + case bool b: + Env->Functions->SetBooleanField(Env, obj.Handle, fieldID, Convert.ToByte(b)); + break; + + case sbyte b: + Env->Functions->SetByteField(Env, obj.Handle, fieldID, b); + break; + + case char c: + Env->Functions->SetCharField(Env, obj.Handle, fieldID, c); + break; + + case short s: + Env->Functions->SetShortField(Env, obj.Handle, fieldID, s); + break; + + case int i: + Env->Functions->SetIntField(Env, obj.Handle, fieldID, i); + break; + + case float f: + Env->Functions->SetFloatField(Env, obj.Handle, fieldID, f); + break; + + case double d: + Env->Functions->SetDoubleField(Env, obj.Handle, fieldID, d); + break; + + default: + throw new ArgumentException($"SetField Type {value?.GetType()} not supported."); + } + } + } + + public static JMethodID GetStaticMethodID(JClass cls, string name, string sig) + { + unsafe + { + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + IntPtr sigAnsi = Marshal.StringToHGlobalAnsi(sig); + JMethodID id = Env->Functions->GetStaticMethodID(Env, cls.Handle, nameAnsi, sigAnsi); + return id; + } + } + + public static T CallStaticObjectMethod(JClass cls, JMethodID methodID, params JValue[] args) where T : JObject, new() + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + IntPtr res = Env->Functions->CallStaticObjectMethodA(Env, cls.Handle, methodID.Handle, argsPtr); + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static T CallStaticMethod(JClass cls, JMethodID methodID, params JValue[] args) + { + Type t = typeof(T); + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + + if (t == typeof(bool)) + { + return (T)(object)Convert.ToBoolean(Env->Functions->CallStaticBooleanMethodA(Env, cls.Handle, methodID, argsPtr)); + } + else if (t == typeof(sbyte)) + { + return (T)(object)Env->Functions->CallStaticByteMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(char)) + { + return (T)(object)Env->Functions->CallStaticCharMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(short)) + { + return (T)(object)Env->Functions->CallStaticShortMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(int)) + { + return (T)(object)Env->Functions->CallStaticIntMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(long)) + { + return (T)(object)Env->Functions->CallStaticLongMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(float)) + { + return (T)(object)Env->Functions->CallStaticFloatMethodA(Env, cls.Handle, methodID, argsPtr); + } + else if (t == typeof(double)) + { + return (T)(object)Env->Functions->CallStaticDoubleMethodA(Env, cls.Handle, methodID, argsPtr); + } + else + { + throw new ArgumentException($"CallStaticMethod Type {t} not supported."); + } + } + + public static void CallStaticVoidMethod(JClass cls, JMethodID methodID, params JValue[] args) + { + unsafe + { + IntPtr argsPtr = Marshal.UnsafeAddrOfPinnedArrayElement(args, 0); + Env->Functions->CallStaticVoidMethodA(Env, cls.Handle, methodID, argsPtr); + } + } + + public static JFieldID GetStaticFieldID(JClass cls, string name, string sig) + { + unsafe + { + IntPtr nameAnsi = Marshal.StringToHGlobalAnsi(name); + IntPtr sigAnsi = Marshal.StringToHGlobalAnsi(sig); + + JFieldID id = Env->Functions->GetStaticFieldID(Env, cls.Handle, nameAnsi, sigAnsi); + + Marshal.FreeHGlobal(nameAnsi); + Marshal.FreeHGlobal(sigAnsi); + return id; + } + } + + public static T GetStaticObjectField(JClass cls, JFieldID fieldID) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->GetStaticObjectField(Env, cls.Handle, fieldID); + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static T GetStaticField(JClass cls, JFieldID fieldID) + { + unsafe + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + return (T)(object)Convert.ToBoolean(Env->Functions->GetStaticBooleanField(Env, cls.Handle, fieldID)); + } + else if (t == typeof(sbyte)) + { + return (T)(object)Env->Functions->GetStaticByteField(Env, cls.Handle, fieldID); + } + else if (t == typeof(char)) + { + return (T)(object)Env->Functions->GetStaticCharField(Env, cls.Handle, fieldID); + } + else if (t == typeof(short)) + { + return (T)(object)Env->Functions->GetStaticShortField(Env, cls.Handle, fieldID); + } + else if (t == typeof(int)) + { + return (T)(object)Env->Functions->GetStaticIntField(Env, cls.Handle, fieldID); + } + else if (t == typeof(long)) + { + return (T)(object)Env->Functions->GetStaticLongField(Env, cls.Handle, fieldID); + } + else if (t == typeof(float)) + { + return (T)(object)Env->Functions->GetStaticFloatField(Env, cls.Handle, fieldID); + } + else if (t == typeof(double)) + { + return (T)(object)Env->Functions->GetStaticDoubleField(Env, cls.Handle, fieldID); + } + else + { + throw new ArgumentException($"GetStaticField Type {t} not supported."); + } + } + } + + public static void SetStaticObjectField(JClass cls, JFieldID fieldID, T value) where T : JObject, new() + { + unsafe + { + Env->Functions->SetStaticObjectField(Env, cls.Handle, fieldID, value.Handle); + } + } + + public static void SetStaticField(JClass cls, JFieldID fieldID, T value) + { + unsafe + { + switch (value) + { + case bool b: + Env->Functions->SetStaticBooleanField(Env, cls.Handle, fieldID, Convert.ToByte(b)); + break; + + case sbyte b: + Env->Functions->SetStaticByteField(Env, cls.Handle, fieldID, b); + break; + + case char c: + Env->Functions->SetStaticCharField(Env, cls.Handle, fieldID, c); + break; + + case short s: + Env->Functions->SetStaticShortField(Env, cls.Handle, fieldID, s); + break; + + case int i: + Env->Functions->SetStaticIntField(Env, cls.Handle, fieldID, i); + break; + + case float f: + Env->Functions->SetStaticFloatField(Env, cls.Handle, fieldID, f); + break; + + case double d: + Env->Functions->SetStaticDoubleField(Env, cls.Handle, fieldID, d); + break; + + default: + throw new ArgumentException($"SetField Type {value.GetType()} not supported."); + } + } + } + + public static JString NewString(string str) + { + unsafe + { + IntPtr strUni = Marshal.StringToHGlobalUni(str); + + IntPtr res = Env->Functions->NewString(Env, strUni, str.Length); + + Marshal.FreeHGlobal(strUni); + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static int GetStringLength(JString str) + { + unsafe + { + return Env->Functions->GetStringLength(Env, str.Handle); + } + } + + public static string GetJStringString(JString str) + { + unsafe + { + if (!str.Valid()) + return ""; + + IntPtr res = Env->Functions->GetStringUTFChars(Env, str.Handle, out byte isCopy); + string? resultString = Marshal.PtrToStringAuto(res); + Env->Functions->ReleaseStringChars(Env, str.Handle, res); + return resultString ?? ""; + } + } + + private static void ReleaseStringChars(JString str, IntPtr chars) + { + Env->Functions->ReleaseStringChars(Env, str.Handle, chars); + } + + public static int GetArrayLength(JArray jarray) + { + unsafe + { + return Env->Functions->GetArrayLength(Env, jarray.Handle); + } + } + + public static int GetArrayLength(JObjectArray jarray) where T : JObject, new() + { + unsafe + { + return Env->Functions->GetArrayLength(Env, jarray.Handle); + } + } + + public static T GetObjectArrayElement(JObjectArray array, int index) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->GetObjectArrayElement(Env, array.Handle, index); + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef(local); + } + } + + public static void SetObjectArrayElement(JObjectArray array, int index, T value) where T : JObject, new() + { + unsafe + { + Env->Functions->SetObjectArrayElement(Env, array.Handle, index, value.Handle); + } + } + + public static JArray NewArray(int length) + { + unsafe + { + Type t = typeof(T); + IntPtr res; + + if (t == typeof(bool)) + { + res = Env->Functions->NewBooleanArray(Env, length); + } + else if (t == typeof(sbyte)) + { + res = Env->Functions->NewByteArray(Env, length); + } + else if (t == typeof(char)) + { + res = Env->Functions->NewCharArray(Env, length); + } + else if (t == typeof(short)) + { + res = Env->Functions->NewShortArray(Env, length); + } + else if (t == typeof(int)) + { + res = Env->Functions->NewIntArray(Env, length); + } + else if (t == typeof(long)) + { + res = Env->Functions->NewBooleanArray(Env, length); + } + else if (t == typeof(float)) + { + res = Env->Functions->NewBooleanArray(Env, length); + } + else if (t == typeof(double)) + { + res = Env->Functions->NewBooleanArray(Env, length); + } + else + { + throw new ArgumentException($"CallStaticMethod Type {t} not supported."); + } + + using JObject local = new() { Handle = res, ReferenceType = JNI.ReferenceType.Local }; + return NewGlobalRef>(local); + } + } + + public static T[] GetArrayElements(JArray array) + { + unsafe + { + Type t = typeof(T); + int length = GetArrayLength(array); + + if (t == typeof(bool)) + { + byte* arr = Env->Functions->GetBooleanArrayElements(Env, array.Handle, out byte isCopy); + + bool[] buf = new bool[length]; + + for (int i = 0; i < length; i++) + buf[i] = Convert.ToBoolean(arr[i]); + + Env->Functions->ReleaseBooleanArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(sbyte)) + { + sbyte* arr = Env->Functions->GetByteArrayElements(Env, array.Handle, out byte isCopy); + + sbyte[] buf = new sbyte[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseByteArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(char)) + { + char* arr = Env->Functions->GetCharArrayElements(Env, array.Handle, out byte isCopy); + + char[] buf = new char[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseCharArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(short)) + { + short* arr = Env->Functions->GetShortArrayElements(Env, array.Handle, out byte isCopy); + + short[] buf = new short[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseShortArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(int)) + { + int* arr = Env->Functions->GetIntArrayElements(Env, array.Handle, out byte isCopy); + + int[] buf = new int[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseIntArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(long)) + { + long* arr = Env->Functions->GetLongArrayElements(Env, array.Handle, out byte isCopy); + + long[] buf = new long[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseLongArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(float)) + { + float* arr = Env->Functions->GetFloatArrayElements(Env, array.Handle, out byte isCopy); + + float[] buf = new float[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseFloatArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else if (t == typeof(double)) + { + double* arr = Env->Functions->GetDoubleArrayElements(Env, array.Handle, out byte isCopy); + + double[] buf = new double[length]; + + for (int i = 0; i < length; i++) + buf[i] = arr[i]; + + Env->Functions->ReleaseDoubleArrayElements(Env, array.Handle, arr, (int)JNI.ReleaseMode.Abort); + return (T[])(object)buf; + } + else + { + throw new ArgumentException($"GetArrayElements Type {t} not supported."); + } + } + } + + public static T[] GetArrayRegion(JArray array, int start, int len) + { + unsafe + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + fixed (byte* buf = new byte[len]) + { + Env->Functions->GetBooleanArrayRegion(Env, array.Handle, start, len, buf); + + bool[] res = new bool[len]; + for (int i = 0; i < len; i++) + { + res[i] = Convert.ToBoolean(buf[i]); + } + return (T[])(object)res; + } + } + else if (t == typeof(sbyte)) + { + sbyte[] buf = new sbyte[len]; + + fixed (sbyte* b = buf) + { + Env->Functions->GetByteArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(char)) + { + char[] buf = new char[len]; + + fixed (char* b = buf) + { + Env->Functions->GetCharArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(short)) + { + short[] buf = new short[len]; + + fixed (short* b = buf) + { + Env->Functions->GetShortArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(int)) + { + int[] buf = new int[len]; + + fixed (int* b = buf) + { + Env->Functions->GetIntArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(long)) + { + long[] buf = new long[len]; + + fixed (long* b = buf) + { + Env->Functions->GetLongArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(float)) + { + float[] buf = new float[len]; + + fixed (float* b = buf) + { + Env->Functions->GetFloatArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else if (t == typeof(double)) + { + double[] buf = new double[len]; + + fixed (double* b = buf) + { + Env->Functions->GetDoubleArrayRegion(Env, array.Handle, start, len, b); + return (T[])(object)buf; + } + } + else + { + throw new ArgumentException($"GetArrayRegion Type {t} not supported."); + } + } + } + + public static T GetArrayElement(JArray array, int index) + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + byte b; + Env->Functions->GetBooleanArrayRegion(Env, array.Handle, index, 1, &b); + return (T)(object)Convert.ToBoolean(b); + } + else if (t == typeof(sbyte)) + { + sbyte b; + Env->Functions->GetByteArrayRegion(Env, array.Handle, index, 1, &b); + return (T)(object)b; + } + else if (t == typeof(char)) + { + char c; + Env->Functions->GetCharArrayRegion(Env, array.Handle, index, 1, &c); + return (T)(object)c; + } + else if (t == typeof(short)) + { + short s; + Env->Functions->GetShortArrayRegion(Env, array.Handle, index, 1, &s); + return (T)(object)s; + } + else if (t == typeof(int)) + { + int i; + Env->Functions->GetIntArrayRegion(Env, array.Handle, index, 1, &i); + return (T)(object)i; + } + else if (t == typeof(long)) + { + long l; + Env->Functions->GetLongArrayRegion(Env, array.Handle, index, 1, &l); + return (T)(object)l; + } + else if (t == typeof(float)) + { + float f; + Env->Functions->GetFloatArrayRegion(Env, array.Handle, index, 1, &f); + return (T)(object)f; + } + else if (t == typeof(double)) + { + double d; + Env->Functions->GetDoubleArrayRegion(Env, array.Handle, index, 1, &d); + return (T)(object)d; + } + else + { + throw new ArgumentException($"GetArrayElement Type {t} not supported."); + } + } + + public static void SetArrayRegion(JArray array, int start, int len, T[] elems) + { + unsafe + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + fixed (byte* buf = elems.Select(b => Convert.ToByte(b)).ToArray()) + { + Env->Functions->SetBooleanArrayRegion(Env, array.Handle, start, len, null); + } + } + else if (t == typeof(sbyte)) + { + fixed (sbyte* buf = (sbyte[])(object)elems) + { + Env->Functions->SetByteArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(char)) + { + fixed (char* buf = (char[])(object)elems) + { + Env->Functions->SetCharArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(short)) + { + fixed (short* buf = (short[])(object)elems) + { + Env->Functions->SetShortArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(int)) + { + fixed (int* buf = (int[])(object)elems) + { + Env->Functions->SetIntArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(long)) + { + fixed (long* buf = (long[])(object)elems) + { + Env->Functions->SetLongArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(float)) + { + fixed (float* buf = (float[])(object)elems) + { + Env->Functions->SetFloatArrayRegion(Env, array.Handle, start, len, buf); + } + } + else if (t == typeof(double)) + { + fixed (double* buf = (double[])(object)elems) + { + Env->Functions->SetDoubleArrayRegion(Env, array.Handle, start, len, buf); + } + } + else + { + throw new ArgumentException($"SetArrayRegion Type {t} not supported."); + } + } + } + + public static void SetArrayElement(JArray array, int index, T value) + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + byte b = Convert.ToByte(value); + Env->Functions->SetBooleanArrayRegion(Env, array.Handle, index, 1, &b); + } + else if (t == typeof(sbyte)) + { + sbyte b = (sbyte)(object)value; + Env->Functions->SetByteArrayRegion(Env, array.Handle, index, 1, &b); + } + else if (t == typeof(char)) + { + char c = (char)(object)value; + Env->Functions->SetCharArrayRegion(Env, array.Handle, index, 1, &c); + } + else if (t == typeof(short)) + { + short s = (short)(object)value; + Env->Functions->SetShortArrayRegion(Env, array.Handle, index, 1, &s); + } + else if (t == typeof(int)) + { + int c = (int)(object)value; + Env->Functions->SetIntArrayRegion(Env, array.Handle, index, 1, &c); + } + else if (t == typeof(long)) + { + long l = (long)(object)value; + Env->Functions->SetLongArrayRegion(Env, array.Handle, index, 1, &l); + } + else if (t == typeof(float)) + { + float f = (float)(object)value; + Env->Functions->SetFloatArrayRegion(Env, array.Handle, index, 1, &f); + } + else if (t == typeof(double)) + { + double d = (double)(object)value; + Env->Functions->SetDoubleArrayRegion(Env, array.Handle, index, 1, &d); + } + else + { + throw new ArgumentException($"SetArrayElement Type {t} not supported."); + } + } + + private static int RegisterNatives(JClass cls, IntPtr methods, int nmethods) + { + throw new NotImplementedException(); + } + + private static int UnregisterNatives(JClass cls) + { + throw new NotImplementedException(); + } + + public static int MonitorEnter(JObject obj) + { + unsafe + { + return Env->Functions->MonitorEnter(Env, obj.Handle); + } + } + + public static int MonitorExit(JObject obj) + { + unsafe + { + return Env->Functions->MonitorExit(Env, obj.Handle); + } + } + + private static JavaVM* GetJavaVM() + { + throw new NotImplementedException(); + } + + public static string? GetStringRegion(JString str, int start, int len) + { + unsafe + { + //Probably need to allocate space? + IntPtr buf = IntPtr.Zero; + Env->Functions->GetStringRegion(Env, str.Handle, start, len, buf); + + if (buf != IntPtr.Zero) + { + return Marshal.PtrToStringUni(buf); + } + + return null; + } + } + + private static IntPtr GetPrimitiveArrayCritical(JArray array) + { + throw new NotImplementedException(); + } + + private static void ReleasePrimitiveArrayCritical(JArray array, IntPtr carray, int mode) + { + throw new NotImplementedException(); + } + + private static string GetStringCritical(JString str) + { + throw new NotImplementedException(); + } + + private static void ReleaseStringCritical(JString str) + { + throw new NotImplementedException(); + } + + public static T NewWeakGlobalRef(JObject obj) where T : JObject, new() + { + unsafe + { + IntPtr res = Env->Functions->NewWeakGlobalRef(Env, obj.Handle); + return new T() { Handle = res, ReferenceType = JNI.ReferenceType.WeakGlobal }; + } + } + + public static void DeleteWeakGlobalRef(JObject obj) + { + unsafe + { + Env->Functions->DeleteWeakGlobalRef(Env, obj.Handle); + } + } + + public static bool ExceptionCheck() + { + unsafe + { + return Convert.ToBoolean(Env->Functions->ExceptionCheck(Env)); + } + } + + private static JObject NewDirectByteBuffer(IntPtr address, int capacity) + { + throw new NotImplementedException(); + } + + private static IntPtr GetDirectBufferAddress(JObject buf) + { + throw new NotImplementedException(); + } + + private static int GetDirectBufferCapacity(JObject obj) + { + throw new NotImplementedException(); + } +} +#endif + +#pragma warning restore CS8600 +#pragma warning restore CS8605 +#pragma warning restore CS8632 +#pragma warning restore CS8602 \ No newline at end of file diff --git a/MelonLoader/JNI/JNIEnv.cs b/MelonLoader/JNI/JNIEnv.cs new file mode 100644 index 000000000..85a93cc1c --- /dev/null +++ b/MelonLoader/JNI/JNIEnv.cs @@ -0,0 +1,713 @@ +#if ANDROID +namespace MelonLoader.Java; + +using System; +using System.Runtime.InteropServices; + +/// +/// Represents a JNIEnv C struct, the FunctionTable contains +/// all the JNI functions. +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly unsafe struct JNIEnv +{ + [StructLayout(LayoutKind.Sequential)] + internal readonly unsafe struct FunctionTable + { + internal readonly IntPtr Reserved0; + + internal readonly IntPtr Reserved1; + + internal readonly IntPtr Reserved2; + + internal readonly IntPtr Reserved3; + + // jint (JNICALL *GetVersion)(JNIEnv *env); + internal readonly delegate* unmanaged[Stdcall] GetVersion; + + // jclass (JNICALL *DefineClass) (JNIEnv* env, const char* name, jobject loader, const jbyte* buf, jsize len); + internal readonly delegate* unmanaged[Stdcall] DefineClass; + + // jclass (JNICALL *FindClass) (JNIEnv* env, const char* name); + internal readonly delegate* unmanaged[Stdcall] FindClass; + + // jmethodID (JNICALL *FromReflectedMethod) (JNIEnv* env, jobject method); + internal readonly delegate* unmanaged[Stdcall] FromReflectedMethod; + + // jfieldID (JNICALL *FromReflectedField) (JNIEnv* env, jobject field); + internal readonly delegate* unmanaged[Stdcall] FromReflectedField; + + // jobject (JNICALL *ToReflectedMethod) (JNIEnv* env, jclass cls, jmethodID methodID, jboolean isStatic); + internal readonly delegate* unmanaged[Stdcall] ToReflectedMethod; + + // jclass (JNICALL *GetSuperclass) (JNIEnv* env, jclass sub); + internal readonly delegate* unmanaged[Stdcall] GetSuperClass; + + // jboolean (JNICALL *IsAssignableFrom) (JNIEnv* env, jclass sub, jclass sup); + internal readonly delegate* unmanaged[Stdcall] IsAssignableFrom; + + // jobject (JNICALL *ToReflectedField) (JNIEnv* env, jclass cls, jfieldID fieldID, jboolean isStatic); + internal readonly delegate* unmanaged[Stdcall] ToReflectedField; + + // jint (JNICALL *Throw) (JNIEnv* env, jthrowable obj); + internal readonly delegate* unmanaged[Stdcall] Throw; + + // jint (JNICALL *ThrowNew) (JNIEnv* env, jclass clazz, const char* msg); + internal readonly delegate* unmanaged[Stdcall] ThrowNew; + + // jthrowable (JNICALL *ExceptionOccurred) (JNIEnv* env); + internal readonly delegate* unmanaged[Stdcall] ExceptionOccurred; + + // void (JNICALL *ExceptionDescribe) (JNIEnv* env); + internal readonly delegate* unmanaged[Stdcall] ExceptionDescribe; + + // void (JNICALL *ExceptionClear) (JNIEnv* env); + internal readonly delegate* unmanaged[Stdcall] ExceptionClear; + + // void (JNICALL *FatalError) (JNIEnv* env, const char* msg); + internal readonly delegate* unmanaged[Stdcall] FatalError; + + // jint (JNICALL *PushLocalFrame) (JNIEnv* env, jint capacity); + internal readonly delegate* unmanaged[Stdcall] PushLocalFrame; + + // jobject (JNICALL *PopLocalFrame) (JNIEnv* env, jobject result); + internal readonly delegate* unmanaged[Stdcall] PopLocalFrame; + + // jobject (JNICALL *NewGlobalRef) (JNIEnv* env, jobject lobj); + internal readonly delegate* unmanaged[Stdcall] NewGlobalRef; + + // void (JNICALL *DeleteGlobalRef) (JNIEnv* env, jobject gref); + internal readonly delegate* unmanaged[Stdcall] DeleteGlobalRef; + + // void (JNICALL *DeleteLocalRef) (JNIEnv* env, jobject obj); + internal readonly delegate* unmanaged[Stdcall] DeleteLocalRef; + + // jboolean (JNICALL *IsSameObject) (JNIEnv* env, jobject obj1, jobject obj2); + internal readonly delegate* unmanaged[Stdcall] IsSameObject; + + // jobject (JNICALL *NewLocalRef) (JNIEnv* env, jobject ref); + internal readonly delegate* unmanaged[Stdcall] NewLocalRef; + + // jint (JNICALL *EnsureLocalCapacity) (JNIEnv* env, jint capacity); + internal readonly delegate* unmanaged[Stdcall] EnsureLocalCapacity; + + // jobject (JNICALL *AllocObject) (JNIEnv* env, jclass clazz); + internal readonly delegate* unmanaged[Stdcall] AllocObject; + + // jobject (JNICALL *NewObject) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr NewObject; + + // jobject (JNICALL *NewObjectV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr NewObjectV; + + // jobject (JNICALL *NewObjectA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] NewObjectA; + + // jclass (JNICALL *GetObjectClass) (JNIEnv* env, jobject obj); + internal readonly delegate* unmanaged[Stdcall] GetObjectClass; + + // jboolean (JNICALL *IsInstanceOf) (JNIEnv* env, jobject obj, jclass clazz); + internal readonly delegate* unmanaged[Stdcall] IsInstanceOf; + + // jmethodID (JNICALL *GetMethodID) (JNIEnv* env, jclass clazz, const char* name, const char* sig); + internal readonly delegate* unmanaged[Stdcall] GetMethodID; + + // jobject (JNICALL *CallObjectMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallObjectMethod; + + // jobject (JNICALL *CallObjectMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallObjectMethodV; + + // jobject (JNICALL *CallObjectMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallObjectMethodA; + + // jboolean (JNICALL *CallBooleanMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallBooleanMethod; + + // jboolean (JNICALL *CallBooleanMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallBooleanMethodV; + + // jboolean (JNICALL *CallBooleanMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallBooleanMethodA; + + // jbyte (JNICALL *CallByteMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallByteMethod; + + // jbyte (JNICALL *CallByteMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallByteMethodV; + + // jbyte (JNICALL *CallByteMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallByteMethodA; + + // jchar (JNICALL *CallCharMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallCharMethod; + + // jchar (JNICALL *CallCharMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallCharMethodV; + + // jchar (JNICALL *CallCharMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallCharMethodA; + + // jshort (JNICALL *CallShortMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallShortMethod; + + // jshort (JNICALL *CallShortMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallShortMethodV; + + // jshort (JNICALL *CallShortMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallShortMethodA; + + // jint (JNICALL *CallIntMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallIntMethod; + + // jshort (JNICALL *CallShortMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallIntMethodV; + + // jint (JNICALL *CallIntMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallIntMethodA; + + // jlong (JNICALL *CallLongMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallLongMethod; + + // jlong (JNICALL *CallLongMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallLongMethodV; + + // jlong (JNICALL *CallLongMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallLongMethodA; + + // jfloat (JNICALL *CallFloatMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallFloatMethod; + + // jfloat (JNICALL *CallFloatMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallFloatMethodV; + + // jfloat (JNICALL *CallFloatMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallFloatMethodA; + + // jdouble (JNICALL *CallDoubleMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallDoubleMethod; + + // jdouble (JNICALL *CallDoubleMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallDoubleMethodV; + + // jdouble (JNICALL *CallDoubleMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallDoubleMethodA; + + // void (JNICALL *CallVoidMethod) (JNIEnv* env, jobject obj, jmethodID methodID, ...); + internal readonly IntPtr CallVoidMethod; + + // void (JNICALL *CallVoidMethodV) (JNIEnv* env, jobject obj, jmethodID methodID, va_list args); + internal readonly IntPtr CallVoidMethodV; + + // void (JNICALL *CallVoidMethodA) (JNIEnv* env, jobject obj, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallVoidMethodA; + + // jobject (JNICALL *CallNonvirtualObjectMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualObjectMethod; + + // jobject (JNICALL *CallNonvirtualObjectMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualObjectMethodV; + + // jobject (JNICALL *CallNonvirtualObjectMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualObjectMethodA; + + // jboolean (JNICALL *CallNonvirtualBooleanMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualBooleanMethod; + + // jboolean (JNICALL *CallNonvirtualBooleanMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualBooleanMethodV; + + // jboolean (JNICALL *CallNonvirtualBooleanMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualBooleanMethodA; + + // jbyte (JNICALL *CallNonvirtualByteMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualByteMethod; + + // jbyte (JNICALL *CallNonvirtualByteMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualByteMethodV; + + // jbyte (JNICALL *CallNonvirtualByteMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualByteMethodA; + + // jchar (JNICALL *CallNonvirtualCharMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualCharMethod; + + // jchar (JNICALL *CallNonvirtualCharMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualCharMethodV; + + // jchar (JNICALL *CallNonvirtualCharMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualCharMethodA; + + // jshort (JNICALL *CallNonvirtualShortMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualShortMethod; + + // jshort (JNICALL *CallNonvirtualShortMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualShortMethodV; + + // jshort (JNICALL *CallNonvirtualShortMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualShortMethodA; + + // jint (JNICALL *CallNonvirtualIntMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualIntMethod; + + // jint (JNICALL *CallNonvirtualIntMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualIntMethodV; + + // jint (JNICALL *CallNonvirtualIntMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualIntMethodA; + + // jlong (JNICALL *CallNonvirtualLongMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualLongMethod; + + // jlong (JNICALL *CallNonvirtualLongMethodV)(JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualLongMethodV; + + // jlong (JNICALL *CallNonvirtualLongMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualLongMethodA; + + // jfloat (JNICALL *CallNonvirtualFloatMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualFloatMethod; + + // jfloat (JNICALL *CallNonvirtualFloatMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualFloatMethodV; + + // jfloat (JNICALL *CallNonvirtualFloatMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualFloatMethodA; + + // jdouble (JNICALL *CallNonvirtualDoubleMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualDoubleMethod; + + // jdouble (JNICALL *CallNonvirtualDoubleMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualDoubleMethodV; + + // jdouble (JNICALL *CallNonvirtualDoubleMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualDoubleMethodA; + + // void (JNICALL *CallNonvirtualVoidMethod) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallNonvirtualVoidMethod; + + // void (JNICALL *CallNonvirtualVoidMethodV) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallNonvirtualVoidMethodV; + + // void (JNICALL *CallNonvirtualVoidMethodA) (JNIEnv* env, jobject obj, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallNonvirtualVoidMethodA; + + // jfieldID (JNICALL *GetFieldID) (JNIEnv* env, jclass clazz, const char* name, const char* sig); + internal readonly delegate* unmanaged[Stdcall] GetFieldID; + + // jobject (JNICALL *GetObjectField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetObjectField; + + // jboolean (JNICALL *GetBooleanField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetBooleanField; + + // jbyte (JNICALL *GetByteField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetByteField; + + // jchar (JNICALL *GetCharField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetCharField; + + // jshort (JNICALL *GetShortField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetShortField; + + // jint (JNICALL *GetIntField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetIntField; + + // jlong (JNICALL *GetLongField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetLongField; + + // jfloat (JNICALL *GetFloatField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetFloatField; + + // jdouble (JNICALL *GetDoubleField) (JNIEnv* env, jobject obj, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetDoubleField; + + // void (JNICALL *SetObjectField) (JNIEnv* env, jobject obj, jfieldID fieldID, jobject val); + internal readonly delegate* unmanaged[Stdcall] SetObjectField; + + // void (JNICALL *SetBooleanField) (JNIEnv* env, jobject obj, jfieldID fieldID, jboolean val); + internal readonly delegate* unmanaged[Stdcall] SetBooleanField; + + // void (JNICALL *SetByteField) (JNIEnv* env, jobject obj, jfieldID fieldID, jbyte val); + internal readonly delegate* unmanaged[Stdcall] SetByteField; + + // void (JNICALL *SetCharField) (JNIEnv* env, jobject obj, jfieldID fieldID, jchar val); + internal readonly delegate* unmanaged[Stdcall] SetCharField; + + // void (JNICALL *SetShortField) (JNIEnv* env, jobject obj, jfieldID fieldID, jshort val); + internal readonly delegate* unmanaged[Stdcall] SetShortField; + + // void (JNICALL *SetIntField) (JNIEnv* env, jobject obj, jfieldID fieldID, jint val); + internal readonly delegate* unmanaged[Stdcall] SetIntField; + + // void (JNICALL *SetLongField) (JNIEnv* env, jobject obj, jfieldID fieldID, jlong val); + internal readonly delegate* unmanaged[Stdcall] SetLongField; + + // void (JNICALL *SetFloatField) (JNIEnv* env, jobject obj, jfieldID fieldID, jfloat val); + internal readonly delegate* unmanaged[Stdcall] SetFloatField; + + // void (JNICALL *SetDoubleField) (JNIEnv* env, jobject obj, jfieldID fieldID, jdouble val); + internal readonly delegate* unmanaged[Stdcall] SetDoubleField; + + // jmethodID (JNICALL *GetStaticMethodID) (JNIEnv* env, jclass clazz, const char* name, const char* sig); + internal readonly delegate* unmanaged[Stdcall] GetStaticMethodID; + + // jobject (JNICALL *CallStaticObjectMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticObjectMethod; + + // jobject (JNICALL *CallStaticObjectMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticObjectMethodV; + + // jobject (JNICALL *CallStaticObjectMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticObjectMethodA; + + // jboolean (JNICALL *CallStaticBooleanMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticBooleanMethod; + + // jboolean (JNICALL *CallStaticBooleanMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticBooleanMethodV; + + // jboolean (JNICALL *CallStaticBooleanMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticBooleanMethodA; + + // jbyte (JNICALL *CallStaticByteMethod)(JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticByteMethod; + + // jbyte (JNICALL *CallStaticByteMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticByteMethodV; + + // jbyte (JNICALL *CallStaticByteMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticByteMethodA; + + // jchar (JNICALL *CallStaticCharMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticCharMethod; + + // jchar (JNICALL *CallStaticCharMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticCharMethodV; + + // jchar (JNICALL *CallStaticCharMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticCharMethodA; + + // jshort (JNICALL *CallStaticShortMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticShortMethod; + + // jshort (JNICALL *CallStaticShortMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticShortMethodV; + + // jshort (JNICALL *CallStaticShortMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticShortMethodA; + + // jint (JNICALL *CallStaticIntMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticIntMethod; + + // jint (JNICALL *CallStaticIntMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticIntMethodV; + + // jint (JNICALL *CallStaticIntMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticIntMethodA; + + // jlong (JNICALL *CallStaticLongMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticLongMethod; + + // jlong (JNICALL *CallStaticLongMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticLongMethodV; + + // jlong (JNICALL *CallStaticLongMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticLongMethodA; + + // jfloat (JNICALL *CallStaticFloatMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticFloatMethod; + + // jfloat (JNICALL *CallStaticFloatMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticFloatMethodV; + + // jfloat (JNICALL *CallStaticFloatMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticFloatMethodA; + + // jdouble (JNICALL *CallStaticDoubleMethod) (JNIEnv* env, jclass clazz, jmethodID methodID, ...); + internal readonly IntPtr CallStaticDoubleMethod; + + // jdouble (JNICALL *CallStaticDoubleMethodV) (JNIEnv* env, jclass clazz, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticDoubleMethodV; + + // jdouble (JNICALL *CallStaticDoubleMethodA) (JNIEnv* env, jclass clazz, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticDoubleMethodA; + + // void (JNICALL *CallStaticVoidMethod) (JNIEnv* env, jclass cls, jmethodID methodID, ...); + internal readonly IntPtr CallStaticVoidMethod; + + // void (JNICALL *CallStaticVoidMethodV) (JNIEnv* env, jclass cls, jmethodID methodID, va_list args); + internal readonly IntPtr CallStaticVoidMethodV; + + // void (JNICALL *CallStaticVoidMethodA) (JNIEnv* env, jclass cls, jmethodID methodID, const jvalue* args); + internal readonly delegate* unmanaged[Stdcall] CallStaticVoidMethodA; + + // jfieldID (JNICALL *GetStaticFieldID) (JNIEnv* env, jclass clazz, const char* name, const char* sig); + internal readonly delegate* unmanaged[Stdcall] GetStaticFieldID; + + // jobject (JNICALL *GetStaticObjectField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticObjectField; + + // jboolean (JNICALL *GetStaticBooleanField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticBooleanField; + + // jbyte (JNICALL *GetStaticByteField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticByteField; + + // jchar (JNICALL *GetStaticCharField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticCharField; + + // jshort (JNICALL *GetStaticShortField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticShortField; + + // jint (JNICALL *GetStaticIntField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticIntField; + + // jlong (JNICALL *GetStaticLongField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticLongField; + + // jfloat (JNICALL *GetStaticFloatField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticFloatField; + + // jdouble (JNICALL *GetStaticDoubleField) (JNIEnv* env, jclass clazz, jfieldID fieldID); + internal readonly delegate* unmanaged[Stdcall] GetStaticDoubleField; + + // void (JNICALL *SetStaticObjectField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jobject value); + internal readonly delegate* unmanaged[Stdcall] SetStaticObjectField; + + // void (JNICALL *SetStaticBooleanField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jboolean value); + internal readonly delegate* unmanaged[Stdcall] SetStaticBooleanField; + + // void (JNICALL *SetStaticByteField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jbyte value); + internal readonly delegate* unmanaged[Stdcall] SetStaticByteField; + + // void (JNICALL *SetStaticCharField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jchar value); + internal readonly delegate* unmanaged[Stdcall] SetStaticCharField; + + // void (JNICALL *SetStaticShortField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jshort value); + internal readonly delegate* unmanaged[Stdcall] SetStaticShortField; + + // void (JNICALL *SetStaticIntField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jint value); + internal readonly delegate* unmanaged[Stdcall] SetStaticIntField; + + // void (JNICALL *SetStaticLongField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jlong value); + internal readonly delegate* unmanaged[Stdcall] SetStaticLongField; + + // void (JNICALL *SetStaticFloatField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jfloat value); + internal readonly delegate* unmanaged[Stdcall] SetStaticFloatField; + + // void (JNICALL *SetStaticDoubleField) (JNIEnv* env, jclass clazz, jfieldID fieldID, jdouble value); + internal readonly delegate* unmanaged[Stdcall] SetStaticDoubleField; + + // jstring (JNICALL *NewString) (JNIEnv* env, const jchar* unicode, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewString; + + // jsize (JNICALL *GetStringLength) (JNIEnv* env, jstring str); + internal readonly delegate* unmanaged[Stdcall] GetStringLength; + + // const jchar *(JNICALL *GetStringChars) (JNIEnv* env, jstring str, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetStringChars; + + // void (JNICALL *ReleaseStringChars) (JNIEnv* env, jstring str, const jchar* chars); + internal readonly delegate* unmanaged[Stdcall] ReleaseStringChars; + + // jstring (JNICALL *NewStringUTF) (JNIEnv* env, const char* utf); + internal readonly delegate* unmanaged[Stdcall] NewStringUTF; + + // jsize (JNICALL *GetStringUTFLength) (JNIEnv* env, jstring str); + internal readonly delegate* unmanaged[Stdcall] GetStringUTFLength; + + // const char* (JNICALL *GetStringUTFChars) (JNIEnv* env, jstring str, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetStringUTFChars; + + // void (JNICALL *ReleaseStringUTFChars) (JNIEnv* env, jstring str, const char* chars); + internal readonly delegate* unmanaged[Stdcall] ReleaseStringUTFChars; + + // jsize (JNICALL *GetArrayLength) (JNIEnv* env, jarray array); + internal readonly delegate* unmanaged[Stdcall] GetArrayLength; + + // jobjectArray (JNICALL *NewObjectArray) (JNIEnv* env, jsize len, jclass clazz, jobject init); + internal readonly delegate* unmanaged[Stdcall] NewObjectArray; + + // jobject (JNICALL *GetObjectArrayElement) (JNIEnv* env, jobjectArray array, jsize index); + internal readonly delegate* unmanaged[Stdcall] GetObjectArrayElement; + + // void (JNICALL *SetObjectArrayElement) (JNIEnv* env, jobjectArray array, jsize index, jobject val); + internal readonly delegate* unmanaged[Stdcall] SetObjectArrayElement; + + // jbooleanArray (JNICALL *NewBooleanArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewBooleanArray; + + // jbyteArray (JNICALL *NewByteArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewByteArray; + + // jcharArray (JNICALL *NewCharArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewCharArray; + + // jshortArray (JNICALL *NewShortArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewShortArray; + + // jintArray (JNICALL *NewIntArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewIntArray; + + // jlongArray (JNICALL *NewLongArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewLongArray; + + // jfloatArray (JNICALL *NewFloatArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewFloatArray; + + // jdoubleArray (JNICALL *NewDoubleArray) (JNIEnv* env, jsize len); + internal readonly delegate* unmanaged[Stdcall] NewDoubleArray; + + // jboolean * (JNICALL *GetBooleanArrayElements) (JNIEnv* env, jbooleanArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetBooleanArrayElements; + + // jbyte * (JNICALL *GetByteArrayElements) (JNIEnv* env, jbyteArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetByteArrayElements; + + // jchar * (JNICALL *GetCharArrayElements) (JNIEnv* env, jcharArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetCharArrayElements; + + // jshort * (JNICALL *GetShortArrayElements) (JNIEnv* env, jshortArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetShortArrayElements; + + // jint * (JNICALL *GetIntArrayElements) (JNIEnv* env, jintArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetIntArrayElements; + + // jlong * (JNICALL *GetLongArrayElements) (JNIEnv* env, jlongArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetLongArrayElements; + + // jfloat * (JNICALL *GetFloatArrayElements) (JNIEnv* env, jfloatArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetFloatArrayElements; + + // jdouble * (JNICALL *GetDoubleArrayElements) (JNIEnv* env, jdoubleArray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetDoubleArrayElements; + + // void (JNICALL *ReleaseBooleanArrayElements) (JNIEnv* env, jbooleanArray array, jboolean* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseBooleanArrayElements; + + // void (JNICALL *ReleaseByteArrayElements) (JNIEnv* env, jbyteArray array, jbyte* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseByteArrayElements; + + // void (JNICALL *ReleaseCharArrayElements) (JNIEnv* env, jcharArray array, jchar* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseCharArrayElements; + + // void (JNICALL *ReleaseShortArrayElements) (JNIEnv* env, jshortArray array, jshort* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseShortArrayElements; + + // void (JNICALL *ReleaseIntArrayElements) (JNIEnv* env, jintArray array, jint* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseIntArrayElements; + + // void (JNICALL *ReleaseLongArrayElements) (JNIEnv* env, jlongArray array, jlong* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseLongArrayElements; + + // void (JNICALL *ReleaseFloatArrayElements) (JNIEnv* env, jfloatArray array, jfloat* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseFloatArrayElements; + + // void (JNICALL *ReleaseDoubleArrayElements) (JNIEnv* env, jdoubleArray array, jdouble* elems, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleaseDoubleArrayElements; + + // void (JNICALL *GetBooleanArrayRegion) (JNIEnv* env, jbooleanArray array, jsize start, jsize l, jboolean* buf); + internal readonly delegate* unmanaged[Stdcall] GetBooleanArrayRegion; + + // void (JNICALL *GetByteArrayRegion) (JNIEnv* env, jbyteArray array, jsize start, jsize len, jbyte* buf); + internal readonly delegate* unmanaged[Stdcall] GetByteArrayRegion; + + // void (JNICALL *GetCharArrayRegion) (JNIEnv* env, jcharArray array, jsize start, jsize len, jchar* buf); + internal readonly delegate* unmanaged[Stdcall] GetCharArrayRegion; + + // void (JNICALL *GetShortArrayRegion) (JNIEnv* env, jshortArray array, jsize start, jsize len, jshort* buf); + internal readonly delegate* unmanaged[Stdcall] GetShortArrayRegion; + + // void (JNICALL *GetIntArrayRegion) (JNIEnv* env, jintArray array, jsize start, jsize len, jint* buf); + internal readonly delegate* unmanaged[Stdcall] GetIntArrayRegion; + + // void (JNICALL *GetLongArrayRegion) (JNIEnv* env, jlongArray array, jsize start, jsize len, jlong* buf); + internal readonly delegate* unmanaged[Stdcall] GetLongArrayRegion; + + // void (JNICALL *GetFloatArrayRegion) (JNIEnv* env, jfloatArray array, jsize start, jsize len, jfloat* buf); + internal readonly delegate* unmanaged[Stdcall] GetFloatArrayRegion; + + // void (JNICALL *GetDoubleArrayRegion) (JNIEnv* env, jdoubleArray array, jsize start, jsize len, jdouble* buf); + internal readonly delegate* unmanaged[Stdcall] GetDoubleArrayRegion; + + // void (JNICALL *SetBooleanArrayRegion) (JNIEnv* env, jbooleanArray array, jsize start, jsize l, const jboolean* buf); + internal readonly delegate* unmanaged[Stdcall] SetBooleanArrayRegion; + + // void (JNICALL *SetByteArrayRegion) (JNIEnv* env, jbyteArray array, jsize start, jsize len, const jbyte* buf); + internal readonly delegate* unmanaged[Stdcall] SetByteArrayRegion; + + // void (JNICALL *SetCharArrayRegion) (JNIEnv* env, jcharArray array, jsize start, jsize len, const jchar* buf); + internal readonly delegate* unmanaged[Stdcall] SetCharArrayRegion; + + // void (JNICALL *SetShortArrayRegion) (JNIEnv* env, jshortArray array, jsize start, jsize len, const jshort* buf); + internal readonly delegate* unmanaged[Stdcall] SetShortArrayRegion; + + // void (JNICALL *SetIntArrayRegion) (JNIEnv* env, jintArray array, jsize start, jsize len, const jint* buf); + internal readonly delegate* unmanaged[Stdcall] SetIntArrayRegion; + + // void (JNICALL *SetLongArrayRegion) (JNIEnv* env, jlongArray array, jsize start, jsize len, const jlong* buf); + internal readonly delegate* unmanaged[Stdcall] SetLongArrayRegion; + + // void (JNICALL *SetFloatArrayRegion) (JNIEnv* env, jfloatArray array, jsize start, jsize len, const jfloat* buf); + internal readonly delegate* unmanaged[Stdcall] SetFloatArrayRegion; + + // void (JNICALL *SetDoubleArrayRegion) (JNIEnv* env, jdoubleArray array, jsize start, jsize len, const jdouble* buf); + internal readonly delegate* unmanaged[Stdcall] SetDoubleArrayRegion; + +// jint (JNICALL *RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* methods, jint nMethods); + // internal readonly delegate* unmanaged[Stdcall] RegisterNatives; + internal readonly delegate* unmanaged[Stdcall] RegisterNatives; + + // jint (JNICALL *UnregisterNatives) (JNIEnv* env, jclass clazz); + internal readonly delegate* unmanaged[Stdcall] UnregisterNatives; + + // jint (JNICALL *MonitorEnter) (JNIEnv* env, jobject obj); + internal readonly delegate* unmanaged[Stdcall] MonitorEnter; + + // jint (JNICALL *MonitorExit) (JNIEnv* env, jobject obj); + internal readonly delegate* unmanaged[Stdcall] MonitorExit; + + // jint (JNICALL *GetJavaVM) (JNIEnv* env, JavaVM** vm); + internal readonly delegate* unmanaged[Stdcall] GetJavaVM; + + // void (JNICALL *GetStringRegion) (JNIEnv* env, jstring str, jsize start, jsize len, jchar* buf); + internal readonly delegate* unmanaged[Stdcall] GetStringRegion; + + // void (JNICALL *GetStringUTFRegion) (JNIEnv* env, jstring str, jsize start, jsize len, char* buf); + internal readonly delegate* unmanaged[Stdcall] GetStringUTFRegion; + + // void * (JNICALL *GetPrimitiveArrayCritical) (JNIEnv* env, jarray array, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetPrimitiveArrayCritical; + + // void (JNICALL *ReleasePrimitiveArrayCritical) (JNIEnv* env, jarray array, void* carray, jint mode); + internal readonly delegate* unmanaged[Stdcall] ReleasePrimitiveArrayCritical; + + // const jchar * (JNICALL *GetStringCritical) (JNIEnv* env, jstring string, jboolean* isCopy); + internal readonly delegate* unmanaged[Stdcall] GetStringCritical; + + // void (JNICALL *ReleaseStringCritical) (JNIEnv* env, jstring string, const jchar* cstring); + internal readonly delegate* unmanaged[Stdcall] ReleaseStringCritical; + + // jweak (JNICALL *NewWeakGlobalRef) (JNIEnv* env, jobject obj); + internal readonly delegate* unmanaged[Stdcall] NewWeakGlobalRef; + + // void (JNICALL *DeleteWeakGlobalRef) (JNIEnv* env, jweak ref); + internal readonly delegate* unmanaged[Stdcall] DeleteWeakGlobalRef; + + // jboolean (JNICALL *ExceptionCheck) (JNIEnv* env); + internal readonly delegate* unmanaged[Stdcall] ExceptionCheck; + + // jobject (JNICALL *NewDirectByteBuffer) (JNIEnv* env, void* address, jlong capacity); + internal readonly delegate* unmanaged[Stdcall] NewDirectByteBuffer; + + // void* (JNICALL *GetDirectBufferAddress) (JNIEnv* env, jobject buf); + internal readonly delegate* unmanaged[Stdcall] GetDirectBufferAddress; + + // jlong (JNICALL *GetDirectBufferCapacity) (JNIEnv* env, jobject buf); + internal readonly delegate* unmanaged[Stdcall] GetDirectBufferCapacity; + } + + internal readonly FunctionTable* Functions; +} +#endif diff --git a/MelonLoader/JNI/JNIResultException.cs b/MelonLoader/JNI/JNIResultException.cs new file mode 100644 index 000000000..948d840b6 --- /dev/null +++ b/MelonLoader/JNI/JNIResultException.cs @@ -0,0 +1,15 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public class JNIResultException : Exception +{ + public JNI.Result Result { get; set;} + + public JNIResultException(JNI.Result result) : base($"JNI error occurred: {result}") + { + this.Result = result; + } +} +#endif diff --git a/MelonLoader/JNI/JObject.cs b/MelonLoader/JNI/JObject.cs new file mode 100644 index 000000000..6d8a07a49 --- /dev/null +++ b/MelonLoader/JNI/JObject.cs @@ -0,0 +1,67 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public class JObject : IDisposable +{ + private bool Disposed { get; set; } + + public IntPtr Handle { get; set;} + + internal JNI.ReferenceType ReferenceType { get; set;} + + public JObject() { } + + public JObject(IntPtr handle, JNI.ReferenceType referenceType) + { + this.Handle = handle; + this.ReferenceType = referenceType; + } + + public JObject(JObject obj) : this(obj.Handle, obj.ReferenceType) + { + this.Disposed = obj.Disposed; + obj.Disposed = true; + } + + protected virtual void Dispose(bool disposing) + { + if (Disposed || this.Handle == IntPtr.Zero) + return; + + switch (this.ReferenceType) + { + case JNI.ReferenceType.Local: + JNI.DeleteLocalRef(this); + break; + + case JNI.ReferenceType.Global: + JNI.DeleteGlobalRef(this); + break; + + case JNI.ReferenceType.WeakGlobal: + JNI.DeleteWeakGlobalRef(this); + break; + } + + Disposed = true; + } + + public bool Valid() + { + return this.Handle != IntPtr.Zero; + } + + ~JObject() + { + Dispose(disposing: false); + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } +} +#endif diff --git a/MelonLoader/JNI/JObjectArray.cs b/MelonLoader/JNI/JObjectArray.cs new file mode 100644 index 000000000..f1506cb51 --- /dev/null +++ b/MelonLoader/JNI/JObjectArray.cs @@ -0,0 +1,35 @@ +#if ANDROID +namespace MelonLoader.Java; + +using System.Collections; +using System.Collections.Generic; + +public class JObjectArray : JObject, IEnumerable where T : JObject, new() +{ + public int Length => JNI.GetArrayLength(this); + + public T this[int index] + { + get => JNI.GetObjectArrayElement(this, index); + set => JNI.SetObjectArrayElement(this, index, value); + } + + public void SetElement(T value, int index) + { + JNI.SetObjectArrayElement(this, index, value); + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < this.Length; i++) + { + yield return JNI.GetObjectArrayElement(this, i); + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } +} +#endif diff --git a/MelonLoader/JNI/JString.cs b/MelonLoader/JNI/JString.cs new file mode 100644 index 000000000..064f0ce06 --- /dev/null +++ b/MelonLoader/JNI/JString.cs @@ -0,0 +1,10 @@ +#if ANDROID +namespace MelonLoader.Java; + +public class JString : JObject +{ + public JString() : base() { } + + public string GetString() => JNI.GetJStringString(this); +} +#endif diff --git a/MelonLoader/JNI/JThrowable.cs b/MelonLoader/JNI/JThrowable.cs new file mode 100644 index 000000000..3413cb445 --- /dev/null +++ b/MelonLoader/JNI/JThrowable.cs @@ -0,0 +1,12 @@ +#if ANDROID +namespace MelonLoader.Java; + +public class JThrowable : JObject +{ + public JThrowable() { } + + public string GetMessage() => JNI.FindClass("java/lang/Throwable").CallObjectMethod(this, "getMessage", "()Ljava/lang/String;").GetString(); + + public override string ToString() => JNI.FindClass("java/lang/Throwable").CallObjectMethod(this, "toString", "()Ljava/lang/String;").GetString(); +} +#endif diff --git a/MelonLoader/JNI/JThrowableException.cs b/MelonLoader/JNI/JThrowableException.cs new file mode 100644 index 000000000..a013495ef --- /dev/null +++ b/MelonLoader/JNI/JThrowableException.cs @@ -0,0 +1,17 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public class JThrowableException : Exception +{ + public JThrowable? Throwable { get; set;} + + public JThrowableException() { } + + public JThrowableException(JThrowable throwable) : base() + { + this.Throwable = throwable; + } +} +#endif diff --git a/MelonLoader/JNI/JValue.cs b/MelonLoader/JNI/JValue.cs new file mode 100644 index 000000000..2928fd8d6 --- /dev/null +++ b/MelonLoader/JNI/JValue.cs @@ -0,0 +1,158 @@ +#if ANDROID +namespace MelonLoader.Java; + +using System; +using System.Runtime.InteropServices; + +[StructLayout(LayoutKind.Explicit, Size = 8)] +public readonly struct JValue +{ + [FieldOffset(0)] + public readonly byte Z; + + [FieldOffset(0)] + public readonly sbyte B; + + [FieldOffset(0)] + public readonly char C; + + [FieldOffset(0)] + public readonly short S; + + [FieldOffset(0)] + public readonly int I; + + [FieldOffset(0)] + public readonly long J; + + [FieldOffset(0)] + public readonly float F; + + [FieldOffset(0)] + public readonly double D; + + [FieldOffset(0)] + public readonly IntPtr L; + + public JValue(bool value) + { + this = new JValue(); + Z = Convert.ToByte(value); + } + + public JValue(sbyte value) + { + this = new JValue(); + B = value; + } + + public JValue(char value) + { + this = new JValue(); + C = value; + } + + public JValue(short value) + { + this = new JValue(); + S = value; + } + + public JValue(int value) + { + this = new JValue(); + I = value; + } + + public JValue(long value) + { + this = new JValue(); + J = value; + } + + public JValue(float value) + { + this = new JValue(); + F = value; + } + + public JValue(double value) + { + this = new JValue(); + D = value; + } + + public JValue(IntPtr value) + { + this = new JValue(); + L = value; + } + + public JValue(JObject obj) + { + this = new JValue(); + this.L = obj.Handle; + } + + public JValue(object value) + { + this = new JValue(); + switch (value) + { + case bool z: + this.Z = Convert.ToByte(z); + break; + + case sbyte b: + this.B = b; + break; + + case char c: + this.C = c; + break; + + case short s: + this.S = s; + break; + + case int i: + this.I = i; + break; + + case long j: + this.J = j; + break; + + case float f: + this.F = f; + break; + + case double d: + this.D = d; + break; + + case JObject obj: + this.L = obj.Handle; + break; + } + } + + public static implicit operator JValue(bool b) => new JValue(b); + + public static implicit operator JValue(sbyte b) => new JValue(b); + + public static implicit operator JValue(char c) => new JValue(c); + + public static implicit operator JValue(short s) => new JValue(s); + + public static implicit operator JValue(int i) => new JValue(i); + + public static implicit operator JValue(long j) => new JValue(j); + + public static implicit operator JValue(float f) => new JValue(f); + + public static implicit operator JValue(double d) => new JValue(d); + + public static implicit operator JValue(JObject obj) => new JValue(obj); +} +#endif diff --git a/MelonLoader/JNI/JavaVM.cs b/MelonLoader/JNI/JavaVM.cs new file mode 100644 index 000000000..d3ec83119 --- /dev/null +++ b/MelonLoader/JNI/JavaVM.cs @@ -0,0 +1,40 @@ +#if ANDROID +namespace MelonLoader.Java; + +using System; +using System.Runtime.InteropServices; + +/// +/// Represents a JavaVM C struct. +/// +[StructLayout(LayoutKind.Sequential)] +internal readonly unsafe struct JavaVM +{ + [StructLayout(LayoutKind.Sequential)] + internal readonly unsafe struct FunctionTable + { + internal readonly IntPtr Reserved0; + + internal readonly IntPtr Reserved1; + + internal readonly IntPtr Reserved2; + + // jint (JNICALL *DestroyJavaVM)(JavaVM *vm); + internal readonly delegate* unmanaged[Stdcall] DestroyJavaVM; + + // jint (JNICALL *AttachCurrentThread)(JavaVM *vm, void **penv, void *args); + internal readonly delegate* unmanaged[Stdcall] AttachCurrentThread; + + // jint (JNICALL *DetachCurrentThread)(JavaVM *vm); + internal readonly delegate* unmanaged[Stdcall] DetachCurrentThread; + + // jint (JNICALL *GetEnv)(JavaVM *vm, void **penv, jint version); + internal readonly delegate* unmanaged[Stdcall] GetEnv; + + // jint (JNICALL *AttachCurrentThreadAsDaemon)(JavaVM *vm, void **penv, void *args); + internal readonly delegate* unmanaged[Stdcall] AttachCurrentThreadAsDaemon; + } + + internal readonly FunctionTable* Functions; +} +#endif diff --git a/MelonLoader/JNI/ReferenceType.cs b/MelonLoader/JNI/ReferenceType.cs new file mode 100644 index 000000000..2df597e9e --- /dev/null +++ b/MelonLoader/JNI/ReferenceType.cs @@ -0,0 +1,13 @@ +#if ANDROID +namespace MelonLoader.Java; + +public static partial class JNI +{ + public enum ReferenceType + { + Local = 0, + Global = 1, + WeakGlobal = 2 + } +} +#endif diff --git a/MelonLoader/JNI/ReleaseMode.cs b/MelonLoader/JNI/ReleaseMode.cs new file mode 100644 index 000000000..09c47a813 --- /dev/null +++ b/MelonLoader/JNI/ReleaseMode.cs @@ -0,0 +1,13 @@ +#if ANDROID +namespace MelonLoader.Java; + +public static partial class JNI +{ + public enum ReleaseMode + { + Default = 0, + Commit = 1, + Abort = 2 + } +} +#endif diff --git a/MelonLoader/JNI/Result.cs b/MelonLoader/JNI/Result.cs new file mode 100644 index 000000000..47dba5747 --- /dev/null +++ b/MelonLoader/JNI/Result.cs @@ -0,0 +1,41 @@ +#if ANDROID +namespace MelonLoader.Java; + +public static partial class JNI +{ + /// + /// Represents a JNI error. + /// + public enum Result + { + /// + /// Success. + /// + Ok = 0, + /// + /// Unknown error occured. + /// + Unknown = -1, + /// + /// Thread detached from the JVM. + /// + Detached = -2, + /// + /// Version unsupported. + /// + Version = -3, + /// + /// JVM out of memory. + /// + OutOfMemory = -4, + /// + /// JVM already exists. + /// + AlreadyExists = -5, + /// + /// Invalid arguments passed. + /// + InvalidArguments = -6 + } +} +#endif diff --git a/MelonLoader/JNI/TypeSignature.cs b/MelonLoader/JNI/TypeSignature.cs new file mode 100644 index 000000000..d1f7cac08 --- /dev/null +++ b/MelonLoader/JNI/TypeSignature.cs @@ -0,0 +1,69 @@ +#if ANDROID +using System; + +namespace MelonLoader.Java; + +public static partial class JNI +{ + public static class TypeSignature + { + public const string Bool = "Z"; + + public const string Byte = "B"; + + public const string Char = "C"; + + public const string Short = "S"; + + public const string Int = "I"; + + public const string Long = "J"; + + public const string Float = "F"; + + public const string Double = "D"; + } + + public static string GetTypeSignature() + { + Type t = typeof(T); + + if (t == typeof(bool)) + { + return TypeSignature.Bool; + } + else if (t == typeof(sbyte)) + { + return TypeSignature.Byte; + } + else if (t == typeof(char)) + { + return TypeSignature.Char; + } + else if (t == typeof(short)) + { + return TypeSignature.Short; + } + else if (t == typeof(int)) + { + return TypeSignature.Int; + } + else if (t == typeof(long)) + { + return TypeSignature.Long; + } + else if (t == typeof(float)) + { + return TypeSignature.Float; + } + else if (t == typeof(double)) + { + return TypeSignature.Double; + } + else + { + throw new ArgumentException($"GetTypeSignature Type {t} not supported."); + } + } +} +#endif diff --git a/MelonLoader/JNI/Version.cs b/MelonLoader/JNI/Version.cs new file mode 100644 index 000000000..0d53a7f03 --- /dev/null +++ b/MelonLoader/JNI/Version.cs @@ -0,0 +1,15 @@ +#if ANDROID +namespace MelonLoader.Java; + +public static partial class JNI +{ + public enum Version + { + V1_1 = 0x00010001, + V1_2 = 0x00010001, + V1_4 = 0x00010004, + V1_6 = 0x00010006, + V1_8 = 0x00010008 + } +} +#endif diff --git a/MelonLoader/LoaderConfig.cs b/MelonLoader/LoaderConfig.cs index 50b137320..878dc9cad 100644 --- a/MelonLoader/LoaderConfig.cs +++ b/MelonLoader/LoaderConfig.cs @@ -25,10 +25,10 @@ private static string GetParentDirectory(string path, int level) #endif [RequiresDynamicCode("Dynamically accesses LoaderConfig properties")] - internal static void Initialize() + internal static void Initialize(string? baseDir = null) { var customBaseDir = ArgParser.GetValue("melonloader.basedir"); - var baseDir = Path.GetDirectoryName(Environment.ProcessPath)!; + baseDir ??= Path.GetDirectoryName(Environment.ProcessPath)!; #if OSX baseDir = GetParentDirectory(baseDir, 3); @@ -323,7 +323,7 @@ internal static void Initialize() private const string MonoPathSeparatorDescription = #if WINDOWS "semicolon (;)"; -#elif LINUX || OSX +#elif LINUX || OSX || ANDROID "colon (:)"; #endif diff --git a/MelonLoader/MelonUtils.cs b/MelonLoader/MelonUtils.cs index 043e1701a..df5ce0ce4 100644 --- a/MelonLoader/MelonUtils.cs +++ b/MelonLoader/MelonUtils.cs @@ -105,6 +105,12 @@ public static bool IsOldMono() public static bool IsUnix => GetPlatform is PlatformID.Unix; public static bool IsWindows => GetPlatform is PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows or PlatformID.WinCE; public static bool IsMac => GetPlatform is PlatformID.MacOSX; + public static bool IsAndroid => +#if ANDROID // This is what .NET does internally for OperatingSystem.IsAndroid, this just prevents errors on net35 + true; +#else + false; +#endif #if NET6_0_OR_GREATER private static PlatformID GetPlatformFromRuntimeInformation() @@ -569,6 +575,23 @@ internal struct OsVersionInfo internal static string GetOSVersion() { + if (IsAndroid) + { + StringBuilder sb = new(); + sb.Append("Android "); + int apiLevel = Environment.OSVersion.Version.Major; + // https://apilevels.com/ + string androidVersion = apiLevel switch + { + 27 => "8.1", 28 => "9", 29 => "10", + 30 => "11", 31 => "12", 32 => "12L", 33 => "13", 34 => "14", 35 => "15", 36 => "16", + _ => $"API Level {apiLevel}" + }; + sb.Append(androidVersion); + + return sb.ToString(); + } + if (IsUnix || IsMac) return Environment.OSVersion.VersionString; diff --git a/MelonLoader/Resolver/MelonAssemblyResolver.cs b/MelonLoader/Resolver/MelonAssemblyResolver.cs index 5624ad228..239a31e56 100644 --- a/MelonLoader/Resolver/MelonAssemblyResolver.cs +++ b/MelonLoader/Resolver/MelonAssemblyResolver.cs @@ -20,6 +20,9 @@ internal static void Setup() // Setup Search Directories AddSearchDirectory(MelonEnvironment.OurRuntimeDirectory); +#if ANDROID // Needed for loading Cpp2IL and deps + AddSearchDirectory(Path.Combine(MelonEnvironment.DependenciesDirectory, "Il2CppAssemblyGenerator")); +#endif // Setup Redirections OverrideBaseAssembly(); diff --git a/MelonLoader/Utils/APKAssetManager.cs b/MelonLoader/Utils/APKAssetManager.cs new file mode 100644 index 000000000..3287a62af --- /dev/null +++ b/MelonLoader/Utils/APKAssetManager.cs @@ -0,0 +1,277 @@ +#nullable enable + +#if ANDROID +using MelonLoader.Java; +using System; +using System.IO; +using System.Linq; + +namespace MelonLoader.Utils; + +public static class APKAssetManager +{ + private static JObject? assetManager; + + public static void Initialize() + { + GetAndroidAssetManager(); + } + + private static void EnsureInitialized() + { + if (assetManager == null || !assetManager.Valid()) + GetAndroidAssetManager(); + + if (assetManager == null || !assetManager.Valid()) + throw new InvalidOperationException("[APKAssetManager] Asset manager is not initialized."); + } + + public static void SaveItemToDirectory(string itemPath, string copyBase, bool includeInitial = true) + { + EnsureInitialized(); + + string[] contents = GetDirectoryContents(itemPath); + if (contents.Length == 0) + { + string path = includeInitial ? itemPath : itemPath[(itemPath.IndexOf('/') + 1)..]; + if (string.IsNullOrEmpty(path)) + return; + + string outPath = Path.Combine(copyBase, path); + string outDir = Path.GetDirectoryName(outPath)!; + + if (!Directory.Exists(outDir)) + Directory.CreateDirectory(outDir); + + using FileStream fileStream = File.Open(outPath, FileMode.Create); + using Stream? assetStream = GetAssetStream(itemPath); + if (assetStream == null) + throw new Exception("[APKAssetManager] Failed to get asset stream: " + itemPath); + + byte[] buffer = new byte[81920]; + int bytesRead; + while ((bytesRead = assetStream.Read(buffer, 0, buffer.Length)) > 0) + fileStream.Write(buffer, 0, bytesRead); + + return; + } + + foreach (string item in contents) + { + SaveItemToDirectory(Path.Combine(itemPath, item), copyBase, includeInitial); + } + } + + public static byte[] GetAssetBytes(string path) + { + EnsureInitialized(); + + using JString pathString = JNI.NewString(path); + JClass assetManagerClass = JNI.GetObjectClass(assetManager!); + using JObject asset = JNI.CallObjectMethod(assetManager!, JNI.GetMethodID(assetManagerClass, "open", "(Ljava/lang/String;)Ljava/io/InputStream;"), new JValue(pathString)); + if (asset == null || !asset.Valid()) + return []; + + using MemoryStream outputStream = new(); + + using JArray buffer = JNI.NewArray(1024); + JMethodID readMethodID = JNI.GetMethodID(JNI.GetObjectClass(asset), "read", "([B)I"); + + int bytesRead = 0; + while ((bytesRead = JNI.CallMethod(asset, readMethodID, new JValue(buffer))) > 0) + { + byte[] managedBuffer = (byte[])(Array)buffer.GetElements(); + outputStream.Write(managedBuffer, 0, bytesRead); + } + + JClass assetClass = JNI.GetObjectClass(asset); + JMethodID closeMethodID = JNI.GetMethodID(assetClass, "close", "()V"); + JNI.CallVoidMethod(asset, closeMethodID); + + HandleException(); + + return outputStream.ToArray(); + } + + public static Stream? GetAssetStream(string path) + { + EnsureInitialized(); + + using JString pathString = JNI.NewString(path); + JClass assetManagerClass = JNI.GetObjectClass(assetManager); + JObject asset = JNI.CallObjectMethod(assetManager, JNI.GetMethodID(assetManagerClass, "open", "(Ljava/lang/String;)Ljava/io/InputStream;"), new JValue(pathString)); + if (asset == null || !asset.Valid()) + return null; + + HandleException(); + + return new APKAssetStream(asset); + } + + public static string[] GetDirectoryContents(string directory) + { + EnsureInitialized(); + + using JString pathString = JNI.NewString(directory); + JClass assetManagerClass = JNI.GetObjectClass(assetManager); + using JObjectArray assets = JNI.CallObjectMethod>(assetManager, JNI.GetMethodID(assetManagerClass, "list", "(Ljava/lang/String;)[Ljava/lang/String;"), new JValue(pathString)); + + string[] cleanAssets = [.. assets.Select(a => a.GetString())]; + HandleException(); + + return cleanAssets; + } + + public static bool DoesAssetExist(string path) + { + EnsureInitialized(); + + // using `list` isn't as fast as just calling open, but this allows the function to not crash on debuggable builds of apps + string containingDir = path[..path.LastIndexOf('/')]; + using JString pathString = JNI.NewString(containingDir); + JClass assetManagerClass = JNI.GetObjectClass(assetManager); + using JObjectArray assets = JNI.CallObjectMethod>(assetManager, JNI.GetMethodID(assetManagerClass, "list", "(Ljava/lang/String;)[Ljava/lang/String;"), new JValue(pathString)); + + bool exists = assets.Any(js => + { + string asset = JNI.GetJStringString(js); + return path.EndsWith(asset); + }); + + HandleException(); + + return exists; + } + + private static void HandleException() + { + if (JNI.ExceptionCheck()) + JNI.ExceptionClear(); + } + + private static void GetAndroidAssetManager() + { + if (assetManager?.Valid() ?? false) + return; + + JClass unityClass = JNI.FindClass("com/unity3d/player/UnityPlayer"); + JFieldID activityFieldId = JNI.GetStaticFieldID(unityClass, "currentActivity", "Landroid/app/Activity;"); + using JObject currentActivityObj = JNI.GetStaticObjectField(unityClass, activityFieldId); + + JClass activityClass = JNI.GetObjectClass(currentActivityObj); + JObject localAssetManager = JNI.CallObjectMethod( + currentActivityObj, + JNI.GetMethodID(activityClass, "getAssets", "()Landroid/content/res/AssetManager;") + ); + + // promote to global ref so GC can't kill it + assetManager = JNI.NewGlobalRef(localAssetManager); + + HandleException(); + } + + public class APKAssetStream : Stream, IDisposable + { + private readonly JMethodID _availableJmid; + private readonly JMethodID _markSupportedJmid; + private readonly JMethodID _skipJmid; + private readonly JMethodID _resetJmid; + private readonly JMethodID _readJmid; + private readonly JMethodID _closeJmid; + + private readonly JObject _streamObject; + + private long _pos = 0; + private bool _disposed = false; + + public APKAssetStream(JObject obj) + { + _streamObject = obj; + + JClass streamClass = JNI.GetObjectClass(_streamObject); + + _availableJmid = JNI.GetMethodID(streamClass, "available", "()I"); + _readJmid = JNI.GetMethodID(streamClass, "read", "([BII)I"); + _markSupportedJmid = JNI.GetMethodID(streamClass, "markSupported", "()Z"); + _skipJmid = JNI.GetMethodID(streamClass, "skip", "(J)J"); + _resetJmid = JNI.GetMethodID(streamClass, "reset", "()V"); + _closeJmid = JNI.GetMethodID(streamClass, "close", "()V"); + } + + public override bool CanRead => true; + + public override bool CanSeek => false; + + public override bool CanWrite => false; + + public override long Length + { + get + { + int length = JNI.CallMethod(_streamObject, _availableJmid); + HandleException(); + return length; + } + } + + public override long Position + { + get => _pos; + set + { + bool canMark = JNI.CallMethod(_streamObject, _markSupportedJmid); + if (!canMark) + throw new NotImplementedException(); + + JNI.CallVoidMethod(_streamObject, _resetJmid); + if (value > 0) + { + long val = JNI.CallMethod(_streamObject, _skipJmid, new JValue(value)); + _pos = val; + } + + HandleException(); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + using JArray javaBuffer = JNI.NewArray(count); + + int read = JNI.CallMethod(_streamObject, _readJmid, new JValue(javaBuffer), new JValue(offset), new JValue(count)); + HandleException(); + + if (read == -1) + return 0; + + for (int i = 0; i < read; i++) + { + buffer[i] = (byte)javaBuffer[i]; + } + + _pos += read; + return read; + } + + public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException(); + public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException(); + public override void SetLength(long value) => throw new NotImplementedException(); + public override void Flush() { } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + JNI.CallVoidMethod(_streamObject, _closeJmid); + _streamObject.Dispose(); + HandleException(); + } + _disposed = true; + } + base.Dispose(disposing); + } + } +} +#endif \ No newline at end of file diff --git a/MelonLoader/Utils/MelonEnvironment.cs b/MelonLoader/Utils/MelonEnvironment.cs index d5e0f50be..77f490e0c 100644 --- a/MelonLoader/Utils/MelonEnvironment.cs +++ b/MelonLoader/Utils/MelonEnvironment.cs @@ -24,7 +24,14 @@ public static class MelonEnvironment Process.GetCurrentProcess().MainModule.FileName; #endif public static string MelonLoaderDirectory { get; } = Path.Combine(MelonBaseDirectory, "MelonLoader"); +#if !ANDROID public static string GameRootDirectory { get; } = Path.GetDirectoryName(GameExecutablePath); +#else + public static string GameRootDirectory { get; } = MelonBaseDirectory; + + // MelonBaseDirectory is named after the current package by the Bootstrap + public static string PackageName { get; } = Path.GetFileName(MelonBaseDirectory); +#endif public static string DependenciesDirectory { get; } = Path.Combine(MelonLoaderDirectory, "Dependencies"); @@ -39,14 +46,23 @@ public static class MelonEnvironment public static string OurRuntimeDirectory { get; } = Path.Combine(MelonLoaderDirectory, OurRuntimeName); public static string GameExecutableName { get; } = Path.GetFileNameWithoutExtension(GameExecutablePath); - public static string UnityGameDataDirectory { get; } = + public static string UnityGameDataDirectory { get; } = #if OSX Path.Combine(GameExecutablePath!, "Contents/Resources/Data"); +#elif ANDROID + // inside the APK's assets directory; we're interacting with it via the AssetManager API so this makes sense even if it looks weird + "bin/Data/"; #else Path.Combine(GameRootDirectory, GameExecutableName + "_Data"); #endif public static string UnityGameManagedDirectory { get; } = Path.Combine(UnityGameDataDirectory, "Managed"); - public static string Il2CppDataDirectory { get; } = Path.Combine(UnityGameDataDirectory, "il2cpp_data"); + public static string Il2CppDataDirectory { get; } = +#if !ANDROID + Path.Combine(UnityGameDataDirectory, "il2cpp_data"); +#else + // Managed on Android, when built with IL2CPP, is the same as il2cpp_data + Path.Combine(UnityGameDataDirectory, "Managed"); +#endif public static string UnityPlayerPath { get; } = Path.Combine(GameRootDirectory, "UnityPlayer.dll"); public static string MelonManagedDirectory { get; } = Path.Combine(DependenciesDirectory, "Mono"); diff --git a/MelonLoader/Utils/NativeLibrary.cs b/MelonLoader/Utils/NativeLibrary.cs index 5ec51fa37..eff6ae9c9 100644 --- a/MelonLoader/Utils/NativeLibrary.cs +++ b/MelonLoader/Utils/NativeLibrary.cs @@ -1,3 +1,4 @@ +using MelonLoader.Utils; using System; using System.IO; using System.Reflection; @@ -27,8 +28,16 @@ public static IntPtr LoadLib(string filepath) if (string.IsNullOrEmpty(filepath)) throw new ArgumentNullException(nameof(filepath)); IntPtr ptr = AgnosticLoadLibrary(filepath); +#if !ANDROID if (ptr == IntPtr.Zero) throw new Exception($"Unable to Load Native Library {filepath}!"); +#else + if (ptr == IntPtr.Zero) + { + var error = Marshal.PtrToStringAnsi(dlerror()); + throw new DlErrorException($"Unable to Load Native Library {filepath}!\ndlerror: {error}"); + } +#endif return ptr; } @@ -68,6 +77,27 @@ public static IntPtr AgnosticLoadLibrary(string name) name += ".dylib"; return dlopen(name, RTLD_NOW); +#elif ANDROID + // Workaround that allows us to bypass Android's security that prevents libraries from being loaded from outside APKs + string path = name; + if (File.Exists(path)) + { + string fileName = Path.GetFileName(path); + path = Path.Combine("/data/data/", MelonEnvironment.PackageName); + path = Path.Combine(path, fileName); + + FileInfo newLib = new(name); + FileInfo copiedLib = new(path); + if (copiedLib.Exists && newLib.LastWriteTime > copiedLib.LastWriteTime) + { + copiedLib.Delete(); + File.Copy(name, path); + } + else if (!copiedLib.Exists) + File.Copy(name, path); + } + + return dlopen(path, RTLD_NOW); #endif } @@ -75,7 +105,7 @@ public static IntPtr AgnosticGetProcAddress(IntPtr hModule, string lpProcName) { #if WINDOWS return GetProcAddress(hModule, lpProcName); -#elif LINUX || OSX +#elif LINUX || OSX || ANDROID return dlsym(hModule, lpProcName); #endif } @@ -95,6 +125,17 @@ public static IntPtr AgnosticGetProcAddress(IntPtr hModule, string lpProcName) protected static extern IntPtr dlsym(IntPtr handle, string symbol); const int RTLD_NOW = 2; // for dlopen's flags +#elif ANDROID + [DllImport("libdl.so")] + protected static extern IntPtr dlopen(string filename, int flags); + + [DllImport("libdl.so")] + protected static extern IntPtr dlsym(IntPtr handle, string symbol); + + [DllImport("libdl.so")] + protected static extern IntPtr dlerror(); + + const int RTLD_NOW = 2; // for dlopen's flags #elif OSX [DllImport("libSystem.B.dylib")] protected static extern IntPtr dlopen(string filename, int flags); @@ -147,4 +188,13 @@ public NativeLibrary(IntPtr ptr) : base(ptr) } } } + +#if ANDROID + public class DlErrorException : Exception + { + public DlErrorException() { } + public DlErrorException(string message) : base(message) { } + public DlErrorException(string message, Exception inner) : base(message, inner) { } + } +#endif } diff --git a/NuGet.config b/NuGet.config index 063fa5b3a..06cfe7781 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,6 +4,8 @@ + + diff --git a/README.md b/README.md index 7ac486a07..1c70109a6 100644 --- a/README.md +++ b/README.md @@ -296,6 +296,9 @@ Third-party Libraries used as Source Code and/or bundled in Binary Form: - [WebSocketDotNet](https://github.com/SamboyCoding/WebSocketDotNet) is licensed under the MIT License. See [LICENSE](https://github.com/SamboyCoding/WebSocketDotNet/blob/master/LICENSE) for the full License. - [Pastel](https://github.com/silkfire/Pastel) is licensed under the MIT License. See [LICENSE](https://github.com/silkfire/Pastel/blob/master/LICENSE) for the full License. - [Il2CppInterop](https://github.com/BepInEx/Il2CppInterop) is licensed under the LGPLv3 License. See [LICENSE](https://github.com/BepInEx/Il2CppInterop/blob/master/LICENSE) for the full License. +- [JNISharp](https://github.com/WarrenUlrich/JNISharp) is licensed under the MIT License. See [LICENSE](https://github.com/WarrenUlrich/JNISharp/blob/master/LICENSE) for the full License. +- [OpenSSL](https://github.com/openssl/openssl) is licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/openssl/openssl/blob/master/LICENSE.txt) for the full License. +- [NativeAOT-AndroidHelloJniLib](https://github.com/josephmoresena/NativeAOT-AndroidHelloJniLib) is licensed under the MIT License. See [LICENSE](https://github.com/josephmoresena/NativeAOT-AndroidHelloJniLib/blob/main/LICENSE) for the full License. External Libraries and Tools that are downloaded and used at Runtime: - [Cpp2IL](https://github.com/SamboyCoding/Cpp2IL) is licensed under the MIT License. See [LICENSE](https://github.com/SamboyCoding/Cpp2IL/blob/master/LICENSE) for the full License.