diff --git a/Il2CppInterop.Generator/Extensions/ILGeneratorEx.cs b/Il2CppInterop.Generator/Extensions/ILGeneratorEx.cs index dd2cc1ae..bfd95646 100644 --- a/Il2CppInterop.Generator/Extensions/ILGeneratorEx.cs +++ b/Il2CppInterop.Generator/Extensions/ILGeneratorEx.cs @@ -172,14 +172,16 @@ public static void EmitObjectToPointer(this ILProcessor body, TypeReference orig } var imports = enclosingType.AssemblyContext.Imports; - if (originalType is ByReferenceType) + if (originalType is ByReferenceType originalRefType) { - if (newType.GetElementType().IsValueType) + if (newType is not ByReferenceType newRefType) + throw new ArgumentException($"{nameof(newType)} must be {nameof(ByReferenceType)} if {nameof(originalType)} is", nameof(newType)); + if (newRefType.ElementType.IsValueType) { body.Emit(OpCodes.Ldarg, argumentIndex); body.Emit(OpCodes.Conv_I); } - else if (originalType.GetElementType().IsValueType) + else if (originalRefType.ElementType.IsValueType) { body.Emit(OpCodes.Ldarg, argumentIndex); body.Emit(OpCodes.Ldind_Ref); @@ -192,7 +194,7 @@ public static void EmitObjectToPointer(this ILProcessor body, TypeReference orig body.Body.Variables.Add(pointerVar); body.Emit(OpCodes.Ldarg, argumentIndex); body.Emit(OpCodes.Ldind_Ref); - if (originalType.GetElementType().FullName == "System.String") + if (originalRefType.ElementType.FullName == "System.String") body.Emit(OpCodes.Call, imports.IL2CPP_ManagedStringToIl2Cpp.Value); else body.Emit(OpCodes.Call, imports.IL2CPP_Il2CppObjectBaseToPtr.Value); diff --git a/Il2CppInterop.Generator/Passes/Pass20GenerateStaticConstructors.cs b/Il2CppInterop.Generator/Passes/Pass20GenerateStaticConstructors.cs index 1a81795b..fccadf6a 100644 --- a/Il2CppInterop.Generator/Passes/Pass20GenerateStaticConstructors.cs +++ b/Il2CppInterop.Generator/Passes/Pass20GenerateStaticConstructors.cs @@ -191,7 +191,7 @@ private static void EmitLoadTypeNameString(this ILProcessor ctorBuilder, Runtime imports.Module.ImportReference(new GenericInstanceMethod(imports.IL2CPP_RenderTypeName.Value) { GenericArguments = - {newTypeReference.IsByReference ? newTypeReference.GetElementType() : newTypeReference} + {newTypeReference is ByReferenceType newTypeRefType ? newTypeRefType.ElementType : newTypeReference} })); } } diff --git a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs index d64773dc..8c883b4b 100644 --- a/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs +++ b/Il2CppInterop.Generator/Passes/Pass80UnstripMethods.cs @@ -143,29 +143,44 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth } internal static TypeReference? ResolveTypeInNewAssemblies(RewriteGlobalContext context, TypeReference unityType, - RuntimeAssemblyReferences imports) + RuntimeAssemblyReferences imports, bool resolveValueTypes = false) { - var resolved = ResolveTypeInNewAssembliesRaw(context, unityType, imports); + return ResolveTypeInNewAssemblies(context, unityType, imports, out var _, resolveValueTypes); + } + + internal static TypeReference? ResolveTypeInNewAssemblies(RewriteGlobalContext context, TypeReference unityType, + RuntimeAssemblyReferences imports, out TypeRewriteContext rwContext, bool resolveValueTypes = false) + { + var resolved = ResolveTypeInNewAssembliesRaw(context, unityType, imports, out rwContext, resolveValueTypes); return resolved != null ? imports.Module.ImportReference(resolved) : null; } internal static TypeReference? ResolveTypeInNewAssembliesRaw(RewriteGlobalContext context, TypeReference unityType, - RuntimeAssemblyReferences imports) + RuntimeAssemblyReferences imports, bool resolveValueTypes = false) + { + return ResolveTypeInNewAssembliesRaw(context, unityType, imports, out var _, resolveValueTypes); + } + + internal static TypeReference? ResolveTypeInNewAssembliesRaw(RewriteGlobalContext context, TypeReference unityType, + RuntimeAssemblyReferences imports, out TypeRewriteContext rwContext, bool resolveValueTypes = false) { - if (unityType is ByReferenceType) + if (unityType is ByReferenceType unityRefType) { - var resolvedElementType = ResolveTypeInNewAssemblies(context, unityType.GetElementType(), imports); + var resolvedElementType = ResolveTypeInNewAssemblies(context, unityRefType.ElementType, imports, out rwContext); return resolvedElementType == null ? null : new ByReferenceType(resolvedElementType); } if (unityType is GenericParameter) + { + rwContext = null; return null; + } if (unityType is ArrayType arrayType) { - if (arrayType.Rank != 1) return null; - var resolvedElementType = ResolveTypeInNewAssemblies(context, unityType.GetElementType(), imports); - if (resolvedElementType == null) return null; + if (arrayType.Rank != 1) { rwContext = null; return null; } + var resolvedElementType = ResolveTypeInNewAssemblies(context, unityType.GetElementType(), imports, out rwContext, resolveValueTypes); + if (resolvedElementType == null) { rwContext = null; return null; } if (resolvedElementType.FullName == "System.String") return imports.Il2CppStringArray; var genericBase = resolvedElementType.IsValueType @@ -176,7 +191,7 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth if (unityType.DeclaringType != null) { - var enclosingResolvedType = ResolveTypeInNewAssembliesRaw(context, unityType.DeclaringType, imports); + var enclosingResolvedType = ResolveTypeInNewAssembliesRaw(context, unityType.DeclaringType, imports, out rwContext, resolveValueTypes); if (enclosingResolvedType == null) return null; var resolvedNestedType = enclosingResolvedType.Resolve().NestedTypes .FirstOrDefault(it => it.Name == unityType.Name); @@ -186,13 +201,13 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth if (unityType is PointerType) { - var resolvedElementType = ResolveTypeInNewAssemblies(context, unityType.GetElementType(), imports); + var resolvedElementType = ResolveTypeInNewAssemblies(context, unityType.GetElementType(), imports, out rwContext, resolveValueTypes); return resolvedElementType == null ? null : new PointerType(resolvedElementType); } if (unityType is GenericInstanceType genericInstance) { - var baseRef = ResolveTypeInNewAssembliesRaw(context, genericInstance.ElementType, imports); + var baseRef = ResolveTypeInNewAssembliesRaw(context, genericInstance.ElementType, imports, out rwContext, resolveValueTypes); if (baseRef == null) return null; var newInstance = new GenericInstanceType(baseRef); foreach (var unityGenericArgument in genericInstance.GenericArguments) @@ -208,24 +223,29 @@ private static PropertyDefinition GetOrCreateProperty(MethodDefinition unityMeth var targetAssemblyName = unityType.Scope.Name; if (targetAssemblyName.EndsWith(".dll")) targetAssemblyName = targetAssemblyName.Substring(0, targetAssemblyName.Length - 4); - if ((targetAssemblyName == "mscorlib" || targetAssemblyName == "netstandard") && - (unityType.IsValueType || unityType.FullName == "System.String" || - unityType.FullName == "System.Void") && unityType.FullName != "System.RuntimeTypeHandle") + if ((targetAssemblyName == "mscorlib" || targetAssemblyName == "netstandard") && ( + (!resolveValueTypes && (unityType.IsValueType || unityType.FullName == "System.String")) || + unityType.FullName == "System.Void" || unityType.IsPrimitive + ) && unityType.FullName != "System.RuntimeTypeHandle") + { + rwContext = null; return imports.Module.ImportCorlibReference(unityType.Namespace, unityType.Name); + } if (targetAssemblyName == "UnityEngine") foreach (var assemblyRewriteContext in context.Assemblies) { if (!assemblyRewriteContext.NewAssembly.Name.Name.StartsWith("UnityEngine")) continue; - var newTypeInAnyUnityAssembly = - assemblyRewriteContext.TryGetTypeByName(unityType.FullName)?.NewType; + rwContext = assemblyRewriteContext.TryGetTypeByName(unityType.FullName); + var newTypeInAnyUnityAssembly = rwContext?.NewType; if (newTypeInAnyUnityAssembly != null) return newTypeInAnyUnityAssembly; } var targetAssembly = context.TryGetAssemblyByName(targetAssemblyName); - var newType = targetAssembly?.TryGetTypeByName(unityType.FullName)?.NewType; + rwContext = targetAssembly?.TryGetTypeByName(unityType.FullName); + var newType = rwContext?.NewType; return newType; } diff --git a/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs b/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs index eadd4df7..76807429 100644 --- a/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs +++ b/Il2CppInterop.Generator/Passes/Pass81FillUnstrippedMethodBodies.cs @@ -1,9 +1,12 @@ using System.Collections.Generic; +using System.IO.Compression; +using System.Text; using Il2CppInterop.Common; using Il2CppInterop.Generator.Contexts; using Il2CppInterop.Generator.Utils; using Microsoft.Extensions.Logging; using Mono.Cecil; +using Mono.Cecil.Cil; namespace Il2CppInterop.Generator.Passes; @@ -17,14 +20,14 @@ private static readonly public static void DoPass(RewriteGlobalContext context) { var methodsSucceeded = 0; - var methodsFailed = 0; + var methodsFailed = new List<(MethodDefinition method, UnstripTranslator.Result result)>(); foreach (var (unityMethod, newMethod, processedType, imports) in StuffToProcess) { - var success = UnstripTranslator.TranslateMethod(unityMethod, newMethod, processedType, imports); - if (success == false) + var result = UnstripTranslator.TranslateMethod(unityMethod, newMethod, processedType, imports); + if (result.IsError) { - methodsFailed++; + methodsFailed.Add((unityMethod, result)); UnstripTranslator.ReplaceBodyWithException(newMethod, imports); } else @@ -34,7 +37,83 @@ public static void DoPass(RewriteGlobalContext context) } Logger.Instance.LogInformation("IL unstrip statistics: {MethodsSucceeded} successful, {MethodsFailed} failed", methodsSucceeded, - methodsFailed); + methodsFailed.Count); + + SaveResults(context, methodsFailed); + } + + private static void SaveResults(RewriteGlobalContext context, + List<(MethodDefinition method, UnstripTranslator.Result result)> results) + { + var outPath = Path.GetDirectoryName(context.Options.OutputDir); + outPath = Path.Combine(outPath, "unstrip.json.gz"); + outPath = Path.GetFullPath(outPath); + var cwd = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar; + if (!outPath.StartsWith(cwd)) + outPath = "unstrip.json.gz"; + else + outPath = outPath.Substring(cwd.Length); + Logger.Instance.LogInformation("Saving IL unstrip statistics to {OutPath}", outPath); + + results.Sort((left, right) => StringComparer.OrdinalIgnoreCase + .Compare(left.method.MemberFullName(), right.method.MemberFullName())); + using (var gzStream = + new StreamWriter( + new GZipStream( + File.Open(outPath, FileMode.Create), + CompressionLevel.Fastest), + new UTF8Encoding(false))) + { + using (var arr = SimpleJsonWriter.Create(gzStream).Array()) + { + foreach (var (method, result) in results) + { + using (var entry = arr.Object()) + { + entry.Property("name") + .Value(method.MemberFullName()); + entry.Property("fullName") + .Value(method.FullName); + entry.Property("scope") + .Value(method.DeclaringType.Scope.Name); + entry.Property("namespace") + .Value(method.DeclaringType.Namespace); + entry.Property("type") + .Value(method.DeclaringType.Name); + entry.Property("method") + .Value(method.Name); + var insProp = entry.Property("instruction"); + if (result.offendingInstruction == null) + insProp.Value(null); + else + using (var ins = insProp.Object()) + { + ins.Property("description") + .Value(result.offendingInstruction.ToString()); + ins.Property("opCode") + .Value(result.offendingInstruction.OpCode.Name); + ins.Property("operandType") + .Value(Enum.GetName(typeof(OperandType), result.offendingInstruction.OpCode.OperandType)); + ins.Property("operandValueType") + .Value(result.offendingInstruction.Operand?.GetType().Name); + ins.Property("operand") + .Value(result.offendingInstruction.Operand?.ToString()); + } + entry.Property("result") + .Value(Enum.GetName(typeof(UnstripTranslator.ErrorType), result.type)); + entry.Property("reason") + .Value(result.reason); + } + } + } + } + } + + private static string MemberFullName(this MethodDefinition method) + { + if (method.DeclaringType == null) + return method.Name; + return $"{method.DeclaringType.FullName}::{method.Name}"; } public static void PushMethod(MethodDefinition unityMethod, MethodDefinition newMethod, diff --git a/Il2CppInterop.Generator/Utils/InstructionExtensions.cs b/Il2CppInterop.Generator/Utils/InstructionExtensions.cs new file mode 100644 index 00000000..7612c231 --- /dev/null +++ b/Il2CppInterop.Generator/Utils/InstructionExtensions.cs @@ -0,0 +1,185 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Il2CppInterop.Generator.Utils; + +internal static class InstructionExtensions +{ + public static bool IsStelem(this Code opCode) + { + switch (opCode) + { + case Code.Stelem_I: + case Code.Stelem_I1: + case Code.Stelem_I2: + case Code.Stelem_I4: + case Code.Stelem_I8: + case Code.Stelem_R4: + case Code.Stelem_R8: + case Code.Stelem_Ref: + return true; + default: return false; + } + } + + public static bool IsLdelem(this Code opCode) + { + switch (opCode) + { + case Code.Ldelem_I: + case Code.Ldelem_I1: + case Code.Ldelem_I2: + case Code.Ldelem_I4: + case Code.Ldelem_I8: + case Code.Ldelem_R4: + case Code.Ldelem_R8: + case Code.Ldelem_Ref: + return true; + default: return false; + } + } + + public static bool IsLdind(this Code opCode) + { + switch (opCode) + { + case Code.Ldind_I: + case Code.Ldind_I1: + case Code.Ldind_I2: + case Code.Ldind_I4: + case Code.Ldind_I8: + case Code.Ldind_R4: + case Code.Ldind_R8: + case Code.Ldind_Ref: + case Code.Ldind_U1: + case Code.Ldind_U2: + case Code.Ldind_U4: + return true; + default: return false; + } + } + + public static bool BreaksFlow(this OpCode opCode) + { + if (opCode == OpCodes.Jmp) return true; + switch (opCode.FlowControl) + { + case FlowControl.Return: + case FlowControl.Branch: + case FlowControl.Cond_Branch: + return true; + default: return false; + } + } + + public static int PushAmount(this Instruction ins) + { + return ins.OpCode.StackBehaviourPush switch + { + StackBehaviour.Push0 => 0, + StackBehaviour.Varpush => ((MethodReference)ins.Operand) + .ReturnType.FullName == "System.Void" ? 0 : 1, + StackBehaviour.Push1_push1 => 2, + _ => 1, + }; + } + + public static int PopAmount(this Instruction ins) + { + return ins.OpCode.StackBehaviourPop switch + { + StackBehaviour.Pop0 => 0, + StackBehaviour.Pop1 => 1, + StackBehaviour.Popi => 1, + StackBehaviour.Popref => 1, + StackBehaviour.Pop1_pop1 => 2, + StackBehaviour.Popi_pop1 => 2, + StackBehaviour.Popi_popi => 2, + StackBehaviour.Popi_popi8 => 2, + StackBehaviour.Popi_popi_popi => 3, + StackBehaviour.Popi_popr4 => 2, + StackBehaviour.Popi_popr8 => 2, + StackBehaviour.Popref_pop1 => 2, + StackBehaviour.Popref_popi => 2, + StackBehaviour.Popref_popi_popi => 3, + StackBehaviour.Popref_popi_popi8 => 3, + StackBehaviour.Popref_popi_popr4 => 3, + StackBehaviour.Popref_popi_popr8 => 3, + StackBehaviour.Popref_popi_popref => 3, + StackBehaviour.Varpop => GetParameterCount(ins), + var pop => throw new NotSupportedException( + $"{Enum.GetName(typeof(StackBehaviour), pop)} is not a pop behaviour"), + }; + } + + public static int GetParameterCount(this Instruction ins) + { + if (ins.Operand is not MethodReference method) + throw new ArgumentException("Operand must be a method", nameof(ins)); + if (method.HasThis && ins.OpCode.Code != Code.Newobj) + return method.Parameters.Count + 1; + return method.Parameters.Count; + } + + public static bool TryGetLdlocIndex(this Instruction ins, out int index) + { + index = ins.OpCode.Code switch + { + Code.Ldloc_0 => 0, + Code.Ldloc_1 => 1, + Code.Ldloc_2 => 2, + Code.Ldloc_3 => 3, + Code.Ldloc or + Code.Ldloc_S => ((VariableReference)ins.Operand).Index, + _ => -1, + }; + return index >= 0; + } + + public static bool TryGetLdargIndex(this Instruction ins, bool hasThis, out int index) + { + var thisOffset = hasThis ? -1 : 0; + index = ins.OpCode.Code switch + { + Code.Ldarg_0 => thisOffset + 0, + Code.Ldarg_1 => thisOffset + 1, + Code.Ldarg_2 => thisOffset + 2, + Code.Ldarg_3 => thisOffset + 3, + Code.Ldarg or + Code.Ldarg_S => ((ParameterReference)ins.Operand).Index, + _ => -2, + }; + return index >= -1; + } + + public static OpCode GetLong(this OpCode opCode) + { + return opCode.OperandType switch + { + OperandType.ShortInlineArg => throw new NotImplementedException(opCode.OperandType.ToString()), + OperandType.ShortInlineBrTarget => opCode.Code switch + { + Code.Br_S => OpCodes.Br, + Code.Brfalse_S => OpCodes.Brfalse, + Code.Brtrue_S => OpCodes.Brtrue, + Code.Beq_S => OpCodes.Beq, + Code.Bge_S => OpCodes.Bge, + Code.Bgt_S => OpCodes.Bgt, + Code.Ble_S => OpCodes.Ble, + Code.Blt_S => OpCodes.Blt, + Code.Bne_Un_S => OpCodes.Bne_Un, + Code.Bge_Un_S => OpCodes.Bge_Un, + Code.Bgt_Un_S => OpCodes.Bgt_Un, + Code.Ble_Un_S => OpCodes.Ble_Un, + Code.Blt_Un_S => OpCodes.Blt_Un, + Code.Leave_S => OpCodes.Leave, + _ => throw new NotImplementedException($"{opCode.OperandType} {opCode.Code}"), + }, + OperandType.ShortInlineI => throw new NotImplementedException(opCode.OperandType.ToString()), + OperandType.ShortInlineR => throw new NotImplementedException(opCode.OperandType.ToString()), + OperandType.ShortInlineVar => throw new NotImplementedException(opCode.OperandType.ToString()), + _ => throw new NotSupportedException($"{opCode.OperandType} is not a short version OpCode"), + }; + } + +} diff --git a/Il2CppInterop.Generator/Utils/RetargetingILProcessor.cs b/Il2CppInterop.Generator/Utils/RetargetingILProcessor.cs new file mode 100644 index 00000000..92d0009f --- /dev/null +++ b/Il2CppInterop.Generator/Utils/RetargetingILProcessor.cs @@ -0,0 +1,135 @@ +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Il2CppInterop.Generator.Utils; + +internal class RetargetingILProcessor +{ + private readonly MethodDefinition _target; + private readonly Builder _builder; + + // + private readonly Dictionary _replacementBranches = new(); + // + private readonly Dictionary> _originalBranches = new(); + public bool NeedsRetargeting => _originalBranches.Count > 0; + public IReadOnlyDictionary> IncompleteBranches => _originalBranches; + + private Instruction _originalInstruction; + private int _trackedIdx = -1; + + public RetargetingILProcessor(MethodDefinition target) + { + _target = target; + _builder = new(this); + } + + public Builder Track(Instruction originalInstruction) + { + if (_originalInstruction != null) + throw new InvalidOperationException("track called before builder disposed"); + _originalInstruction = originalInstruction; + _trackedIdx = _target.Body.Instructions.Count; + return _builder; + } + + private void TrackBranch(Instruction instruction) + { + var operandType = instruction.OpCode.OperandType; + if (operandType != OperandType.InlineBrTarget && + operandType != OperandType.ShortInlineBrTarget) + return; + + var dst = (Instruction)instruction.Operand; + if (operandType == OperandType.ShortInlineBrTarget) + instruction.OpCode = instruction.OpCode.GetLong(); + + if (_replacementBranches.TryGetValue(dst, out var newDst)) + instruction.Operand = newDst; + else + { + if (!_originalBranches.TryGetValue(dst, out var oldBranches)) + _originalBranches.Add(dst, oldBranches = new()); + oldBranches.Add(instruction); + } + } + + private void RetargetBranches() + { + if (_originalInstruction == null) + throw new InvalidOperationException("builder disposed without calling track"); + + if (_trackedIdx < _target.Body.Instructions.Count) + { + var newDst = _target.Body.Instructions[_trackedIdx]; + _replacementBranches.Add(_originalInstruction, newDst); + if (_originalBranches.TryGetValue(_originalInstruction, out var oldBranches)) + { + foreach (var oldBranch in oldBranches) + oldBranch.Operand = newDst; + _originalBranches.Remove(_originalInstruction); + } + } + + _originalInstruction = null; + _trackedIdx = -1; + } + + public class Builder : IDisposable + { + private readonly RetargetingILProcessor _processor; + private readonly ILProcessor _targetBuilder; + + public Builder(RetargetingILProcessor processor) + { + _processor = processor; + _targetBuilder = processor._target.Body.GetILProcessor(); + } + + public void Dispose() => _processor.RetargetBranches(); + + public void Append(Instruction instruction) + { + _processor.TrackBranch(instruction); + _targetBuilder.Append(instruction); + } + + public void InsertAfter(Instruction target, Instruction instruction) + { + var index = _targetBuilder.Body.Instructions.IndexOf(target); + _targetBuilder.InsertAfter(index, instruction); + } + + public void InsertAfter(int index, Instruction instruction) + { + _processor.TrackBranch(instruction); + _targetBuilder.InsertAfter(index, instruction); + if (index < _processor._trackedIdx) + _processor._trackedIdx++; + } + + // Add whatever equivalent _targetBuilder.Emit/Create you need below + + public void Emit(OpCode opCode) => + Append(Create(opCode)); + public void Emit(OpCode opCode, FieldReference field) => + Append(Create(opCode, field)); + public void Emit(OpCode opCode, MethodReference method) => + Append(Create(opCode, method)); + public void Emit(OpCode opCode, TypeReference type) => + Append(Create(opCode, type)); + public void Emit(OpCode opCode, VariableDefinition variable) => + Append(Create(opCode, variable)); + + public Instruction Create(OpCode opCode) => + _targetBuilder.Create(opCode); + public Instruction Create(OpCode opCode, FieldReference field) => + _targetBuilder.Create(opCode, field); + public Instruction Create(OpCode opCode, MethodReference method) => + _targetBuilder.Create(opCode, method); + public Instruction Create(OpCode opCode, TypeReference type) => + _targetBuilder.Create(opCode, type); + public Instruction Create(OpCode opCode, VariableDefinition variable) => + _targetBuilder.Create(opCode, variable); + } +} diff --git a/Il2CppInterop.Generator/Utils/RuntimeAssemblyReferences.cs b/Il2CppInterop.Generator/Utils/RuntimeAssemblyReferences.cs index 6646699a..11fa8978 100644 --- a/Il2CppInterop.Generator/Utils/RuntimeAssemblyReferences.cs +++ b/Il2CppInterop.Generator/Utils/RuntimeAssemblyReferences.cs @@ -24,6 +24,9 @@ public RuntimeAssemblyReferences(ModuleDefinition module, RewriteGlobalContext g public ModuleDefinition Module { get; } + public Memoize Il2CppArrayBase_set_Item { get; private set; } + public Memoize Il2CppArrayBase_get_Item { get; private set; } + public Memoize Il2CppArrayBase_get_Length { get; private set; } public Memoize Il2CppRefrenceArrayctor { get; private set; } public Lazy Il2CppStringArrayctor { get; private set; } public Memoize Il2CppStructArrayctor { get; private set; } @@ -71,6 +74,9 @@ public RuntimeAssemblyReferences(ModuleDefinition module, RewriteGlobalContext g public Lazy Il2CppSystemDelegateCombine { get; private set; } public Lazy Il2CppSystemDelegateRemove { get; private set; } public Lazy Il2CppSystemRuntimeTypeHandleGetRuntimeTypeHandle { get; private set; } + public Lazy Il2CppObject { get; private set; } + public Memoize Il2CppObject_op_Implicit { get; private set; } + public Lazy Il2CppString_op_Implicit { get; private set; } public MethodReference WriteFieldWBarrier => globalCtx.HasGcWbarrierFieldWrite ? IL2CPP_il2cpp_gc_wbarrier_set_field.Value @@ -148,6 +154,7 @@ private void InitTypeRefs() allTypes["Il2CppInterop.Runtime.InteropTypes.Il2CppObjectBase"] = Il2CppObjectBase; allTypes["Il2CppInterop.Runtime.Runtime.Il2CppObjectPool"] = Il2CppObjectPool; allTypes["Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStringArray"] = Il2CppStringArray; + allTypes["Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase"] = Il2CppArrayBase; allTypes["Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray"] = Il2CppReferenceArray; allTypes["Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppStructArray"] = Il2CppStructArray; allTypes["Il2CppInterop.Runtime.Il2CppException"] = Il2CppException; @@ -156,6 +163,44 @@ private void InitTypeRefs() private void InitMethodRefs() { + Il2CppArrayBase_set_Item = new((param) => + { + var owner = ResolveType("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase"); + var gp = owner.GenericParameters[0]; + var giOwner = new GenericInstanceType(owner); + giOwner.GenericArguments.Add(param); + var mr = new MethodReference("set_Item", ResolveType("System.Void"), + giOwner) + { HasThis = true }; + mr.Parameters.Add(new ParameterDefinition("", ParameterAttributes.None, ResolveType("System.Int32"))); + mr.Parameters.Add(new ParameterDefinition("", ParameterAttributes.None, gp)); + return mr; + }); + + Il2CppArrayBase_get_Item = new((param) => + { + var owner = ResolveType("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase"); + var gp = owner.GenericParameters[0]; + var giOwner = new GenericInstanceType(owner); + giOwner.GenericArguments.Add(param); + var mr = new MethodReference("get_Item", gp, + giOwner) + { HasThis = true }; + mr.Parameters.Add(new ParameterDefinition("", ParameterAttributes.None, ResolveType("System.Int32"))); + return mr; + }); + + Il2CppArrayBase_get_Length = new((param) => + { + var owner = ResolveType("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppArrayBase"); + var giOwner = new GenericInstanceType(owner); + giOwner.GenericArguments.Add(param); + var mr = new MethodReference("get_Length", ResolveType("System.Int32"), + giOwner) + { HasThis = true }; + return mr; + }); + Il2CppRefrenceArrayctor = new((param) => { var owner = ResolveType("Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray"); @@ -619,5 +664,17 @@ private void InitMethodRefs() methodReference.GenericParameters.Add(new GenericParameter("T", methodReference)); return Module.ImportReference(methodReference); }); + + Il2CppObject = new(() => globalCtx.GetAssemblyByName("mscorlib") + .NewAssembly.MainModule.GetType("Il2CppSystem.Object")); + + Il2CppObject_op_Implicit = new((param) => + Module.ImportReference(Il2CppObject.Value.Methods.Single(m => m.Name == "op_Implicit" && + m.Parameters[0].ParameterType.FullName == param.FullName))); + + Il2CppString_op_Implicit = new(() => + Module.ImportReference(globalCtx.GetAssemblyByName("mscorlib").NewAssembly.MainModule + .GetType("Il2CppSystem.String").Methods.Single(m => m.Name == "op_Implicit" && + m.ReturnType.FullName == "System.String"))); } } diff --git a/Il2CppInterop.Generator/Utils/SimpleJsonWriter.cs b/Il2CppInterop.Generator/Utils/SimpleJsonWriter.cs new file mode 100644 index 00000000..da6781b9 --- /dev/null +++ b/Il2CppInterop.Generator/Utils/SimpleJsonWriter.cs @@ -0,0 +1,183 @@ +using static Il2CppInterop.Generator.Utils.SimpleJsonWriter; + +namespace Il2CppInterop.Generator.Utils; + +internal class SimpleJsonWriter : JsonValue, JsonObject, JsonArray +{ + public interface JsonValue + { + public JsonArray Array(); + public JsonObject Object(); + public void Value(string value); + } + + public interface JsonObject : IDisposable + { + public JsonValue Property(string name); + + } + + public interface JsonArray : JsonValue, IDisposable { } + + public static JsonValue Create(StreamWriter writer) => Create(writer, 0); + + public static JsonValue Create(StreamWriter writer, int startIndentation) => + new SimpleJsonWriter(writer, startIndentation); + + private readonly StreamWriter _writer; + private int _indent; + private bool _newline = true, _comma = false; + private readonly Stack _closeStack = new(); + + private SimpleJsonWriter(StreamWriter writer, int startIndentation) + { + _writer = writer; + _indent = startIndentation; + } + + public JsonArray Array() + { + Open("[", "]"); + return this; + } + + public JsonObject Object() + { + Open("{", "}"); + return this; + } + + public JsonValue Property(string name) + { + Comma(); + Indent(); + name = Encode(name, true); + _writer.Write(name); + _writer.Write(": "); + return this; + } + + public void Value(string value) + { + if (value == null) + _writer.Write("null"); + else + { + value = Encode(value, true); + _writer.Write(value); + } + _comma = true; + } + + private string Encode(string value, bool addDoubleQuotes = false) + { +#if NETSTANDARD + return System.Web.HttpUtility.JavaScriptStringEncode(value, addDoubleQuotes); +#else + // Stolen from https://github.com/mono/mono/blob/89f1d3cc22fd3b0848ecedbd6215b0bdfeea9477/mcs/class/System.Web/System.Web/HttpUtility.cs#L528 + if (string.IsNullOrEmpty(value)) + return addDoubleQuotes ? "\"\"" : string.Empty; + + var len = value.Length; + var needEncode = false; + char c; + for (var i = 0; i < len; i++) + { + c = value[i]; + + if (c >= 0 && c <= 31 || c == 34 || c == 39 || c == 60 || c == 62 || c == 92) + { + needEncode = true; + break; + } + } + + if (!needEncode) + return addDoubleQuotes ? "\"" + value + "\"" : value; + + var sb = new System.Text.StringBuilder(); + if (addDoubleQuotes) + sb.Append('"'); + + for (var i = 0; i < len; i++) + { + c = value[i]; + if (c >= 0 && c <= 7 || c == 11 || c >= 14 && c <= 31 || c == 39 || c == 60 || c == 62) + sb.AppendFormat("\\u{0:x4}", (int)c); + else switch ((int)c) + { + case 8: + sb.Append("\\b"); + break; + + case 9: + sb.Append("\\t"); + break; + + case 10: + sb.Append("\\n"); + break; + + case 12: + sb.Append("\\f"); + break; + + case 13: + sb.Append("\\r"); + break; + + case 34: + sb.Append("\\\""); + break; + + case 92: + sb.Append("\\\\"); + break; + + default: + sb.Append(c); + break; + } + } + + if (addDoubleQuotes) + sb.Append('"'); + + return sb.ToString(); +#endif + } + + private void Comma() + { + if (!_comma) return; + _comma = false; + _writer.WriteLine(','); + _newline = true; + } + + private void Indent() + { + if (!_newline) return; + _writer.Write(new string(' ', _indent * 2)); + _newline = false; + } + + private void Open(string open, string close) + { + Comma(); + Indent(); + _writer.WriteLine(open); + _newline = true; + _indent++; + _closeStack.Push(close); + } + + public void Dispose() + { + var close = _closeStack.Pop(); + _indent--; + Indent(); + _writer.Write(close); + _comma = true; + } +} diff --git a/Il2CppInterop.Generator/Utils/StackWalker.cs b/Il2CppInterop.Generator/Utils/StackWalker.cs new file mode 100644 index 00000000..082cfeb8 --- /dev/null +++ b/Il2CppInterop.Generator/Utils/StackWalker.cs @@ -0,0 +1,139 @@ +using Il2CppInterop.Common; +using Microsoft.Extensions.Logging; +using Mono.Cecil; +using Mono.Cecil.Cil; + +namespace Il2CppInterop.Generator.Utils; + +internal static class StackWalker +{ + public static bool TryWalkStack(RuntimeAssemblyReferences imports, + MethodDefinition target, int stackTarget, + out TypeReference type) + { + return TryWalkStack(imports, target, stackTarget, out type, out var _); + } + + public static bool TryWalkStack(RuntimeAssemblyReferences imports, + MethodDefinition target, int stackTarget, + out TypeReference type, out Instruction source) + { + if (TryWalkStack(imports, target, + target.Body.Instructions.Count - 1, + new[] { stackTarget }, out var results)) + { + type = results[0].type; + source = results[0].source; + return true; + } + type = null; + source = null; + return false; + } + + public static bool TryWalkStack(RuntimeAssemblyReferences imports, + MethodDefinition target, IEnumerable stackTargets, + out (TypeReference type, Instruction source, int index)[] results) + { + return TryWalkStack(imports, target, + target.Body.Instructions.Count - 1, + stackTargets, out results); + } + + /// + /// Walks the instructions of in reverse order + /// starting from .
+ /// The state of the stack is recreated and the s + /// at the positions described by is returned + /// along with the instructions responsible. + ///
+ /// + /// Method to walk + /// Instruction index of where to start walking + /// + /// Stack positions to resolve
+ /// Note that stackTarget 0 is the last argument to a method, not the first (nor ) + /// + /// Resolved s and their source instructions + /// if all where found, otherwise + /// + public static bool TryWalkStack(RuntimeAssemblyReferences imports, + MethodDefinition target, int startInstruction, + IEnumerable stackTargets, out (TypeReference type, Instruction source, int index)[] results) + { + var _stackTargets = new List(stackTargets); + _stackTargets.Sort(); + + results = new (TypeReference type, Instruction source, int index)[_stackTargets.Count]; + for (var i = 0; i < _stackTargets.Count; i++) + results[i].index = _stackTargets[i]; + + var stackPos = 0; + var stackTargetIdx = 0; + var stackTarget = _stackTargets[0]; + + for (var i = startInstruction; i >= 0; i--) + { + var ins = target.Body.Instructions[i]; + if (ins.OpCode.BreaksFlow()) + { + // TODO follow branches + // Without branch logic there's a possibility that we walk the stack incorrectly + // causing the found TypeReference to be bogus + return false; + } + + var nPush = ins.PushAmount(); + if (stackPos == stackTarget && nPush > 0) + { + results[stackTargetIdx].source ??= ins; + var code = ins.OpCode.Code; + if (code == Code.Dup || + code.IsLdind()) + { + stackPos = 0; + for (var j = stackTargetIdx; j < _stackTargets.Count; j++) + _stackTargets[j] -= stackTarget; + stackTarget = 0; + continue; + } + else if (code == Code.Call || + code == Code.Callvirt) + results[stackTargetIdx].type = ((MethodReference)ins.Operand).ReturnType; + else if (code == Code.Newobj) + results[stackTargetIdx].type = ((MethodReference)ins.Operand).DeclaringType; + else if (code == Code.Ldfld || + code == Code.Ldsfld) + results[stackTargetIdx].type = ((FieldReference)ins.Operand).FieldType; + else if (ins.TryGetLdlocIndex(out var varArgIdx)) + { + var varArg = target.Body.Variables[varArgIdx]; + results[stackTargetIdx].type = varArg.VariableType; + } + else if (ins.TryGetLdargIndex(target.HasThis, out var paramArgIdx)) + { + var paramArg = paramArgIdx switch + { + -1 => target.Body.ThisParameter, + _ => target.Parameters[paramArgIdx], + }; + results[stackTargetIdx].type = paramArg.ParameterType; + } + else if (code == Code.Ldstr) + results[stackTargetIdx].type = imports.Module.String(); + else + return false; + + if (++stackTargetIdx < _stackTargets.Count) + stackTarget = _stackTargets[stackTargetIdx]; + else + return true; + } + + var nPop = ins.PopAmount(); + stackPos += nPush - nPop; + } + return false; + } +} + diff --git a/Il2CppInterop.Generator/Utils/UnstripGenerator.cs b/Il2CppInterop.Generator/Utils/UnstripGenerator.cs index dabd4801..c3a95fde 100644 --- a/Il2CppInterop.Generator/Utils/UnstripGenerator.cs +++ b/Il2CppInterop.Generator/Utils/UnstripGenerator.cs @@ -65,7 +65,7 @@ public static void GenerateInvokerMethodBody(MethodDefinition newMethod, FieldDe { var param = newMethod.Parameters[i]; var paramType = param.ParameterType; - if (paramType.IsValueType || (paramType.IsByReference && paramType.GetElementType().IsValueType)) + if (paramType.IsValueType || (paramType is ByReferenceType paramRefType && paramRefType.ElementType.IsValueType)) { body.Emit(OpCodes.Ldarg, i + argOffset); } diff --git a/Il2CppInterop.Generator/Utils/UnstripTranslator.cs b/Il2CppInterop.Generator/Utils/UnstripTranslator.cs index 06df34a0..feca2ce1 100644 --- a/Il2CppInterop.Generator/Utils/UnstripTranslator.cs +++ b/Il2CppInterop.Generator/Utils/UnstripTranslator.cs @@ -3,183 +3,455 @@ using Il2CppInterop.Generator.Passes; using Mono.Cecil; using Mono.Cecil.Cil; +using Mono.Cecil.Rocks; +using static Il2CppInterop.Generator.Contexts.TypeRewriteContext; namespace Il2CppInterop.Generator.Utils; -public static class UnstripTranslator +public class UnstripTranslator { - public static bool TranslateMethod(MethodDefinition original, MethodDefinition target, + + public readonly struct Result + { + public static readonly Result OK = new(ErrorType.None, null, null); + public static Result Unimplemented(Instruction ins) => + new(ErrorType.Unimplemented, ins, null); + + public readonly ErrorType type; + public readonly Instruction offendingInstruction; + public readonly string reason; + public bool IsError => type != ErrorType.None; + + public Result(ErrorType type, Instruction offendingInstruction, string reason) + { + this.type = type; + this.offendingInstruction = offendingInstruction; + this.reason = reason; + } + } + + public enum ErrorType + { + None, + Unimplemented, + Unresolved, + FieldProxy, + NonBlittableStruct, + Stack, + Retargeting, + } + + public static Result TranslateMethod(MethodDefinition original, MethodDefinition target, + TypeRewriteContext typeRewriteContext, RuntimeAssemblyReferences imports) + { + var translator = new UnstripTranslator(original, target, typeRewriteContext, imports); + return translator.Translate(); + } + + private readonly MethodDefinition _original, _target; + private readonly RuntimeAssemblyReferences _imports; + + private readonly RewriteGlobalContext _globalContext; + private readonly RetargetingILProcessor _retargeter; + private RetargetingILProcessor.Builder _targetBuilder; + + private UnstripTranslator(MethodDefinition original, MethodDefinition target, TypeRewriteContext typeRewriteContext, RuntimeAssemblyReferences imports) { - if (!original.HasBody) return true; + _original = original; + _target = target; + _imports = imports; - var globalContext = typeRewriteContext.AssemblyContext.GlobalContext; - foreach (var variableDefinition in original.Body.Variables) + _globalContext = typeRewriteContext.AssemblyContext.GlobalContext; + _retargeter = new RetargetingILProcessor(target); + } + + private Result Translate() + { + if (!_original.HasBody) return Result.OK; + + foreach (var variableDefinition in _original.Body.Variables) { - var variableType = - Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, variableDefinition.VariableType, - imports); - if (variableType == null) return false; - target.Body.Variables.Add(new VariableDefinition(variableType)); + var variableType = variableDefinition.VariableType switch + { + GenericParameter genericParam => genericParam, + _ => Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, variableDefinition.VariableType, _imports), + }; + if (variableType == null) + return new(ErrorType.Unresolved, null, $"Could not resolve variable #{variableDefinition.Index} {variableDefinition.VariableType}"); + _target.Body.Variables.Add(new VariableDefinition(variableType)); } - var targetBuilder = target.Body.GetILProcessor(); - foreach (var bodyInstruction in original.Body.Instructions) - if (bodyInstruction.OpCode.OperandType == OperandType.InlineField) + foreach (var bodyInstruction in _original.Body.Instructions) + using (_targetBuilder = _retargeter.Track(bodyInstruction)) { - var fieldArg = (FieldReference)bodyInstruction.Operand; - var fieldDeclarer = - Pass80UnstripMethods.ResolveTypeInNewAssembliesRaw(globalContext, fieldArg.DeclaringType, imports); - if (fieldDeclarer == null) return false; - var newField = fieldDeclarer.Resolve().Fields.SingleOrDefault(it => it.Name == fieldArg.Name); - if (newField != null) - { - targetBuilder.Emit(bodyInstruction.OpCode, imports.Module.ImportReference(newField)); - } - else - { - if (bodyInstruction.OpCode == OpCodes.Ldfld || bodyInstruction.OpCode == OpCodes.Ldsfld) - { - var getterMethod = fieldDeclarer.Resolve().Properties - .SingleOrDefault(it => it.Name == fieldArg.Name)?.GetMethod; - if (getterMethod == null) return false; - - targetBuilder.Emit(OpCodes.Call, imports.Module.ImportReference(getterMethod)); - } - else if (bodyInstruction.OpCode == OpCodes.Stfld || bodyInstruction.OpCode == OpCodes.Stsfld) - { - var setterMethod = fieldDeclarer.Resolve().Properties - .SingleOrDefault(it => it.Name == fieldArg.Name)?.SetMethod; - if (setterMethod == null) return false; - - targetBuilder.Emit(OpCodes.Call, imports.Module.ImportReference(setterMethod)); - } - else - { - return false; - } - } + var result = Translate(bodyInstruction); + if (result.IsError) + return result; } - else if (bodyInstruction.OpCode.OperandType == OperandType.InlineMethod) + + if (_retargeter.NeedsRetargeting) + { + // This is most likely due to not translating some instructions, + // i.e. the original instruction results in zero instructions in the new method, + // which doesn't really make sense, so something is probably broken + var branches = string.Join("\n\t", _retargeter.IncompleteBranches.Values.SelectMany(x => x)); + return new(ErrorType.Retargeting, null, $"Incomplete branch retargeting:\n\t{branches}"); + } + + // Retargeter uses long branches everywhere for convenience + // but it is safe to optimize them when everything is translated + _target.Body.Optimize(); + return Result.OK; + } + + private Result Translate(Instruction ins) + { + return ins.OpCode.OperandType switch + { + OperandType.InlineField => InlineField(ins), + OperandType.InlineMethod => InlineMethod(ins), + OperandType.InlineType => InlineType(ins), + OperandType.InlineSig => InlineSig(ins), + OperandType.InlineTok => InlineTok(ins), + OperandType.InlineNone => InlineNone(ins), + _ => Copy(ins), + }; + } + + private Result InlineField(Instruction ins) + { + var fieldArg = (FieldReference)ins.Operand; + var fieldDeclarer = + Pass80UnstripMethods.ResolveTypeInNewAssembliesRaw(_globalContext, fieldArg.DeclaringType, _imports); + if (fieldDeclarer == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve declaring type {fieldArg.DeclaringType}"); + + var newField = fieldDeclarer.Resolve().Fields.SingleOrDefault(it => it.Name == fieldArg.Name); + if (newField != null) + { + _targetBuilder.Emit(ins.OpCode, _imports.Module.ImportReference(newField)); + return Result.OK; + } + + if (ins.OpCode == OpCodes.Ldfld || ins.OpCode == OpCodes.Ldsfld || + ins.OpCode == OpCodes.Ldflda || ins.OpCode == OpCodes.Ldsflda) + { + var getterMethod = fieldDeclarer.Resolve().Properties + .SingleOrDefault(it => it.Name == fieldArg.Name)?.GetMethod; + if (getterMethod == null) + return new(ErrorType.FieldProxy, ins, $"Could not find getter for proxy property {fieldArg}"); + _targetBuilder.Emit(OpCodes.Call, _imports.Module.ImportReference(getterMethod)); + + if (ins.OpCode == OpCodes.Ldflda || ins.OpCode == OpCodes.Ldsflda) { - var methodArg = (MethodReference)bodyInstruction.Operand; - var methodDeclarer = - Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, methodArg.DeclaringType, imports); - if (methodDeclarer == null) return false; // todo: generic methods - - var newReturnType = - Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, methodArg.ReturnType, imports); - if (newReturnType == null) return false; - - var newMethod = new MethodReference(methodArg.Name, newReturnType, methodDeclarer); - newMethod.HasThis = methodArg.HasThis; - foreach (var methodArgParameter in methodArg.Parameters) - { - var newParamType = Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, - methodArgParameter.ParameterType, imports); - if (newParamType == null) return false; + TypeReference variableType; + if (getterMethod.ReturnType is GenericParameter genReturnType + && genReturnType.Type == GenericParameterType.Type + && getterMethod.MethodReturnType.Method is MemberReference getterMethodRef + && getterMethodRef.DeclaringType is IGenericInstance genGetterMethod) + variableType = genGetterMethod.GenericArguments[genReturnType.Position]; + else + variableType = getterMethod.ReturnType; - var newParam = new ParameterDefinition(methodArgParameter.Name, methodArgParameter.Attributes, - newParamType); - newMethod.Parameters.Add(newParam); - } + var varDef = new VariableDefinition(variableType); + _target.Body.Variables.Add(varDef); - targetBuilder.Emit(bodyInstruction.OpCode, imports.Module.ImportReference(newMethod)); + _targetBuilder.Emit(OpCodes.Stloc, varDef); + _targetBuilder.Emit(OpCodes.Ldloca, varDef); } - else if (bodyInstruction.OpCode.OperandType == OperandType.InlineType) + return Result.OK; + } + + if (ins.OpCode == OpCodes.Stfld || ins.OpCode == OpCodes.Stsfld) + { + var setterMethod = fieldDeclarer.Resolve().Properties + .SingleOrDefault(it => it.Name == fieldArg.Name)?.SetMethod; + if (setterMethod == null) + return new(ErrorType.FieldProxy, ins, $"Could not find setter for proxy property {fieldArg}"); + + _targetBuilder.Emit(OpCodes.Call, _imports.Module.ImportReference(setterMethod)); + return Result.OK; + } + + return Result.Unimplemented(ins); + } + + private Result InlineMethod(Instruction ins) + { + var methodArg = (MethodReference)ins.Operand; + if (ins.OpCode == OpCodes.Callvirt && + ins.Previous?.OpCode == OpCodes.Constrained && + ins.Previous.Operand is TypeReference constrainedType && + constrainedType.Resolve().IsEnum) + { + // Keep virtual enum calls as they are, they can't (and don't need to) be translated into il2cpp + // E.g: + // constrained. Some.EnumType + // callvirt instance string System.Object::ToString() + _targetBuilder.Emit(ins.OpCode, _imports.Module.ImportReference(methodArg)); + return Result.OK; + } + + var methodDeclarer = + Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, methodArg.DeclaringType, _imports, resolveValueTypes: true); + if (methodDeclarer == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve declaring type {methodArg.DeclaringType}"); + + var newReturnType = methodArg.ReturnType switch + { + GenericParameter genericParam => genericParam, + _ => Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, methodArg.ReturnType, _imports), + }; + if (newReturnType == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve return type {methodArg.ReturnType}"); + + var newMethod = new MethodReference(methodArg.Name, newReturnType, methodDeclarer); + newMethod.HasThis = methodArg.HasThis; + List objectParamIndices = null; + foreach (var methodArgParameter in methodArg.Parameters) + { + var newParamType = methodArgParameter.ParameterType switch { - var targetType = (TypeReference)bodyInstruction.Operand; - if (targetType is GenericParameter genericParam) - { - if (genericParam.Owner is TypeReference paramOwner) - { - var newTypeOwner = - Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, paramOwner, imports); - if (newTypeOwner == null) return false; - targetType = newTypeOwner.GenericParameters.Single(it => it.Name == targetType.Name); - } - else - { - targetType = target.GenericParameters.Single(it => it.Name == targetType.Name); - } - } - else - { - targetType = Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, targetType, imports); - if (targetType == null) return false; - } + ByReferenceType byRef when byRef.ElementType is GenericParameter => byRef, + GenericParameter genericParam => genericParam, + _ => Pass80UnstripMethods.ResolveTypeInNewAssemblies( + _globalContext, methodArgParameter.ParameterType, _imports), + }; + if (newParamType == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve parameter #{methodArgParameter.Index} {methodArgParameter.ParameterType} {methodArgParameter.Name}"); - if (bodyInstruction.OpCode == OpCodes.Castclass && !targetType.IsValueType) - { - targetBuilder.Emit(OpCodes.Call, - imports.Module.ImportReference(new GenericInstanceMethod(imports.Il2CppObjectBase_Cast.Value) - { GenericArguments = { targetType } })); - } - else if (bodyInstruction.OpCode == OpCodes.Isinst && !targetType.IsValueType) - { - targetBuilder.Emit(OpCodes.Call, - imports.Module.ImportReference(new GenericInstanceMethod(imports.Il2CppObjectBase_TryCast.Value) - { GenericArguments = { targetType } })); - } - else if (bodyInstruction.OpCode == OpCodes.Newarr && !targetType.IsValueType) - { - targetBuilder.Emit(OpCodes.Conv_I8); - - var il2cppTypeArray = new GenericInstanceType(imports.Il2CppReferenceArray) - { GenericArguments = { targetType } }; - targetBuilder.Emit(OpCodes.Newobj, imports.Module.ImportReference( - new MethodReference(".ctor", imports.Module.Void(), il2cppTypeArray) - { - HasThis = true, - Parameters = { new ParameterDefinition(imports.Module.Long()) } - })); - } - else + if (newParamType.FullName == _imports.Il2CppObject.Value.FullName) + (objectParamIndices ??= new()) + .Add(methodArgParameter); + + var newParam = new ParameterDefinition(methodArgParameter.Name, methodArgParameter.Attributes, + newParamType); + newMethod.Parameters.Add(newParam); + } + + if (objectParamIndices != null) + { + var stackIndices = objectParamIndices.Select(x => methodArg.Parameters.Count - 1 - x.Index); + if (!StackWalker.TryWalkStack(_imports, _target, stackIndices, out var results)) + return new(ErrorType.Stack, ins, $"Unable to find targets of {objectParamIndices.Count} parameters #" + + string.Join(", #", objectParamIndices.Select(x => x.Index)) + + $" in {ins.OpCode.Name} instruction"); + + foreach (var (targetType, targetInstruction, _) in results) + if (targetType.IsPrimitive || targetType.FullName == "System.String") { - targetBuilder.Emit(bodyInstruction.OpCode, targetType); + var box = _targetBuilder.Create(OpCodes.Call, _imports.Il2CppObject_op_Implicit.Get(targetType)); + _targetBuilder.InsertAfter(targetInstruction, box); } + } + + _targetBuilder.Emit(ins.OpCode, _imports.Module.ImportReference(newMethod)); + return Result.OK; + } + + private Result InlineType(Instruction ins) + { + var targetType = (TypeReference)ins.Operand; + TypeRewriteContext rwContext; + if (targetType is GenericParameter genericParam) + { + if (genericParam.Owner is TypeReference paramTypeOwner) + { + targetType = paramTypeOwner.GenericParameters.Single(it => it.Name == genericParam.Name); + targetType = + Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, targetType, _imports, out rwContext); + if (targetType == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve generic {genericParam.Name} in type owner {paramTypeOwner}"); } - else if (bodyInstruction.OpCode.OperandType == OperandType.InlineSig) + else if (genericParam.Owner is MethodDefinition paramMethodOwner) + { + targetType = paramMethodOwner.GenericParameters.Single(it => it.Name == genericParam.Name); + targetType = + Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, targetType, _imports, out rwContext); + if (targetType == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve generic {genericParam.Name} in method owner {paramMethodOwner}"); + } + else + return new(ErrorType.Unresolved, ins, $"Could not resolve generic {genericParam.Name}, unknown owner {genericParam.Owner.GetType().Name}"); + } + else + { + var oldTargetType = targetType; + targetType = Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, targetType, _imports, out rwContext); + if (targetType == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve type {oldTargetType}"); + } + + if ((ins.OpCode == OpCodes.Castclass || ins.OpCode == OpCodes.Unbox_Any) && + !targetType.IsValueType) + { + // unbox.any T where T is a reference is equivalent to castclass + if (targetType.FullName == "System.String") + _targetBuilder.Emit(OpCodes.Call, _imports.Il2CppString_op_Implicit.Value); + else + _targetBuilder.Emit(OpCodes.Call, + _imports.Module.ImportReference(new GenericInstanceMethod(_imports.Il2CppObjectBase_Cast.Value) + { GenericArguments = { targetType } })); + return Result.OK; + } + + if (ins.OpCode == OpCodes.Isinst && !targetType.IsValueType) + { + _targetBuilder.Emit(OpCodes.Call, + _imports.Module.ImportReference(new GenericInstanceMethod(_imports.Il2CppObjectBase_TryCast.Value) + { GenericArguments = { targetType } })); + return Result.OK; + } + + if (ins.OpCode == OpCodes.Newarr) + { + MethodReference il2cppArrayctor_size; + if (targetType.IsValueType) { - // todo: rewrite sig if this ever happens in unity types - return false; + if (rwContext != null && // primitives does not have a rewrite context but are blittable + rwContext.ComputedTypeSpecifics != TypeSpecifics.BlittableStruct) + return new(ErrorType.NonBlittableStruct, ins, $"Expected {nameof(TypeSpecifics.BlittableStruct)} but found {Enum.GetName(typeof(TypeSpecifics), rwContext.ComputedTypeSpecifics)}"); + il2cppArrayctor_size = _imports.Il2CppStructArrayctor_size.Get(targetType); } - else if (bodyInstruction.OpCode.OperandType == OperandType.InlineTok) + else if (targetType.FullName == "System.String") + il2cppArrayctor_size = _imports.Il2CppStringArrayctor_size.Value; + else + il2cppArrayctor_size = _imports.Il2CppRefrenceArrayctor_size.Get(targetType); + + _targetBuilder.Emit(OpCodes.Conv_I8); + _targetBuilder.Emit(OpCodes.Newobj, _imports.Module.ImportReference( + il2cppArrayctor_size)); + return Result.OK; + } + + if (ins.OpCode == OpCodes.Box) + { + if (targetType.IsPrimitive || targetType.FullName == "System.String") { - var targetTok = bodyInstruction.Operand as TypeReference; - if (targetTok == null) - return false; - if (targetTok is GenericParameter genericParam) - { - if (genericParam.Owner is TypeReference paramOwner) - { - var newTypeOwner = - Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, paramOwner, imports); - if (newTypeOwner == null) return false; - targetTok = newTypeOwner.GenericParameters.Single(it => it.Name == targetTok.Name); - } - else - { - targetTok = target.GenericParameters.Single(it => it.Name == targetTok.Name); - } - } - else - { - targetTok = Pass80UnstripMethods.ResolveTypeInNewAssemblies(globalContext, targetTok, imports); - if (targetTok == null) return false; - } + _targetBuilder.Emit(OpCodes.Call, _imports.Il2CppObject_op_Implicit.Get(targetType)); + return Result.OK; + } + + // TODO implement (blittable?) struct boxing + return Result.Unimplemented(ins); + } - targetBuilder.Emit(OpCodes.Call, - imports.Module.ImportReference( - new GenericInstanceMethod(imports.Il2CppSystemRuntimeTypeHandleGetRuntimeTypeHandle.Value) - { GenericArguments = { targetTok } })); + if (ins.OpCode == OpCodes.Unbox || ins.OpCode == OpCodes.Unbox_Any) + { + if (targetType.FullName == "System.String") + { + _targetBuilder.Emit(OpCodes.Call, _imports.Il2CppString_op_Implicit.Value); + return Result.OK; + } + + // TODO implement unboxing + // The type on the stack is not System.Object, it is Il2CppSystem.Object + return Result.Unimplemented(ins); + } + + if (ins.OpCode == OpCodes.Ldelema || + ins.OpCode == OpCodes.Ldelem_Any) + { + // TODO implement Ldelema with Stloc & Ldloca + // TODO implement Ldelem_Any + // Can probably share code with Stelem/Ldelem/Ldlen + return Result.Unimplemented(ins); + } + + _targetBuilder.Emit(ins.OpCode, targetType); + return Result.OK; + } + + private Result InlineSig(Instruction ins) + { + // todo: rewrite sig if this ever happens in unity types + return Result.Unimplemented(ins); + } + + private Result InlineTok(Instruction ins) + { + var targetTok = ins.Operand as TypeReference; + if (targetTok == null) + return Result.Unimplemented(ins); + + if (targetTok is GenericParameter genericParam) + { + if (genericParam.Owner is TypeReference paramOwner) + { + var newTypeOwner = + Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, paramOwner, _imports); + if (newTypeOwner == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve owner type {paramOwner}"); + targetTok = newTypeOwner.GenericParameters.Single(it => it.Name == targetTok.Name); } else { - targetBuilder.Append(bodyInstruction); + targetTok = _target.GenericParameters.Single(it => it.Name == targetTok.Name); + } + } + else + { + var oldTargetTok = targetTok; + targetTok = Pass80UnstripMethods.ResolveTypeInNewAssemblies(_globalContext, targetTok, _imports); + if (targetTok == null) + return new(ErrorType.Unresolved, ins, $"Could not resolve type {oldTargetTok}"); + } + + _targetBuilder.Emit(OpCodes.Call, + _imports.Module.ImportReference( + new GenericInstanceMethod(_imports.Il2CppSystemRuntimeTypeHandleGetRuntimeTypeHandle.Value) + { GenericArguments = { targetTok } })); + return Result.OK; + } + + private Result InlineNone(Instruction ins) + { + var code = ins.OpCode.Code; + var stackTarget = code switch + { + _ when code.IsStelem() => 2, + _ when code.IsLdelem() => 1, + Code.Ldlen => 0, + _ => -1, + }; + if (stackTarget >= 0) + { + if (!StackWalker.TryWalkStack(_imports, _target, stackTarget, out var il2cppArray)) + { + // TODO follow branches in StackWalker + // We're probably here because of our naive stack walking + return new(ErrorType.Stack, ins, $"Unable to find target of {ins.OpCode.Name}"); } - return true; + TypeReference genericArg; + if (il2cppArray == _imports.Il2CppStringArray) + genericArg = _imports.Module.String(); + else if (il2cppArray is GenericInstanceType il2cppArrayInstance && ( + il2cppArrayInstance.ElementType == _imports.Il2CppReferenceArray || + il2cppArrayInstance.ElementType == _imports.Il2CppStructArray)) + genericArg = il2cppArrayInstance.GenericArguments[0]; + else + return new(ErrorType.Stack, ins, $"Unexpected target of {ins.OpCode.Name} is {il2cppArray}"); + + var mRef = stackTarget switch + { + 2 => _imports.Il2CppArrayBase_set_Item.Get(genericArg), + 1 => _imports.Il2CppArrayBase_get_Item.Get(genericArg), + _ => _imports.Il2CppArrayBase_get_Length.Get(genericArg), + }; + _targetBuilder.Emit(OpCodes.Callvirt, _imports.Module.ImportReference(mRef)); + return Result.OK; + } + + _targetBuilder.Append(ins); + return Result.OK; + } + + private Result Copy(Instruction ins) + { + _targetBuilder.Append(ins); + return Result.OK; } public static void ReplaceBodyWithException(MethodDefinition newMethod, RuntimeAssemblyReferences imports) @@ -194,3 +466,4 @@ public static void ReplaceBodyWithException(MethodDefinition newMethod, RuntimeA processor.Emit(OpCodes.Ret); } } +