diff --git a/Directory.Build.props b/Directory.Build.props index 42589063..c121dfab 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 1.4.6 + 1.5.1 BepInEx ../bin/NuGet ../bin/$(MSBuildProjectName) diff --git a/Il2CppInterop.Common/Il2CppInterop.Common.csproj b/Il2CppInterop.Common/Il2CppInterop.Common.csproj index 671f18c2..c3c7b441 100644 --- a/Il2CppInterop.Common/Il2CppInterop.Common.csproj +++ b/Il2CppInterop.Common/Il2CppInterop.Common.csproj @@ -8,7 +8,7 @@ - + diff --git a/Il2CppInterop.Common/XrefScans/XrefScannerLowLevel.cs b/Il2CppInterop.Common/XrefScans/XrefScannerLowLevel.cs index 17e5eda6..535734eb 100644 --- a/Il2CppInterop.Common/XrefScans/XrefScannerLowLevel.cs +++ b/Il2CppInterop.Common/XrefScans/XrefScannerLowLevel.cs @@ -13,10 +13,13 @@ public static IEnumerable JumpTargets(IntPtr codeStart, bool ignoreRetn if (instruction.Mnemonic == Arm64Mnemonic.RET && !ignoreRetn) yield break; - var jump = instruction.Mnemonic is Arm64Mnemonic.B or Arm64Mnemonic.BC or Arm64Mnemonic.BR; - var call = instruction.Mnemonic is Arm64Mnemonic.BL or Arm64Mnemonic.BLR; - - if ((jump || call) && instruction.MnemonicConditionCode == Arm64ConditionCode.NONE && instruction.FinalOpConditionCode == Arm64ConditionCode.NONE) + if (instruction is + { + // Check if jump or call instruction + Mnemonic: Arm64Mnemonic.B or Arm64Mnemonic.BC or Arm64Mnemonic.BR or Arm64Mnemonic.BL or Arm64Mnemonic.BLR, + MnemonicConditionCode: Arm64ConditionCode.NONE, + FinalOpConditionCode: Arm64ConditionCode.NONE + }) { var target = XrefScanUtilFinder.ExtractTargetAddress(instruction); yield return (IntPtr)target; diff --git a/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs b/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs index 01a714cf..057a2ac3 100644 --- a/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs +++ b/Il2CppInterop.Generator/Contexts/AssemblyRewriteContext.cs @@ -9,9 +9,6 @@ namespace Il2CppInterop.Generator.Contexts; [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] public class AssemblyRewriteContext { - // TODO: Dispose - private static readonly Dictionary ImportsMap = new(); - public readonly RewriteGlobalContext GlobalContext; public readonly RuntimeAssemblyReferences Imports; @@ -30,7 +27,7 @@ public AssemblyRewriteContext(RewriteGlobalContext globalContext, AssemblyDefini NewAssembly = newAssembly; GlobalContext = globalContext; - Imports = ImportsMap.GetOrCreate(newAssembly.ManifestModule!, + Imports = globalContext.ImportsMap.GetOrCreate(newAssembly.ManifestModule!, mod => new RuntimeAssemblyReferences(mod, globalContext)); } diff --git a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs index a9f390f1..bab812ea 100644 --- a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs +++ b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs @@ -19,6 +19,8 @@ public class RewriteGlobalContext : IDisposable internal readonly Dictionary<(object?, string, int), List> RenameGroups = new(); + internal readonly Dictionary ImportsMap = new(); + public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess gameAssemblies, IMetadataAccess unityAssemblies) { @@ -53,6 +55,7 @@ public RewriteGlobalContext(GeneratorOptions options, IIl2CppMetadataAccess game public IMetadataAccess UnityAssemblies { get; } public IEnumerable Assemblies => myAssemblies.Values; + public AssemblyRewriteContext CorLib => myAssemblies["mscorlib"]; internal bool HasGcWbarrierFieldWrite { get; set; } diff --git a/Il2CppInterop.Generator/Il2CppInterop.Generator.csproj b/Il2CppInterop.Generator/Il2CppInterop.Generator.csproj index 9eee813c..5e27cc0e 100644 --- a/Il2CppInterop.Generator/Il2CppInterop.Generator.csproj +++ b/Il2CppInterop.Generator/Il2CppInterop.Generator.csproj @@ -12,7 +12,7 @@ - + MonoModBackports diff --git a/Il2CppInterop.Generator/Passes/Pass16ScanMethodRefs.cs b/Il2CppInterop.Generator/Passes/Pass16ScanMethodRefs.cs index 9dea948c..f16a3541 100644 --- a/Il2CppInterop.Generator/Passes/Pass16ScanMethodRefs.cs +++ b/Il2CppInterop.Generator/Passes/Pass16ScanMethodRefs.cs @@ -10,8 +10,8 @@ namespace Il2CppInterop.Generator.Passes; public static class Pass16ScanMethodRefs { - public static readonly HashSet NonDeadMethods = new(); - public static IDictionary> MapOfCallers = new Dictionary>(); + internal static HashSet NonDeadMethods = new(); + internal static IDictionary> MapOfCallers = new Dictionary>(); public static void DoPass(RewriteGlobalContext context, GeneratorOptions options) { diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs new file mode 100644 index 00000000..037cdcac --- /dev/null +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -0,0 +1,94 @@ +using System.Runtime.CompilerServices; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Cloning; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using AsmResolver.PE.DotNet.Metadata.Tables; +using Il2CppInterop.Common; +using Il2CppInterop.Generator.Contexts; +using Microsoft.Extensions.Logging; + +namespace Il2CppInterop.Generator.Passes; + +public static class Pass61ImplementAwaiters +{ + public static void DoPass(RewriteGlobalContext context) + { + var corlib = context.CorLib; + + var actionUntyped = corlib.GetTypeByName("System.Action"); + + var actionConversion = actionUntyped.NewType.Methods.Single(m => m.Name == "op_Implicit"); + + foreach (var assemblyContext in context.Assemblies) + { + // Use Lazy as a lazy way to not actually import the references until they're needed + + Lazy actionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(actionConversion.Parameters[0].ParameterType.ToTypeDefOrRef())!); + Lazy actionConversionRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(actionConversion)); + Lazy notifyCompletionRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(INotifyCompletion))); + var voidRef = assemblyContext.NewAssembly.ManifestModule!.CorLibTypeFactory.Void; + + foreach (var typeContext in assemblyContext.Types) + { + // Odds are a majority of types won't implement any interfaces. Skip them to save time. + if (typeContext.OriginalType.IsInterface || typeContext.OriginalType.Interfaces.Count == 0) + continue; + + var iNotifyCompletion = typeof(INotifyCompletion); + var interfaceImplementation = typeContext.OriginalType.Interfaces.SingleOrDefault(interfaceImpl => interfaceImpl.Interface?.Namespace == iNotifyCompletion.Namespace && interfaceImpl.Interface?.Name == iNotifyCompletion.Name); + if (interfaceImplementation is null) + continue; + + var allOnCompleted = typeContext.Methods.Where(m => m.OriginalMethod.Name == nameof(INotifyCompletion.OnCompleted)).Select(mc => mc.NewMethod).ToArray(); + + // Conversion spits out an Il2CppSystem.Action, so look for methods that take that (and only that) in & return void, so the stack is balanced + // And use SignatureComparer because otherwise equality checks would fail due to the TypeSignatures being different references + var interopOnCompleted = allOnCompleted.FirstOrDefault(m => !m.IsStatic && m.Parameters.Count == 1 && m.Signature is not null && SignatureComparer.Default.Equals(m.Signature.ReturnType, voidRef) && SignatureComparer.Default.Equals(m.Signature.ParameterTypes[0], actionConversion.Signature?.ReturnType)); + + if (interopOnCompleted is null) + { + var typeName = typeContext.OriginalType.FullName; + var foundMethodCount = allOnCompleted.Length; + Logger.Instance.LogInformation("Type {typeName} was found to implement INotifyCompletion, but no suitable method was found. {foundMethodCount} method(s) were found with the required name.", typeName, foundMethodCount); + continue; + } + + var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual; + var sig = MethodSignature.CreateInstance(voidRef, [actionUntypedRef.Value.ToTypeSignature()]); + + var proxyOnCompleted = new MethodDefinition(nameof(INotifyCompletion.OnCompleted), onCompletedAttr, sig); + var parameter = proxyOnCompleted.Parameters[0].GetOrCreateDefinition(); + parameter.Name = "continuation"; + + var body = proxyOnCompleted.CilMethodBody ??= new(proxyOnCompleted); + + typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value)); + typeContext.NewType.Methods.Add(proxyOnCompleted); + + var instructions = body.Instructions; + instructions.Add(CilOpCodes.Ldarg_0); // load "this" + instructions.Add(CilOpCodes.Ldarg_1); // not static, so ldarg1 loads "continuation" + instructions.Add(CilOpCodes.Call, actionConversionRef.Value); + + // The titular jump to the interop method -- it's gotta reference the method on the right type, so we need to handle generic parameters + // Without this, awaiters declared in generic types like UniTask.Awaiter would effectively try to cast themselves to their untyped versions (UniTask<>.Awaiter in this case, which isn't a thing) + var genericParameterCount = typeContext.NewType.GenericParameters.Count; + if (genericParameterCount > 0) + { + var typeArguments = Enumerable.Range(0, genericParameterCount).Select(i => new GenericParameterSignature(GenericParameterType.Type, i)).ToArray(); + var interopOnCompleteGeneric = typeContext.NewType.MakeGenericInstanceType(typeArguments) + .ToTypeDefOrRef() + .CreateMemberReference(interopOnCompleted.Name, interopOnCompleted.Signature); + instructions.Add(CilOpCodes.Call, interopOnCompleteGeneric); + } + else + { + instructions.Add(CilOpCodes.Call, interopOnCompleted); + } + + instructions.Add(CilOpCodes.Ret); + } + } + } +} diff --git a/Il2CppInterop.Generator/Passes/Pass70GenerateProperties.cs b/Il2CppInterop.Generator/Passes/Pass70GenerateProperties.cs index 921075f4..e80e7911 100644 --- a/Il2CppInterop.Generator/Passes/Pass70GenerateProperties.cs +++ b/Il2CppInterop.Generator/Passes/Pass70GenerateProperties.cs @@ -42,17 +42,13 @@ public static void DoPass(RewriteGlobalContext context) } string? defaultMemberName = null; - var defaultMemberAttributeAttribute = type.CustomAttributes.FirstOrDefault(it => - it.AttributeType()?.Name == "AttributeAttribute" && it.Signature!.NamedArguments.Any(it => - it.MemberName == "Name" && it.Argument.GetElementAsString() == nameof(DefaultMemberAttribute))); - if (defaultMemberAttributeAttribute != null) + if (type.CustomAttributes.FirstOrDefault(IsDefaultMemberAttributeFake) != null) { defaultMemberName = "Item"; } else { - var realDefaultMemberAttribute = - type.CustomAttributes.FirstOrDefault(it => it.AttributeType()?.Name == nameof(DefaultMemberAttribute)); + var realDefaultMemberAttribute = type.CustomAttributes.FirstOrDefault(IsDefaultMemberAttributeReal); if (realDefaultMemberAttribute != null) defaultMemberName = realDefaultMemberAttribute.Signature?.FixedArguments[0].Element?.ToString() ?? "Item"; } @@ -63,6 +59,21 @@ public static void DoPass(RewriteGlobalContext context) assemblyContext.Imports.Module.DefaultMemberAttribute().ToTypeDefOrRef(), assemblyContext.Imports.Module.String()), new CustomAttributeSignature(new CustomAttributeArgument(assemblyContext.Imports.Module.String(), defaultMemberName)))); } + + static bool IsDefaultMemberAttributeFake(CustomAttribute attribute) + { + return attribute.AttributeType()?.Name == "AttributeAttribute" && attribute.Signature!.NamedArguments.Any(it => + { + // Name support is for backwards compatibility. + return (it.MemberName == "Type" && it.Argument.Element is ITypeDescriptor { Namespace: "System.Reflection", Name: nameof(DefaultMemberAttribute) }) + || (it.MemberName == "Name" && it.Argument.GetElementAsString() == nameof(DefaultMemberAttribute)); + }); + } + + static bool IsDefaultMemberAttributeReal(CustomAttribute attribute) + { + return attribute.AttributeType() is { Namespace.Value: "System.Reflection", Name.Value: nameof(DefaultMemberAttribute) }; + } } private static string UnmanglePropertyName(AssemblyRewriteContext assemblyContext, PropertyDefinition prop, diff --git a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs index 11165cc2..9617dbfd 100644 --- a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs +++ b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs @@ -200,9 +200,12 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth if (resolvedElementType == null) return null; if (resolvedElementType.FullName == "System.String") return imports.Il2CppStringArray; - var genericBase = resolvedElementType.IsValueType - ? imports.Il2CppStructArray - : imports.Il2CppReferenceArray; + var genericBase = resolvedElementType switch + { + GenericParameterSignature => imports.Il2CppArrayBase, + { IsValueType: true } => imports.Il2CppStructArray, + _ => imports.Il2CppReferenceArray + }; return new GenericInstanceTypeSignature(genericBase.ToTypeDefOrRef(), false, resolvedElementType); } diff --git a/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs b/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs index a440c860..d684cdcc 100644 --- a/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs +++ b/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs @@ -42,6 +42,9 @@ public static void DoPass(RewriteGlobalContext context) } } + StuffToProcess.Clear(); + StuffToProcess.Capacity = 0; + Logger.Instance.LogInformation("IL unstrip statistics: {MethodsSucceeded} successful, {MethodsFailed} failed", methodsSucceeded, methodsFailed); } diff --git a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs index b1c66b7c..72d46862 100644 --- a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs +++ b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs @@ -1,4 +1,5 @@ using Il2CppInterop.Common; +using Il2CppInterop.Common.XrefScans; using Il2CppInterop.Generator.Contexts; using Il2CppInterop.Generator.MetadataAccess; using Il2CppInterop.Generator.Passes; @@ -148,6 +149,11 @@ public void Run(GeneratorOptions options) Pass60AddImplicitConversions.DoPass(rewriteContext); } + using (new TimingCookie("Implementing awaiters")) + { + Pass61ImplementAwaiters.DoPass(rewriteContext); + } + using (new TimingCookie("Creating properties")) { Pass70GenerateProperties.DoPass(rewriteContext); @@ -201,6 +207,12 @@ public void Run(GeneratorOptions options) Pass91GenerateMethodPointerMap.DoPass(rewriteContext, options); } + using (new TimingCookie("Clearing static data")) + { + Pass16ScanMethodRefs.MapOfCallers = new Dictionary>(); + Pass16ScanMethodRefs.NonDeadMethods = []; + } + Logger.Instance.LogInformation("Done!"); rewriteContext.Dispose(); diff --git a/Il2CppInterop.Runtime/Injection/ClassInjector.cs b/Il2CppInterop.Runtime/Injection/ClassInjector.cs index 6db08110..befdab6d 100644 --- a/Il2CppInterop.Runtime/Injection/ClassInjector.cs +++ b/Il2CppInterop.Runtime/Injection/ClassInjector.cs @@ -843,7 +843,7 @@ private static Delegate CreateInvoker(MethodInfo monoMethod) body.Emit(OpCodes.Add_Ovf_Un); var nativeType = parameterInfo.ParameterType.NativeType(); body.Emit(OpCodes.Ldobj, typeof(IntPtr)); - if (nativeType != typeof(IntPtr)) + if (nativeType != typeof(IntPtr) && !nativeType.IsByRef) // if it's a byref, we already have the pointer i think? body.Emit(OpCodes.Ldobj, nativeType); } diff --git a/Il2CppInterop.Runtime/Injection/InjectorHelpers.cs b/Il2CppInterop.Runtime/Injection/InjectorHelpers.cs index 9bed22f2..485f9b11 100644 --- a/Il2CppInterop.Runtime/Injection/InjectorHelpers.cs +++ b/Il2CppInterop.Runtime/Injection/InjectorHelpers.cs @@ -149,6 +149,16 @@ private static d_ClassInit FindClassInit() { static nint GetClassInitSubstitute() { + if (TryGetIl2CppExport(nameof(IL2CPP.il2cpp_array_new_specific), out nint arrayNewSpecific)) + { + // https://github.com/ByNameModding/BNM-Android/blob/3edeec43d74fc4392ba1b1eb9d5002e1b2ef2a67/src/Loading.cpp#L296 + var bnmClassInit = XrefScannerLowLevel.JumpTargets(XrefScannerLowLevel.JumpTargets(arrayNewSpecific).First()).First(); + if (bnmClassInit != IntPtr.Zero) + { + Logger.Instance.LogTrace("Used BNM Method to find Class::Init."); + return bnmClassInit; + } + } if (TryGetIl2CppExport("mono_class_instance_size", out nint classInit)) { Logger.Instance.LogTrace("Picked mono_class_instance_size as a Class::Init substitute"); diff --git a/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs b/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs index ef7c40e4..08e09523 100644 --- a/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs +++ b/Il2CppInterop.Runtime/InteropTypes/Il2CppObjectBase.cs @@ -84,8 +84,8 @@ public T Unbox() where T : unmanaged private static readonly Type[] _intPtrTypeArray = { typeof(IntPtr) }; private static readonly MethodInfo _getUninitializedObject = typeof(RuntimeHelpers).GetMethod(nameof(RuntimeHelpers.GetUninitializedObject))!; private static readonly MethodInfo _getTypeFromHandle = typeof(Type).GetMethod(nameof(Type.GetTypeFromHandle))!; - private static readonly MethodInfo _createGCHandle = typeof(Il2CppObjectBase).GetMethod(nameof(CreateGCHandle))!; - private static readonly FieldInfo _isWrapped = typeof(Il2CppObjectBase).GetField(nameof(isWrapped))!; + private static readonly MethodInfo _createGCHandle = typeof(Il2CppObjectBase).GetMethod(nameof(CreateGCHandle), BindingFlags.Instance | BindingFlags.NonPublic)!; + private static readonly FieldInfo _isWrapped = typeof(Il2CppObjectBase).GetField(nameof(isWrapped), BindingFlags.Instance | BindingFlags.NonPublic)!; internal static class InitializerStore { @@ -112,7 +112,7 @@ private static Func Create() // However, it could be be user-made or implicit // In that case we set the GCHandle and then call the ctor and let GC destroy any objects created by DerivedConstructorPointer - // var obj = (T)FormatterServices.GetUninitializedObject(type); + // var obj = (T)RuntimeHelpers.GetUninitializedObject(type); il.Emit(OpCodes.Ldtoken, type); il.Emit(OpCodes.Call, _getTypeFromHandle); il.Emit(OpCodes.Call, _getUninitializedObject); @@ -126,7 +126,7 @@ private static Func Create() // obj.isWrapped = true; il.Emit(OpCodes.Dup); il.Emit(OpCodes.Ldc_I4_1); - il.Emit(OpCodes.Stsfld, _isWrapped); + il.Emit(OpCodes.Stfld, _isWrapped); var parameterlessConstructor = type.GetConstructor(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, Type.EmptyTypes); if (parameterlessConstructor != null)