Skip to content
Open
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
51 changes: 51 additions & 0 deletions Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
using System.Linq;
using Cpp2IL.Core.Model.Contexts;

namespace Cpp2IL.Core.Tests;

public class TypeAnalysisContextTests
Expand Down Expand Up @@ -49,4 +52,52 @@ public void StaticClassesHaveObjectBaseType()
Assert.That(count, Is.GreaterThan(0));
}
}

[Test]
public void GenericInstanceTypeHasInstantiatedBaseType()
{
var appContext = TestGameLoader.LoadSimple2019Game();

// ObjectComparer<T> inherits from Comparer<T>
var objectComparerType = appContext.SystemTypes.SystemObjectType.DeclaringAssembly.GetTypeByFullName("System.Collections.Generic.ObjectComparer`1");

Assert.That(objectComparerType, Is.Not.Null);
Assert.That(objectComparerType.BaseType, Is.Not.Null);
using (Assert.EnterMultipleScope())
{
Assert.That(objectComparerType.GenericParameters, Has.Count.EqualTo(1));
Assert.That(objectComparerType.BaseType, Is.InstanceOf<GenericInstanceTypeAnalysisContext>());
}
Assert.That(((GenericInstanceTypeAnalysisContext)objectComparerType.BaseType).GenericArguments[0], Is.EqualTo(objectComparerType.GenericParameters[0]));

var objectComparerStringType = objectComparerType.MakeGenericInstanceType(appContext.SystemTypes.SystemStringType);
Assert.That(objectComparerStringType.BaseType, Is.Not.Null);
Assert.That(objectComparerStringType.BaseType, Is.InstanceOf<GenericInstanceTypeAnalysisContext>());

var baseType = (GenericInstanceTypeAnalysisContext)objectComparerStringType.BaseType;
Assert.That(baseType.GenericArguments, Has.Count.EqualTo(1));
Assert.That(baseType.GenericArguments[0], Is.EqualTo(appContext.SystemTypes.SystemStringType));
}

[Test]
public void SelfReferencingGenericInstanceTypeHasInstantiatedInterfaces()
{
var appContext = TestGameLoader.LoadSimple2019Game();

// NativeArray<T> implements IEquatable<NativeArray<T>>
var iequatableType = appContext.SystemTypes.SystemObjectType.DeclaringAssembly.GetTypeByFullName("System.IEquatable`1");
var nativeArrayType = appContext.AssembliesByName["UnityEngine.CoreModule"].GetTypeByFullName("Unity.Collections.NativeArray`1");

using (Assert.EnterMultipleScope())
{
Assert.That(iequatableType, Is.Not.Null);
Assert.That(nativeArrayType, Is.Not.Null);
}

var nativeByteArrayType = nativeArrayType.MakeGenericInstanceType(appContext.SystemTypes.SystemByteType);
var implementedInterface = nativeByteArrayType.InterfaceContexts.OfType<GenericInstanceTypeAnalysisContext>().FirstOrDefault(i => i.GenericType == iequatableType);
Assert.That(implementedInterface, Is.Not.Null);
Assert.That(implementedInterface.GenericArguments, Has.Count.EqualTo(1));
Assert.That(implementedInterface.GenericArguments[0], Is.EqualTo(nativeByteArrayType));
}
}
2 changes: 1 addition & 1 deletion Cpp2IL.Core/Cpp2IL.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<Description>Reverses Unity's IL2CPP Build Process</Description>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<LangVersion>13</LangVersion>
<LangVersion>14</LangVersion>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<PackageId>Samboy063.Cpp2IL.Core</PackageId>
Expand Down
4 changes: 4 additions & 0 deletions Cpp2IL.Core/Model/Contexts/ApplicationAnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ public class ApplicationAnalysisContext : ContextWithDataStorage
/// Cache for <see cref="GenericInstanceTypeAnalysisContext.GetOrCreate(Il2CppType, AssemblyAnalysisContext)"/>
/// </summary>
internal readonly ConcurrentDictionary<Il2CppType, Lazy<GenericInstanceTypeAnalysisContext>> GenericInstanceTypesByIl2CppType = new();
/// <summary>
/// Cache for <see cref="GenericInstanceTypeAnalysisContext.GetOrCreate(TypeAnalysisContext, IEnumerable{TypeAnalysisContext})"/>
/// </summary>
internal readonly ConcurrentDictionary<(TypeAnalysisContext, GenericArgumentList), Lazy<GenericInstanceTypeAnalysisContext>> GenericInstanceTypesByConstruction = new();

public ApplicationAnalysisContext(LibCpp2IlContext context)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ private static TypeAnalysisContext ResolveDeclaringType(Cpp2IlMethodRef methodRe

var genericParams = ResolveTypeArray(methodRef.TypeGenericParams, appContext);

return new GenericInstanceTypeAnalysisContext(baseType, genericParams);
return GenericInstanceTypeAnalysisContext.GetOrCreate(baseType, genericParams);
}

private static TypeAnalysisContext[] ResolveTypeArray(Il2CppTypeReflectionData[] array, ApplicationAnalysisContext appContext)
Expand Down
21 changes: 21 additions & 0 deletions Cpp2IL.Core/Model/Contexts/GenericArgumentList.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;

namespace Cpp2IL.Core.Model.Contexts;

internal readonly record struct GenericArgumentList(List<TypeAnalysisContext> Arguments)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This list is a) exposed publicly, b) used for hash code calculation, and c) the same reference is exposed as GenericArguments on the GIT. GenericArgumentList is used as part of the key in the GenericInstanceTypesByConstruction dict. Creates kind of a footgun because someone could change a GIT's arguments, which would change the key of an item in the dictionary which can break shit. E.g. if you have a Dictionary<string, string>, change it to Dictionary<string, object>, then call GetOrCreate to try to get a Dictionary<string, object>, the key won't be found in the dict because its hashcode has changed, and you'll get a second (or potentially third, when combined with the other issue) instance for the same GIT.

{
public static implicit operator GenericArgumentList(List<TypeAnalysisContext> arguments) => new(arguments);
public static implicit operator List<TypeAnalysisContext>(GenericArgumentList list) => list.Arguments;

public bool Equals(GenericArgumentList other) => Arguments.SequenceEqual(other.Arguments);
public override int GetHashCode()
{
HashCode hash = new();
foreach (var arg in Arguments)
hash.Add(arg);
return hash.ToHashCode();
}
public override string? ToString() => Arguments?.ToString();
}
89 changes: 73 additions & 16 deletions Cpp2IL.Core/Model/Contexts/GenericInstanceTypeAnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public class GenericInstanceTypeAnalysisContext : ReferencedTypeAnalysisContext
{
public TypeAnalysisContext GenericType { get; }

public List<TypeAnalysisContext> GenericArguments { get; } = [];
public List<TypeAnalysisContext> GenericArguments { get; }

public sealed override TypeAttributes DefaultAttributes => GenericType.DefaultAttributes;

Expand All @@ -34,7 +34,27 @@ public sealed override string? OverrideNamespace
set => GenericType.OverrideNamespace = value;
}

public sealed override TypeAnalysisContext? DefaultBaseType { get; }
public sealed override TypeAnalysisContext? DefaultBaseType
{
get
{
field ??= GenericType.DefaultBaseType is null ? null : GenericInstantiation.Instantiate(GenericType.DefaultBaseType, GenericArguments, []);
return field;
}
}

public sealed override TypeAnalysisContext? OverrideBaseType
{
get
{
if (base.OverrideBaseType is null && GenericType.OverrideBaseType is not null)
{
base.OverrideBaseType = GenericInstantiation.Instantiate(GenericType.OverrideBaseType, GenericArguments, []);
}
return base.OverrideBaseType;
}
set => throw new NotSupportedException();
}

public sealed override Il2CppTypeEnum Type => Il2CppTypeEnum.IL2CPP_TYPE_GENERICINST;

Expand All @@ -43,35 +63,53 @@ public sealed override string? OverrideNamespace
public sealed override bool IsValueType => GenericType.IsValueType; //We don't set a definition so the default implementation cannot determine if we're a value type or not.

// instances being constructed on the current thread, keyed by their cache identity (see GetOrCreate for why)
[ThreadStatic] private static Dictionary<(ApplicationAnalysisContext, Il2CppType), GenericInstanceTypeAnalysisContext>? _underConstruction;
[ThreadStatic] private static Dictionary<(ApplicationAnalysisContext, Il2CppType), GenericInstanceTypeAnalysisContext>? _underConstruction1;
[ThreadStatic] private static Dictionary<(ApplicationAnalysisContext, TypeAnalysisContext, GenericArgumentList), GenericInstanceTypeAnalysisContext>? _underConstruction2;

private GenericInstanceTypeAnalysisContext(Il2CppType rawType, ApplicationAnalysisContext context) : base(context.ResolveContextForAssembly(rawType.GetGenericClass().TypeDefinition.DeclaringAssembly!))
{
var underConstruction = _underConstruction ??= new();
underConstruction[(context, rawType)] = this;
var underConstruction1 = _underConstruction1 ??= new();
underConstruction1[(context, rawType)] = this;
try
{
//Generic type has to be a type definition
var gClass = rawType.GetGenericClass();
GenericType = context.ResolveContextForType(gClass.TypeDefinition) ?? throw new($"Could not resolve type {gClass.TypeDefinition.FullName} for generic instance base type");

GenericArguments.AddRange(gClass.Context.ClassInst!.Types.Select(context.ResolveIl2CppType)!);

SetDeclaringType();
GenericType = AppContext.ResolveContextForType(gClass.TypeDefinition) ?? throw new($"Could not resolve type {gClass.TypeDefinition.FullName} for generic instance base type");

GenericArguments = [.. gClass.Context.ClassInst!.Types.Select(t => context.ResolveIl2CppType(t))];

var underConstruction2 = _underConstruction2 ??= new();
underConstruction2.Add((context, GenericType, GenericArguments), this);
try
{
SetDeclaringType();
}
finally
{
underConstruction2.Remove((context, GenericType, GenericArguments));
}
}
finally
{
underConstruction.Remove((context, rawType));
underConstruction1.Remove((context, rawType));
}
}

public GenericInstanceTypeAnalysisContext(TypeAnalysisContext genericType, IEnumerable<TypeAnalysisContext> genericArguments) : base(genericType.CustomAttributeAssembly)
private GenericInstanceTypeAnalysisContext(TypeAnalysisContext genericType, List<TypeAnalysisContext> genericArguments) : base(genericType.DeclaringAssembly)
{
GenericType = genericType;
GenericArguments.AddRange(genericArguments);
DefaultBaseType = genericType.BaseType;
GenericArguments = genericArguments;

SetDeclaringType();
var underConstruction = _underConstruction2 ??= new();
underConstruction.Add((genericType.AppContext, genericType, genericArguments), this);
try
{
SetDeclaringType();
}
finally
{
underConstruction.Remove((genericType.AppContext, genericType, genericArguments));
}
}

/// <summary>
Expand All @@ -87,14 +125,28 @@ public static GenericInstanceTypeAnalysisContext GetOrCreate(Il2CppType rawType,

// A self-referencing generic re-enters here while we're still building it (#469). In this case, the constructing thread gets
// its own in-progress instance from a thread-local map instead (otherwise it recurses into Lazy.Value, which throws).
if (_underConstruction != null && _underConstruction.TryGetValue((referencedFrom, rawType), out var partial))
if (_underConstruction1 != null && _underConstruction1.TryGetValue((referencedFrom, rawType), out var partial))
return partial;

return referencedFrom.GenericInstanceTypesByIl2CppType
.GetOrAdd(rawType, key => new Lazy<GenericInstanceTypeAnalysisContext>(() => new GenericInstanceTypeAnalysisContext(key, referencedFrom)))
.Value;
}

public static GenericInstanceTypeAnalysisContext GetOrCreate(TypeAnalysisContext genericType, IEnumerable<TypeAnalysisContext> genericArguments)
{
var genericArgumentsList = genericArguments.ToList();

// A self-referencing generic re-enters here while we're still building it (#469). In this case, the constructing thread gets
// its own in-progress instance from a thread-local map instead (otherwise it recurses into Lazy.Value, which throws).
if (_underConstruction2 != null && _underConstruction2.TryGetValue((genericType.AppContext, genericType, genericArgumentsList), out var partial))
return partial;

return genericType.AppContext.GenericInstanceTypesByConstruction

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we have 2 separate caches now, GenericInstanceTypesByIl2CppType, and this one. Which means you can have 2 instances of the exact same GIT, both considered canonical, in 2 different dictionaries, and break reference equality, which we do rely on in some places. Needs to be unified somehow.

.GetOrAdd((genericType, genericArgumentsList), key => new Lazy<GenericInstanceTypeAnalysisContext>(() => new GenericInstanceTypeAnalysisContext(key.Item1, key.Item2)))
.Value;
}

public override string GetCSharpSourceString()
{
var sb = new StringBuilder();
Expand All @@ -117,6 +169,11 @@ public override string GetCSharpSourceString()
return sb.ToString();
}

protected override List<TypeAnalysisContext> GetInterfaceContexts()
{
return GenericType.InterfaceContexts.Select(i => GenericInstantiation.Instantiate(i, GenericArguments, [])).ToList();
}

private void SetDeclaringType()
{
var declaringType = GenericType.DeclaringType;
Expand Down
7 changes: 4 additions & 3 deletions Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public TypeAttributes Attributes

public virtual TypeAnalysisContext? DefaultBaseType => Definition == null || DefaultAttributes.HasFlag(TypeAttributes.Interface) ? null : AppContext.ResolveIl2CppType(Definition.RawBaseType);

public TypeAnalysisContext? OverrideBaseType { get; set; }
public virtual TypeAnalysisContext? OverrideBaseType { get; set; }

public TypeAnalysisContext? BaseType
{
Expand All @@ -97,10 +97,11 @@ public List<TypeAnalysisContext> InterfaceContexts
get
{
// Lazy load the interface contexts
_interfaceContexts ??= (Definition?.RawInterfaces.Select(AppContext.ResolveIl2CppType).ToList() ?? [])!;
_interfaceContexts ??= GetInterfaceContexts();
return _interfaceContexts;
}
}
protected virtual List<TypeAnalysisContext> GetInterfaceContexts() => (Definition?.RawInterfaces.Select(AppContext.ResolveIl2CppType).ToList() ?? [])!;

private List<GenericParameterTypeAnalysisContext>? _genericParameters;
public override List<GenericParameterTypeAnalysisContext> GenericParameters
Expand Down Expand Up @@ -254,7 +255,7 @@ public ByRefTypeAnalysisContext MakeByReferenceType()

public GenericInstanceTypeAnalysisContext MakeGenericInstanceType(params IEnumerable<TypeAnalysisContext> genericArguments)
{
return new(this, genericArguments);
return GenericInstanceTypeAnalysisContext.GetOrCreate(this, genericArguments);
}

public PointerTypeAnalysisContext MakePointerType()
Expand Down
2 changes: 1 addition & 1 deletion Cpp2IL.Core/Utils/GenericInstantiation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public static TypeAnalysisContext Instantiate(TypeAnalysisContext type, IReadOnl
}

return createNew
? new GenericInstanceTypeAnalysisContext(genericType, genericArguments)
? GenericInstanceTypeAnalysisContext.GetOrCreate(genericType, genericArguments)
: genericInstanceTypeAnalysisContext;
}
default:
Expand Down
2 changes: 1 addition & 1 deletion Cpp2IL.Core/Utils/Il2CppTypeReflectionDataToContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public static class Il2CppTypeReflectionDataToContext
genericParams[i] = param;
}

pointerElementType = new GenericInstanceTypeAnalysisContext(baseType, genericParams);
pointerElementType = GenericInstanceTypeAnalysisContext.GetOrCreate(baseType, genericParams);
}

if (reflectionData.isPointer && pointerElementType is not null)
Expand Down
Loading