Files
BepInEx/Runtimes/Unity/BepInEx.Unity.IL2CPP/Il2CppInteropManager.cs
T

547 lines
24 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using BepInEx.Configuration;
using BepInEx.Logging;
using BepInEx.Unity.Common;
using BepInEx.Unity.IL2CPP.Hook;
using BepInEx.Unity.IL2CPP.Logging;
using Cpp2IL.Core;
using Cpp2IL.Core.Api;
using Cpp2IL.Core.InstructionSets;
using Cpp2IL.Core.OutputFormats;
using Cpp2IL.Core.ProcessingLayers;
using HarmonyLib;
using Il2CppInterop.Common;
using Il2CppInterop.Generator;
using Il2CppInterop.Generator.Runners;
using Il2CppInterop.HarmonySupport;
using Il2CppInterop.Runtime.Startup;
using LibCpp2IL;
using Microsoft.Extensions.Logging;
using Mono.Cecil;
using MonoMod.Utils;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using MSLoggerFactory = Microsoft.Extensions.Logging.LoggerFactory;
namespace BepInEx.Unity.IL2CPP;
internal static partial class Il2CppInteropManager
{
static Il2CppInteropManager()
{
InstructionSetRegistry.RegisterInstructionSet<X86InstructionSet>(DefaultInstructionSets.X86_32);
InstructionSetRegistry.RegisterInstructionSet<X86InstructionSet>(DefaultInstructionSets.X86_64);
LibCpp2IlBinaryRegistry.RegisterBuiltInBinarySupport();
}
private static readonly ConfigEntry<bool> UpdateInteropAssemblies =
ConfigFile.CoreConfig.Bind("IL2CPP",
"UpdateInteropAssemblies",
true,
new StringBuilder()
.AppendLine("Whether to run Il2CppInterop automatically to generate Il2Cpp support assemblies when they are outdated.")
.AppendLine("If disabled assemblies in `BepInEx/interop` won't be updated between game or BepInEx updates!")
.ToString());
private static readonly ConfigEntry<string> UnityBaseLibrariesSource = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UnityBaseLibrariesSource",
"https://unity.bepinex.dev/libraries/{VERSION}.zip",
new StringBuilder()
.AppendLine("URL to a ZIP file with managed Unity base libraries. They are used by Il2CppInterop to generate interop assemblies.")
.AppendLine("The URL can include {VERSION} template which will be replaced with the game's Unity engine version.")
.AppendLine("If a .zip file with the same filename as the URL (after template replacement) already exists in unity-libs, it will be used instead of downloading a new copy.")
.AppendLine("If you want to ensure BepInEx doesn't try to connect to the internet, set this to only the .zip filename (without a URL) and manually place the file in the unity-libs directory.")
.ToString());
private static readonly ConfigEntry<string> ConfigUnhollowerDeobfuscationRegex = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UnhollowerDeobfuscationRegex",
string.Empty,
new StringBuilder()
.AppendLine("The RegEx string to pass to Il2CppAssemblyUnhollower for renaming obfuscated names.")
.AppendLine("All types and members matching this RegEx will get a name based on their signature,")
.AppendLine("resulting in names that persist after game updates.")
.ToString());
private static readonly ConfigEntry<bool> ScanMethodRefs = ConfigFile.CoreConfig.Bind(
"IL2CPP", "ScanMethodRefs",
Environment.Is64BitProcess,
"If enabled, Il2CppInterop will use xref to find dead methods and generate CallerCount attributes.");
private static readonly ConfigEntry<bool> DumpDummyAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "DumpDummyAssemblies",
false,
"If enabled, BepInEx will save dummy assemblies generated by an Cpp2IL dumper into BepInEx/dummy.");
private static readonly ConfigEntry<string> IL2CPPInteropAssembliesPath = ConfigFile.CoreConfig.Bind(
"IL2CPP", "IL2CPPInteropAssembliesPath",
"{BepInEx}",
new StringBuilder()
.AppendLine("The path to the folder where IL2CPPInterop assemblies are stored.")
.AppendLine("Supports the following placeholders:")
.AppendLine("{BepInEx} - Path to the BepInEx folder.")
.AppendLine("{ProcessName} - Name of the current process")
.AppendLine("{ApplicationData} - Path to the user AppData/Roaming folder")
.ToString());
private static readonly ConfigEntry<bool> PreloadIL2CPPInteropAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "PreloadIL2CPPInteropAssemblies",
true,
new StringBuilder()
.AppendLine("Automatically load all interop assemblies right before loading plugins.")
.AppendLine("Some plugins may not work properly without this, but it may cause issues in some games.")
.ToString());
private static readonly ConfigEntry<string> GlobalMetadataPath = ConfigFile.CoreConfig.Bind(
"IL2CPP", "GlobalMetadataPath",
"{GameDataPath}/il2cpp_data/Metadata/global-metadata.dat",
new StringBuilder()
.AppendLine("The path to the IL2CPP metadata file.")
.AppendLine("Supports the following placeholders:")
.AppendLine("{BepInEx} - Path to the BepInEx folder.")
.AppendLine("{ProcessName} - Name of the current process")
.AppendLine("{GameDataPath} - Path to the game's Data folder.")
.ToString());
private static readonly ConfigEntry<bool> UseRuntimeAssemblies = ConfigFile.CoreConfig.Bind(
"IL2CPP", "UseRuntimeAssemblies",
true,
new StringBuilder()
.AppendLine("If enabled, BepInEx will save runtime binaries from process memory into BepInEx/runtime-bins.")
.AppendLine("IL2CPPDumper will then use them to generate dummy assemblies for IL2CPPInterop.")
.AppendLine("Use this if GameAssembly.dll is packed or global-metadata.dat is embedded.")
.ToString());
private static readonly ConfigEntry<string> MetadataSignatureToScan = ConfigFile.CoreConfig.Bind(
"IL2CPP", "MetadataSignatureToScan",
"AC0E0000000000000000000001000000000000000400000001000000040000000500000006000000090000000D0000000F000000040000001C00000005000000200000000500000025000000070000002A0000000A00000031000000070000003B00000005000000420000000500000047000000050000004C0000000F0000005100000005000000600000000700000065000000090000006C0000000600000075000000030000007B0000000D0000007E000000030000008B0000001A0000008E00000001000000A800000004000000A90000000D000000AD00000005000000BA00000007000000BF0000000F000000C600000001000000D500000001000000",
new StringBuilder()
.AppendLine("The hex string of the metadata signature to scan when searching embedded global-metadata.dat.")
.AppendLine("Use this if global-metadata.dat is embedded and beginning of the header is obfuscated.")
.AppendLine("IL2CPPInterop will search for global-metadata.dat using this signature.")
.ToString());
private static readonly ConfigEntry<int> ObfuscatedMetadataHeaderOffset = ConfigFile.CoreConfig.Bind(
"IL2CPP", "ObfuscatedMetadataHeaderOffset",
252,
new StringBuilder()
.AppendLine("The offset of the standard metadata header to skip when searching embedded global-metadata.dat.")
.AppendLine("Use this if global-metadata.dat is embedded and beginning of the header is obfuscated.")
.AppendLine("IL2CPPInterop will search for global-metadata.dat using this offset.")
.ToString());
private static readonly ConfigEntry<string> MagicToFix = ConfigFile.CoreConfig.Bind(
"IL2CPP", "MagicToFix",
string.Empty,
new StringBuilder()
.AppendLine("The magic bytes to fix the header of global-metadta.dat so il2cppdumper can use to determine version.")
.AppendLine("Use this if global-metadata.dat is embedded and beginning of the header is obfuscated.")
.AppendLine("IL2CPPInterop will fix global-metadata.dat using this magic.")
.ToString());
private static readonly ManualLogSource Logger = BepInEx.Logging.Logger.CreateLogSource("InteropManager");
private static string il2cppInteropBasePath;
private static bool initialized;
private static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "BepInEx", Paths.ProcessName);
public static string GameAssemblyPath => Environment.GetEnvironmentVariable("BEPINEX_GAME_ASSEMBLY_PATH") ??
Path.Combine(Paths.GameRootPath,
"GameAssembly." + PlatformHelper.LibrarySuffix);
private static string HashPath => Path.Combine(IL2CPPInteropAssemblyPath, "assembly-hash.txt");
private static string IL2CPPBasePath {
get {
if (il2cppInteropBasePath != null)
return il2cppInteropBasePath;
var path = Utility.GetCommandLineArgValue("--unhollowed-path") ?? IL2CPPInteropAssembliesPath.Value;
il2cppInteropBasePath = path.Replace("{BepInEx}", Paths.BepInExRootPath)
.Replace("{ProcessName}", Paths.ProcessName)
.Replace("{ApplicationData}", ApplicationDataPath);
return il2cppInteropBasePath;
}
}
private static string UnityBaseLibsDirectory => Path.Combine(IL2CPPBasePath, "unity-libs");
internal static string IL2CPPInteropAssemblyPath => Path.Combine(IL2CPPBasePath, "interop");
internal static string IL2CPPRuntimeBinariesPath => Path.Combine(IL2CPPBasePath, "runtime-bins");
private static string RenameMapPath => Path.Combine(Paths.BepInExRootPath, "DeobfuscationMap.csv.gz");
private static ILoggerFactory LoggerFactory { get; } = MSLoggerFactory.Create(b =>
{
b.AddProvider(new BepInExLoggerProvider())
.SetMinimumLevel(LogLevel.Trace); // Each BepInEx log listener has its own filtering
});
private static string ComputeHash()
{
using var md5 = MD5.Create();
static void HashFile(ICryptoTransform hash, string file)
{
const int defaultCopyBufferSize = 81920;
using var fs = File.OpenRead(file);
var buffer = new byte[defaultCopyBufferSize];
int read;
while ((read = fs.Read(buffer)) > 0)
hash.TransformBlock(buffer, 0, read, buffer, 0);
}
static void HashString(ICryptoTransform hash, string str)
{
var buffer = Encoding.UTF8.GetBytes(str);
hash.TransformBlock(buffer, 0, buffer.Length, buffer, 0);
}
HashFile(md5, GameAssemblyPath);
if (Directory.Exists(UnityBaseLibsDirectory))
foreach (var file in Directory.EnumerateFiles(UnityBaseLibsDirectory, "*.dll",
SearchOption.TopDirectoryOnly))
{
HashString(md5, Path.GetFileName(file));
HashFile(md5, file);
}
if (File.Exists(RenameMapPath))
{
HashFile(md5, RenameMapPath);
}
// Hash some common dependencies as they can affect output
HashString(md5, typeof(InteropAssemblyGenerator).Assembly.GetName().Version.ToString());
HashString(md5, typeof(Cpp2IlApi).Assembly.GetName().Version.ToString());
md5.TransformFinalBlock(new byte[0], 0, 0);
return Utility.ByteArrayToString(md5.Hash);
}
private static bool CheckIfGenerationRequired()
{
static bool NeedGenerationOrSkip()
{
if (!UpdateInteropAssemblies.Value)
{
var hash = ComputeHash();
Logger.LogWarning($"Interop assemblies are possibly out of date. To disable this message, create file {HashPath} with the following contents: {hash}");
return false;
}
return true;
}
if (!Directory.Exists(IL2CPPInteropAssemblyPath))
return true;
if (!File.Exists(HashPath))
return NeedGenerationOrSkip();
if (ComputeHash() != File.ReadAllText(HashPath) && NeedGenerationOrSkip())
{
Logger.LogInfo("Detected outdated interop assemblies, will regenerate them now");
return true;
}
return false;
}
private static Assembly ResolveInteropAssemblies(object sender, ResolveEventArgs args)
{
var assemblyName = new AssemblyName(args.Name);
return Utility.TryResolveDllAssembly(assemblyName, IL2CPPInteropAssemblyPath, out var foundAssembly)
? foundAssembly
: null;
}
public static void Initialize()
{
if (initialized)
throw new InvalidOperationException("Already initialized");
initialized = true;
Environment.SetEnvironmentVariable("IL2CPP_INTEROP_DATABASES_LOCATION", IL2CPPInteropAssemblyPath);
AppDomain.CurrentDomain.AssemblyResolve += ResolveInteropAssemblies;
GenerateInteropAssemblies();
var interopLogger = LoggerFactory.CreateLogger("Il2CppInterop");
var unityVersion = UnityInfo.Version;
Il2CppInteropRuntime.Create(new RuntimeConfiguration
{
UnityVersion = new Version(unityVersion.Major, unityVersion.Minor, unityVersion.Build),
DetourProvider = new Il2CppInteropDetourProvider()
})
.AddLogger(interopLogger)
.AddHarmonySupport()
.Start();
}
private static void GenerateInteropAssemblies()
{
if (!CheckIfGenerationRequired())
return;
try
{
Directory.CreateDirectory(IL2CPPInteropAssemblyPath);
Directory.EnumerateFiles(IL2CPPInteropAssemblyPath, "*.dll").Do(File.Delete);
AppDomain.CurrentDomain.AddCecilPlatformAssemblies(UnityBaseLibsDirectory);
DownloadUnityAssemblies();
var asmResolverAssemblies = UseRuntimeAssemblies.Value ? RunIl2CppDumperWithRuntimeBinaries() : RunCpp2Il();
if (DumpDummyAssemblies.Value)
{
var dummyPath = Path.Combine(IL2CPPBasePath, "dummy");
Directory.CreateDirectory(dummyPath);
foreach (var assemblyDefinition in asmResolverAssemblies)
assemblyDefinition.Write(Path.Combine(dummyPath, $"{assemblyDefinition.Name}.dll"));
}
RunIl2CppInteropGenerator(asmResolverAssemblies);
File.WriteAllText(HashPath, ComputeHash());
}
catch (Exception e)
{
Logger.LogError($"Failed to generate Il2Cpp interop assemblies: {e}");
}
}
private static void DownloadUnityAssemblies() {
var unityVersion = UnityInfo.Version;
var version = $"{unityVersion.Major}.{unityVersion.Minor}.{unityVersion.Build}";
var source = UnityBaseLibrariesSource.Value.Replace("{VERSION}", version);
if (string.IsNullOrEmpty(source)) return;
var baseFolder = Directory.CreateDirectory(UnityBaseLibsDirectory);
baseFolder.EnumerateFiles("*.dll").Do(a=>a.Delete());
var uriIsValid = Uri.TryCreate(source, UriKind.Absolute, out var uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
var fileName = Path.GetFileName(uriIsValid ? uri.AbsolutePath : source);
var zipFilePath = Path.Combine(baseFolder.FullName, fileName);
if (!File.Exists(zipFilePath))
{
// Check if URI is valid before attempting download
if (!uriIsValid)
throw new ArgumentException($"Unity base libraries source \"{source}\" is not a valid URL and the .zip file does not exist locally. Either provide a valid HTTP/HTTPS URL, or place the .zip file in the unity-libs directory.");
Logger.LogMessage($"Downloading unity base libraries from {source}");
using var httpClient = new HttpClient();
Stream zipStream;
try
{
zipStream = httpClient.GetStreamAsync(uri).GetAwaiter().GetResult();
}
catch (Exception e)
{
throw new IOException(
$"Failed to download Unity base libraries for Unity {unityVersion} from {source}: {e.Message}. " +
$"Base libraries for this Unity version may not be available on the server yet. Download a matching " +
$"base-libraries .zip and place it in the \"{UnityBaseLibsDirectory}\" (unity-libs) directory, or set " +
$"the [IL2CPP] UnityBaseLibrariesSource config option to a valid URL.", e);
}
using (zipStream)
{
try
{
using var writeStream = new FileStream(zipFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
zipStream.CopyTo(writeStream);
}
catch
{
// Delete the incomplete file to avoid issues on next startup
try { File.Delete(zipFilePath); } catch { }
throw;
}
}
}
Logger.LogMessage($"Extracting unity base libraries from {zipFilePath}");
using var readStream = File.OpenRead(zipFilePath);
using var zipArchive = new ZipArchive(readStream, ZipArchiveMode.Read);
zipArchive.ExtractToDirectory(UnityBaseLibsDirectory);
}
private static List<AsmResolver.DotNet.AssemblyDefinition> RunCpp2Il()
{
var metadataPath = Path.Combine(Paths.GameRootPath,
GlobalMetadataPath.Value
.Replace("{BepInEx}", Paths.BepInExRootPath)
.Replace("{ProcessName}", Paths.ProcessName)
.Replace("{GameDataPath}", Paths.GameDataPath));
Logger.LogMessage("Running Cpp2IL to generate dummy assemblies from " + metadataPath);
var stopwatch = new Stopwatch();
stopwatch.Start();
var cpp2IlLogger = BepInEx.Logging.Logger.CreateLogSource("Cpp2IL");
Cpp2IL.Core.Logging.Logger.VerboseLog += (message, s) =>
cpp2IlLogger.LogDebug($"[{s}] {message.Trim()}");
Cpp2IL.Core.Logging.Logger.InfoLog += (message, s) =>
cpp2IlLogger.LogInfo($"[{s}] {message.Trim()}");
Cpp2IL.Core.Logging.Logger.WarningLog += (message, s) =>
cpp2IlLogger.LogWarning($"[{s}] {message.Trim()}");
Cpp2IL.Core.Logging.Logger.ErrorLog += (message, s) =>
cpp2IlLogger.LogError($"[{s}] {message.Trim()}");
var unityVersion = UnityInfo.Version;
Cpp2IlApi.InitializeLibCpp2Il(GameAssemblyPath, metadataPath, unityVersion, false);
List<Cpp2IlProcessingLayer> processingLayers = new() { new AttributeInjectorProcessingLayer(), };
foreach (var cpp2IlProcessingLayer in processingLayers)
{
cpp2IlProcessingLayer.PreProcess(Cpp2IlApi.CurrentAppContext, processingLayers);
}
foreach (var cpp2IlProcessingLayer in processingLayers)
{
cpp2IlProcessingLayer.Process(Cpp2IlApi.CurrentAppContext);
}
var assemblies = new AsmResolverDllOutputFormatDefault().BuildAssemblies(Cpp2IlApi.CurrentAppContext);
LibCpp2IlMain.Reset();
Cpp2IlApi.CurrentAppContext = null;
stopwatch.Stop();
Logger.LogInfo($"Cpp2IL finished in {stopwatch.Elapsed}");
return assemblies;
}
private static List<AsmResolver.DotNet.AssemblyDefinition> RunIl2CppDumperWithRuntimeBinaries()
{
var metadataPath = Path.Combine(Paths.GameRootPath,
$"{Paths.ProcessName}_Data",
"il2cpp_data",
"Metadata",
"global-metadata.dat");
static void WriteBytesToFile(string filePath, byte[] bytes)
{
using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write);
stream.Write(bytes, 0, bytes.Length);
}
static void WriteBinariesToFile(byte[] il2cppBytes, byte[] metadataBytes)
{
Directory.CreateDirectory(IL2CPPRuntimeBinariesPath);
Directory.GetFiles(IL2CPPRuntimeBinariesPath).Do(File.Delete);
WriteBytesToFile(Path.Combine(IL2CPPRuntimeBinariesPath, "GameAssembly.dll"), il2cppBytes);
WriteBytesToFile(Path.Combine(IL2CPPRuntimeBinariesPath, "global-metadata.dat"), metadataBytes);
}
var stopwatch = new Stopwatch();
stopwatch.Start();
Logger.LogMessage("Reading process memory to generate runtime binaries");
var logger = LoggerFactory.CreateLogger("Il2CppInteropGen");
Il2CppInterop.Runtime.MemoryUtils.RuntimeModuleDump(logger, out var il2cppBytes, out var metadataBytes, Convert.FromHexString(MetadataSignatureToScan.Value), Convert.FromHexString(MagicToFix.Value), ObfuscatedMetadataHeaderOffset.Value);
// we write to disk once so we can still inspect the byproduct if validation fails later on.
WriteBinariesToFile(il2cppBytes, metadataBytes);
Logger.LogMessage("Validating runtime binaries");
Il2CppInterop.Runtime.MemoryUtils.ValidateMetadata(logger, metadataPath, il2cppBytes, ref metadataBytes);
// this time the product will be final
WriteBinariesToFile(il2cppBytes, metadataBytes);
Logger.LogMessage("Running Il2CppDumper with runtime binaries to generate dummy assemblies");
Il2CppDumper.ExtensionMethods.Init(il2cppBytes, metadataBytes, out var metadata, out var il2Cpp);
Il2CppDumper.ExtensionMethods.GenerateCecilAssemblies(metadata, il2Cpp, out var assemblies);
stopwatch.Stop();
Logger.LogInfo($"Il2CppDumper finished in {stopwatch.Elapsed}");
return assemblies;
}
private static void RunIl2CppInteropGenerator(List<AsmResolver.DotNet.AssemblyDefinition> sourceAssemblies)
{
var opts = new GeneratorOptions
{
GameAssemblyPath = ScanMethodRefs.Value ? GameAssemblyPath : null,
Source = sourceAssemblies,
OutputDir = IL2CPPInteropAssemblyPath,
UnityBaseLibsDir = Directory.Exists(UnityBaseLibsDirectory) ? UnityBaseLibsDirectory : null,
ObfuscatedNamesRegex = !string.IsNullOrEmpty(ConfigUnhollowerDeobfuscationRegex.Value)
? new Regex(ConfigUnhollowerDeobfuscationRegex.Value)
: null,
};
if (File.Exists(RenameMapPath))
{
Logger.LogInfo("Parsing deobfuscation rename mappings");
opts.ReadRenameMap(RenameMapPath);
}
Logger.LogInfo("Generating interop assemblies");
var logger = LoggerFactory.CreateLogger("Il2CppInteropGen");
Il2CppInteropGenerator.Create(opts)
.AddLogger(logger)
.AddInteropAssemblyGenerator()
.Run();
}
internal static void PreloadInteropAssemblies()
{
if (!PreloadIL2CPPInteropAssemblies.Value)
return;
var sw = Stopwatch.StartNew();
var files = Directory.EnumerateFiles(IL2CPPInteropAssemblyPath);
var loaded = 0;
Parallel.ForEach(files, file =>
{
if (!file.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) return;
var name = Path.GetFileNameWithoutExtension(file);
if (name.Equals("netstandard", StringComparison.OrdinalIgnoreCase)) return;
if (name.Equals("Il2Cppnetstandard", StringComparison.OrdinalIgnoreCase)) return;
try
{
// Do not use LoadFrom since it overrides preloader patches
Assembly.Load(name);
Interlocked.Increment(ref loaded);
}
catch (Exception e)
{
Logger.LogWarning($"Failed to preload {file} - {e}");
}
});
Logger.LogDebug($"Preloaded {loaded} interop assemblies in {sw.ElapsedMilliseconds}ms");
}
}