From db19c06a509314a2dd7499708e2f90d3c7fe3ae2 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Sat, 4 Jul 2026 18:35:56 -0700 Subject: [PATCH] Ensure the base type and interfaces of generic instance types are instantiated --- Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs | 51 +++++++++++ Cpp2IL.Core/Cpp2IL.Core.csproj | 2 +- .../Contexts/ApplicationAnalysisContext.cs | 4 + .../ConcreteGenericMethodAnalysisContext.cs | 2 +- .../Model/Contexts/GenericArgumentList.cs | 21 +++++ .../GenericInstanceTypeAnalysisContext.cs | 89 +++++++++++++++---- .../Model/Contexts/TypeAnalysisContext.cs | 7 +- Cpp2IL.Core/Utils/GenericInstantiation.cs | 2 +- .../Il2CppTypeReflectionDataToContext.cs | 2 +- 9 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 Cpp2IL.Core/Model/Contexts/GenericArgumentList.cs diff --git a/Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs b/Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs index e9d36ca6..2ce92c2e 100644 --- a/Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs +++ b/Cpp2IL.Core.Tests/TypeAnalysisContextTests.cs @@ -1,3 +1,6 @@ +using System.Linq; +using Cpp2IL.Core.Model.Contexts; + namespace Cpp2IL.Core.Tests; public class TypeAnalysisContextTests @@ -49,4 +52,52 @@ public void StaticClassesHaveObjectBaseType() Assert.That(count, Is.GreaterThan(0)); } } + + [Test] + public void GenericInstanceTypeHasInstantiatedBaseType() + { + var appContext = TestGameLoader.LoadSimple2019Game(); + + // ObjectComparer inherits from Comparer + 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()); + } + 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()); + + 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 implements IEquatable> + 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().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)); + } } diff --git a/Cpp2IL.Core/Cpp2IL.Core.csproj b/Cpp2IL.Core/Cpp2IL.Core.csproj index b30f4ebb..60cbf0fa 100644 --- a/Cpp2IL.Core/Cpp2IL.Core.csproj +++ b/Cpp2IL.Core/Cpp2IL.Core.csproj @@ -7,7 +7,7 @@ Reverses Unity's IL2CPP Build Process true true - 13 + 14 enable true Samboy063.Cpp2IL.Core diff --git a/Cpp2IL.Core/Model/Contexts/ApplicationAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/ApplicationAnalysisContext.cs index bfbbf961..c95d98cc 100644 --- a/Cpp2IL.Core/Model/Contexts/ApplicationAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/ApplicationAnalysisContext.cs @@ -93,6 +93,10 @@ public class ApplicationAnalysisContext : ContextWithDataStorage /// Cache for /// internal readonly ConcurrentDictionary> GenericInstanceTypesByIl2CppType = new(); + /// + /// Cache for + /// + internal readonly ConcurrentDictionary<(TypeAnalysisContext, GenericArgumentList), Lazy> GenericInstanceTypesByConstruction = new(); public ApplicationAnalysisContext(LibCpp2IlContext context) { diff --git a/Cpp2IL.Core/Model/Contexts/ConcreteGenericMethodAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/ConcreteGenericMethodAnalysisContext.cs index 6527d111..31f21c97 100644 --- a/Cpp2IL.Core/Model/Contexts/ConcreteGenericMethodAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/ConcreteGenericMethodAnalysisContext.cs @@ -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) diff --git a/Cpp2IL.Core/Model/Contexts/GenericArgumentList.cs b/Cpp2IL.Core/Model/Contexts/GenericArgumentList.cs new file mode 100644 index 00000000..a5168c43 --- /dev/null +++ b/Cpp2IL.Core/Model/Contexts/GenericArgumentList.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Cpp2IL.Core.Model.Contexts; + +internal readonly record struct GenericArgumentList(List Arguments) +{ + public static implicit operator GenericArgumentList(List arguments) => new(arguments); + public static implicit operator List(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(); +} diff --git a/Cpp2IL.Core/Model/Contexts/GenericInstanceTypeAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/GenericInstanceTypeAnalysisContext.cs index 2a91a545..dee02628 100644 --- a/Cpp2IL.Core/Model/Contexts/GenericInstanceTypeAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/GenericInstanceTypeAnalysisContext.cs @@ -12,7 +12,7 @@ public class GenericInstanceTypeAnalysisContext : ReferencedTypeAnalysisContext { public TypeAnalysisContext GenericType { get; } - public List GenericArguments { get; } = []; + public List GenericArguments { get; } public sealed override TypeAttributes DefaultAttributes => GenericType.DefaultAttributes; @@ -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; @@ -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 genericArguments) : base(genericType.CustomAttributeAssembly) + private GenericInstanceTypeAnalysisContext(TypeAnalysisContext genericType, List 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)); + } } /// @@ -87,7 +125,7 @@ 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 @@ -95,6 +133,20 @@ public static GenericInstanceTypeAnalysisContext GetOrCreate(Il2CppType rawType, .Value; } + public static GenericInstanceTypeAnalysisContext GetOrCreate(TypeAnalysisContext genericType, IEnumerable 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 + .GetOrAdd((genericType, genericArgumentsList), key => new Lazy(() => new GenericInstanceTypeAnalysisContext(key.Item1, key.Item2))) + .Value; + } + public override string GetCSharpSourceString() { var sb = new StringBuilder(); @@ -117,6 +169,11 @@ public override string GetCSharpSourceString() return sb.ToString(); } + protected override List GetInterfaceContexts() + { + return GenericType.InterfaceContexts.Select(i => GenericInstantiation.Instantiate(i, GenericArguments, [])).ToList(); + } + private void SetDeclaringType() { var declaringType = GenericType.DeclaringType; diff --git a/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs index 9218c3ca..3c753daa 100644 --- a/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs @@ -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 { @@ -97,10 +97,11 @@ public List InterfaceContexts get { // Lazy load the interface contexts - _interfaceContexts ??= (Definition?.RawInterfaces.Select(AppContext.ResolveIl2CppType).ToList() ?? [])!; + _interfaceContexts ??= GetInterfaceContexts(); return _interfaceContexts; } } + protected virtual List GetInterfaceContexts() => (Definition?.RawInterfaces.Select(AppContext.ResolveIl2CppType).ToList() ?? [])!; private List? _genericParameters; public override List GenericParameters @@ -254,7 +255,7 @@ public ByRefTypeAnalysisContext MakeByReferenceType() public GenericInstanceTypeAnalysisContext MakeGenericInstanceType(params IEnumerable genericArguments) { - return new(this, genericArguments); + return GenericInstanceTypeAnalysisContext.GetOrCreate(this, genericArguments); } public PointerTypeAnalysisContext MakePointerType() diff --git a/Cpp2IL.Core/Utils/GenericInstantiation.cs b/Cpp2IL.Core/Utils/GenericInstantiation.cs index 0bc54da7..ca7cef40 100644 --- a/Cpp2IL.Core/Utils/GenericInstantiation.cs +++ b/Cpp2IL.Core/Utils/GenericInstantiation.cs @@ -86,7 +86,7 @@ public static TypeAnalysisContext Instantiate(TypeAnalysisContext type, IReadOnl } return createNew - ? new GenericInstanceTypeAnalysisContext(genericType, genericArguments) + ? GenericInstanceTypeAnalysisContext.GetOrCreate(genericType, genericArguments) : genericInstanceTypeAnalysisContext; } default: diff --git a/Cpp2IL.Core/Utils/Il2CppTypeReflectionDataToContext.cs b/Cpp2IL.Core/Utils/Il2CppTypeReflectionDataToContext.cs index 05307e0f..6957f321 100644 --- a/Cpp2IL.Core/Utils/Il2CppTypeReflectionDataToContext.cs +++ b/Cpp2IL.Core/Utils/Il2CppTypeReflectionDataToContext.cs @@ -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)