From d89f9ec9ed1c4efa99e755677fb8767c684066dd Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Mon, 17 Jun 2024 16:49:36 -0700 Subject: [PATCH 1/8] Add awaiter implementation --- .../Passes/Pass61ImplementAwaiters.cs | 59 +++++++++++++++++++ .../Runners/InteropAssemblyGenerator.cs | 5 ++ 2 files changed, 64 insertions(+) create mode 100644 Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs new file mode 100644 index 00000000..fad329a3 --- /dev/null +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using Il2CppInterop.Generator.Contexts; +using Il2CppInterop.Generator.Utils; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Il2CppInterop.Generator.Passes; + +internal class Pass61ImplementAwaiters +{ + public static void DoPass(RewriteGlobalContext context) + { + var corlib = context.GetAssemblyByName("mscorlib"); + var actionUntyped = corlib.GetTypeByName("System.Action"); + + var actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); + + foreach (var assemblyContext in context.Assemblies) + { + // dont actually import the references until they're needed + Lazy actionUntypedRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(actionUntyped.OriginalType)); + Lazy actionConversionUntypedRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(actionConversionUntyped)); + Lazy notifyCompletionRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(typeof(INotifyCompletion))); + var voidRef = assemblyContext.Imports.Module.Void(); + foreach (var typeContext in assemblyContext.Types) + { + var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.InterfaceType.Name == nameof(INotifyCompletion)); + if (interfaceImplementation is null) + continue; + + var isGeneric = typeContext.OriginalType.ContainsGenericParameter; + + var awaiterType = typeContext.OriginalType; + + var originalOnComplete = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)) ?? throw new MissingMethodException("Original OnComplete"); + + var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; + var onComplete = new MethodDefinition(nameof(INotifyCompletion.OnCompleted), onCompletedAttr, voidRef); + typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value)); + typeContext.NewType.Methods.Add(onComplete); + + onComplete.Parameters.Add(new ParameterDefinition("continuation", ParameterAttributes.None, actionUntypedRef.Value)); + + var onCompleteIl = onComplete.Body.GetILProcessor(); + + onCompleteIl.Emit(OpCodes.Nop); + onCompleteIl.Emit(OpCodes.Ldarg_0); + onCompleteIl.Emit(OpCodes.Ldarg_1); // ldarg1 bc not static, so ldarg0 is this & ldarg1 is the parameter + onCompleteIl.Emit(OpCodes.Call, actionConversionUntypedRef.Value); + onCompleteIl.Emit(OpCodes.Call, originalOnComplete.NewMethod); + onCompleteIl.Emit(OpCodes.Nop); + onCompleteIl.Emit(OpCodes.Ret); + } + } + } +} diff --git a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs index f380fb2b..38542a34 100644 --- a/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs +++ b/Il2CppInterop.Generator/Runners/InteropAssemblyGenerator.cs @@ -151,6 +151,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); From d06f960c05ef3f6a78ba0c2a939793d7932e4cc6 Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:21:03 -0800 Subject: [PATCH 2/8] Modder commits "worst code ever", asked to leave library Paste from ML plugin, will be changed to remove unnecessary code/comments & improve generic handling --- .../Passes/Pass61ImplementAwaiters.cs | 170 ++++++++++++++---- 1 file changed, 136 insertions(+), 34 deletions(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index fad329a3..8836a871 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -1,59 +1,161 @@ -using System; -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using AsmResolver.PE.DotNet.Metadata.Tables; using Il2CppInterop.Generator.Contexts; -using Il2CppInterop.Generator.Utils; -using Mono.Cecil; -using Mono.Cecil.Cil; +using MelonLoader; +using System.Runtime.CompilerServices; namespace Il2CppInterop.Generator.Passes; -internal class Pass61ImplementAwaiters +internal static class Pass61ImplementAwaiters { public static void DoPass(RewriteGlobalContext context) { - var corlib = context.GetAssemblyByName("mscorlib"); - var actionUntyped = corlib.GetTypeByName("System.Action"); + AssemblyRewriteContext corlib = context.GetAssemblyByName("mscorlib"); + TypeRewriteContext actionUntyped = corlib.GetTypeByName("System.Action"); - var actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); + MethodDefinition actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); - foreach (var assemblyContext in context.Assemblies) + foreach (AssemblyRewriteContext assemblyContext in context.Assemblies) { + // dont actually import the references until they're needed - Lazy actionUntypedRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(actionUntyped.OriginalType)); - Lazy actionConversionUntypedRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(actionConversionUntyped)); - Lazy notifyCompletionRef = new(() => assemblyContext.NewAssembly.MainModule.ImportReference(typeof(INotifyCompletion))); - var voidRef = assemblyContext.Imports.Module.Void(); - foreach (var typeContext in assemblyContext.Types) + + Lazy actionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(actionConversionUntyped.Parameters[0].ParameterType.ToTypeDefOrRef())!); + Lazy interopActionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(actionUntyped.NewType)!); + Lazy actionConversionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(actionConversionUntyped)); + Lazy notifyCompletionRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(INotifyCompletion))); + Lazy voidRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(void))); + + foreach (TypeRewriteContext typeContext in assemblyContext.Types) { - var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.InterfaceType.Name == nameof(INotifyCompletion)); + InterfaceImplementation? interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.Interface.Name == nameof(INotifyCompletion)); if (interfaceImplementation is null) continue; + if (typeContext.OriginalType.IsInterface) + continue; - var isGeneric = typeContext.OriginalType.ContainsGenericParameter; + bool isGeneric = typeContext.OriginalType.GenericParameters.Count > 0; - var awaiterType = typeContext.OriginalType; + TypeDefinition awaiterType = typeContext.OriginalType; - var originalOnComplete = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)) ?? throw new MissingMethodException("Original OnComplete"); + MethodRewriteContext? onCompleteContext = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)); + //System.Reflection.MethodInfo newOncompleteTyped = typeof(string).GetMethod(""); + MethodDefinition? interopOnComplete = typeContext.NewType.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); + IMethodDefOrRef? interopOnCompleteRef = interopOnComplete; + //var j = + + if (interopOnComplete?.CilMethodBody is null) + continue; + + + //var interopInstructions = interopOnComplete.CilMethodBody.Instructions; + //foreach (var item in interopInstructions) + //{ + // // loads the static field that holds the IL2CPP method pointer + // if (item.OpCode.Code != CilCode.Ldsfld) + // continue; + + // if (item.Operand is not MemberReference field) + // continue; + + // // handles generic instance types (hopefully) (if youre seeing this comment on github, it does) + // if (field.DeclaringType is not TypeSpecification spec) + // continue; + + + // var mSig = interopOnComplete.Signature; + // spec.CreateMemberReference(interopOnComplete.Name!, mSig!); + // //interopOnComplete = spec!.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); + // //Debugger.Break(); + //} - var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; - var onComplete = new MethodDefinition(nameof(INotifyCompletion.OnCompleted), onCompletedAttr, voidRef); - typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value)); - typeContext.NewType.Methods.Add(onComplete); - onComplete.Parameters.Add(new ParameterDefinition("continuation", ParameterAttributes.None, actionUntypedRef.Value)); + if (onCompleteContext is null || interopOnComplete is null) + continue; + + MethodAttributes onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; + MethodSignature sig = MethodSignature.CreateInstance(voidRef.Value.ToTypeSignature(), new TypeSignature[] { actionUntypedRef.Value.ToTypeSignature() }); + + #region Handle generic declaring types of nested types + // many awaiters are nested types, so we need to handle generic declaring types + // shit like UniTask.Awaiter + // our new OnCompletes cant just call Task<>.Awaiter.OnComplete + // this is probably hacky, only checking the single declaring type, but honestly, it works, i dont care. + + #endregion + + MethodDefinition proxyOnComplete = new MethodDefinition(onCompleteContext.NewMethod.Name, onCompletedAttr, sig); + ParameterDefinition parameter = proxyOnComplete.Parameters[0].GetOrCreateDefinition(); + parameter.Name = "continuation"; + //(1, , interopOnComplete.ParameterDefinitions[0].Attributes); + + CilMethodBody body = proxyOnComplete.CilMethodBody ??= new(proxyOnComplete); + + typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value)); + typeContext.NewType.Methods.Add(proxyOnComplete); - var onCompleteIl = onComplete.Body.GetILProcessor(); + CilInstructionCollection instructions = body.Instructions; + instructions.Add(CilOpCodes.Nop); + instructions.Add(CilOpCodes.Ldarg_0); + instructions.Add(CilOpCodes.Ldarg_1); // ldarg1 bc not static, so ldarg0 is "this" & ldarg1 is the parameter + instructions.Add(CilOpCodes.Call, actionConversionUntypedRef.Value); + if (typeContext.NewType.DeclaringType?.GenericParameters.Count > 0) + { + var interopOnCompleteGeneric = typeContext.NewType.MakeGenericInstanceType(false, new GenericParameterSignature(GenericParameterType.Type, 0)) + .ToTypeDefOrRef() + .CreateMemberReference(interopOnComplete.Name!, interopOnComplete.Signature); + instructions.Add(CilOpCodes.Call, interopOnCompleteGeneric); + } + else + instructions.Add(CilOpCodes.Call, interopOnComplete); + instructions.Add(CilOpCodes.Nop); + instructions.Add(CilOpCodes.Ret); - onCompleteIl.Emit(OpCodes.Nop); - onCompleteIl.Emit(OpCodes.Ldarg_0); - onCompleteIl.Emit(OpCodes.Ldarg_1); // ldarg1 bc not static, so ldarg0 is this & ldarg1 is the parameter - onCompleteIl.Emit(OpCodes.Call, actionConversionUntypedRef.Value); - onCompleteIl.Emit(OpCodes.Call, originalOnComplete.NewMethod); - onCompleteIl.Emit(OpCodes.Nop); - onCompleteIl.Emit(OpCodes.Ret); + MelonLogger.Msg("Created member: " + body.Owner.FullName); + foreach (var item in instructions) + { + MelonLogger.Msg("\t" + CilInstructionFormatter.Instance.FormatInstruction(item)); + } + //body.MaxStack = 8; + //proxyOnComplete.CilMethodBody.ComputeMaxStackOnBuild = false; } } } + + //static TypeReference MakeGenericType(this TypeReference self, params TypeReference[] arguments) + //{ + // if (self.GenericParameters.Count != arguments.Length) + // throw new ArgumentException(); + + // var instance = new GenericInstanceType(self); + // foreach (var argument in arguments) + // instance.GenericArguments.Add(argument); + + // return instance; + //} + + //public static MethodReference MakeGeneric(this MethodReference self, TypeReference declaringType) + //{ + // var reference = new MethodReference(self.Name, self.ReturnType) + // { + // Name = self.Name, + // DeclaringType = declaringType, + // HasThis = self.HasThis, + // ExplicitThis = self.ExplicitThis, + // ReturnType = self.ReturnType, + // CallingConvention = MethodCallingConvention.Generic, + // }; + + // foreach (var parameter in self.Parameters) + // reference.Parameters.Add(new ParameterDefinition + // (parameter.ParameterType)); + + // foreach (var generic_parameter in self.GenericParameters) + // reference.GenericParameters.Add(new GenericParameter(reference)); + + // return reference; + //} } From 5737180a084cb059b7f3ed44575d7303a175ae01 Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Sat, 21 Dec 2024 22:51:16 -0800 Subject: [PATCH 3/8] Clean up code & remove vestiges of ML/Cecil --- .../Passes/Pass61ImplementAwaiters.cs | 149 ++++-------------- 1 file changed, 35 insertions(+), 114 deletions(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index 8836a871..aa1c23d1 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -4,158 +4,79 @@ using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; using Il2CppInterop.Generator.Contexts; -using MelonLoader; using System.Runtime.CompilerServices; namespace Il2CppInterop.Generator.Passes; -internal static class Pass61ImplementAwaiters +public static class Pass61ImplementAwaiters { public static void DoPass(RewriteGlobalContext context) { - AssemblyRewriteContext corlib = context.GetAssemblyByName("mscorlib"); - TypeRewriteContext actionUntyped = corlib.GetTypeByName("System.Action"); + var corlib = context.GetAssemblyByName("mscorlib"); + var actionUntyped = corlib.GetTypeByName("System.Action"); - MethodDefinition actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); + var actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); - foreach (AssemblyRewriteContext assemblyContext in context.Assemblies) + foreach (var assemblyContext in context.Assemblies) { - - // dont actually import the references until they're needed + // Use Lazy as a lazy way to not actually import the references until they're needed Lazy actionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(actionConversionUntyped.Parameters[0].ParameterType.ToTypeDefOrRef())!); - Lazy interopActionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(actionUntyped.NewType)!); Lazy actionConversionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(actionConversionUntyped)); Lazy notifyCompletionRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(INotifyCompletion))); Lazy voidRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(void))); - foreach (TypeRewriteContext typeContext in assemblyContext.Types) + foreach (var typeContext in assemblyContext.Types) { - InterfaceImplementation? interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.Interface.Name == nameof(INotifyCompletion)); - if (interfaceImplementation is null) - continue; - if (typeContext.OriginalType.IsInterface) + var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.Interface?.Name == nameof(INotifyCompletion)); + if (interfaceImplementation is null || typeContext.OriginalType.IsInterface) continue; - bool isGeneric = typeContext.OriginalType.GenericParameters.Count > 0; - - TypeDefinition awaiterType = typeContext.OriginalType; - - MethodRewriteContext? onCompleteContext = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)); - //System.Reflection.MethodInfo newOncompleteTyped = typeof(string).GetMethod(""); - MethodDefinition? interopOnComplete = typeContext.NewType.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); - IMethodDefOrRef? interopOnCompleteRef = interopOnComplete; - //var j = + var onCompletedContext = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)); + var interopOnCompleted = typeContext.NewType.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); + IMethodDefOrRef? interopOnCompletedRef = interopOnCompleted; - if (interopOnComplete?.CilMethodBody is null) + if (interopOnCompleted?.CilMethodBody is null || onCompletedContext is null || interopOnCompleted is null) continue; + // Established that INotifyCompletion.OnCompleted is implemented, & interop method is defined, now create the .NET interface implementation method that jumps to the proxy + var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; + var sig = MethodSignature.CreateInstance(voidRef.Value.ToTypeSignature(), [actionUntypedRef.Value.ToTypeSignature()]); - //var interopInstructions = interopOnComplete.CilMethodBody.Instructions; - //foreach (var item in interopInstructions) - //{ - // // loads the static field that holds the IL2CPP method pointer - // if (item.OpCode.Code != CilCode.Ldsfld) - // continue; - - // if (item.Operand is not MemberReference field) - // continue; - - // // handles generic instance types (hopefully) (if youre seeing this comment on github, it does) - // if (field.DeclaringType is not TypeSpecification spec) - // continue; - - - // var mSig = interopOnComplete.Signature; - // spec.CreateMemberReference(interopOnComplete.Name!, mSig!); - // //interopOnComplete = spec!.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); - // //Debugger.Break(); - //} - - - if (onCompleteContext is null || interopOnComplete is null) - continue; - - MethodAttributes onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; - MethodSignature sig = MethodSignature.CreateInstance(voidRef.Value.ToTypeSignature(), new TypeSignature[] { actionUntypedRef.Value.ToTypeSignature() }); - - #region Handle generic declaring types of nested types - // many awaiters are nested types, so we need to handle generic declaring types - // shit like UniTask.Awaiter - // our new OnCompletes cant just call Task<>.Awaiter.OnComplete - // this is probably hacky, only checking the single declaring type, but honestly, it works, i dont care. - - #endregion - - MethodDefinition proxyOnComplete = new MethodDefinition(onCompleteContext.NewMethod.Name, onCompletedAttr, sig); - ParameterDefinition parameter = proxyOnComplete.Parameters[0].GetOrCreateDefinition(); + var proxyOnCompleted = new MethodDefinition(onCompletedContext.NewMethod.Name, onCompletedAttr, sig); + var parameter = proxyOnCompleted.Parameters[0].GetOrCreateDefinition(); parameter.Name = "continuation"; - //(1, , interopOnComplete.ParameterDefinitions[0].Attributes); - CilMethodBody body = proxyOnComplete.CilMethodBody ??= new(proxyOnComplete); + var body = proxyOnCompleted.CilMethodBody ??= new(proxyOnCompleted); typeContext.NewType.Interfaces.Add(new(notifyCompletionRef.Value)); - typeContext.NewType.Methods.Add(proxyOnComplete); + typeContext.NewType.Methods.Add(proxyOnCompleted); - CilInstructionCollection instructions = body.Instructions; + var instructions = body.Instructions; instructions.Add(CilOpCodes.Nop); - instructions.Add(CilOpCodes.Ldarg_0); - instructions.Add(CilOpCodes.Ldarg_1); // ldarg1 bc not static, so ldarg0 is "this" & ldarg1 is the parameter + instructions.Add(CilOpCodes.Ldarg_0); // load "this" + instructions.Add(CilOpCodes.Ldarg_1); // not static, so ldarg1 loads "continuation" instructions.Add(CilOpCodes.Call, actionConversionUntypedRef.Value); - if (typeContext.NewType.DeclaringType?.GenericParameters.Count > 0) + + // 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 interopOnCompleteGeneric = typeContext.NewType.MakeGenericInstanceType(false, new GenericParameterSignature(GenericParameterType.Type, 0)) + var typeArguments = Enumerable.Range(0, genericParameterCount).Select(i => new GenericParameterSignature(GenericParameterType.Type, i)).ToArray(); + var interopOnCompleteGeneric = typeContext.NewType.MakeGenericInstanceType(typeArguments) .ToTypeDefOrRef() - .CreateMemberReference(interopOnComplete.Name!, interopOnComplete.Signature); + .CreateMemberReference(interopOnCompleted.Name!, interopOnCompleted.Signature!); // MemberReference ctor uses nullables, so we can tell the compiler "shut up I know what I'm doing" instructions.Add(CilOpCodes.Call, interopOnCompleteGeneric); } else - instructions.Add(CilOpCodes.Call, interopOnComplete); - instructions.Add(CilOpCodes.Nop); - instructions.Add(CilOpCodes.Ret); - - MelonLogger.Msg("Created member: " + body.Owner.FullName); - foreach (var item in instructions) { - MelonLogger.Msg("\t" + CilInstructionFormatter.Instance.FormatInstruction(item)); + instructions.Add(CilOpCodes.Call, interopOnCompleted); } - //body.MaxStack = 8; - //proxyOnComplete.CilMethodBody.ComputeMaxStackOnBuild = false; + + instructions.Add(CilOpCodes.Nop); + instructions.Add(CilOpCodes.Ret); } } } - - //static TypeReference MakeGenericType(this TypeReference self, params TypeReference[] arguments) - //{ - // if (self.GenericParameters.Count != arguments.Length) - // throw new ArgumentException(); - - // var instance = new GenericInstanceType(self); - // foreach (var argument in arguments) - // instance.GenericArguments.Add(argument); - - // return instance; - //} - - //public static MethodReference MakeGeneric(this MethodReference self, TypeReference declaringType) - //{ - // var reference = new MethodReference(self.Name, self.ReturnType) - // { - // Name = self.Name, - // DeclaringType = declaringType, - // HasThis = self.HasThis, - // ExplicitThis = self.ExplicitThis, - // ReturnType = self.ReturnType, - // CallingConvention = MethodCallingConvention.Generic, - // }; - - // foreach (var parameter in self.Parameters) - // reference.Parameters.Add(new ParameterDefinition - // (parameter.ParameterType)); - - // foreach (var generic_parameter in self.GenericParameters) - // reference.GenericParameters.Add(new GenericParameter(reference)); - - // return reference; - //} } From 1a1f541daebd6f70c0cb5c8851b1e27ecc89e36f Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Sat, 21 Dec 2024 23:10:47 -0800 Subject: [PATCH 4/8] Fix import ordering & remove an unused `using` --- Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index aa1c23d1..5efb2270 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -1,10 +1,9 @@ -using AsmResolver.DotNet; -using AsmResolver.DotNet.Code.Cil; +using System.Runtime.CompilerServices; +using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Cil; using AsmResolver.PE.DotNet.Metadata.Tables; using Il2CppInterop.Generator.Contexts; -using System.Runtime.CompilerServices; namespace Il2CppInterop.Generator.Passes; From 4710b3f5570bb1a4a766f5ba1980e0eb72e554c1 Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:09:33 -0800 Subject: [PATCH 5/8] Add "CorLib" reference for mscorlib to global context --- Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs | 1 + Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs index 454a926f..bab812ea 100644 --- a/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs +++ b/Il2CppInterop.Generator/Contexts/RewriteGlobalContext.cs @@ -55,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/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index 5efb2270..1c39be38 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -11,7 +11,7 @@ public static class Pass61ImplementAwaiters { public static void DoPass(RewriteGlobalContext context) { - var corlib = context.GetAssemblyByName("mscorlib"); + var corlib = context.CorLib; var actionUntyped = corlib.GetTypeByName("System.Action"); var actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); From f5743214f7ff3e3d362cdea2f8082091b4bdc49c Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Wed, 25 Dec 2024 00:12:16 -0800 Subject: [PATCH 6/8] Changes for PR review - Use CorLibTypeFactory.Void for void reference - Added a couple more early-outs before LINQ - Use .Single - Use MemberCloner (and define a nested type for that) - Check method signature & forward to methods that may have been wonkily unhollowed --- .../Passes/Pass61ImplementAwaiters.cs | 76 ++++++++++++++----- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index 1c39be38..dc55c341 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -1,61 +1,103 @@ 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 { + private class ParameterCloneListener(TypeSignature corLibAction) : MemberClonerListener + { + public override void OnClonedMethod(MethodDefinition original, MethodDefinition cloned) + { + if (cloned.Signature is not null && cloned.Signature.ParameterTypes.Count > 0) + cloned.Signature.ParameterTypes[0] = corLibAction; + + cloned.Name = nameof(INotifyCompletion.OnCompleted); // in case it's explicitly implemented and was unhollowed as "System_Runtime_CompilerServices_INotifyCompletion_OnCompleted" + cloned.CilMethodBody = new(cloned); + cloned.CustomAttributes.Clear(); + original.DeclaringType?.Methods.Add(cloned); + } + } + public static void DoPass(RewriteGlobalContext context) { var corlib = context.CorLib; var actionUntyped = corlib.GetTypeByName("System.Action"); - var actionConversionUntyped = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); + var actionConversion = actionUntyped.NewType.Methods.FirstOrDefault(m => m.Name == "op_Implicit") ?? throw new MissingMethodException("Untyped action conversion"); 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(actionConversionUntyped.Parameters[0].ParameterType.ToTypeDefOrRef())!); - Lazy actionConversionUntypedRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportMethod(actionConversionUntyped)); + 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))); - Lazy voidRef = new(() => assemblyContext.NewAssembly.ManifestModule!.DefaultImporter.ImportType(typeof(void))); + var voidRef = assemblyContext.NewAssembly.ManifestModule!.CorLibTypeFactory.Void; foreach (var typeContext in assemblyContext.Types) { - var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(InterfaceImplementation => InterfaceImplementation.Interface?.Name == nameof(INotifyCompletion)); - if (interfaceImplementation is null || typeContext.OriginalType.IsInterface) + // Used later for MemberCloner, just putting up here as an early exit in case .Module is ever null + if (typeContext.NewType.Module is null) + continue; + + // 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 onCompletedContext = typeContext.TryGetMethodByName(nameof(INotifyCompletion.OnCompleted)); - var interopOnCompleted = typeContext.NewType.Methods.FirstOrDefault(m => m.Name == nameof(INotifyCompletion.OnCompleted)); - IMethodDefOrRef? interopOnCompletedRef = interopOnCompleted; + var interfaceImplementation = typeContext.OriginalType.Interfaces.FirstOrDefault(interfaceImpl => interfaceImpl.Interface?.Name == nameof(INotifyCompletion)); + if (interfaceImplementation is null) + continue; + + var allOnCompleted = typeContext.NewType.Methods.Where(m => m.Name == nameof(INotifyCompletion.OnCompleted)).ToArray(); + if (allOnCompleted.Length == 0) + { + // Likely defined as INotifyCompletion.OnCompleted & the name is unhollowed as something like "System_Runtime_CompilerServices_INotifyCompletion_OnCompleted" + allOnCompleted = typeContext.NewType.Methods.Where(m => ((string?)m.Name)?.EndsWith(nameof(INotifyCompletion.OnCompleted)) ?? false).ToArray(); + var typeName = typeContext.OriginalType.FullName; + Logger.Instance.LogInformation("Found explicit implementation of INotifyCompletion on {typeName}", typeName); + } - if (interopOnCompleted?.CilMethodBody is null || onCompletedContext is null || interopOnCompleted is null) + // 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 IsAssignableTo because otherwise equality checks would fail due to the TypeSignatures being different references + var interopOnCompleted = allOnCompleted.FirstOrDefault(m => m.Parameters.Count == 1 && m.Signature is not null && 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 cloner = new MemberCloner(typeContext.NewType.Module, new ParameterCloneListener(actionUntypedRef.Value.ToTypeSignature())) + .Include(interopOnCompleted); + var cloneResult = cloner.Clone(); // Established that INotifyCompletion.OnCompleted is implemented, & interop method is defined, now create the .NET interface implementation method that jumps to the proxy - var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; - var sig = MethodSignature.CreateInstance(voidRef.Value.ToTypeSignature(), [actionUntypedRef.Value.ToTypeSignature()]); + //var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; + //var sig = MethodSignature.CreateInstance(voidRef, [actionUntypedRef.Value.ToTypeSignature()]); + //var proxyOnCompleted = new MethodDefinition(onCompletedContext.NewMethod.Name, onCompletedAttr, sig); - var proxyOnCompleted = new MethodDefinition(onCompletedContext.NewMethod.Name, onCompletedAttr, sig); + var proxyOnCompleted = (MethodDefinition)cloneResult.ClonedMembers.Single(); + proxyOnCompleted.Signature!.ParameterTypes[0] = actionUntypedRef.Value.ToTypeSignature(); 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.Nop); instructions.Add(CilOpCodes.Ldarg_0); // load "this" instructions.Add(CilOpCodes.Ldarg_1); // not static, so ldarg1 loads "continuation" - instructions.Add(CilOpCodes.Call, actionConversionUntypedRef.Value); + 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) From 7272bdf3c12758c00001cd6893652719ab8e023c Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Wed, 25 Dec 2024 10:14:00 -0800 Subject: [PATCH 7/8] Remove commented vestigial code --- Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index dc55c341..ec98085c 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -80,11 +80,7 @@ public static void DoPass(RewriteGlobalContext context) .Include(interopOnCompleted); var cloneResult = cloner.Clone(); - // Established that INotifyCompletion.OnCompleted is implemented, & interop method is defined, now create the .NET interface implementation method that jumps to the proxy - //var onCompletedAttr = MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot; - //var sig = MethodSignature.CreateInstance(voidRef, [actionUntypedRef.Value.ToTypeSignature()]); - //var proxyOnCompleted = new MethodDefinition(onCompletedContext.NewMethod.Name, onCompletedAttr, sig); - + // Established that INotifyCompletion.OnCompleted is implemented, & interop method is defined, now clone it to create the .NET interface implementation method that jumps straight to it var proxyOnCompleted = (MethodDefinition)cloneResult.ClonedMembers.Single(); proxyOnCompleted.Signature!.ParameterTypes[0] = actionUntypedRef.Value.ToTypeSignature(); var parameter = proxyOnCompleted.Parameters[0].GetOrCreateDefinition(); From 3f509b2f1209d474a532edba5c1dcaf983a27a2f Mon Sep 17 00:00:00 2001 From: extraes <52384576+extraes@users.noreply.github.com> Date: Wed, 25 Dec 2024 10:29:02 -0800 Subject: [PATCH 8/8] Remove nullability shut-ups from CreateMemberReference --- Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs index ec98085c..3a6ecea2 100644 --- a/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs +++ b/Il2CppInterop.Generator/Passes/Pass61ImplementAwaiters.cs @@ -103,7 +103,7 @@ public static void DoPass(RewriteGlobalContext context) 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!); // MemberReference ctor uses nullables, so we can tell the compiler "shut up I know what I'm doing" + .CreateMemberReference(interopOnCompleted.Name, interopOnCompleted.Signature); instructions.Add(CilOpCodes.Call, interopOnCompleteGeneric); } else