From ad6403c369125a140e015d16767813f835d70929 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Sat, 19 Oct 2024 11:33:28 -0700 Subject: [PATCH] Support emitting metadata for explicit interface implementations --- Cpp2IL.Core/Cpp2IlApi.cs | 3 + .../AsmResolverDummyDllOutputFormat.cs | 1 + .../AsmResolverAssemblyPopulator.cs | 76 ++++++++++ .../Utils/AsmResolver/AsmResolverUtils.cs | 131 ++++++++++++------ 4 files changed, 166 insertions(+), 45 deletions(-) diff --git a/Cpp2IL.Core/Cpp2IlApi.cs b/Cpp2IL.Core/Cpp2IlApi.cs index 9e1ffbf7..3c56da19 100644 --- a/Cpp2IL.Core/Cpp2IlApi.cs +++ b/Cpp2IL.Core/Cpp2IlApi.cs @@ -8,6 +8,7 @@ using Cpp2IL.Core.Logging; using Cpp2IL.Core.Model.Contexts; using Cpp2IL.Core.Utils; +using Cpp2IL.Core.Utils.AsmResolver; using LibCpp2IL; using LibCpp2IL.Logging; @@ -116,6 +117,8 @@ public static void ResetInternalState() MiscUtils.Reset(); + AsmResolverUtils.Reset(); + LibCpp2IlMain.Reset(); CurrentAppContext = null; diff --git a/Cpp2IL.Core/OutputFormats/AsmResolverDummyDllOutputFormat.cs b/Cpp2IL.Core/OutputFormats/AsmResolverDummyDllOutputFormat.cs index e906ae1d..a31b98a9 100644 --- a/Cpp2IL.Core/OutputFormats/AsmResolverDummyDllOutputFormat.cs +++ b/Cpp2IL.Core/OutputFormats/AsmResolverDummyDllOutputFormat.cs @@ -92,6 +92,7 @@ public List BuildAssemblies(ApplicationAnalysisContext conte #endif MiscUtils.ExecuteParallel(context.Assemblies, AsmResolverAssemblyPopulator.CopyDataFromIl2CppToManaged); + MiscUtils.ExecuteParallel(context.Assemblies, AsmResolverAssemblyPopulator.InferExplicitInterfaceImplementations); MiscUtils.ExecuteParallel(context.Assemblies, FillMethodBodies); Logger.VerboseNewline($"{(DateTime.Now - start).TotalMilliseconds:F1}ms", "DllOutput"); diff --git a/Cpp2IL.Core/Utils/AsmResolver/AsmResolverAssemblyPopulator.cs b/Cpp2IL.Core/Utils/AsmResolver/AsmResolverAssemblyPopulator.cs index 5f202503..9210404b 100644 --- a/Cpp2IL.Core/Utils/AsmResolver/AsmResolverAssemblyPopulator.cs +++ b/Cpp2IL.Core/Utils/AsmResolver/AsmResolverAssemblyPopulator.cs @@ -526,4 +526,80 @@ private static void CopyEventsInType(ReferenceImporter importer, TypeAnalysisCon ilTypeDefinition.Events.Add(managedEvent); } } + + public static void InferExplicitInterfaceImplementations(AssemblyAnalysisContext asmContext) + { + var managedAssembly = asmContext.GetExtraData("AsmResolverAssembly") ?? throw new("AsmResolver assembly not found in assembly analysis context for " + asmContext); + + var importer = managedAssembly.GetImporter(); + + foreach (var typeContext in asmContext.Types) + { + if (IsTypeContextModule(typeContext)) + continue; + + var managedType = typeContext.GetExtraData("AsmResolverType") ?? throw new($"AsmResolver type not found in type analysis context for {typeContext.Definition?.FullName}"); + +#if !DEBUG + try +#endif + { + InferExplicitInterfaceImplementations(managedType, importer); + } +#if !DEBUG + catch (Exception e) + { + throw new Exception($"Failed to process type {managedType.FullName} (module {managedType.Module?.Name}, declaring type {managedType.DeclaringType?.FullName}) in {asmContext.Definition.AssemblyName.Name}", e); + } +#endif + } + } + + private static void InferExplicitInterfaceImplementations(TypeDefinition type, ReferenceImporter importer) + { + foreach (var method in type.Methods) + { + if (Utf8String.IsNullOrEmpty(method.Name)) + continue; + + // Explicit interface implementation + // Note: This does not handle all cases. + // Specifically, it does not handle the case where the interface has multiple methods with the same name. + var periodLastIndex = method.Name.LastIndexOf('.'); + if (periodLastIndex < 0 || !method.IsPrivate || !method.IsVirtual || !method.IsFinal || !method.IsNewSlot) + { + continue; + } + + var methodName = method.Name.Value[(periodLastIndex + 1)..]; + var interfaceName = method.Name.Value[..periodLastIndex]; + var genericParameterNames = type.GenericParameters.Count > 0 + ? type.GenericParameters.Select(p => (string?)p.Name ?? "").ToArray() + : []; + var interfaceType = AsmResolverUtils.TryLookupTypeSignatureByName(interfaceName, genericParameterNames); + + IMethodDefOrRef? interfaceMethod = null; + var underlyingInterface = interfaceType?.GetUnderlyingTypeDefOrRef(); + foreach (var interfaceMethodDef in (underlyingInterface as TypeDefinition)?.Methods ?? []) + { + if (interfaceMethodDef.Name != methodName) + continue; + + if (interfaceMethod is not null) + { + // Ambiguity. Checking the method signature would be required to disambiguate. + interfaceMethod = null; + break; + } + + interfaceMethod = new MemberReference(interfaceType?.ToTypeDefOrRef(), interfaceMethodDef.Name, interfaceMethodDef.Signature); + } + + if (interfaceMethod != null) + { + type.MethodImplementations.Add(new MethodImplementation(importer.ImportMethod(interfaceMethod), method)); + } + } + } + } diff --git a/Cpp2IL.Core/Utils/AsmResolver/AsmResolverUtils.cs b/Cpp2IL.Core/Utils/AsmResolver/AsmResolverUtils.cs index 99c90a95..07427eb5 100644 --- a/Cpp2IL.Core/Utils/AsmResolver/AsmResolverUtils.cs +++ b/Cpp2IL.Core/Utils/AsmResolver/AsmResolverUtils.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Linq; using AsmResolver.DotNet; @@ -12,12 +13,22 @@ namespace Cpp2IL.Core.Utils.AsmResolver; public static class AsmResolverUtils { - private static readonly Dictionary CachedTypeDefsByName = new(); + private static readonly Dictionary CachedTypeDefsByName = new(); + private static readonly Dictionary CachedTypeSignaturesByName = new(); private static readonly ConcurrentDictionary ImportersByAssembly = new(); public static readonly ConcurrentDictionary TypeDefsByIndex = new(); public static readonly ConcurrentDictionary GenericParamsByIndexNew = new(); + internal static void Reset() + { + CachedTypeDefsByName.Clear(); + CachedTypeSignaturesByName.Clear(); + ImportersByAssembly.Clear(); + TypeDefsByIndex.Clear(); + GenericParamsByIndexNew.Clear(); + } + public static TypeDefinition GetPrimitiveTypeDef(Il2CppTypeEnum type) => type switch { @@ -188,29 +199,54 @@ public static ITypeDefOrRef ImportReferenceFromIl2CppType(ModuleDefinition modul } } - public static TypeDefinition? TryLookupTypeDefKnownNotGeneric(string? name) => TryLookupTypeDefByName(name)?.typeDefinition; - - public static (TypeDefinition typeDefinition, string[] genericParams)? TryLookupTypeDefByName(string? name) + public static TypeDefinition? TryLookupTypeDefKnownNotGeneric(string? name) { if (name == null) return null; + if (TypeDefinitionsAsmResolver.GetPrimitive(name) is { } primitive) + return primitive; + var key = name.ToLower(CultureInfo.InvariantCulture); if (CachedTypeDefsByName.TryGetValue(key, out var ret)) return ret; - var result = InternalTryLookupTypeDefByName(name); + var definedType = Cpp2IlApi.CurrentAppContext!.AllTypes.FirstOrDefault(t => t.Definition != null && string.Equals(t.Definition.FullName, name, StringComparison.OrdinalIgnoreCase)); + + //Try subclasses + definedType ??= Cpp2IlApi.CurrentAppContext.AllTypes.FirstOrDefault(t => + { + return t.Definition?.FullName != null + && t.Definition.FullName.Contains('/') + && string.Equals(t.Definition.FullName.Replace('/', '.'), name, StringComparison.OrdinalIgnoreCase); + }); + + return definedType?.GetExtraData("AsmResolverType"); + } + + public static TypeSignature? TryLookupTypeSignatureByName(string? name, ReadOnlySpan genericParameterNames = default) + { + if (name == null) + return null; + + var key = name.ToLower(CultureInfo.InvariantCulture); + + if (genericParameterNames.Length == 0 && CachedTypeSignaturesByName.TryGetValue(key, out var ret)) + return ret; + + var result = InternalTryLookupTypeSignatureByName(name, genericParameterNames); - CachedTypeDefsByName[key] = result; + if (genericParameterNames.Length == 0) + CachedTypeSignaturesByName[key] = result; return result; } - private static (TypeDefinition typeDefinition, string[] genericParams)? InternalTryLookupTypeDefByName(string name) + private static TypeSignature? InternalTryLookupTypeSignatureByName(string name, ReadOnlySpan genericParameterNames = default) { if (TypeDefinitionsAsmResolver.GetPrimitive(name) is { } primitive) - return new(primitive, []); + return primitive.ToTypeSignature(); //The only real cases we end up here are: //From explicit override resolving, because that has to be done by name @@ -219,57 +255,62 @@ private static (TypeDefinition typeDefinition, string[] genericParams)? Internal //And during exception helper location, which is always a system type. //So really the only remapping we should have to handle is during explicit override restoration. - var definedType = Cpp2IlApi.CurrentAppContext!.AllTypes.FirstOrDefault(t => t.Definition != null && string.Equals(t.Definition.FullName, name, StringComparison.OrdinalIgnoreCase)); - - if (name.EndsWith("[]")) + if (name.EndsWith("[]", StringComparison.Ordinal)) { var without = name[..^2]; - var result = TryLookupTypeDefByName(without); - return result; + var result = InternalTryLookupTypeSignatureByName(without, genericParameterNames); + return result?.MakeSzArrayType(); } - //Generics are dumb. - var genericParams = Array.Empty(); - if (definedType == null && name.Contains("<")) - { - //Replace < > with the number of generic params after a ` - genericParams = MiscUtils.GetGenericParams(name[(name.IndexOf("<", StringComparison.Ordinal) + 1)..^1]); - name = name[..name.IndexOf("<", StringComparison.Ordinal)]; - if (!name.Contains("`")) - name = name + "`" + (genericParams.Length); + var parsedType = Parse(name); - definedType = Cpp2IlApi.CurrentAppContext.AllTypes.FirstOrDefault(t => t.Definition?.FullName == name); - } + // Arrays should be handled above + Debug.Assert(parsedType.Suffix is ""); - if (definedType != null) return (definedType.GetExtraData("AsmResolverType")!, genericParams); + var genericParameterIndex = genericParameterNames.IndexOf(parsedType.BaseType); + if (genericParameterIndex >= 0) + return new GenericParameterSignature(GenericParameterType.Type, genericParameterIndex); + + var baseType = TryLookupTypeDefKnownNotGeneric(parsedType.BaseType); + if (baseType == null) + return null; - //It's possible they didn't specify a `System.` prefix - var searchString = $"System.{name}"; - definedType = Cpp2IlApi.CurrentAppContext.AllTypes.FirstOrDefault(t => string.Equals(t.Definition?.FullName, searchString, StringComparison.OrdinalIgnoreCase)); + if (parsedType.GenericArguments.Length == 0) + return baseType.ToTypeSignature(); - if (definedType != null) return (definedType.GetExtraData("AsmResolverType")!, genericParams); + var typeArguments = new TypeSignature[parsedType.GenericArguments.Length]; + for (var i = 0; i < parsedType.GenericArguments.Length; i++) + { + var typeArgument = InternalTryLookupTypeSignatureByName(parsedType.GenericArguments[i], genericParameterNames); + if (typeArgument == null) + return null; + typeArguments[i] = typeArgument; + } - //Still not got one? Ok, is there only one match for non FQN? - var matches = Cpp2IlApi.CurrentAppContext.AllTypes.Where(t => string.Equals(t.Definition?.Name, name, StringComparison.OrdinalIgnoreCase)).ToList(); - if (matches.Count == 1) - definedType = matches.First(); + return baseType.MakeGenericInstanceType(typeArguments); + } - if (definedType != null) - return (definedType.GetExtraData("AsmResolverType")!, genericParams); + private readonly record struct ParsedTypeString(string BaseType, string Suffix, string[] GenericArguments); - if (!name.Contains(".")) - return null; + private static ParsedTypeString Parse(string name) + { + var firstAngleBracket = name.IndexOf('<'); + if (firstAngleBracket < 0) + { + var firstSquareBracket = name.IndexOf('['); + if (firstSquareBracket < 0) + return new ParsedTypeString(name, "", []); + else + return new ParsedTypeString(name[..firstSquareBracket], name[(firstSquareBracket + 1)..], []); + } - searchString = name; - //Try subclasses - matches = Cpp2IlApi.CurrentAppContext.AllTypes.Where(t => t.Definition != null && t.Definition.FullName!.Replace('/', '.').EndsWith(searchString)).ToList(); - if (matches.Count == 1) - definedType = matches.First(); + var lastAngleBracket = name.LastIndexOf('>'); + var genericParams = MiscUtils.GetGenericParams(name[(firstAngleBracket + 1)..(lastAngleBracket)]); - if (definedType == null) - return null; + var baseType = $"{name[..firstAngleBracket]}`{genericParams.Length}"; + var suffix = name[(lastAngleBracket + 1)..]; - return new(definedType.GetExtraData("AsmResolverType")!, genericParams); + return new ParsedTypeString(baseType, suffix, genericParams); } public static ReferenceImporter GetImporter(this AssemblyDefinition assemblyDefinition)