Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support emitting metadata for explicit interface implementations #346

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cpp2IL.Core/Cpp2IlApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -116,6 +117,8 @@ public static void ResetInternalState()

MiscUtils.Reset();

AsmResolverUtils.Reset();

LibCpp2IlMain.Reset();

CurrentAppContext = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ public List<AssemblyDefinition> 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");
Expand Down
76 changes: 76 additions & 0 deletions Cpp2IL.Core/Utils/AsmResolver/AsmResolverAssemblyPopulator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssemblyDefinition>("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<TypeDefinition>("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));
}
}
}

}
131 changes: 86 additions & 45 deletions Cpp2IL.Core/Utils/AsmResolver/AsmResolverUtils.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -12,12 +13,22 @@ namespace Cpp2IL.Core.Utils.AsmResolver;

public static class AsmResolverUtils
{
private static readonly Dictionary<string, (TypeDefinition typeDefinition, string[] genericParams)?> CachedTypeDefsByName = new();
private static readonly Dictionary<string, TypeDefinition?> CachedTypeDefsByName = new();
private static readonly Dictionary<string, TypeSignature?> CachedTypeSignaturesByName = new();
private static readonly ConcurrentDictionary<AssemblyDefinition, ReferenceImporter> ImportersByAssembly = new();

public static readonly ConcurrentDictionary<long, TypeDefinition> TypeDefsByIndex = new();
public static readonly ConcurrentDictionary<long, GenericParameter> GenericParamsByIndexNew = new();

internal static void Reset()
{
CachedTypeDefsByName.Clear();
CachedTypeSignaturesByName.Clear();
ImportersByAssembly.Clear();
TypeDefsByIndex.Clear();
GenericParamsByIndexNew.Clear();
}

public static TypeDefinition GetPrimitiveTypeDef(Il2CppTypeEnum type) =>
type switch
{
Expand Down Expand Up @@ -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<TypeDefinition>("AsmResolverType");
}

public static TypeSignature? TryLookupTypeSignatureByName(string? name, ReadOnlySpan<string> 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<string> 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
Expand All @@ -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<string>();
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<TypeDefinition>("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<TypeDefinition>("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<TypeDefinition>("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<TypeDefinition>("AsmResolverType")!, genericParams);
return new ParsedTypeString(baseType, suffix, genericParams);
}

public static ReferenceImporter GetImporter(this AssemblyDefinition assemblyDefinition)
Expand Down
Loading