diff --git a/Il2CppInterop.CLI/Program.cs b/Il2CppInterop.CLI/Program.cs index 5796435c5..22085796f 100644 --- a/Il2CppInterop.CLI/Program.cs +++ b/Il2CppInterop.CLI/Program.cs @@ -35,6 +35,9 @@ new Option("--passthrough-names", "If specified, names will be copied from input assemblies as-is without renaming or deobfuscation."), new Option("--no-parallel", "Disable parallel processing when writing assemblies. Use if you encounter stability issues when generating assemblies."), + new Option("--hybridclr", "Enable HybridCLR-specific metadata compatibility handling."), + new Option("--existing-interop", "Directory with existing interop assemblies to reference for incremental generation.").ExistingOnly(), + new Option("--no-skip-existing", "Generate assemblies even when they already exist in --existing-interop."), }; generateCommand.Description = "Generate wrapper assemblies that can be used to interop with Il2Cpp"; generateCommand.Handler = CommandHandler.Create((GenerateCommandOptions opts) => @@ -169,7 +172,10 @@ internal record GenerateCommandOptions( string[]? BlacklistAssembly, Regex? ObfRegex, bool PassthroughNames, - bool NoParallel + bool NoParallel, + bool HybridCLR, + DirectoryInfo? ExistingInterop, + bool NoSkipExisting ) : BaseCmdOptions(Verbose) { public override CmdOptionsResult Build() @@ -188,6 +194,9 @@ public override CmdOptionsResult Build() opts.ObfuscatedNamesRegex = ObfRegex; opts.PassthroughNames = PassthroughNames; opts.Parallel = !NoParallel; + opts.IsHybridCLREnvironment = HybridCLR; + opts.ExistingInteropDir = ExistingInterop?.FullName; + opts.SkipExistingAssemblies = !NoSkipExisting; if (AddPrefixTo is not null && AddPrefixTo.Length > 0) { diff --git a/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs b/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs index a5e8f2fd3..127e4fa30 100644 --- a/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs +++ b/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs @@ -18,9 +18,12 @@ public class AssemblyRewriteContext private readonly Dictionary myOldTypeMap = new(); public readonly AssemblyDefinition NewAssembly; +#nullable disable + // OriginalAssembly is null for reference-only contexts (loaded from ExistingInteropDir) public readonly AssemblyDefinition OriginalAssembly; +#nullable enable - public AssemblyRewriteContext(RewriteGlobalContext globalContext, AssemblyDefinition originalAssembly, + public AssemblyRewriteContext(RewriteGlobalContext globalContext, AssemblyDefinition? originalAssembly, AssemblyDefinition newAssembly) { OriginalAssembly = originalAssembly; @@ -56,11 +59,66 @@ public void RegisterTypeRewrite(TypeRewriteContext context) myNameTypeMap[(context.OriginalType ?? context.NewType).FullName] = context; } - public IMethodDefOrRef RewriteMethodRef(IMethodDefOrRef methodRef) + /// + /// Registers a type under an alternative name for lookup purposes. + /// Used for reference-only assemblies where Il2Cpp-prefixed types need to be + /// findable by their original names (e.g., "System.Type" for "Il2CppSystem.Type"). + /// + public void RegisterTypeByAlternativeName(string alternativeName, TypeRewriteContext context) { - var newType = GlobalContext.GetNewTypeForOriginal(methodRef.DeclaringType!.Resolve()!); - var newMethod = newType.GetMethodByOldMethod(methodRef.Resolve()!).NewMethod; - return NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(newMethod); + if (!myNameTypeMap.ContainsKey(alternativeName)) + { + myNameTypeMap[alternativeName] = context; + } + } + + public IMethodDefOrRef? RewriteMethodRef(IMethodDefOrRef? methodRef) + { + if (methodRef?.DeclaringType == null) + { + if (GlobalContext.Options.IsHybridCLREnvironment) + return null; + + throw new ArgumentNullException(nameof(methodRef)); + } + + var declaringType = methodRef.DeclaringType.Resolve(); + if (declaringType == null) + { + if (GlobalContext.Options.IsHybridCLREnvironment) + return null; + + throw new($"Could not resolve declaring type {methodRef.DeclaringType.FullName} for method {methodRef.Name}"); + } + + var newType = GlobalContext.GetNewTypeForOriginal(declaringType); + if (newType == null) + { + if (GlobalContext.Options.IsHybridCLREnvironment) + return null; + + throw new($"Could not find rewrite context for declaring type {declaringType.FullName}"); + } + + var resolvedMethod = methodRef.Resolve(); + if (resolvedMethod == null) + { + if (GlobalContext.Options.IsHybridCLREnvironment) + return null; + + throw new($"Could not resolve method {methodRef.FullName}"); + } + + var methodContext = newType.TryGetMethodByOldMethod(resolvedMethod); + if (methodContext == null) + { + if (GlobalContext.Options.IsHybridCLREnvironment) + return null; + + throw new($"Could not find rewrite context for method {resolvedMethod.FullName}"); + } + + return NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(methodContext.NewMethod); } public ITypeDefOrRef RewriteTypeRef(ITypeDescriptor typeRef) @@ -124,18 +182,81 @@ public TypeSignature RewriteTypeRef(TypeSignature? typeRef) return Imports.Module.String(); if (typeRef.FullName == "System.Object") - return sourceModule.DefaultImporter.ImportType(GlobalContext.GetAssemblyByName("mscorlib") - .GetTypeByName("System.Object").NewType).ToTypeSignature(); + { + var mscorlib = GlobalContext.TryGetAssemblyByName("mscorlib"); + if (mscorlib != null) + return sourceModule.DefaultImporter.ImportType(mscorlib.GetTypeByName("System.Object").NewType).ToTypeSignature(); + if (!GlobalContext.Options.IsHybridCLREnvironment) + throw new KeyNotFoundException("Required corlib type 'System.Object' was not found."); + return sourceModule.CorLibTypeFactory.Object; + } if (typeRef.FullName == "System.Attribute") - return sourceModule.DefaultImporter.ImportType(GlobalContext.GetAssemblyByName("mscorlib") - .GetTypeByName("System.Attribute").NewType).ToTypeSignature(); + { + var mscorlib = GlobalContext.TryGetAssemblyByName("mscorlib"); + if (mscorlib != null) + return sourceModule.DefaultImporter.ImportType(mscorlib.GetTypeByName("System.Attribute").NewType).ToTypeSignature(); + if (!GlobalContext.Options.IsHybridCLREnvironment) + throw new KeyNotFoundException("Required corlib type 'System.Attribute' was not found."); + return sourceModule.ImportCorlibReference("System.Attribute"); + } - var originalTypeDef = typeRef.Resolve()!; - var targetAssembly = GlobalContext.GetNewAssemblyForOriginal(originalTypeDef.DeclaringModule!.Assembly!); - var target = targetAssembly.GetContextForOriginalType(originalTypeDef).NewType; + var originalTypeDef = typeRef.Resolve(); + if (originalTypeDef == null) + { + if (!GlobalContext.Options.IsHybridCLREnvironment) + throw new($"Could not resolve type {typeRef.FullName}"); + + var mscorlib = GlobalContext.TryGetAssemblyByName("mscorlib"); + if (mscorlib != null) + return sourceModule.DefaultImporter.ImportType(mscorlib.GetTypeByName("System.Object").NewType).ToTypeSignature(); + return sourceModule.CorLibTypeFactory.Object; + } + + var targetAssembly = GlobalContext.GetNewAssemblyForOriginal(originalTypeDef.DeclaringModule?.Assembly); + if (targetAssembly == null) + { + // Not a source assembly — try name-based lookup to find reference (existing interop) assembly. + // The resolved TypeDefinition lives in the raw dependency DLL (e.g., mscorlib), + // but the registered context is for the interop DLL (e.g., Il2Cppmscorlib). + var asmName = originalTypeDef.DeclaringModule?.Assembly?.Name?.Value; + if (asmName != null) + targetAssembly = GlobalContext.TryGetAssemblyByName(asmName); + } + if (targetAssembly == null) + { + if (!GlobalContext.Options.IsHybridCLREnvironment) + throw new KeyNotFoundException( + $"Could not find target assembly for type {originalTypeDef.FullName} from assembly {originalTypeDef.DeclaringModule?.Assembly?.Name}"); + + var mscorlib = GlobalContext.TryGetAssemblyByName("mscorlib"); + if (mscorlib != null) + return sourceModule.DefaultImporter.ImportType(mscorlib.GetTypeByName("System.Object").NewType).ToTypeSignature(); + return sourceModule.CorLibTypeFactory.Object; + } + + var typeContext = targetAssembly.TryGetContextForOriginalType(originalTypeDef); + if (typeContext == null) + { + // For reference assemblies, the type IS the new type (no original/new distinction). + // Object-identity lookup won't work because originalTypeDef is from the raw dependency DLL, + // not from the interop DLL. Use name-based lookup instead. + var typeName = originalTypeDef.FullName; + typeContext = targetAssembly.TryGetTypeByName(typeName); + } + if (typeContext == null) + { + if (!GlobalContext.Options.IsHybridCLREnvironment) + throw new KeyNotFoundException( + $"Could not find rewrite context for type {originalTypeDef.FullName} in assembly {targetAssembly.NewAssembly.Name}"); + + var mscorlib = GlobalContext.TryGetAssemblyByName("mscorlib"); + if (mscorlib != null) + return sourceModule.DefaultImporter.ImportType(mscorlib.GetTypeByName("System.Object").NewType).ToTypeSignature(); + return sourceModule.CorLibTypeFactory.Object; + } - return sourceModule.DefaultImporter.ImportType(target).ToTypeSignature(); + return sourceModule.DefaultImporter.ImportType(typeContext.NewType).ToTypeSignature(); } public TypeRewriteContext GetTypeByName(string name) diff --git a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs index 76c1ea2b0..5e9e7d8e7 100644 --- a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs +++ b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs @@ -1,9 +1,11 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using Il2CppInterop.Common; using Il2CppInterop.Generator.Extensions; using Il2CppInterop.Generator.MetadataAccess; using Il2CppInterop.Generator.Utils; +using Microsoft.Extensions.Logging; namespace Il2CppInterop.Generator.Contexts; @@ -21,6 +23,9 @@ public class RewriteGlobalContext : IDisposable internal readonly Dictionary ImportsMap = new(); + // Reference-only assemblies loaded from ExistingInteropDir for incremental mode + private readonly Dictionary myReferenceAssemblies = new(); + public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess gameAssemblies, IMetadataAccess unityAssemblies) { @@ -30,6 +35,12 @@ public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess game Il2CppAssemblyResolver assemblyResolver = new(); + // In incremental mode, load existing interop assemblies as references first + if (!string.IsNullOrEmpty(options.ExistingInteropDir) && Directory.Exists(options.ExistingInteropDir)) + { + LoadExistingInteropAssemblies(options.ExistingInteropDir!, assemblyResolver); + } + foreach (var sourceAssembly in gameAssemblies.Assemblies) { var assemblyName = sourceAssembly.Name!; @@ -42,7 +53,9 @@ public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess game var newModule = new ModuleDefinition(sourceAssembly.ManifestModule?.Name.UnSystemify(options), CorlibReferences.TargetCorlib); newAssembly.Modules.Add(newModule); - newModule.MetadataResolver = new DefaultMetadataResolver(assemblyResolver); + newModule.MetadataResolver = options.IsHybridCLREnvironment + ? new HybridCLRMetadataResolver(assemblyResolver) + : new DefaultMetadataResolver(assemblyResolver); assemblyResolver.AddToCache(newAssembly); var assemblyRewriteContext = new AssemblyRewriteContext(this, sourceAssembly, newAssembly); @@ -50,12 +63,103 @@ public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess game } } + /// + /// Loads existing interop assemblies from ExistingInteropDir as reference-only contexts. + /// These are used to resolve types like System.Type when generating hotfix interop. + /// + private void LoadExistingInteropAssemblies(string existingInteropDir, Il2CppAssemblyResolver assemblyResolver) + { + Logger.Instance.LogInformation("Loading existing interop assemblies from {Dir}", existingInteropDir); + int loadedCount = 0; + + foreach (var dllPath in Directory.EnumerateFiles(existingInteropDir, "*.dll")) + { + try + { + var assembly = AssemblyDefinition.FromFile(dllPath); + if (assembly?.ManifestModule == null) + continue; + + // The assembly name in the file (e.g., "Il2Cppmscorlib") + var assemblyName = assembly.Name?.Value ?? Path.GetFileNameWithoutExtension(dllPath); + + // Create a reference-only context (no original assembly, just the new/interop assembly) + var context = new AssemblyRewriteContext(this, null!, assembly); + + // Register types from the existing interop assembly + foreach (var type in assembly.ManifestModule.TopLevelTypes) + { + RegisterExistingInteropType(context, type); + } + + // Register under the assembly name as it appears in the file + myReferenceAssemblies[assemblyName] = context; + + // Also register under the original name (without Il2Cpp prefix) for lookup compatibility + // e.g., "Il2Cppmscorlib" should also be findable as "mscorlib" + if (assemblyName.StartsWith("Il2Cpp")) + { + var originalName = assemblyName.Substring(6); + if (!myReferenceAssemblies.ContainsKey(originalName)) + { + myReferenceAssemblies[originalName] = context; + } + } + + assemblyResolver.AddToCache(assembly); + loadedCount++; + + Logger.Instance.LogTrace("Loaded reference assembly: {Name}", assemblyName); + } + catch (Exception ex) + { + Logger.Instance.LogWarning("Failed to load reference assembly {Path}: {Error}", + Path.GetFileName(dllPath), ex.Message); + } + } + + Logger.Instance.LogInformation("Loaded {Count} reference assemblies from ExistingInteropDir", loadedCount); + } + + private void RegisterExistingInteropType(AssemblyRewriteContext context, TypeDefinition type) + { + // Create a TypeRewriteContext for the existing type (reference-only, no original) + var typeContext = new TypeRewriteContext(context, null, type); + context.RegisterTypeRewrite(typeContext); + + // Also register under the original (non-Il2Cpp-prefixed) name for lookup compatibility + // e.g., "Il2CppSystem.Type" should also be findable as "System.Type" + var fullName = type.FullName; + if (fullName != null) + { + // Handle Il2Cpp prefix in namespace + if (fullName.StartsWith("Il2Cpp")) + { + var originalName = fullName.Substring(6); // Remove "Il2Cpp" prefix + context.RegisterTypeByAlternativeName(originalName, typeContext); + } + } + + // Register nested types + foreach (var nestedType in type.NestedTypes) + { + RegisterExistingInteropType(context, nestedType); + } + } + public GeneratorOptions Options { get; } public IIl2CppMetadataAccess GameAssemblies { get; } public IMetadataAccess UnityAssemblies { get; } public IEnumerable Assemblies => myAssemblies.Values; - public AssemblyRewriteContext CorLib => myAssemblies["mscorlib"]; + + /// + /// Gets the mscorlib assembly context. Returns null if mscorlib is not loaded. + /// Checks both source assemblies and reference assemblies (from ExistingInteropDir). + /// + public AssemblyRewriteContext? CorLib => + myAssemblies.TryGetValue("mscorlib", out var corlib) ? corlib : + myReferenceAssemblies.TryGetValue("mscorlib", out var refCorlib) ? refCorlib : null; internal bool HasGcWbarrierFieldWrite { get; set; } @@ -72,20 +176,23 @@ internal void AddAssemblyContext(string assemblyName, AssemblyRewriteContext con myAssembliesByNew[context.NewAssembly] = context; } - public AssemblyRewriteContext GetNewAssemblyForOriginal(AssemblyDefinition oldAssembly) + public AssemblyRewriteContext? GetNewAssemblyForOriginal(AssemblyDefinition? oldAssembly) { - return myAssembliesByOld[oldAssembly]; + if (oldAssembly == null) return null; + return myAssembliesByOld.TryGetValue(oldAssembly, out var result) ? result : null; } - public TypeRewriteContext GetNewTypeForOriginal(TypeDefinition originalType) + public TypeRewriteContext? GetNewTypeForOriginal(TypeDefinition? originalType) { - return GetNewAssemblyForOriginal(originalType.DeclaringModule!.Assembly!) - .GetContextForOriginalType(originalType); + if (originalType?.DeclaringModule?.Assembly == null) return null; + var assembly = GetNewAssemblyForOriginal(originalType.DeclaringModule.Assembly); + return assembly?.TryGetContextForOriginalType(originalType); } - public TypeRewriteContext? TryGetNewTypeForOriginal(TypeDefinition originalType) + public TypeRewriteContext? TryGetNewTypeForOriginal(TypeDefinition? originalType) { - if (!myAssembliesByOld.TryGetValue(originalType.DeclaringModule!.Assembly!, out var assembly)) + if (originalType?.DeclaringModule?.Assembly == null) return null; + if (!myAssembliesByOld.TryGetValue(originalType.DeclaringModule.Assembly, out var assembly)) return null; return assembly.TryGetContextForOriginalType(originalType); } @@ -102,13 +209,37 @@ or GenericParameterSignature or GenericInstanceTypeSignature) return TypeRewriteContext.TypeSpecifics.ReferenceType; - var fieldTypeContext = GetNewTypeForOriginal(typeRef.Resolve() ?? throw new($"Could not resolve {typeRef.FullName}")); + var resolved = typeRef.Resolve(); + if (resolved == null) + { + if (Options.IsHybridCLREnvironment) + return TypeRewriteContext.TypeSpecifics.ReferenceType; + + throw new($"Could not resolve {typeRef.FullName}"); + } + + var fieldTypeContext = GetNewTypeForOriginal(resolved); + if (fieldTypeContext == null) + { + if (Options.IsHybridCLREnvironment) + return TypeRewriteContext.TypeSpecifics.ReferenceType; + + throw new($"Could not find rewrite context for {resolved.FullName}"); + } + return fieldTypeContext.ComputedTypeSpecifics; } public AssemblyRewriteContext GetAssemblyByName(string name) { - return myAssemblies[name]; + if (myAssemblies.TryGetValue(name, out var result)) + return result; + + // Fall back to reference assemblies (from ExistingInteropDir) + if (myReferenceAssemblies.TryGetValue(name, out var refResult)) + return refResult; + + throw new KeyNotFoundException($"Assembly '{name}' not found in source or reference assemblies"); } public AssemblyRewriteContext? TryGetAssemblyByName(string? name) @@ -119,20 +250,41 @@ public AssemblyRewriteContext GetAssemblyByName(string name) if (myAssemblies.TryGetValue(name, out var result)) return result; + // Fall back to reference assemblies (from ExistingInteropDir) + if (myReferenceAssemblies.TryGetValue(name, out var refResult)) + return refResult; + if (name == "netstandard") - return myAssemblies.TryGetValue("mscorlib", out var result2) ? result2 : null; + { + if (myAssemblies.TryGetValue("mscorlib", out var result2)) + return result2; + if (myReferenceAssemblies.TryGetValue("mscorlib", out var refResult2)) + return refResult2; + } return null; } - public AssemblyRewriteContext GetContextForNewAssembly(AssemblyDefinition assembly) + public AssemblyRewriteContext? GetContextForNewAssembly(AssemblyDefinition? assembly) { - return myAssembliesByNew[assembly]; + if (assembly == null) return null; + if (myAssembliesByNew.TryGetValue(assembly, out var result)) + return result; + + // Check reference assemblies (they are stored by NewAssembly, not in myAssembliesByNew) + foreach (var refAsm in myReferenceAssemblies.Values) + { + if (refAsm.NewAssembly == assembly) + return refAsm; + } + + return null; } - public TypeRewriteContext GetContextForNewType(TypeDefinition type) + public TypeRewriteContext? GetContextForNewType(TypeDefinition? type) { - return GetContextForNewAssembly(type.DeclaringModule!.Assembly!).GetContextForNewType(type); + if (type?.DeclaringModule?.Assembly == null) return null; + return GetContextForNewAssembly(type.DeclaringModule.Assembly)?.GetContextForNewType(type); } public MethodDefinition? CreateParamsMethod(MethodDefinition originalMethod, MethodDefinition newMethod, diff --git a/Il2CppInterop.Generator/GeneratorOptions.cs b/Il2CppInterop.Generator/GeneratorOptions.cs index 59fe941e6..b916aefd6 100644 --- a/Il2CppInterop.Generator/GeneratorOptions.cs +++ b/Il2CppInterop.Generator/GeneratorOptions.cs @@ -31,6 +31,27 @@ public class GeneratorOptions public List DeobfuscationGenerationAssemblies { get; } = new(); public string? DeobfuscationNewAssembliesPath { get; set; } + /// + /// Indicates whether the target environment uses HybridCLR for hot-update support. + /// When true, the generator may apply additional compatibility handling. + /// + public bool IsHybridCLREnvironment { get; set; } + + /// + /// Path to existing interop assemblies directory. + /// When set, generator runs in incremental mode - only generates + /// interop for Source assemblies while referencing existing interop. + /// This is useful for generating interop for HybridCLR hotfix assemblies + /// that need to reference already-generated AOT interop assemblies. + /// + public string? ExistingInteropDir { get; set; } + + /// + /// When true, skips assemblies that already exist in ExistingInteropDir. + /// Default is true. + /// + public bool SkipExistingAssemblies { get; set; } = true; + /// /// Reads a rename map from the specified name into the specified instance of options /// diff --git a/Il2CppInterop.Generator/MetadataAccess/AssemblyMetadataAccess.cs b/Il2CppInterop.Generator/MetadataAccess/AssemblyMetadataAccess.cs index e0cbd1086..de583830d 100644 --- a/Il2CppInterop.Generator/MetadataAccess/AssemblyMetadataAccess.cs +++ b/Il2CppInterop.Generator/MetadataAccess/AssemblyMetadataAccess.cs @@ -9,16 +9,17 @@ public class AssemblyMetadataAccess : IIl2CppMetadataAccess private readonly List myAssemblies = new(); private readonly Dictionary myAssembliesByName = new(); private readonly Dictionary<(string AssemblyName, string TypeName), TypeDefinition> myTypesByName = new(); + private readonly bool myIsHybridCLREnvironment; - public AssemblyMetadataAccess(IEnumerable assemblyPaths) + public AssemblyMetadataAccess(IEnumerable assemblyPaths, bool isHybridCLREnvironment = false) { + myIsHybridCLREnvironment = isHybridCLREnvironment; Load(assemblyPaths.Select(AssemblyDefinition.FromFile)); } - public AssemblyMetadataAccess(IEnumerable assemblies) + public AssemblyMetadataAccess(IEnumerable assemblies, bool isHybridCLREnvironment = false) { - // Note: At the moment this assumes that passed assemblies have their own assembly resolver set up - // If this is not true, this can cause issues with reference resolving + myIsHybridCLREnvironment = isHybridCLREnvironment; Load(assemblies); } @@ -56,13 +57,25 @@ public void Dispose() return null; } + /// + /// Adds external assemblies to the internal resolver cache so that type references + /// in source assemblies can resolve against them (e.g., reference interop assemblies). + /// + public void AddReferenceAssemblies(IEnumerable assemblies) + { + foreach (var assembly in assemblies) + myAssemblyResolver.AddToCache(assembly); + } + private void Load(IEnumerable assemblies) { foreach (var sourceAssembly in assemblies) { myAssemblies.Add(sourceAssembly); myAssembliesByName[sourceAssembly.Name!] = sourceAssembly; - sourceAssembly.ManifestModule!.MetadataResolver = new DefaultMetadataResolver(myAssemblyResolver); + sourceAssembly.ManifestModule!.MetadataResolver = myIsHybridCLREnvironment + ? new HybridCLRMetadataResolver(myAssemblyResolver) + : new DefaultMetadataResolver(myAssemblyResolver); myAssemblyResolver.AddToCache(sourceAssembly); } diff --git a/Il2CppInterop.Generator/MetadataAccess/HybridCLRMetadataResolver.cs b/Il2CppInterop.Generator/MetadataAccess/HybridCLRMetadataResolver.cs new file mode 100644 index 000000000..9135600ee --- /dev/null +++ b/Il2CppInterop.Generator/MetadataAccess/HybridCLRMetadataResolver.cs @@ -0,0 +1,252 @@ +using AsmResolver; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; + +namespace Il2CppInterop.Generator.MetadataAccess; + +/// +/// Custom metadata resolver for HybridCLR environments. +/// Handles type relocation issues where some types are moved from System.dll to mscorlib by IL2CPP. +/// +internal sealed class HybridCLRMetadataResolver : IMetadataResolver +{ + private readonly IAssemblyResolver _assemblyResolver; + private readonly Il2CppAssemblyResolver? _il2cppResolver; + + public HybridCLRMetadataResolver(IAssemblyResolver assemblyResolver) + { + _assemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); + _il2cppResolver = assemblyResolver as Il2CppAssemblyResolver; + } + + public IAssemblyResolver AssemblyResolver => _assemblyResolver; + + /// + /// Resolves an assembly, using Il2CppAssemblyResolver's signature-ignoring lookup if available. + /// + private AssemblyDefinition? ResolveAssembly(AssemblyDescriptor descriptor) + { + // Use Il2CppAssemblyResolver's Resolve method which ignores signature mismatches + if (_il2cppResolver != null) + return _il2cppResolver.Resolve(descriptor); + + return _assemblyResolver.Resolve(descriptor); + } + + public TypeDefinition? ResolveType(ITypeDescriptor? type) + { + if (type is null) + return null; + + return type switch + { + TypeDefinition definition => definition, + TypeReference reference => ResolveTypeReference(reference), + TypeSpecification specification => ResolveType(specification.Signature?.GetUnderlyingTypeDefOrRef()), + ExportedType exportedType => ResolveExportedType(exportedType), + _ => null + }; + } + + public MethodDefinition? ResolveMethod(IMethodDescriptor? method) + { + if (method is null) + return null; + + if (method is MethodDefinition definition) + return definition; + + if (method is not IMethodDefOrRef methodDefOrRef) + return null; + + var declaringType = ResolveType(methodDefOrRef.DeclaringType); + if (declaringType is null) + return null; + + return FindMethodInType(declaringType, methodDefOrRef); + } + + public FieldDefinition? ResolveField(IFieldDescriptor? field) + { + if (field is null) + return null; + + if (field is FieldDefinition definition) + return definition; + + if (field is not MemberReference memberRef) + return null; + + var declaringType = ResolveType(memberRef.DeclaringType); + if (declaringType is null) + return null; + + return FindFieldInType(declaringType, memberRef); + } + + private TypeDefinition? ResolveTypeReference(TypeReference reference) + { + var scope = reference.Scope; + if (scope is null) + return null; + + AssemblyDefinition? assembly = null; + + switch (scope) + { + case AssemblyDescriptor assemblyDescriptor: + assembly = ResolveAssembly(assemblyDescriptor); + break; + case ModuleDefinition module: + assembly = module.Assembly; + break; + case TypeReference declaringType: + var resolvedDeclaringType = ResolveType(declaringType); + if (resolvedDeclaringType is not null) + return FindNestedType(resolvedDeclaringType, reference.Name); + break; + } + + if (assembly is null) + return null; + + // Try to find the type in the resolved assembly + var result = FindTypeInAssembly(assembly, reference.Namespace, reference.Name); + if (result is not null) + return result; + + // HybridCLR/IL2CPP type relocation fix: + // Some types are moved from System.dll to mscorlib by IL2CPP. + // If we can't find the type in the original assembly, try mscorlib as fallback. + if (!IsCorLib(assembly)) + { + var corLib = GetCorLibAssembly(); + if (corLib is not null) + { + result = FindTypeInAssembly(corLib, reference.Namespace, reference.Name); + if (result is not null) + return result; + } + } + + return null; + } + + private TypeDefinition? ResolveExportedType(ExportedType exportedType) + { + var implementation = exportedType.Implementation; + if (implementation is null) + return null; + + switch (implementation) + { + case AssemblyReference assemblyRef: + var assembly = ResolveAssembly(assemblyRef); + if (assembly is not null) + return FindTypeInAssembly(assembly, exportedType.Namespace, exportedType.Name); + break; + case ExportedType parentExportedType: + var parentType = ResolveExportedType(parentExportedType); + if (parentType is not null) + return FindNestedType(parentType, exportedType.Name); + break; + } + + return null; + } + + private TypeDefinition? FindTypeInAssembly(AssemblyDefinition assembly, Utf8String? ns, Utf8String? name) + { + if (name is null) + return null; + + for (int i = 0; i < assembly.Modules.Count; i++) + { + var module = assembly.Modules[i]; + var type = FindTypeInModule(module, ns, name); + if (type is not null) + return type; + } + + // If not found and the assembly is an Il2Cpp-prefixed interop assembly, + // try with Il2Cpp-prefixed namespace (e.g., "System" -> "Il2CppSystem") + if (ns is not null && assembly.Name is not null && assembly.Name.Value.StartsWith("Il2Cpp")) + { + var prefixedNs = new Utf8String("Il2Cpp" + ns.Value); + for (int i = 0; i < assembly.Modules.Count; i++) + { + var module = assembly.Modules[i]; + var type = FindTypeInModule(module, prefixedNs, name); + if (type is not null) + return type; + } + } + + return null; + } + + private static TypeDefinition? FindTypeInModule(ModuleDefinition module, Utf8String? ns, Utf8String? name) + { + foreach (var type in module.TopLevelTypes) + { + if (type.Name == name && type.Namespace == ns) + return type; + } + + return null; + } + + private static TypeDefinition? FindNestedType(TypeDefinition declaringType, Utf8String? name) + { + if (name is null) + return null; + + foreach (var nestedType in declaringType.NestedTypes) + { + if (nestedType.Name == name) + return nestedType; + } + + return null; + } + + private static MethodDefinition? FindMethodInType(TypeDefinition type, IMethodDefOrRef methodRef) + { + foreach (var method in type.Methods) + { + if (method.Name == methodRef.Name && SignatureComparer.Default.Equals(method.Signature, methodRef.Signature)) + return method; + } + + return null; + } + + private static FieldDefinition? FindFieldInType(TypeDefinition type, MemberReference fieldRef) + { + foreach (var field in type.Fields) + { + if (field.Name == fieldRef.Name) + return field; + } + + return null; + } + + private static bool IsCorLib(AssemblyDefinition assembly) + { + var name = assembly.Name; + return name is not null && ( + name.Contains("mscorlib") || + name.Contains("System.Runtime") || + name.Contains("System.Private.CoreLib") || + name.Contains("netstandard") + ); + } + + private AssemblyDefinition? GetCorLibAssembly() + { + // Try to resolve mscorlib directly via the assembly resolver + var mscorlibRef = new AssemblyReference("mscorlib", new Version(4, 0, 0, 0)); + return ResolveAssembly(mscorlibRef); + } +} diff --git a/Il2CppInterop.Generator/MetadataAccess/Il2CppAssemblyResolver.cs b/Il2CppInterop.Generator/MetadataAccess/Il2CppAssemblyResolver.cs index 8874998ae..83b8126f1 100644 --- a/Il2CppInterop.Generator/MetadataAccess/Il2CppAssemblyResolver.cs +++ b/Il2CppInterop.Generator/MetadataAccess/Il2CppAssemblyResolver.cs @@ -1,18 +1,62 @@ -using AsmResolver.DotNet; +using System.Collections.Concurrent; +using AsmResolver.DotNet; using AsmResolver.IO; -namespace Il2CppInterop.Generator.MetadataAccess; - -internal sealed class Il2CppAssemblyResolver : AssemblyResolverBase -{ - public Il2CppAssemblyResolver() : base(new ByteArrayFileService()) - { - } - - protected override string? ProbeRuntimeDirectories(AssemblyDescriptor assembly) => null; - - public void AddToCache(AssemblyDefinition assembly) - { - AddToCache(assembly, assembly); - } +namespace Il2CppInterop.Generator.MetadataAccess; + +/// +/// Custom assembly resolver for IL2CPP/HybridCLR environments. +/// Handles signature mismatches between hotfix DLLs and AOT DLLs by ignoring public key token comparison. +/// +internal sealed class Il2CppAssemblyResolver : AssemblyResolverBase +{ + // Cache for fast assembly lookup by name (ignoring signature) + private readonly ConcurrentDictionary _assemblyCache = new(); + + public Il2CppAssemblyResolver() : base(new ByteArrayFileService()) + { + } + + protected override string? ProbeRuntimeDirectories(AssemblyDescriptor assembly) => null; + + public void AddToCache(AssemblyDefinition assembly) + { + // Add to base cache with full signature + AddToCache(assembly, assembly); + + // Also add to our name-only cache for signature-ignoring lookups + if (!string.IsNullOrEmpty(assembly.Name)) + { + _assemblyCache.TryAdd(assembly.Name!, assembly); + + // For Il2Cpp-prefixed assemblies, also register under the original name + // so hotfix DLLs referencing "mscorlib" can resolve to "Il2Cppmscorlib" + if (assembly.Name!.Value.StartsWith("Il2Cpp")) + { + var originalName = assembly.Name.Value.Substring(6); + _assemblyCache.TryAdd(originalName, assembly); + } + } + } + + /// + /// Resolves an assembly, ignoring public key token mismatches. + /// This is necessary because IL2CPP remaps DLL signatures differently than the original assemblies. + /// Uses 'new' to hide base method since AssemblyResolverBase.Resolve is not virtual. + /// + public new AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + // First try the standard resolution + var result = base.Resolve(assembly); + if (result != null) + return result; + + // If standard resolution failed, try name-only lookup (ignoring signature) + if (!string.IsNullOrEmpty(assembly.Name) && _assemblyCache.TryGetValue(assembly.Name!, out var cached)) + { + return cached; + } + + return null; + } } diff --git a/Il2CppInterop.Generator/Passes/Pass11ComputeTypeSpecifics.cs b/Il2CppInterop.Generator/Passes/Pass11ComputeTypeSpecifics.cs index b50cd2e89..84d27ef14 100644 --- a/Il2CppInterop.Generator/Passes/Pass11ComputeTypeSpecifics.cs +++ b/Il2CppInterop.Generator/Passes/Pass11ComputeTypeSpecifics.cs @@ -39,7 +39,25 @@ private static void ComputeSpecifics(TypeRewriteContext typeContext) return; } - var fieldTypeContext = typeContext.AssemblyContext.GlobalContext.GetNewTypeForOriginal(fieldType.Resolve()!); + var resolvedFieldType = fieldType.Resolve(); + if (resolvedFieldType == null) + { + if (!typeContext.AssemblyContext.GlobalContext.Options.IsHybridCLREnvironment) + throw new($"Could not resolve {fieldType.FullName}"); + + typeContext.ComputedTypeSpecifics = TypeRewriteContext.TypeSpecifics.NonBlittableStruct; + return; + } + + var fieldTypeContext = typeContext.AssemblyContext.GlobalContext.GetNewTypeForOriginal(resolvedFieldType); + if (fieldTypeContext == null) + { + if (!typeContext.AssemblyContext.GlobalContext.Options.IsHybridCLREnvironment) + throw new($"Could not find rewrite context for {resolvedFieldType.FullName}"); + + typeContext.ComputedTypeSpecifics = TypeRewriteContext.TypeSpecifics.NonBlittableStruct; + return; + } ComputeSpecifics(fieldTypeContext); if (fieldTypeContext.ComputedTypeSpecifics != TypeRewriteContext.TypeSpecifics.BlittableStruct) { diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index 9ed14fa05..fae274408 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -15,6 +15,11 @@ public static class Pass61ImplementAwaiters public static void DoPass(RewriteGlobalContext context) { var corlib = context.CorLib; + if (corlib is null) + { + // mscorlib not available (e.g., HybridCLR hotfix-only generation) + return; + } var actionUntyped = corlib.GetTypeByName("System.Action"); diff --git a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs index 04a285bfd..8e374de45 100644 --- a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs +++ b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs @@ -258,7 +258,9 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth { var enclosingResolvedType = ResolveTypeInNewAssembliesRaw(context, unityType.DeclaringType.ToTypeSignature(), imports); if (enclosingResolvedType == null) return null; - var resolvedNestedType = enclosingResolvedType.Resolve()!.NestedTypes + var enclosingResolved = enclosingResolvedType.Resolve(); + if (enclosingResolved == null) return null; + var resolvedNestedType = enclosingResolved.NestedTypes .FirstOrDefault(it => it.Name == unityType.Name); return resolvedNestedType?.ToTypeSignature(); diff --git a/Il2CppInterop.Generator/Runners/DeobfuscationMapGenerator.cs b/Il2CppInterop.Generator/Runners/DeobfuscationMapGenerator.cs index e6a5e28bb..ede2bd8e3 100644 --- a/Il2CppInterop.Generator/Runners/DeobfuscationMapGenerator.cs +++ b/Il2CppInterop.Generator/Runners/DeobfuscationMapGenerator.cs @@ -193,8 +193,10 @@ private static (TypeRewriteContext?, int) FindBestMatchType(TypeRewriteContext o while (true) { if (currentBase == null) break; + var resolvedBase = currentBase.Resolve(); + if (resolvedBase == null) break; var currentBaseContext = - obfType.AssemblyContext.GlobalContext.TryGetNewTypeForOriginal(currentBase.Resolve()!); + obfType.AssemblyContext.GlobalContext.TryGetNewTypeForOriginal(resolvedBase); if (currentBaseContext == null || !currentBaseContext.OriginalNameWasObfuscated) break; inheritanceDepthOfOriginal++; @@ -206,11 +208,14 @@ private static (TypeRewriteContext?, int) FindBestMatchType(TypeRewriteContext o var source = enclosingCleanType?.OriginalType.NestedTypes.Select(it => - cleanAssembly.GlobalContext.GetNewTypeForOriginal(it)) ?? + cleanAssembly.GlobalContext.GetNewTypeForOriginal(it)).Where(it => it != null) ?? cleanAssembly.Types.Where(it => it.NewType.DeclaringType == null); foreach (var candidateCleanType in source) { + if (candidateCleanType == null) + continue; + if (obfType.OriginalType.HasMethods() != candidateCleanType.OriginalType.HasMethods()) continue; diff --git a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs index 72d468627..d1dfbbe56 100644 --- a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs +++ b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs @@ -1,4 +1,5 @@ -using Il2CppInterop.Common; +using AsmResolver.DotNet; +using Il2CppInterop.Common; using Il2CppInterop.Common.XrefScans; using Il2CppInterop.Generator.Contexts; using Il2CppInterop.Generator.MetadataAccess; @@ -35,23 +36,55 @@ public void Run(GeneratorOptions options) if (!Directory.Exists(options.OutputDir)) Directory.CreateDirectory(options.OutputDir); + // Filter source assemblies if incremental mode is enabled + var sourceAssemblies = options.Source; + if (!string.IsNullOrEmpty(options.ExistingInteropDir) && options.SkipExistingAssemblies) + { + using (new TimingCookie("Filtering existing assemblies")) + { + sourceAssemblies = FilterExistingAssemblies(options.Source, options.ExistingInteropDir!); + if (!sourceAssemblies.Any()) + { + Logger.Instance.LogInformation("All assemblies already exist in ExistingInteropDir, nothing to generate"); + return; + } + Logger.Instance.LogInformation("Incremental mode: generating {Count} assemblies (skipped {Skipped} existing)", + sourceAssemblies.Count, options.Source.Count - sourceAssemblies.Count); + } + } + RewriteGlobalContext rewriteContext; IIl2CppMetadataAccess gameAssemblies; IMetadataAccess unityAssemblies; using (new TimingCookie("Reading assemblies")) { - gameAssemblies = new AssemblyMetadataAccess(options.Source); + gameAssemblies = new AssemblyMetadataAccess(sourceAssemblies, + isHybridCLREnvironment: options.IsHybridCLREnvironment); } + // Load reference assemblies: UnityBaseLibsDir or ExistingInteropDir if (!string.IsNullOrEmpty(options.UnityBaseLibsDir)) using (new TimingCookie("Reading unity assemblies")) { - unityAssemblies = new AssemblyMetadataAccess(Directory.EnumerateFiles(options.UnityBaseLibsDir, "*.dll")); + unityAssemblies = new AssemblyMetadataAccess( + Directory.EnumerateFiles(options.UnityBaseLibsDir, "*.dll"), + isHybridCLREnvironment: options.IsHybridCLREnvironment); + } + else if (!string.IsNullOrEmpty(options.ExistingInteropDir)) + using (new TimingCookie("Reading existing interop assemblies")) + { + unityAssemblies = new AssemblyMetadataAccess( + Directory.EnumerateFiles(options.ExistingInteropDir, "*.dll"), + isHybridCLREnvironment: options.IsHybridCLREnvironment); } else unityAssemblies = NullMetadataAccess.Instance; + // Incremental/reference mode lets source assemblies resolve types from existing interop assemblies. + if (!string.IsNullOrEmpty(options.ExistingInteropDir) && unityAssemblies is AssemblyMetadataAccess refAccess) + ((AssemblyMetadataAccess)gameAssemblies).AddReferenceAssemblies(refAccess.Assemblies); + using (new TimingCookie("Creating rewrite assemblies")) { rewriteContext = new RewriteGlobalContext(options, gameAssemblies, unityAssemblies); @@ -181,6 +214,11 @@ public void Run(GeneratorOptions options) Pass81FillUnstrippedMethodBodies.DoPass(rewriteContext); } } + else if (options.ExistingInteropDir != null) + { + // Incremental mode: skip unstripping as existing interop already has unstripped members + Logger.Instance.LogInformation("Incremental mode: skipping unstripping (using existing interop as reference)"); + } else { Logger.Instance.LogWarning("Not performing unstripping as unity libs are not specified"); @@ -219,4 +257,21 @@ public void Run(GeneratorOptions options) } public void Dispose() { } + + /// + /// Filters out assemblies that already have interop generated in the existing interop directory. + /// + private static List FilterExistingAssemblies( + List source, + string existingInteropDir) + { + var existingFiles = new HashSet( + Directory.EnumerateFiles(existingInteropDir, "*.dll") + .Select(f => Path.GetFileNameWithoutExtension(f)), + StringComparer.OrdinalIgnoreCase); + + return source + .Where(asm => !existingFiles.Contains(asm.Name ?? string.Empty)) + .ToList(); + } } diff --git a/Il2CppInterop.HarmonySupport/Il2CppDetourMethodPatcher.cs b/Il2CppInterop.HarmonySupport/Il2CppDetourMethodPatcher.cs index 2accee362..93c8c2f66 100644 --- a/Il2CppInterop.HarmonySupport/Il2CppDetourMethodPatcher.cs +++ b/Il2CppInterop.HarmonySupport/Il2CppDetourMethodPatcher.cs @@ -63,6 +63,9 @@ private static readonly MethodInfo ReportExceptionMethodInfo private INativeMethodInfoStruct originalNativeMethodInfo; + // HybridCLR hotfix method support + private bool _isHotfixMethod; + /// /// Constructs a new instance of method patcher. /// @@ -96,6 +99,25 @@ private void Init() originalNativeMethodInfo = UnityVersionHandler.Wrap((Il2CppMethodInfo*)(IntPtr)methodField.GetValue(null)); + // HybridCLR hotfix method handling + if (HybridCLRCompat.IsHybridCLRRuntime()) + { + var hybridInfo = HybridCLRCompat.WrapMethodInfo(originalNativeMethodInfo.Pointer); + _isHotfixMethod = hybridInfo?.IsInterpterImpl ?? false; + + if (_isHotfixMethod) + { + if ((hybridInfo?.InterpData ?? IntPtr.Zero) == IntPtr.Zero) + { + Logger.Instance.LogError( + "HybridCLR method {Method} has no interpData - it may have been transformed to native code", + Original.FullDescription()); + } + + // Note: PrepareMethodForDetour is called in DetourTo(). + } + } + // Create a modified native MethodInfo struct, that will point towards the trampoline modifiedNativeMethodInfo = UnityVersionHandler.NewMethod(); Buffer.MemoryCopy(originalNativeMethodInfo.Pointer.ToPointer(), @@ -142,11 +164,26 @@ public override MethodBase DetourTo(MethodBase replacement) var unmanagedDelegate = unmanagedTrampolineMethod.CreateDelegate(unmanagedDelegateType); DelegateCache.Add(unmanagedDelegate); + // HybridCLR: Prepare method for detouring before applying native detour + if (_isHotfixMethod) + { + // PrepareMethodForDetour copies the bridge so we can hook this method independently + HybridCLRCompat.PrepareMethodForDetour(originalNativeMethodInfo.Pointer); + } + nativeDetour = Il2CppInteropRuntime.Instance.DetourProvider.Create(originalNativeMethodInfo.MethodPointer, unmanagedDelegate); nativeDetour.Apply(); modifiedNativeMethodInfo.MethodPointer = nativeDetour.OriginalTrampoline; + // HybridCLR: After detour is applied, restore invoker_method + if (_isHotfixMethod) + { + // Restore invoker_method to original IL interpreter invoker + // This is CRITICAL - without this, calling the original method will fail + HybridCLRCompat.RestoreInvokerMethod(originalNativeMethodInfo.Pointer); + } + // TODO: Add an ILHook for the original unhollowed method to go directly to managedHookedMethod // Right now it goes through three times as much interop conversion as it needs to, when being called from managed side return managedHookedMethod; diff --git a/Il2CppInterop.Runtime/DelegateSupport.cs b/Il2CppInterop.Runtime/DelegateSupport.cs index 5299beffb..af8a69eb8 100644 --- a/Il2CppInterop.Runtime/DelegateSupport.cs +++ b/Il2CppInterop.Runtime/DelegateSupport.cs @@ -9,6 +9,7 @@ using Il2CppInterop.Runtime.Injection; using Il2CppInterop.Runtime.InteropTypes; using Il2CppInterop.Runtime.Runtime; +using Il2CppInterop.Runtime.Runtime.VersionSpecific.MethodInfo; using Microsoft.Extensions.Logging; using Object = Il2CppSystem.Object; using ValueType = Il2CppSystem.ValueType; @@ -282,16 +283,15 @@ private static void LogError(string message) var managedTrampoline = GetOrCreateNativeToManagedTrampoline(signature, nativeDelegateInvokeMethod, managedInvokeMethod); - var methodInfo = UnityVersionHandler.NewMethod(); - methodInfo.MethodPointer = Marshal.GetFunctionPointerForDelegate(managedTrampoline); - methodInfo.ParametersCount = (byte)parameterInfos.Length; - methodInfo.Slot = ushort.MaxValue; - methodInfo.IsMarshalledFromNative = true; + var methodInfo = CreateSyntheticMethodInfo( + Marshal.GetFunctionPointerForDelegate(managedTrampoline), + classTypePtr, + (byte)parameterInfos.Length); var delegateReference = new Il2CppToMonoDelegateReference(@delegate, methodInfo.Pointer); Il2CppSystem.Delegate converted; - if (UnityVersionHandler.MustUseDelegateConstructor) + if (UnityVersionHandler.MustUseDelegateConstructor && HasNativeConstructor(classTypePtr)) { converted = ((TIl2Cpp)Activator.CreateInstance(typeof(TIl2Cpp), delegateReference.Cast(), methodInfo.Pointer)).Cast(); @@ -317,6 +317,68 @@ private static void LogError(string message) return converted.Cast(); } + /// + /// Checks whether the delegate type's .ctor has a native method body. + /// In HybridCLR, hotfix delegate constructors are interpreter methods with no native pointer. + /// + private static unsafe bool HasNativeConstructor(IntPtr classTypePtr) + { + if (!HybridCLRCompat.IsHybridCLRRuntime()) + return true; + + var iter = IntPtr.Zero; + IntPtr method; + while ((method = IL2CPP.il2cpp_class_get_methods(classTypePtr, ref iter)) != IntPtr.Zero) + { + if (IL2CPP.il2cpp_method_get_name_(method) != ".ctor") + continue; + + var hybridInfo = UnityVersionHandler.WrapHybridCLR((Il2CppMethodInfo*)method); + if (hybridInfo != null && hybridInfo.IsInterpterImpl) + return false; + + return true; + } + + return false; + } + + /// + /// Creates a synthetic Il2CppMethodInfo with enough space for HybridCLR extension fields. + /// In HybridCLR, the interpreter reads methodPointerCallByInterp past the standard struct. + /// + private static unsafe INativeMethodInfoStruct CreateSyntheticMethodInfo( + IntPtr methodPointer, IntPtr classTypePtr, byte parametersCount) + { + var baseSize = UnityVersionHandler.MethodSize(); + INativeMethodInfoStruct methodInfo; + + if (HybridCLRCompat.IsHybridCLRRuntime()) + { + var extendedSize = baseSize + IntPtr.Size * 3 + 2; + var ptr = Marshal.AllocHGlobal(extendedSize); + new Span((void*)ptr, extendedSize).Clear(); + methodInfo = UnityVersionHandler.Wrap((Il2CppMethodInfo*)ptr); + + var hybridInfo = UnityVersionHandler.WrapHybridCLR(methodInfo); + hybridInfo.MethodPointerCallByInterp = methodPointer; + hybridInfo.VirtualMethodPointerCallByInterp = methodPointer; + hybridInfo.InitInterpCallMethodPointer = true; + } + else + { + methodInfo = UnityVersionHandler.NewMethod(); + } + + methodInfo.MethodPointer = methodPointer; + methodInfo.Class = (Il2CppClass*)classTypePtr; + methodInfo.ParametersCount = parametersCount; + methodInfo.Slot = ushort.MaxValue; + methodInfo.IsMarshalledFromNative = true; + + return methodInfo; + } + internal class MethodSignature : IEquatable { public readonly bool ConstructedFromNative; diff --git a/Il2CppInterop.Runtime/IL2CPP.cs b/Il2CppInterop.Runtime/IL2CPP.cs index 0cb5acac7..024cddca3 100644 --- a/Il2CppInterop.Runtime/IL2CPP.cs +++ b/Il2CppInterop.Runtime/IL2CPP.cs @@ -21,6 +21,15 @@ public static unsafe class IL2CPP private static readonly Dictionary ourImagesMap = new(); static IL2CPP() + { + RefreshImageCache(); + } + + /// + /// Refreshes the IL2CPP image cache to detect newly loaded assemblies (e.g., HybridCLR hotfix assemblies). + /// Call this after HybridCLR loads hotfix assemblies. + /// + public static void RefreshImageCache() { var domain = il2cpp_domain_get(); if (domain == IntPtr.Zero) @@ -31,11 +40,22 @@ static IL2CPP() uint assembliesCount = 0; var assemblies = il2cpp_domain_get_assemblies(domain, ref assembliesCount); + var newCount = 0; for (var i = 0; i < assembliesCount; i++) { var image = il2cpp_assembly_get_image(assemblies[i]); var name = il2cpp_image_get_name_(image)!; - ourImagesMap[name] = image; + if (!ourImagesMap.ContainsKey(name)) + { + ourImagesMap[name] = image; + newCount++; + Logger.Instance.LogInformation("Registered IL2CPP image: {ImageName}", name); + } + } + + if (newCount > 0) + { + Logger.Instance.LogInformation("Refreshed IL2CPP image cache: {NewCount} new assemblies, {TotalCount} total", newCount, ourImagesMap.Count); } } diff --git a/Il2CppInterop.Runtime/Injection/Hooks/MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.cs b/Il2CppInterop.Runtime/Injection/Hooks/MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.cs index 943a9ad0c..bddb4edc5 100644 --- a/Il2CppInterop.Runtime/Injection/Hooks/MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.cs +++ b/Il2CppInterop.Runtime/Injection/Hooks/MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook.cs @@ -18,8 +18,30 @@ internal unsafe class MetadataCache_GetTypeInfoFromTypeDefinitionIndex_Hook : [UnmanagedFunctionPointer(CallingConvention.Cdecl)] internal delegate Il2CppClass* MethodDelegate(int index); + // [HybridCLR] Mid-function hook signature at +08: test ecx, 0xf0000000; jne ...; mov rax, [rip+...] + // We hook at +08 to avoid short jump corruption at function start + // Extended signature includes the mov instruction to uniquely identify GetTypeInfoFromTypeDefinitionIndex + private static readonly MemoryUtils.SignatureDefinition[] s_HybridCLRSignatures = + { + new MemoryUtils.SignatureDefinition + { + // test ecx, 0xf0000000; jne rel32; mov rax, [rip+...] + // The ?? are wildcards for the relative jump offset + pattern = "\xF7\xC1\x00\x00\x00\xF0\x0F\x85\x00\x00\x00\x00\x48\x8B\x05", + mask = "xxxxxxxx????xxx", + xref = false + } + }; + + // [HybridCLR] Flag indicating mid-function hook is active + private bool _isMidFunctionHook = false; + private Il2CppClass* Hook(int index) { + // [HybridCLR] Handle -1 check when using mid-function hook + if (_isMidFunctionHook && index == -1) + return null; + if (InjectorHelpers.s_InjectedClasses.TryGetValue(index, out IntPtr classPtr)) return (Il2CppClass*)classPtr; @@ -116,6 +138,23 @@ private IntPtr FindGetTypeInfoFromTypeDefinitionIndex(bool forceICallMethod = fa public override IntPtr FindTargetMethod() { + // [HybridCLR] Use mid-function hook to avoid short jump corruption at function start + if (HybridCLRCompat.IsHybridCLRRuntime()) + { + Logger.Instance.LogTrace("HybridCLR detected, using mid-function hook strategy"); + var hookAddress = s_HybridCLRSignatures + .Select(s => MemoryUtils.FindSignatureInModule(InjectorHelpers.Il2CppModule, s)) + .FirstOrDefault(p => p != 0); + + if (hookAddress != 0) + { + _isMidFunctionHook = true; + Logger.Instance.LogTrace("Found via mid-function signature: 0x{Address:X}", (long)hookAddress); + return hookAddress; + } + Logger.Instance.LogTrace("HybridCLR signature not found, falling back to xref method"); + } + return FindGetTypeInfoFromTypeDefinitionIndex(); } } diff --git a/Il2CppInterop.Runtime/Injection/HybridCLRCompat.cs b/Il2CppInterop.Runtime/Injection/HybridCLRCompat.cs new file mode 100644 index 000000000..2c85cfe93 --- /dev/null +++ b/Il2CppInterop.Runtime/Injection/HybridCLRCompat.cs @@ -0,0 +1,657 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using Iced.Intel; +using Il2CppInterop.Common; +using Il2CppInterop.Runtime.Runtime; +using Il2CppInterop.Runtime.Runtime.VersionSpecific.MethodInfo; +using Microsoft.Extensions.Logging; + +namespace Il2CppInterop.Runtime.Injection +{ + /// + /// Compatibility layer for HybridCLR-modified IL2CPP runtimes. + /// Provides APIs for detecting HybridCLR runtime and preparing interpreter methods for detouring. + /// + public static partial class HybridCLRCompat + { + /// + /// Standard subdirectory name for hotfix interop assemblies. + /// Output path should be: {InteropDir}/{HotfixInteropSubdir} + /// e.g., BepInEx/interop/hotfix/ + /// + public const string HotfixInteropSubdir = "hotfix"; + + /// + /// When true, uses the legacy HybridCLR MethodInfo layout where bool fields + /// (initInterpCallMethodPointer, isInterpterImpl) are stored as independent bytes + /// AFTER the three pointer fields. + /// + /// When false (default), uses the newer layout where these bools are stored as + /// bit fields (bits 5-6) in the _bitfield0 byte of the standard MethodInfo struct. + /// + /// This is auto-detected when first encountering an interpreter method. + /// You can also set it manually before any HybridCLR method operations. + /// + public static bool UseLegacyMethodInfoLayout + { + get => s_UseLegacyLayout; + set + { + s_UseLegacyLayout = value; + s_LayoutDetected = true; + } + } + + private static bool s_UseLegacyLayout = false; + private static bool s_LayoutDetected = false; + + /// + /// Attempts to detect the layout from a specific method. + /// Only works reliably with interpreter methods (isInterpterImpl = true). + /// For non-interpreter methods, detection will be inconclusive. + /// Modders can manually set UseLegacyMethodInfoLayout before any HybridCLR operations. + /// + public static unsafe void DetectLayoutFromMethod(IntPtr methodInfoPtr) + { + if (s_LayoutDetected || methodInfoPtr == IntPtr.Zero) + return; + + var result = ProbeMethodInfoLayout(methodInfoPtr); + if (result.HasValue) + { + s_UseLegacyLayout = result.Value; + s_LayoutDetected = true; + Logger.Instance.LogInformation( + "HybridCLR MethodInfo layout detected: {Layout}", + s_UseLegacyLayout ? "legacy (bool after pointers)" : "new (bitfield)"); + } + // If inconclusive, don't log - the method might not be an interpreter method + } + + private static bool? s_IsHybridCLR; + + public static bool IsHybridCLRRuntime() + { + if (s_IsHybridCLR.HasValue) + return s_IsHybridCLR.Value; + + s_IsHybridCLR = DetectHybridCLR(); + if (s_IsHybridCLR.Value) + Logger.Instance.LogWarning("HybridCLR runtime detected - using compatibility mode"); + + return s_IsHybridCLR.Value; + } + + // All known HybridCLR RuntimeApi internal calls (from RuntimeApi.cpp) + private static readonly string[] s_HybridCLRIcalls = + { + "HybridCLR.RuntimeApi::LoadMetadataForAOTAssembly(System.Byte[],HybridCLR.HomologousImageMode)", + "HybridCLR.RuntimeApi::GetRuntimeOption(HybridCLR.RuntimeOptionId)", + "HybridCLR.RuntimeApi::SetRuntimeOption(HybridCLR.RuntimeOptionId,System.Int32)", + "HybridCLR.RuntimeApi::PreJitClass(System.Type)", + "HybridCLR.RuntimeApi::PreJitMethod(System.Reflection.MethodInfo)", + }; + + private static bool DetectHybridCLR() + { + try + { + foreach (var icall in s_HybridCLRIcalls) + { + var icallPtr = IL2CPP.il2cpp_resolve_icall(icall); + if (icallPtr != IntPtr.Zero) + { + Logger.Instance.LogTrace("HybridCLR detected via icall: {Icall}", icall); + return true; + } + } + return false; + } + catch + { + return false; + } + } + + /// + /// Probes a method to determine which layout is in use. + /// Returns true for legacy layout, false for new layout, null if inconclusive. + /// + private static unsafe bool? ProbeMethodInfoLayout(IntPtr methodInfoPtr) + { + if (methodInfoPtr == IntPtr.Zero) + return null; + + byte* ptr = (byte*)methodInfoPtr; + + // Get bitfield offset using ParametersCount address (NOT MethodSize() - 1, which includes padding) + // MethodSize() = 0x58 (with padding), but actual bitfield0 is at 0x53 + var wrapper = UnityVersionHandler.Wrap((Il2CppMethodInfo*)methodInfoPtr); + int bitfieldOffset; + fixed (byte* pCount = &wrapper.ParametersCount) + { + bitfieldOffset = (int)(pCount - ptr) + 1; + } + + // Extension fields start at MethodSize() (after padding) + int extensionStart = UnityVersionHandler.MethodSize(); + + byte bitfield0 = *(ptr + bitfieldOffset); + bool newLayoutValue = (bitfield0 & 0x40) != 0; + + // LEGACY layout: isInterpterImpl is a byte at extensionStart + IntPtr.Size * 3 + 1 + byte legacyValue = *(ptr + extensionStart + IntPtr.Size * 3 + 1); + bool legacyLayoutValue = legacyValue != 0; + + Logger.Instance.LogDebug( + "ProbeMethodInfoLayout: bitfieldOffset=0x{BitfieldOffset:X}, extensionStart=0x{ExtStart:X}, bitfield0=0x{Bitfield:X2}, legacyByte=0x{Legacy:X2}", + bitfieldOffset, extensionStart, bitfield0, legacyValue); + + // Detection logic: + // - In NEW layout: bits 5-6 of bitfield0 are used by HybridCLR + // - bit 5 = initInterpCallMethodPointer + // - bit 6 = isInterpterImpl + // - In LEGACY layout: bits 5-6 of bitfield0 are unused (should be 0) + // - isInterpterImpl is a separate byte (0 or 1) after the pointers + // + // Standard il2cpp does NOT use bits 5-6 of bitfield0. + + bool bit6Set = (bitfield0 & 0x40) != 0; + bool legacyIsValidBool = legacyValue == 0 || legacyValue == 1; + + // Case 1: bit 6 is set -> must be NEW layout (standard il2cpp doesn't use bit 6) + if (bit6Set) + { + return false; // false = new layout + } + + // Case 2: bit 6 is NOT set, legacy position has valid bool = 1 -> LEGACY layout + if (!bit6Set && legacyIsValidBool && legacyValue == 1) + { + return true; // true = legacy layout + } + + // Case 3: bit 6 is NOT set, legacy position is 0 -> inconclusive (method might not be interpreter) + // Case 4: bit 6 is NOT set, legacy position is garbage -> inconclusive + return null; + } + + #region HybridCLR Method Detour Support + + // Cache for prepared methods: methodInfoPtr -> original invoker_method + private static readonly Dictionary s_PreparedMethods = new(); + private static readonly object s_PreparedMethodsLock = new(); + + /// + /// Wraps a MethodInfo pointer with HybridCLR extension support. + /// Returns null if the pointer is invalid. + /// + public static unsafe IHybridCLRMethodInfoStruct WrapMethodInfo(IntPtr methodInfoPtr) + { + if (methodInfoPtr == IntPtr.Zero) + return null; + return UnityVersionHandler.WrapHybridCLR((Il2CppMethodInfo*)methodInfoPtr); + } + + /// + /// Check if a method is implemented by the HybridCLR interpreter using native pointer. + /// + public static bool IsInterpreterMethod(IntPtr methodInfoPtr) + { + if (!IsHybridCLRRuntime() || methodInfoPtr == IntPtr.Zero) + return false; + + DetectLayoutFromMethod(methodInfoPtr); + var methodInfo = WrapMethodInfo(methodInfoPtr); + return methodInfo?.IsInterpterImpl ?? false; + } + + /// + /// Prepares a HybridCLR interpreter method for detouring by copying its bridge code + /// to a new memory location. + /// + /// + /// Why this is needed:
+ /// HybridCLR interpreter methods share bridge functions across multiple methods. + /// To hook a specific method independently, we must: + /// + /// Copy the bridge function to a new memory location (for calling original) + /// Update methodPointer/virtualMethodPointer to the copied bridge + /// Preserve invoker_method for calling original code + /// + ///
+ /// + /// + /// What this method does:
+ /// + /// Copies bridge code to nearby memory (within +/-2GB for rel32 calls) + /// Sets methodPointer/virtualMethodPointer to copied bridge + /// Caches original invoker_method (restore with ) + /// + ///
+ ///
+ /// Pointer to the Il2CppMethodInfo structure. + /// + /// true if the method was prepared successfully; + /// false if not a HybridCLR runtime, method is not an interpreter method, or preparation failed. + /// + /// + /// After calling this method, you MUST call to restore + /// the original invoker_method. Without this, calling the original method will fail. + /// + public static unsafe bool PrepareMethodForDetour(IntPtr methodInfoPtr) + { + Logger.Instance.LogDebug( + "PrepareMethodForDetour called: methodPtr=0x{MethodPtr:X}, isHybridCLR={IsHybridCLR}", + (ulong)methodInfoPtr, IsHybridCLRRuntime()); + + if (!IsHybridCLRRuntime() || methodInfoPtr == IntPtr.Zero) + return false; + + // Check if already prepared + lock (s_PreparedMethodsLock) + { + if (s_PreparedMethods.ContainsKey(methodInfoPtr)) + { + Logger.Instance.LogTrace("HybridCLR method already prepared: 0x{MethodInfo:X}", (ulong)methodInfoPtr); + return true; + } + } + + // Try to auto-detect layout if not yet detected + if (!s_LayoutDetected) + DetectLayoutFromMethod(methodInfoPtr); + + var methodInfo = WrapMethodInfo(methodInfoPtr); + if (methodInfo == null || !methodInfo.IsInterpterImpl) + return false; + + try + { + // Get current method pointer (bridge function) + IntPtr originalMethodPointer = methodInfo.MethodPointer; + + if (originalMethodPointer == IntPtr.Zero) + { + Logger.Instance.LogWarning("HybridCLR method has null methodPointer"); + return false; + } + + // Save original invoker_method - CRITICAL for calling original code later + IntPtr originalInvokerMethod = methodInfo.InvokerMethod; + + Logger.Instance.LogDebug( + "HybridCLR PrepareMethodForDetour: methodInfo=0x{MethodInfo:X}, methodPointer=0x{MethodPointer:X}, invoker=0x{Invoker:X}", + (ulong)methodInfoPtr, (ulong)originalMethodPointer, (ulong)originalInvokerMethod); + + // Calculate bridge function length using disassembly + int methodLength = GetMethodLength(originalMethodPointer); + if (methodLength <= 0) + { + Logger.Instance.LogWarning("Failed to determine HybridCLR bridge length at 0x{Address:X}", (ulong)originalMethodPointer); + return false; + } + + Logger.Instance.LogDebug("HybridCLR bridge length: {Length} bytes", methodLength); + + // Allocate new executable memory NEAR the original code (within ±2GB for rel32 calls) + IntPtr newCode = AllocateExecutableMemoryNear(originalMethodPointer, methodLength); + if (newCode == IntPtr.Zero) + { + Logger.Instance.LogWarning("Failed to allocate executable memory for HybridCLR method"); + return false; + } + + // Copy the original bridge code + Buffer.MemoryCopy((void*)originalMethodPointer, (void*)newCode, methodLength, methodLength); + + // Fix relative instructions in the copied code. + // The bridge contains rel32 branches and RIP-relative memory operands + // encoded as offsets from the instruction address. After copying to a new + // location, those offsets point to wrong addresses and must be adjusted. + RelocateRelativeBranches(newCode, originalMethodPointer, methodLength); + + // Update standard method pointers to point to the new (copied) bridge code + // This ensures calling the "original" method goes through the copied bridge + methodInfo.MethodPointer = newCode; + methodInfo.VirtualMethodPointer = newCode; + + // Cache the original invoker_method for later restoration + lock (s_PreparedMethodsLock) + { + s_PreparedMethods[methodInfoPtr] = originalInvokerMethod; + } + + Logger.Instance.LogInformation( + "HybridCLR method prepared for detour: 0x{Original:X} -> 0x{New:X} (length: {Length})", + (ulong)originalMethodPointer, (ulong)newCode, methodLength); + + return true; + } + catch (Exception ex) + { + Logger.Instance.LogWarning("Failed to prepare HybridCLR method for detour: {Error}", ex.Message); + return false; + } + } + + /// + /// Restores the original invoker_method after detour is applied. + /// This is CRITICAL - without this, calling the original method will fail. + /// + public static unsafe void RestoreInvokerMethod(IntPtr methodInfoPtr) + { + if (methodInfoPtr == IntPtr.Zero) + return; + + IntPtr originalInvoker; + lock (s_PreparedMethodsLock) + { + if (!s_PreparedMethods.TryGetValue(methodInfoPtr, out originalInvoker)) + return; + } + + try + { + var methodInfo = WrapMethodInfo(methodInfoPtr); + if (methodInfo != null) + { + methodInfo.InvokerMethod = originalInvoker; + Logger.Instance.LogTrace("HybridCLR invoker_method restored: 0x{Invoker:X}", (ulong)originalInvoker); + } + } + catch (Exception ex) + { + Logger.Instance.LogWarning("Failed to restore HybridCLR invoker_method: {Error}", ex.Message); + } + } + + /// + /// Determines the length of a method by disassembling until int3 padding or ret is found. + /// + private static unsafe int GetMethodLength(IntPtr codeStart, int maxLength = 0x10000) + { + if (codeStart == IntPtr.Zero) + return 0; + + try + { + var stream = new UnmanagedMemoryStream((byte*)codeStart, maxLength, maxLength, FileAccess.Read); + var codeReader = new StreamCodeReader(stream); + var decoder = Decoder.Create(IntPtr.Size * 8, codeReader); + decoder.IP = (ulong)codeStart; + + int length = 0; + bool foundRet = false; + int nopCount = 0; + + while (length < maxLength) + { + decoder.Decode(out var instruction); + if (decoder.LastError == DecoderError.NoMoreBytes) + break; + + // int3 padding indicates end of function + if (instruction.Mnemonic == Mnemonic.Int3) + break; + + // Track consecutive nops after ret - indicates function end + if (foundRet && instruction.Mnemonic == Mnemonic.Nop) + { + nopCount++; + if (nopCount >= 2) // 2+ nops after ret = end of function + break; + } + else + { + nopCount = 0; + } + + length += instruction.Length; + + // Track if we've seen a ret instruction + if (instruction.FlowControl == FlowControl.Return) + foundRet = true; + } + + return length > 0 ? length : 0; + } + catch (Exception ex) + { + Logger.Instance.LogWarning("GetMethodLength failed: {Error}", ex.Message); + return 0; + } + } + + /// + /// Fixes relative instructions in copied bridge code. + /// After memcpy, rel32 branch and RIP-relative memory offsets still point relative + /// to the original location. This method recalculates them to point to the correct + /// absolute targets. + /// + private static unsafe void RelocateRelativeBranches(IntPtr newCode, IntPtr originalCode, int length) + { + var stream = new UnmanagedMemoryStream((byte*)newCode, length, length, FileAccess.Read); + var codeReader = new StreamCodeReader(stream); + var decoder = Decoder.Create(64, codeReader); + decoder.IP = (ulong)newCode; + + long delta = (long)originalCode - (long)newCode; + int branchPatchCount = 0; + int ipRelativePatchCount = 0; + + while (decoder.IP < (ulong)newCode + (ulong)length) + { + int instrOffset = (int)(decoder.IP - (ulong)newCode); + decoder.Decode(out var instr); + if (decoder.LastError != DecoderError.None) + break; + var constantOffsets = decoder.GetConstantOffsets(ref instr); + + if (instr.FlowControl == FlowControl.Call || + instr.FlowControl == FlowControl.UnconditionalBranch || + instr.FlowControl == FlowControl.ConditionalBranch) + { + ulong target = instr.NearBranchTarget; + if (target == 0) continue; + + bool isInsideBlock = target >= (ulong)newCode && target < (ulong)newCode + (ulong)length; + if (isInsideBlock) continue; + + ulong originalTarget = (ulong)((long)target + delta); + int instrEnd = instrOffset + instr.Length; + int newRel32 = (int)((long)originalTarget - ((long)newCode + instrEnd)); + int rel32Pos = instrOffset + instr.Length - 4; + + byte* patchAddr = (byte*)newCode + rel32Pos; + *(int*)patchAddr = newRel32; + branchPatchCount++; + + Logger.Instance.LogTrace( + "RelocateRelativeBranches: patched {Mnemonic} at +0x{Offset:X}: 0x{OldTarget:X} -> 0x{NewTarget:X}", + instr.Mnemonic, instrOffset, target, originalTarget); + } + + if (instr.IsIPRelativeMemoryOperand && + constantOffsets.HasDisplacement && + constantOffsets.DisplacementSize == 4) + { + ulong copiedTarget = instr.IPRelativeMemoryAddress; + bool isInsideBlock = copiedTarget >= (ulong)newCode && copiedTarget < (ulong)newCode + (ulong)length; + if (isInsideBlock) + continue; + + ulong originalTarget = (ulong)((long)copiedTarget + delta); + long newDisplacement = (long)originalTarget - ((long)newCode + instrOffset + instr.Length); + if (newDisplacement < int.MinValue || newDisplacement > int.MaxValue) + { + // We fail there to prevent the illegal instruction from being executed, which would crash the process. + throw new InvalidOperationException( + $"Cannot patch RIP-relative {instr.Mnemonic} at +0x{instrOffset:X}: target 0x{originalTarget:X} out of rel32 range"); + } + + byte* patchAddr = (byte*)newCode + instrOffset + constantOffsets.DisplacementOffset; + *(int*)patchAddr = (int)newDisplacement; + ipRelativePatchCount++; + + Logger.Instance.LogTrace( + "RelocateRelativeBranches: patched RIP-relative {Mnemonic} at +0x{Offset:X}: 0x{OldTarget:X} -> 0x{NewTarget:X}", + instr.Mnemonic, instrOffset, copiedTarget, originalTarget); + } + } + + int patchCount = branchPatchCount + ipRelativePatchCount; + if (patchCount > 0) + Logger.Instance.LogInformation( + "RelocateRelativeBranches: fixed {BranchCount} branch(es) and {IpRelativeCount} RIP-relative operand(s) in {Length} bytes", + branchPatchCount, ipRelativePatchCount, length); + } + + /// + /// Allocates executable memory near a target address (within +/-2GB for rel32 addressing). + /// This is critical for HybridCLR bridge duplication - bridge code contains relative calls + /// that will break if the copy is too far from the original. + /// + private static IntPtr AllocateExecutableMemoryNear(IntPtr targetAddress, int size) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return AllocateExecutableMemoryNearWindows(targetAddress, size); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // EXPERIMENTAL: Linux/macOS support via mmap - not yet tested in production + return AllocateExecutableMemoryUnix(size); + } + + Logger.Instance.LogWarning("Unsupported platform for executable memory allocation"); + return IntPtr.Zero; + } + + private static IntPtr AllocateExecutableMemoryNearWindows(IntPtr targetAddress, int size) + { + // Get system allocation granularity (typically 64KB on Windows) + int allocGranularity = GetAllocationGranularity(); + uint allocSize = (uint)((size + allocGranularity - 1) & ~(allocGranularity - 1)); + if (allocSize < allocGranularity) + allocSize = (uint)allocGranularity; + + // If no target address, use simple allocation + if (targetAddress == IntPtr.Zero) + { + return VirtualAlloc(IntPtr.Zero, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + } + + // Search for free memory within ±2GB of target address + // rel32 range is ±2GB, but we use a smaller range for safety + const long searchRange = 0x7FFF0000; // ~2GB + long targetAddr = targetAddress.ToInt64(); + long minAddr = Math.Max(targetAddr - searchRange, 0x10000); // Don't go below 64KB + long maxAddr = targetAddr + searchRange; + + // Try to allocate at addresses below the target first (usually more space available) + long searchStart = (targetAddr - allocGranularity) & ~(allocGranularity - 1); + + // Search downward + for (long addr = searchStart; addr >= minAddr; addr -= allocGranularity) + { + IntPtr result = VirtualAlloc(new IntPtr(addr), allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + if (result != IntPtr.Zero) + { + Logger.Instance.LogDebug( + "Allocated executable memory near 0x{Target:X} at 0x{Allocated:X} (delta: {Delta})", + (ulong)targetAddress, (ulong)result, result.ToInt64() - targetAddr); + return result; + } + } + + // Search upward + searchStart = (targetAddr + allocGranularity) & ~(allocGranularity - 1); + for (long addr = searchStart; addr <= maxAddr; addr += allocGranularity) + { + IntPtr result = VirtualAlloc(new IntPtr(addr), allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + if (result != IntPtr.Zero) + { + Logger.Instance.LogDebug( + "Allocated executable memory near 0x{Target:X} at 0x{Allocated:X} (delta: {Delta})", + (ulong)targetAddress, (ulong)result, result.ToInt64() - targetAddr); + return result; + } + } + + // Fallback: let system choose address (may be too far for rel32) + Logger.Instance.LogWarning( + "Could not allocate memory near 0x{Target:X}, falling back to system allocation", + (ulong)targetAddress); + return VirtualAlloc(IntPtr.Zero, allocSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + } + + /// + /// EXPERIMENTAL: Allocates executable memory on Linux/macOS using mmap. + /// This implementation has not been tested in production environments. + /// + private static IntPtr AllocateExecutableMemoryUnix(int size) + { + // mmap with PROT_READ | PROT_WRITE | PROT_EXEC and MAP_PRIVATE | MAP_ANONYMOUS + IntPtr result = mmap(IntPtr.Zero, (UIntPtr)size, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (result == MAP_FAILED) + { + Logger.Instance.LogWarning("mmap failed for executable memory allocation"); + return IntPtr.Zero; + } + + return result; + } + + private static int GetAllocationGranularity() + { + GetSystemInfo(out var sysInfo); + return (int)sysInfo.dwAllocationGranularity; + } + + // Windows API + private const uint MEM_COMMIT = 0x1000; + private const uint MEM_RESERVE = 0x2000; + private const uint PAGE_EXECUTE_READWRITE = 0x40; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect); + + [DllImport("kernel32.dll")] + private static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); + + [StructLayout(LayoutKind.Sequential)] + private struct SYSTEM_INFO + { + public ushort wProcessorArchitecture; + public ushort wReserved; + public uint dwPageSize; + public IntPtr lpMinimumApplicationAddress; + public IntPtr lpMaximumApplicationAddress; + public IntPtr dwActiveProcessorMask; + public uint dwNumberOfProcessors; + public uint dwProcessorType; + public uint dwAllocationGranularity; + public ushort wProcessorLevel; + public ushort wProcessorRevision; + } + + // Unix/POSIX API (Linux/macOS) + private const int PROT_READ = 0x1; + private const int PROT_WRITE = 0x2; + private const int PROT_EXEC = 0x4; + private const int MAP_PRIVATE = 0x02; + private const int MAP_ANONYMOUS_LINUX = 0x20; + private const int MAP_ANONYMOUS_OSX = 0x1000; + private static int MAP_ANONYMOUS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? MAP_ANONYMOUS_OSX : MAP_ANONYMOUS_LINUX; + private static readonly IntPtr MAP_FAILED = new(-1); + + [DllImport("libc", SetLastError = true)] + private static extern IntPtr mmap(IntPtr addr, UIntPtr length, int prot, int flags, int fd, long offset); + + #endregion + } +} diff --git a/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs b/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs index 0e4e1f7d0..9a82de975 100644 --- a/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs +++ b/Il2CppInterop.Runtime/Runtime/UnityVersionHandler.cs @@ -253,6 +253,27 @@ public static unsafe INativeMethodInfoStruct Wrap(Il2CppMethodInfo* methodPointe return methodInfoStructHandler.Wrap(methodPointer); } + /// + /// Wraps a MethodInfo pointer with HybridCLR extension support. + /// Returns an IHybridCLRMethodInfoStruct that provides access to HybridCLR-specific fields. + /// + public static unsafe IHybridCLRMethodInfoStruct WrapHybridCLR(Il2CppMethodInfo* methodPointer) + { + var inner = methodInfoStructHandler.Wrap(methodPointer); + if (inner == null) return null; + return new HybridCLRMethodInfoWrapper(inner, methodInfoStructHandler.Size()); + } + + /// + /// Wraps an existing INativeMethodInfoStruct with HybridCLR extension support. + /// + public static IHybridCLRMethodInfoStruct WrapHybridCLR(INativeMethodInfoStruct methodInfo) + { + if (methodInfo == null) return null; + if (methodInfo is IHybridCLRMethodInfoStruct hybridCLR) return hybridCLR; + return new HybridCLRMethodInfoWrapper(methodInfo, methodInfoStructHandler.Size()); + } + public static int MethodSize() { return methodInfoStructHandler.Size(); diff --git a/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/HybridCLRMethodInfoWrapper.cs b/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/HybridCLRMethodInfoWrapper.cs new file mode 100644 index 000000000..2fb3923a2 --- /dev/null +++ b/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/HybridCLRMethodInfoWrapper.cs @@ -0,0 +1,145 @@ +using System; +using Il2CppInterop.Runtime.Injection; + +namespace Il2CppInterop.Runtime.Runtime.VersionSpecific.MethodInfo; + +/// +/// Wraps an existing INativeMethodInfoStruct to provide access to HybridCLR-specific fields. +/// This wrapper dynamically calculates field offsets based on the base MethodInfo size. +/// +/// HybridCLR has two different MethodInfo extension layouts: +/// +/// NEW layout (default, newer il2cpp_plus branches): +/// - initInterpCallMethodPointer: bit 5 of _bitfield0 (last byte of standard MethodInfo) +/// - isInterpterImpl: bit 6 of _bitfield0 +/// - void* interpData (at baseSize) +/// - void* methodPointerCallByInterp +/// - void* virtualMethodPointerCallByInterp +/// +/// LEGACY layout (older il2cpp_plus branches, set HybridCLRCompat.UseLegacyMethodInfoLayout = true): +/// - void* interpData (at baseSize) +/// - void* methodPointerCallByInterp +/// - void* virtualMethodPointerCallByInterp +/// - bool initInterpCallMethodPointer (1 byte) +/// - bool isInterpterImpl (1 byte) +/// +public unsafe class HybridCLRMethodInfoWrapper : IHybridCLRMethodInfoStruct +{ + private readonly INativeMethodInfoStruct _inner; + private readonly int _baseSize; + + // Bit positions within _bitfield0 (the last byte of standard MethodInfo) - NEW layout only + private const byte InitInterpCallMethodPointerBit = 0x20; // bit 5 + private const byte IsInterpterImplBit = 0x40; // bit 6 + + /// + /// Creates a HybridCLR wrapper around an existing MethodInfo struct. + /// + /// The base MethodInfo struct to wrap. + /// The size of the base MethodInfo struct (without HybridCLR extensions). + public HybridCLRMethodInfoWrapper(INativeMethodInfoStruct inner, int baseSize) + { + _inner = inner ?? throw new ArgumentNullException(nameof(inner)); + _baseSize = baseSize; + } + + private bool UseLegacyLayout => HybridCLRCompat.UseLegacyMethodInfoLayout; + + // Delegate all standard INativeMethodInfoStruct properties to inner + public IntPtr Pointer => _inner.Pointer; + public Il2CppMethodInfo* MethodInfoPointer => _inner.MethodInfoPointer; + public ref IntPtr Name => ref _inner.Name; + public ref ushort Slot => ref _inner.Slot; + public ref IntPtr MethodPointer => ref _inner.MethodPointer; + public ref IntPtr VirtualMethodPointer => ref _inner.VirtualMethodPointer; + public ref Il2CppClass* Class => ref _inner.Class; + public ref IntPtr InvokerMethod => ref _inner.InvokerMethod; + public ref Il2CppTypeStruct* ReturnType => ref _inner.ReturnType; + public ref Il2CppMethodFlags Flags => ref _inner.Flags; + public ref byte ParametersCount => ref _inner.ParametersCount; + public ref Il2CppParameterInfo* Parameters => ref _inner.Parameters; + public ref uint Token => ref _inner.Token; + public bool IsGeneric { get => _inner.IsGeneric; set => _inner.IsGeneric = value; } + public bool IsInflated { get => _inner.IsInflated; set => _inner.IsInflated = value; } + public bool IsMarshalledFromNative { get => _inner.IsMarshalledFromNative; set => _inner.IsMarshalledFromNative = value; } + + // HybridCLR-specific fields + + // _bitfield0 is right after parameters_count in the MethodInfo struct. + // We MUST use ParametersCount address to find it, because MethodSize() includes padding. + // MethodSize() = 0x58 (with padding), but actual bitfield0 is at 0x53. + private byte* Bitfield0Ptr + { + get + { + fixed (byte* pCount = &_inner.ParametersCount) + { + return pCount + 1; // _bitfield0 is right after parameters_count + } + } + } + + // HybridCLR extension fields start at MethodSize() (after padding). + // This is correct because HybridCLR adds fields after the standard struct. + private int ExtensionFieldsStart => _baseSize; + + private IntPtr* InterpDataPtr => (IntPtr*)((byte*)Pointer + ExtensionFieldsStart); + private IntPtr* MethodPointerCallByInterpPtr => (IntPtr*)((byte*)Pointer + ExtensionFieldsStart + IntPtr.Size); + private IntPtr* VirtualMethodPointerCallByInterpPtr => (IntPtr*)((byte*)Pointer + ExtensionFieldsStart + IntPtr.Size * 2); + + // LEGACY layout: bool fields are after the 3 pointers + private byte* LegacyInitInterpCallMethodPointerPtr => (byte*)Pointer + ExtensionFieldsStart + IntPtr.Size * 3; + private byte* LegacyIsInterpterImplPtr => (byte*)Pointer + ExtensionFieldsStart + IntPtr.Size * 3 + 1; + + public bool IsInterpterImpl + { + get + { + if (UseLegacyLayout) + return *LegacyIsInterpterImplPtr != 0; + return (*Bitfield0Ptr & IsInterpterImplBit) != 0; + } + set + { + if (UseLegacyLayout) + { + *LegacyIsInterpterImplPtr = value ? (byte)1 : (byte)0; + } + else + { + if (value) + *Bitfield0Ptr |= IsInterpterImplBit; + else + *Bitfield0Ptr = (byte)(*Bitfield0Ptr & ~IsInterpterImplBit); + } + } + } + + public bool InitInterpCallMethodPointer + { + get + { + if (UseLegacyLayout) + return *LegacyInitInterpCallMethodPointerPtr != 0; + return (*Bitfield0Ptr & InitInterpCallMethodPointerBit) != 0; + } + set + { + if (UseLegacyLayout) + { + *LegacyInitInterpCallMethodPointerPtr = value ? (byte)1 : (byte)0; + } + else + { + if (value) + *Bitfield0Ptr |= InitInterpCallMethodPointerBit; + else + *Bitfield0Ptr = (byte)(*Bitfield0Ptr & ~InitInterpCallMethodPointerBit); + } + } + } + + public ref IntPtr InterpData => ref *InterpDataPtr; + public ref IntPtr MethodPointerCallByInterp => ref *MethodPointerCallByInterpPtr; + public ref IntPtr VirtualMethodPointerCallByInterp => ref *VirtualMethodPointerCallByInterpPtr; +} diff --git a/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/Interfaces.cs b/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/Interfaces.cs index 8c5531786..c1bcb860a 100644 --- a/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/Interfaces.cs +++ b/Il2CppInterop.Runtime/Runtime/VersionSpecific/MethodInfo/Interfaces.cs @@ -27,3 +27,48 @@ public interface INativeMethodInfoStruct : INativeStruct bool IsInflated { get; set; } bool IsMarshalledFromNative { get; set; } } + +/// +/// Extended interface for HybridCLR-modified MethodInfo structures. +/// HybridCLR adds additional fields after the standard MethodInfo for interpreter support. +/// +/// There are two different layouts depending on HybridCLR version: +/// +/// NEW layout (default): bool fields are stored as bit flags in _bitfield0 +/// - IsInterpterImpl: bit 6 of _bitfield0 +/// - InitInterpCallMethodPointer: bit 5 of _bitfield0 +/// - Then: interpData, methodPointerCallByInterp, virtualMethodPointerCallByInterp +/// +/// LEGACY layout (set HybridCLRCompat.UseLegacyMethodInfoLayout = true): +/// - interpData, methodPointerCallByInterp, virtualMethodPointerCallByInterp +/// - Then: initInterpCallMethodPointer (1 byte), isInterpterImpl (1 byte) +/// +public interface IHybridCLRMethodInfoStruct : INativeMethodInfoStruct +{ + /// + /// Whether this method is implemented by the HybridCLR interpreter. + /// Location depends on layout: bit 6 of _bitfield0 (new) or byte after pointers (legacy). + /// + bool IsInterpterImpl { get; set; } + + /// + /// Whether the interpreter call method pointer has been initialized. + /// Location depends on layout: bit 5 of _bitfield0 (new) or byte after pointers (legacy). + /// + bool InitInterpCallMethodPointer { get; set; } + + /// + /// Interpreter-specific data pointer. + /// + ref IntPtr InterpData { get; } + + /// + /// Method pointer used when calling from interpreter context. + /// + ref IntPtr MethodPointerCallByInterp { get; } + + /// + /// Virtual method pointer used when calling from interpreter context. + /// + ref IntPtr VirtualMethodPointerCallByInterp { get; } +}