From 96c369233d12b544c91d6788b13f5349a74ac56f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 11:34:21 +0200 Subject: [PATCH 1/8] Fix inherited generic base typemap refs Port the trimmable typemap generator fix from PR #11617 for constructed generic base types. Preserve constructed generic type refs through scanning and emit TypeSpec member refs for inherited callbacks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 29 +++- .../Generator/Model/TypeMapAssemblyData.cs | 12 +- .../Generator/ModelBuilder.cs | 7 +- .../Generator/PEAssemblyBuilder.cs | 85 +++++++++- .../Generator/TypeMapAssemblyEmitter.cs | 2 +- .../Scanner/JavaPeerInfo.cs | 15 ++ .../Scanner/JavaPeerScanner.cs | 154 +++++++++++------- .../Scanner/SignatureTypeProvider.cs | 4 +- .../TrimmableTypeMapGeneratorTests.cs | 18 +- .../TypeMapAssemblyGeneratorTests.cs | 36 ++++ .../Scanner/OverrideDetectionTests.cs | 26 +++ .../TestFixtures/TestTypes.cs | 32 ++++ 12 files changed, 337 insertions(+), 83 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 93f8ce5c5fc..6e667520af6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -38,11 +38,25 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) } foreach (var proxy in data.ProxyTypes) { writer.Write (proxy.TypeName); - writer.Write (proxy.TargetType.ManagedTypeName); - writer.Write (proxy.TargetType.AssemblyName); + writer.WriteTypeRef (proxy.TargetType); writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); + if (proxy.ActivationCtor is not null) { + writer.WriteTypeRef (proxy.ActivationCtor.DeclaringType); + } writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); } + foreach (var proxy in data.ArrayProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.JniName); + writer.WriteTypeRef (proxy.ElementType); + writer.Write (proxy.Rank); + if (proxy.Primitive is null) { + writer.Write ((byte) 0); + } else { + writer.Write ((byte) 1); + writer.WriteTypeRef (proxy.Primitive.ConcreteArrayType); + } + } foreach (var assoc in data.Associations) { writer.Write (assoc.SourceTypeReference); writer.Write (assoc.AliasProxyTypeReference); @@ -50,4 +64,15 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) writer.Flush (); return sha.ComputeHash (stream.ToArray ()); } + + static void WriteTypeRef (this System.IO.BinaryWriter writer, TypeRefData type) + { + writer.Write (type.ManagedTypeName); + writer.Write (type.AssemblyName); + writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); + writer.Write (type.GenericArguments.Count); + foreach (var argument in type.GenericArguments) { + writer.WriteTypeRef (argument); + } + } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index a90f0d7dbeb..b127037ae9b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,5 +1,5 @@ -using System; using System.Collections.Generic; +using System.Linq; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -185,12 +185,22 @@ sealed record TypeRefData /// public required string AssemblyName { get; init; } + /// + /// Generic arguments for a constructed generic type. Empty for non-generic + /// types and open generic definitions. + /// + public IReadOnlyList GenericArguments { get; init; } = []; + /// /// True if this type — or, for array types, the element type — is an enum. /// Used by the IL emitter to encode the type as ELEMENT_TYPE_VALUETYPE /// rather than ELEMENT_TYPE_CLASS in member references and signatures. /// public bool IsEnum { get; init; } + + public string DisplayName => GenericArguments.Count == 0 + ? ManagedTypeName + : $"{ManagedTypeName}<{string.Join (",", GenericArguments.Select (t => t.DisplayName))}>"; } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4671394e936..ceba650aaff 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -304,10 +304,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash if (peer.ActivationCtor != null) { bool isOnLeaf = string.Equals (peer.ActivationCtor.DeclaringTypeName, peer.ManagedTypeName, StringComparison.Ordinal); proxy.ActivationCtor = new ActivationCtorData { - DeclaringType = new TypeRefData { - ManagedTypeName = peer.ActivationCtor.DeclaringTypeName, - AssemblyName = peer.ActivationCtor.DeclaringAssemblyName, - }, + DeclaringType = peer.ActivationCtor.DeclaringType, IsOnLeafType = isOnLeaf, Style = peer.ActivationCtor.Style, }; @@ -333,7 +330,7 @@ static void BuildUcoMethods (JavaPeerInfo peer, JavaPeerProxyData proxy) proxy.UcoMethods.Add (new UcoMethodData { WrapperName = $"n_{mm.JniName}_uco_{ucoIndex}", CallbackMethodName = mm.NativeCallbackName, - CallbackType = new TypeRefData { + CallbackType = mm.DeclaringType ?? new TypeRefData { ManagedTypeName = !mm.DeclaringTypeName.IsNullOrEmpty () ? mm.DeclaringTypeName : peer.ManagedTypeName, AssemblyName = !mm.DeclaringAssemblyName.IsNullOrEmpty () ? mm.DeclaringAssemblyName : peer.AssemblyName, }, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 57e57b7a835..0452daf120f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -23,7 +24,7 @@ sealed class PEAssemblyBuilder static readonly byte [] MonoAndroidPublicKeyToken = { 0x84, 0xe0, 0x4f, 0xf9, 0xcf, 0xb7, 0x90, 0x65 }; readonly Dictionary _asmRefCache = new (StringComparer.OrdinalIgnoreCase); - readonly Dictionary<(string Assembly, string Type), EntityHandle> _typeRefCache = new (); + readonly Dictionary _typeRefCache = new (); // Reusable scratch BlobBuilders — avoids allocating a new one per method body / attribute / member ref. // Each is Clear()'d before use. Safe because all emission is single-threaded and non-reentrant. @@ -148,16 +149,94 @@ public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Act /// public EntityHandle ResolveTypeRef (TypeRefData typeRef) { - var cacheKey = (typeRef.AssemblyName, typeRef.ManagedTypeName); + var cacheKey = GetTypeRefCacheKey (typeRef); if (_typeRefCache.TryGetValue (cacheKey, out var cached)) { return cached; } var asmRef = FindOrAddAssemblyRef (typeRef.AssemblyName); - var result = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName); + EntityHandle result; + if (typeRef.GenericArguments.Count > 0) { + var openType = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName); + var blob = new BlobBuilder (64); + WriteGenericInstantiationSignature (blob, openType, typeRef); + result = Metadata.AddTypeSpecification (Metadata.GetOrAddBlob (blob)); + } else { + result = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName); + } _typeRefCache [cacheKey] = result; return result; } + static string GetTypeRefCacheKey (TypeRefData typeRef) + { + if (typeRef.GenericArguments.Count == 0) { + return $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}"; + } + return $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}<{string.Join (",", typeRef.GenericArguments.Select (GetTypeRefCacheKey))}>"; + } + + void WriteGenericInstantiationSignature (BlobBuilder blob, EntityHandle openType, TypeRefData typeRef) + { + blob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + blob.WriteByte (typeRef.IsEnum ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType)); + blob.WriteCompressedInteger (typeRef.GenericArguments.Count); + foreach (var argument in typeRef.GenericArguments) { + WriteTypeSignature (blob, argument); + } + } + + public void WriteTypeSignature (BlobBuilder blob, TypeRefData typeRef) + { + if (typeRef.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + blob.WriteByte (0x1D); // ELEMENT_TYPE_SZARRAY + WriteTypeSignature (blob, typeRef with { + ManagedTypeName = typeRef.ManagedTypeName.Substring (0, typeRef.ManagedTypeName.Length - 2), + }); + return; + } + + switch (typeRef.ManagedTypeName) { + case "System.Boolean": blob.WriteByte (0x02); return; + case "System.Char": blob.WriteByte (0x03); return; + case "System.SByte": blob.WriteByte (0x04); return; + case "System.Byte": blob.WriteByte (0x05); return; + case "System.Int16": blob.WriteByte (0x06); return; + case "System.UInt16": blob.WriteByte (0x07); return; + case "System.Int32": blob.WriteByte (0x08); return; + case "System.UInt32": blob.WriteByte (0x09); return; + case "System.Int64": blob.WriteByte (0x0A); return; + case "System.UInt64": blob.WriteByte (0x0B); return; + case "System.Single": blob.WriteByte (0x0C); return; + case "System.Double": blob.WriteByte (0x0D); return; + case "System.String": blob.WriteByte (0x0E); return; + case "System.Object": blob.WriteByte (0x1C); return; + case "System.IntPtr": blob.WriteByte (0x18); return; + } + + if (typeRef.GenericArguments.Count > 0) { + var asmRef = FindOrAddAssemblyRef (typeRef.AssemblyName); + var openType = MakeTypeRefForManagedName (asmRef, typeRef.ManagedTypeName); + WriteGenericInstantiationSignature (blob, openType, typeRef); + return; + } + + if (typeRef.ManagedTypeName.StartsWith ("!!", StringComparison.Ordinal)) { + blob.WriteByte (0x1E); // ELEMENT_TYPE_MVAR + blob.WriteCompressedInteger (int.Parse (typeRef.ManagedTypeName.Substring (2), System.Globalization.CultureInfo.InvariantCulture)); + return; + } + if (typeRef.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal)) { + blob.WriteByte (0x13); // ELEMENT_TYPE_VAR + blob.WriteCompressedInteger (int.Parse (typeRef.ManagedTypeName.Substring (1), System.Globalization.CultureInfo.InvariantCulture)); + return; + } + + var typeHandle = ResolveTypeRef (typeRef); + blob.WriteByte (typeRef.IsEnum ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeHandle)); + } + TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string managedTypeName) { int plusIndex = managedTypeName.IndexOf ('+'); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8ab18800ea5..cc507f2ee3e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1059,7 +1059,7 @@ MemberReferenceHandle AddManagedCtorRef (EntityHandle declaringTypeRef, IReadOnl blob.WriteCompressedInteger (parameterTypes.Count); blob.WriteByte (0x01); // ELEMENT_TYPE_VOID foreach (var parameterType in parameterTypes) { - WriteManagedTypeSignature (blob, parameterType.ManagedTypeName, parameterType.AssemblyName); + _pe.WriteTypeSignature (blob, parameterType); } return _pe.Metadata.AddMemberReference (declaringTypeRef, _pe.Metadata.GetOrAddString (".ctor"), _pe.Metadata.GetOrAddBlob (blob)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 7382a4ed60a..0307ac79638 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -200,6 +200,12 @@ public sealed record MarshalMethodInfo /// public string DeclaringAssemblyName { get; init; } = ""; + /// + /// Exact type that declares the managed method. This can be a constructed + /// generic base type, e.g. BaseAdapter<object>. + /// + internal TypeRefData? DeclaringType { get; init; } + /// /// The native callback method name, e.g., "n_onCreate". /// This is the Java/JNI-visible native method name that the generated JCW calls. @@ -376,6 +382,15 @@ public sealed record ActivationCtorInfo /// public required string DeclaringAssemblyName { get; init; } + /// + /// Exact type that declares the activation constructor. This can be a + /// constructed generic base type. + /// + internal TypeRefData DeclaringType { get; init; } = new () { + ManagedTypeName = "", + AssemblyName = "", + }; + /// /// The style of activation constructor found. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08316ff1787..f23f8e880ce 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -525,7 +525,8 @@ void CollectInterfaceMethodImplementations (TypeDefinition typeDef, AssemblyInde continue; } - var (ifaceTypeName, ifaceAssemblyName) = resolved.Value; + var ifaceTypeName = resolved.ManagedTypeName; + var ifaceAssemblyName = resolved.AssemblyName; if (!TryResolveType (ifaceTypeName, ifaceAssemblyName, out var ifaceHandle, out var ifaceIndex)) { continue; } @@ -904,7 +905,8 @@ List CollectBaseRegisteredCtors (TypeDefinition typeDef, AssemblyI var currentTypeDef = typeDef; var currentIndex = index; - while (TryResolveBaseType (currentTypeDef, currentIndex, out var baseTypeDef, out var baseHandle, out var baseIndex, out _, out _)) { + TypeRefData? currentTypeRef = null; + while (TryResolveBaseType (currentTypeDef, currentIndex, currentTypeRef, out var baseTypeDef, out var baseHandle, out var baseIndex, out _, out _, out var baseTypeRef)) { foreach (var methodHandle in baseTypeDef.GetMethods ()) { var methodDef = baseIndex.Reader.GetMethodDefinition (methodHandle); var name = baseIndex.Reader.GetString (methodDef.Name); @@ -926,6 +928,7 @@ List CollectBaseRegisteredCtors (TypeDefinition typeDef, AssemblyI currentTypeDef = baseTypeDef; currentIndex = baseIndex; + currentTypeRef = baseTypeRef; } return result; @@ -937,20 +940,32 @@ List CollectBaseRegisteredCtors (TypeDefinition typeDef, AssemblyI /// bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, out TypeDefinition baseTypeDef, out TypeDefinitionHandle baseHandle, [NotNullWhen (true)] out AssemblyIndex? baseIndex, - out string baseTypeName, out string baseAssemblyName) + out string baseTypeName, out string baseAssemblyName, out TypeRefData baseTypeRef) + => TryResolveBaseType (typeDef, index, currentTypeRef: null, out baseTypeDef, out baseHandle, out baseIndex, out baseTypeName, out baseAssemblyName, out baseTypeRef); + + bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, TypeRefData? currentTypeRef, + out TypeDefinition baseTypeDef, out TypeDefinitionHandle baseHandle, [NotNullWhen (true)] out AssemblyIndex? baseIndex, + out string baseTypeName, out string baseAssemblyName, out TypeRefData baseTypeRef) { baseTypeDef = default; baseHandle = default; baseIndex = null; baseTypeName = ""; baseAssemblyName = ""; + baseTypeRef = new TypeRefData { + ManagedTypeName = "", + AssemblyName = "", + }; var baseInfo = GetBaseTypeInfo (typeDef, index); if (baseInfo is null) { return false; } - (baseTypeName, baseAssemblyName) = baseInfo.Value; + baseTypeRef = currentTypeRef is null ? baseInfo : SubstituteGenericArguments (baseInfo, currentTypeRef); + baseTypeName = baseTypeRef.ManagedTypeName; + baseAssemblyName = baseTypeRef.AssemblyName; + if (!TryResolveType (baseTypeName, baseAssemblyName, out baseHandle, out baseIndex)) { return false; } @@ -959,6 +974,28 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, return true; } + static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData context) + { + if (type.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal) && + !type.ManagedTypeName.StartsWith ("!!", StringComparison.Ordinal) && + int.TryParse (type.ManagedTypeName.Substring (1), out int parameterIndex) && + (uint) parameterIndex < (uint) context.GenericArguments.Count) { + return context.GenericArguments [parameterIndex]; + } + + if (type.GenericArguments.Count == 0) { + return type; + } + + var arguments = new TypeRefData [type.GenericArguments.Count]; + for (int i = 0; i < arguments.Length; i++) { + arguments [i] = SubstituteGenericArguments (type.GenericArguments [i], context); + } + return type with { + GenericArguments = arguments, + }; + } + readonly record struct BaseCtorInfo (MethodDefinition Method, AssemblyIndex Index, RegisterInfo RegisterInfo); /// @@ -967,10 +1004,10 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, /// info along with the declaring type's full name and assembly name (needed so /// UCO wrappers call n_* on the correct base type). /// - (RegisterInfo Info, string DeclaringTypeName, string DeclaringAssemblyName)? FindBaseRegisteredMethodInfo ( - TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) + (RegisterInfo Info, TypeRefData DeclaringType)? FindBaseRegisteredMethodInfo ( + TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod, TypeRefData? currentTypeRef = null) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, currentTypeRef, out var baseTypeDef, out _, out var baseIndex, out _, out _, out var baseTypeRef)) { return null; } @@ -997,13 +1034,13 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, // derived override must NOT inherit a base [Export] registration — // only [Register]-driven entries propagate through inheritance. if (TryGetMethodRegisterInfo (baseMethodDef, baseIndex, out var registerInfo, out var exportInfo) && registerInfo is not null && exportInfo is null) { - return (registerInfo, baseTypeName, baseAssemblyName); + return (registerInfo, baseTypeRef); } } // Keep walking the full base hierarchy so overrides can inherit [Register] // metadata declared above an intermediate MCW base type. - return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod); + return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod, baseTypeRef); } MarshalMethodInfo? FindBaseRegisteredMethod (TypeDefinition typeDef, AssemblyIndex index, @@ -1023,8 +1060,9 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, ManagedMethodName = methodName, NativeCallbackName = GetNativeCallbackName (registerInfo.Connector, methodName, isConstructor), IsConstructor = isConstructor, - DeclaringTypeName = result.Value.DeclaringTypeName, - DeclaringAssemblyName = result.Value.DeclaringAssemblyName, + DeclaringTypeName = result.Value.DeclaringType.ManagedTypeName, + DeclaringAssemblyName = result.Value.DeclaringType.AssemblyName, + DeclaringType = result.Value.DeclaringType, }; } @@ -1033,9 +1071,9 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, /// matches the given getter name and has a compatible signature. /// MarshalMethodInfo? FindBaseRegisteredProperty (TypeDefinition typeDef, AssemblyIndex index, - string getterName, MethodDefinition derivedGetter) + string getterName, MethodDefinition derivedGetter, TypeRefData? currentTypeRef = null) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out var baseAssemblyName)) { + if (!TryResolveBaseType (typeDef, index, currentTypeRef, out var baseTypeDef, out _, out var baseIndex, out _, out _, out var baseTypeRef)) { return null; } @@ -1068,15 +1106,16 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, ManagedMethodName = getterName, NativeCallbackName = GetNativeCallbackName (propRegister.Connector, getterName, false), IsConstructor = false, - DeclaringTypeName = baseTypeName, - DeclaringAssemblyName = baseAssemblyName, + DeclaringTypeName = baseTypeRef.ManagedTypeName, + DeclaringAssemblyName = baseTypeRef.AssemblyName, + DeclaringType = baseTypeRef, }; } } // Keep walking the full base hierarchy so property overrides can inherit // [Register] metadata declared above an intermediate MCW base type. - return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter); + return FindBaseRegisteredProperty (baseTypeDef, baseIndex, getterName, derivedGetter, baseTypeRef); } /// @@ -1195,6 +1234,10 @@ static bool SupportsDirectManagedMethodCall (MethodSignature manage static bool SupportsDirectManagedMethodCall (TypeRefData type) { + if (type.GenericArguments.Count > 0) { + return false; + } + var typeName = type.ManagedTypeName; if (typeName.EndsWith ("&", StringComparison.Ordinal) || typeName.EndsWith ("*", StringComparison.Ordinal)) { return false; @@ -1204,7 +1247,7 @@ static bool SupportsDirectManagedMethodCall (TypeRefData type) typeName = typeName.Substring (0, typeName.Length - 2); } - return !typeName.StartsWith ("!", StringComparison.Ordinal) && typeName.IndexOf ('<') < 0; + return !typeName.StartsWith ("!", StringComparison.Ordinal); } static string GetJavaAccess (MethodAttributes access) @@ -1219,7 +1262,7 @@ static string GetJavaAccess (MethodAttributes access) string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { - if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out _)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out _, out _)) { return null; } @@ -1264,7 +1307,7 @@ List ResolveImplementedInterfaceJavaNames (TypeDefinition typeDef, Assem string? ResolveInterfaceJniName (EntityHandle interfaceHandle, AssemblyIndex index) { var resolved = ResolveEntityHandle (interfaceHandle, index); - return resolved is not null ? ResolveRegisterJniName (resolved.Value.typeName, resolved.Value.assemblyName) : null; + return resolved is not null ? ResolveRegisterJniName (resolved.ManagedTypeName, resolved.AssemblyName) : null; } bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, out RegisterInfo? registerInfo, out ExportInfo? exportInfo) @@ -1564,9 +1607,9 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI }; } - ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index) + ActivationCtorInfo? ResolveActivationCtor (string typeName, TypeDefinition typeDef, AssemblyIndex index, TypeRefData? currentTypeRef = null) { - var cacheKey = (typeName, index.AssemblyName); + var cacheKey = (currentTypeRef?.DisplayName ?? typeName, index.AssemblyName); if (activationCtorCache.TryGetValue (cacheKey, out var cached)) { return cached; } @@ -1574,7 +1617,20 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI // Check this type's constructors var ownCtor = FindActivationCtorOnType (typeDef, index); if (ownCtor is not null) { - var info = new ActivationCtorInfo { DeclaringTypeName = typeName, DeclaringAssemblyName = index.AssemblyName, Style = ownCtor.Value }; + var info = new ActivationCtorInfo { + DeclaringTypeName = typeName, + DeclaringAssemblyName = index.AssemblyName, + DeclaringType = new TypeRefData { + ManagedTypeName = typeName, + AssemblyName = index.AssemblyName, + }, + Style = ownCtor.Value, + }; + if (currentTypeRef is not null) { + info = info with { + DeclaringType = currentTypeRef, + }; + } activationCtorCache [cacheKey] = info; return info; } @@ -1582,10 +1638,12 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI // Walk base type hierarchy var baseInfo = GetBaseTypeInfo (typeDef, index); if (baseInfo is not null) { - var (baseTypeName, baseAssemblyName) = baseInfo.Value; + baseInfo = currentTypeRef is null ? baseInfo : SubstituteGenericArguments (baseInfo, currentTypeRef); + var baseTypeName = baseInfo.ManagedTypeName; + var baseAssemblyName = baseInfo.AssemblyName; if (TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { var baseTypeDef = baseIndex.Reader.GetTypeDefinition (baseHandle); - var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex); + var result = ResolveActivationCtor (baseTypeName, baseTypeDef, baseIndex, baseInfo); if (result is not null) { activationCtorCache [cacheKey] = result; } @@ -1630,53 +1688,28 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying /// type's (fullName, assemblyName) by reading the raw signature blob. /// - static (string fullName, string assemblyName)? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) + TypeRefData? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) { var typeSpec = index.Reader.GetTypeSpecification (specHandle); - var blobReader = index.Reader.GetBlobReader (typeSpec.Signature); - - // Generic instantiation blob: GENERICINST (CLASS|VALUETYPE) coded-token count args... - var elementType = blobReader.ReadByte (); - if (elementType != 0x15) { // ELEMENT_TYPE_GENERICINST - return null; - } - - var classOrValueType = blobReader.ReadByte (); - if (classOrValueType != 0x12 && classOrValueType != 0x11) { // CLASS or VALUETYPE - return null; - } - - // TypeDefOrRefOrSpec coded index: 2 tag bits (0=TypeDef, 1=TypeRef, 2=TypeSpec) - var codedToken = blobReader.ReadCompressedInteger (); - var tag = codedToken & 0x3; - var row = codedToken >> 2; - - switch (tag) { - case 0: { // TypeDef - var handle = MetadataTokens.TypeDefinitionHandle (row); - var baseDef = index.Reader.GetTypeDefinition (handle); - return (MetadataTypeNameResolver.GetFullName (baseDef, index.Reader), index.AssemblyName); - } - case 1: // TypeRef - return ResolveTypeReference (MetadataTokens.TypeReferenceHandle (row), index); - default: - return null; - } + return typeSpec.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); } /// /// Resolves an EntityHandle (TypeDef, TypeRef, or TypeSpec) to (typeName, assemblyName). /// Shared by base type resolution, interface resolution, and any handle-to-name lookup. /// - (string typeName, string assemblyName)? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) + TypeRefData? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) { switch (handle.Kind) { case HandleKind.TypeDefinition: { var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); - return (MetadataTypeNameResolver.GetFullName (td, index.Reader), index.AssemblyName); + return new TypeRefData { + ManagedTypeName = MetadataTypeNameResolver.GetFullName (td, index.Reader), + AssemblyName = index.AssemblyName, + }; } case HandleKind.TypeReference: - return ResolveTypeReference ((TypeReferenceHandle)handle, index); + return MetadataTypeNameResolver.GetTypeRefFromReference (index.Reader, (TypeReferenceHandle)handle, index.AssemblyName, rawTypeKind: 0); case HandleKind.TypeSpecification: return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index); default: @@ -1684,7 +1717,7 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI } } - (string typeName, string assemblyName)? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index) + TypeRefData? GetBaseTypeInfo (TypeDefinition typeDef, AssemblyIndex index) { return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); } @@ -1769,7 +1802,8 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) return false; } - var (baseTypeName, baseAssemblyName) = baseInfo.Value; + var baseTypeName = baseInfo.ManagedTypeName; + var baseAssemblyName = baseInfo.AssemblyName; if (!TryResolveType (baseTypeName, baseAssemblyName, out var baseHandle, out var baseIndex)) { return false; @@ -2031,7 +2065,7 @@ List BuildJavaConstructors (List marshal bool unsupportedParam = false; foreach (var p in sig.ParameterTypes) { var paramTypeName = p.ManagedTypeName; - if (paramTypeName.IndexOf ('<') >= 0 || paramTypeName.EndsWith ("&", StringComparison.Ordinal) || paramTypeName.EndsWith ("*", StringComparison.Ordinal)) { + if (p.GenericArguments.Count > 0 || paramTypeName.EndsWith ("&", StringComparison.Ordinal) || paramTypeName.EndsWith ("*", StringComparison.Ordinal)) { unsupportedParam = true; break; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs index 779e4f76f70..1639d582573 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/SignatureTypeProvider.cs @@ -108,9 +108,7 @@ public TypeRefData GetPointerType (TypeRefData elementType) => elementType with public TypeRefData GetGenericInstantiation (TypeRefData genericType, ImmutableArray typeArguments) { - return genericType with { - ManagedTypeName = $"{genericType.ManagedTypeName}<{string.Join (",", typeArguments.Select (t => t.ManagedTypeName))}>", - }; + return genericType with { GenericArguments = typeArguments.ToArray () }; } public TypeRefData GetGenericTypeParameter (AssemblyIndex genericContext, int index) => new () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index b3eba523be1..d793f87bc3b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -55,7 +55,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location; using var peReader = new PEReader (File.OpenRead (testAssemblyPath)); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("TestAssembly", peReader) }, + [Input ("TestAssembly", peReader)], new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); @@ -67,7 +67,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -78,7 +78,7 @@ public void Execute_WithTestFixtures_ProducesOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -139,7 +139,7 @@ public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendan [Fact] public void Execute_NullAssemblyList_Throws () { - IReadOnlyList<(string Name, PEReader Reader)>? n = null; + IReadOnlyList? n = null; #pragma warning disable CS8604 Assert.Throws (() => CreateGenerator ().Execute (n, new Version (11, 0), new HashSet ())); #pragma warning restore CS8604 @@ -149,7 +149,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -162,7 +162,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -172,7 +172,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("Mono.Android", peReader) }, + [Input ("Mono.Android", peReader)], new Version (11, 0), new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }); @@ -196,7 +196,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () """); var result = CreateGenerator ().Execute ( - new List<(string, PEReader)> { ("TestFixtures", peReader) }, + [Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet (), useSharedTypemapUniverse: false, @@ -217,6 +217,8 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); + [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 28b2ad29b2a..e4b86a651fd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -45,6 +45,17 @@ static List FindCtorMemberRefs (MetadataReader reader, st static MemberReferenceHandle FindCtorMemberRef (MetadataReader reader, string parentNamespace, string parentName, params string [] parameterTypes) => FindCtorMemberRefs (reader, parentNamespace, parentName, parameterTypes).First (); + static string GetTypeDefOrRefName (MetadataReader reader, int codedToken) + { + int tag = codedToken & 0x3; + int row = codedToken >> 2; + return tag switch { + 0 => reader.GetString (reader.GetTypeDefinition (MetadataTokens.TypeDefinitionHandle (row)).Name), + 1 => reader.GetString (reader.GetTypeReference (MetadataTokens.TypeReferenceHandle (row)).Name), + _ => throw new InvalidOperationException ($"Unexpected TypeDefOrRefOrSpec tag {tag}."), + }; + } + static TypeRefData TypeRef (string managedTypeName) => new () { ManagedTypeName = managedTypeName, AssemblyName = GetAssemblyNameForManagedType (managedTypeName), @@ -106,6 +117,31 @@ public void Generate_CreatesProxyTypes () Assert.Contains (proxyTypes, t => reader.GetString (t.Name) == "Java_Lang_Object_Proxy"); } + [Theory] + [InlineData ("my/app/GenericSelectableList")] + [InlineData ("my/app/GenericForwardingSelectableList")] + public void Generate_InheritedGenericBaseCallback_UsesConstructedBaseMemberRef (string javaName) + { + var peer = ScanFixtures ().Single (p => p.JavaName == javaName); + using var stream = GenerateAssembly (new [] { peer }); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var member = reader.MemberReferences + .Select (h => reader.GetMemberReference (h)) + .Single (m => reader.GetString (m.Name) == "n_SetSelection_I"); + + Assert.Equal (HandleKind.TypeSpecification, member.Parent.Kind); + var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) member.Parent); + var blob = reader.GetBlobReader (typeSpec.Signature); + + Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + Assert.Equal ("GenericSelectionHost`1", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); + Assert.Equal (1, blob.ReadCompressedInteger ()); + Assert.Equal (0x0E, blob.ReadByte ()); // ELEMENT_TYPE_STRING + } + [Fact] public void Generate_ProxyType_HasCtorAndCreateInstance () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index f1e2cbd0ab8..3e5c024d1e3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Xunit; @@ -125,6 +126,31 @@ public void OverrideAcrossGenericIntermediateMcwBase_Detected () var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); Assert.Equal ("(I)V", setSelection.JniSignature); Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + var declaringType = setSelection.DeclaringType; + Assert.NotNull (declaringType); + if (declaringType is null) { + throw new InvalidOperationException ("Expected override declaring type."); + } + Assert.Equal ("MyApp.GenericSelectionHost`1", declaringType.ManagedTypeName); + var argument = Assert.Single (declaringType.GenericArguments); + Assert.Equal ("System.String", argument.ManagedTypeName); + } + + [Fact] + public void OverrideAcrossGenericForwardingIntermediateMcwBase_Detected () + { + var peer = FindFixtureByJavaName ("my/app/GenericForwardingSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + var declaringType = setSelection.DeclaringType; + Assert.NotNull (declaringType); + if (declaringType is null) { + throw new InvalidOperationException ("Expected override declaring type."); + } + Assert.Equal ("MyApp.GenericSelectionHost`1", declaringType.ManagedTypeName); + var argument = Assert.Single (declaringType.GenericArguments); + Assert.Equal ("System.String", argument.ManagedTypeName); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index ca0b8f4adf8..398c3365721 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1013,6 +1013,26 @@ public abstract class GenericSelectionContainer : GenericSelectionHost protected GenericSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } } + /// + /// Generic intermediate MCW base that forwards its generic parameter to the + /// registered generic base. This mirrors Xamarin.Forms renderer hierarchies + /// such as VisualElementRenderer<TElement>. + /// + [Register ("my/app/GenericForwardingSelectionContainer", DoNotGenerateAcw = true)] + public abstract class GenericForwardingSelectionContainer : GenericSelectionHost where T : class + { + protected GenericForwardingSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Non-generic MCW base that closes the generic forwarding base. + /// + [Register ("my/app/StringForwardingSelectionContainer", DoNotGenerateAcw = true)] + public abstract class StringForwardingSelectionContainer : GenericForwardingSelectionContainer + { + protected StringForwardingSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + /// /// Overrides a registered method declared above the first MCW base in the hierarchy. /// @@ -1035,6 +1055,18 @@ protected GenericSelectableList (IntPtr handle, JniHandleOwnership transfer) : b public override void SetSelection (int position) { } } + /// + /// Overrides a registered method declared above a generic base that forwards + /// type parameters through another generic base. + /// + [Register ("my/app/GenericForwardingSelectableList")] + public class GenericForwardingSelectableList : StringForwardingSelectionContainer + { + protected GenericForwardingSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// From 098bbda81e851247095bde68cec4147e20a2b258 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:06:44 +0200 Subject: [PATCH 2/8] Address inherited generic typemap review feedback Use tuple inputs in generator tests for the current generator API and reject structured generic TypeRefData in export dispatch unsupported-type checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ExportMethodDispatchEmitter.cs | 12 +++++++----- .../TrimmableTypeMapGeneratorTests.cs | 18 ++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs index 82b914900a3..e5cd50ef8ba 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs @@ -323,7 +323,7 @@ internal void LoadManagedArgument (TrackedInstructionEncoder encoder, TypeRefDat { string managedTypeName = managedType.ManagedTypeName; - ThrowIfUnsupportedManagedType (managedTypeName); + ThrowIfUnsupportedManagedType (managedType); if (TryEmitExportParameterArgument (encoder, exportKind, argumentIndex)) { return; @@ -410,8 +410,10 @@ void ConvertManagedReturnValue (TrackedInstructionEncoder encoder, TypeRefData m encoder.Call (_context.JniEnvToLocalJniHandleRef, parameterCount: 1, returnsValue: true); } - void ThrowIfUnsupportedManagedType (string managedTypeName) + void ThrowIfUnsupportedManagedType (TypeRefData managedType) { + string managedTypeName = managedType.ManagedTypeName; + if (managedTypeName.EndsWith ("&", StringComparison.Ordinal) || managedTypeName.EndsWith ("*", StringComparison.Ordinal)) { throw new NotSupportedException ($"[Export] methods with by-ref or pointer signature types are not supported: '{managedTypeName}'."); } @@ -421,8 +423,8 @@ void ThrowIfUnsupportedManagedType (string managedTypeName) nonArrayTypeName = nonArrayTypeName.Substring (0, nonArrayTypeName.Length - 2); } - if (nonArrayTypeName.StartsWith ("!", StringComparison.Ordinal) || nonArrayTypeName.IndexOf ('<') >= 0) { - throw new NotSupportedException ($"[Export] methods with generic signature types are not supported: '{managedTypeName}'."); + if (nonArrayTypeName.StartsWith ("!", StringComparison.Ordinal) || managedType.GenericArguments.Count > 0 || nonArrayTypeName.IndexOf ('<') >= 0) { + throw new NotSupportedException ($"[Export] methods with generic signature types are not supported: '{managedType.DisplayName}'."); } } @@ -571,7 +573,7 @@ void EncodeManagedType (SignatureTypeEncoder encoder, TypeRefData managedType) { string managedTypeName = managedType.ManagedTypeName; - ThrowIfUnsupportedManagedType (managedTypeName); + ThrowIfUnsupportedManagedType (managedType); if (managedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { EncodeManagedType (encoder.SZArray (), managedType with { ManagedTypeName = managedTypeName.Substring (0, managedTypeName.Length - 2), diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index d793f87bc3b..8a8f8a6e792 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -55,7 +55,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location; using var peReader = new PEReader (File.OpenRead (testAssemblyPath)); var result = CreateGenerator ().Execute ( - [Input ("TestAssembly", peReader)], + [("TestAssembly", peReader)], new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); @@ -67,7 +67,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -78,7 +78,7 @@ public void Execute_WithTestFixtures_ProducesOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -139,7 +139,7 @@ public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendan [Fact] public void Execute_NullAssemblyList_Throws () { - IReadOnlyList? n = null; + IReadOnlyList<(string Name, PEReader Reader)>? n = null; #pragma warning disable CS8604 Assert.Throws (() => CreateGenerator ().Execute (n, new Version (11, 0), new HashSet ())); #pragma warning restore CS8604 @@ -149,7 +149,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -162,7 +162,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -172,7 +172,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ( - [Input ("Mono.Android", peReader)], + [("Mono.Android", peReader)], new Version (11, 0), new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }); @@ -196,7 +196,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () """); var result = CreateGenerator ().Execute ( - [Input ("TestFixtures", peReader)], + [("TestFixtures", peReader)], new Version (11, 0), new HashSet (), useSharedTypemapUniverse: false, @@ -217,8 +217,6 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); - static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); - [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] From db2255c1acca614bd386d470ff62dba2a454a7d6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:07:27 +0200 Subject: [PATCH 3/8] Add structured generic export dispatch regression Cover the new TypeRefData.GenericArguments shape in export dispatch unsupported-type validation so constructed generic signature types continue to be rejected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TypeMapAssemblyGeneratorTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index e4b86a651fd..2363129de49 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -1578,6 +1578,44 @@ public void Generate_ExportProxy_UnsupportedManagedShapesThrow (string parameter Assert.Contains (expectedMessage, ex.Message); } + [Fact] + public void Generate_ExportProxy_StructuredGenericArgumentThrows () + { + var peer = MakePeerWithActivation ("my/app/UnsupportedExport", "MyApp.UnsupportedExport", "App") with { + DoNotGenerateAcw = false, + MarshalMethods = new List { + new () { + JniName = "badExport", + NativeCallbackName = "n_badExport", + JniSignature = "(Ljava/lang/Object;)V", + ManagedMethodName = "BadExport", + ManagedParameterTypes = new [] { + new TypeRefData { + ManagedTypeName = "System.Collections.Generic.List`1", + AssemblyName = "System.Collections", + GenericArguments = new [] { + new TypeRefData { + ManagedTypeName = "System.String", + AssemblyName = "System.Runtime", + }, + }, + }, + }, + ManagedReturnType = new TypeRefData { + ManagedTypeName = "System.Void", + AssemblyName = "System.Runtime", + }, + IsExport = true, + }, + }, + }; + + var ex = Assert.Throws (() => { + using var stream = GenerateAssembly (new [] { peer }, "UnsupportedStructuredGenericExport"); + }); + Assert.Contains ("generic", ex.Message); + } + [Fact] public void Generate_MultipleAcwProxies_DeduplicatesUtf8Strings () { From 859b099d580e99d2d116d04d3bdeee3b1db1ae4e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 12:17:12 +0200 Subject: [PATCH 4/8] Keep AssemblyInput for trimmable typemap generator Restore the AssemblyInput generator API from PR #11617 so this branch stays aligned with follow-up trimmable typemap work, while keeping tuple scanner compatibility for existing tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AssemblyInput.cs | 5 +++++ .../Scanner/AssemblyIndex.cs | 8 +++++--- .../Scanner/JavaPeerScanner.cs | 7 +++++-- .../TrimmableTypeMapGenerator.cs | 4 ++-- .../Tasks/GenerateTrimmableTypeMap.cs | 4 ++-- .../TrimmableTypeMapGeneratorTests.cs | 18 ++++++++++-------- 6 files changed, 29 insertions(+), 17 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs new file mode 100644 index 00000000000..d742d120d8c --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/AssemblyInput.cs @@ -0,0 +1,5 @@ +using System.Reflection.PortableExecutable; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +public readonly record struct AssemblyInput (string Name, string Path, PEReader Reader); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 64ca498c814..52ff5b3a9f8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -17,6 +17,7 @@ sealed class AssemblyIndex : IDisposable public MetadataReader Reader { get; } public string AssemblyName { get; } + public string AssemblyPath { get; } /// /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. @@ -48,18 +49,19 @@ sealed class AssemblyIndex : IDisposable /// public bool MayUseJniAddNativeMethodRegistrationAttribute { get; private set; } - AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName) + AssemblyIndex (PEReader peReader, MetadataReader reader, string assemblyName, string assemblyPath) { this.peReader = peReader; this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); Reader = reader; AssemblyName = assemblyName; + AssemblyPath = assemblyPath; } - public static AssemblyIndex Create (PEReader peReader, string assemblyName) + public static AssemblyIndex Create (PEReader peReader, string assemblyName, string assemblyPath = "") { var reader = peReader.GetMetadataReader (); - var index = new AssemblyIndex (peReader, reader, assemblyName); + var index = new AssemblyIndex (peReader, reader, assemblyName, assemblyPath); index.Build (); return index; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index f23f8e880ce..663e12239a8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -96,9 +96,12 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan /// Phase 2: Scan all types and produce JavaPeerInfo records. /// public List Scan (IReadOnlyList<(string Name, PEReader Reader)> assemblies) + => Scan (assemblies.Select (a => new AssemblyInput (a.Name, "", a.Reader))); + + public List Scan (IEnumerable assemblies) { - foreach (var (name, reader) in assemblies) { - var index = AssemblyIndex.Create (reader, name); + foreach (var assembly in assemblies) { + var index = AssemblyIndex.Create (assembly.Reader, assembly.Name, assembly.Path); assemblyCache [index.AssemblyName] = index; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 95d0446b205..416c461fe28 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -27,7 +27,7 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) /// No file IO is performed — all results are returned in memory. /// public TrimmableTypeMapResult Execute ( - IReadOnlyList<(string Name, PEReader Reader)> assemblies, + IReadOnlyList assemblies, Version systemRuntimeVersion, HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, @@ -155,7 +155,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) { using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); var peers = scanner.Scan (assemblies); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 9a03ef4ff21..96a4a9f104c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -135,7 +135,7 @@ public override bool RunTask () Directory.CreateDirectory (JavaSourceOutputDirectory); var peReaders = new List (); - var assemblies = new List<(string Name, PEReader Reader)> (); + var assemblies = new List (); TrimmableTypeMapResult? result = null; try { foreach (var (path, isFrameworkAssembly) in assemblyInputs) { @@ -143,7 +143,7 @@ public override bool RunTask () peReaders.Add (peReader); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - assemblies.Add ((assemblyName, peReader)); + assemblies.Add (new AssemblyInput (assemblyName, path, peReader)); if (isFrameworkAssembly) { frameworkAssemblyNames.Add (assemblyName); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 8a8f8a6e792..d793f87bc3b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -55,7 +55,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () var testAssemblyPath = typeof (TrimmableTypeMapGeneratorTests).Assembly.Location; using var peReader = new PEReader (File.OpenRead (testAssemblyPath)); var result = CreateGenerator ().Execute ( - [("TestAssembly", peReader)], + [Input ("TestAssembly", peReader)], new Version (11, 0), new HashSet ()); Assert.Empty (result.GeneratedAssemblies); @@ -67,7 +67,7 @@ public void Execute_AssemblyWithNoPeers_ReturnsEmpty () public void Execute_WithTestFixtures_ProducesOutputs () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); Assert.NotEmpty (result.GeneratedAssemblies); Assert.NotEmpty (result.GeneratedJavaSources); Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_Microsoft.Android.TypeMaps"); @@ -78,7 +78,7 @@ public void Execute_WithTestFixtures_ProducesOutputs () public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); // Abstract Instrumentation/Application subtypes are included too: their native // methods (e.g. n_OnCreate, n_OnStart) are declared on the abstract base class @@ -139,7 +139,7 @@ public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendan [Fact] public void Execute_NullAssemblyList_Throws () { - IReadOnlyList<(string Name, PEReader Reader)>? n = null; + IReadOnlyList? n = null; #pragma warning disable CS8604 Assert.Throws (() => CreateGenerator ().Execute (n, new Version (11, 0), new HashSet ())); #pragma warning restore CS8604 @@ -149,7 +149,7 @@ public void Execute_NullAssemblyList_Throws () public void Execute_GeneratedAssembliesAreValidPE () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var assembly in result.GeneratedAssemblies) { assembly.Content.Position = 0; using var vr = new PEReader (assembly.Content, PEStreamOptions.LeaveOpen); @@ -162,7 +162,7 @@ public void Execute_GeneratedAssembliesAreValidPE () public void Execute_JavaSourcesHaveCorrectStructure () { using var peReader = CreateTestFixturePEReader (); - var result = CreateGenerator ().Execute ([("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); + var result = CreateGenerator ().Execute ([Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet ()); foreach (var source in result.GeneratedJavaSources) Assert.Contains ("class ", source.Content); } @@ -172,7 +172,7 @@ public void Execute_FrameworkAssembly_GeneratesFrameworkJcwTypes () { using var peReader = CreateTestFixturePEReader (); var result = CreateGenerator ().Execute ( - [("Mono.Android", peReader)], + [Input ("Mono.Android", peReader)], new Version (11, 0), new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }); @@ -196,7 +196,7 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () """); var result = CreateGenerator ().Execute ( - [("TestFixtures", peReader)], + [Input ("TestFixtures", peReader)], new Version (11, 0), new HashSet (), useSharedTypemapUniverse: false, @@ -217,6 +217,8 @@ public void Execute_ManifestPlaceholdersAreResolvedBeforeRooting () TrimmableTypeMapGenerator CreateGenerator (List warnings) => new (new TestTrimmableTypeMapLogger (logMessages, warnings)); + static AssemblyInput Input (string name, PEReader reader) => new (name, "", reader); + [Theory] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", "com.example.MyActivity")] [InlineData ("com/example/MyActivity", "com.example.MyActivity", "com.example", "activity", ".MyActivity")] From a57b4ee04b99df14278d86e1d3d84d1131780c4f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 13:37:11 +0200 Subject: [PATCH 5/8] Fix trimmable typemap build errors Remove array-proxy fingerprinting from this narrower generator fix and add the missing LINQ import needed by the AssemblyInput scanner compatibility overload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 12 ------------ .../Scanner/JavaPeerScanner.cs | 1 + 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 6e667520af6..84db55dad96 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -45,18 +45,6 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) } writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); } - foreach (var proxy in data.ArrayProxyTypes) { - writer.Write (proxy.TypeName); - writer.Write (proxy.JniName); - writer.WriteTypeRef (proxy.ElementType); - writer.Write (proxy.Rank); - if (proxy.Primitive is null) { - writer.Write ((byte) 0); - } else { - writer.Write ((byte) 1); - writer.WriteTypeRef (proxy.Primitive.ConcreteArrayType); - } - } foreach (var assoc in data.Associations) { writer.Write (assoc.SourceTypeReference); writer.Write (assoc.AliasProxyTypeReference); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 663e12239a8..89fd3f01438 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; From dee6f8c2ebd07ffb91c6f728508cd0f4191def86 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 14:02:06 +0200 Subject: [PATCH 6/8] Fix generic base typemap callback matching Substitute constructed generic base arguments before comparing inherited registered method and constructor signatures. Include UCO metadata in deterministic typemap fingerprints so constructed callback type changes produce distinct MVIDs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 64 +++++++++++++++++++ .../Generator/Model/TypeMapAssemblyData.cs | 14 +++- .../Scanner/JavaPeerScanner.cs | 30 ++++++--- .../TypeMapAssemblyGeneratorTests.cs | 42 ++++++++++++ .../Scanner/OverrideDetectionTests.cs | 17 +++++ .../TestFixtures/TestTypes.cs | 35 ++++++++++ 6 files changed, 189 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 84db55dad96..6fafb412a4d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -44,6 +44,18 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) writer.WriteTypeRef (proxy.ActivationCtor.DeclaringType); } writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); + writer.Write (proxy.UcoMethods.Count); + foreach (var method in proxy.UcoMethods) { + writer.WriteUcoMethod (method); + } + writer.Write (proxy.UcoConstructors.Count); + foreach (var constructor in proxy.UcoConstructors) { + writer.WriteUcoConstructor (constructor); + } + writer.Write (proxy.NativeRegistrations.Count); + foreach (var registration in proxy.NativeRegistrations) { + writer.WriteNativeRegistration (registration); + } } foreach (var assoc in data.Associations) { writer.Write (assoc.SourceTypeReference); @@ -63,4 +75,56 @@ static void WriteTypeRef (this System.IO.BinaryWriter writer, TypeRefData type) writer.WriteTypeRef (argument); } } + + static void WriteUcoMethod (this System.IO.BinaryWriter writer, UcoMethodData method) + { + writer.Write (method.WrapperName); + writer.Write (method.CallbackMethodName); + writer.WriteTypeRef (method.CallbackType); + writer.Write (method.JniSignature); + writer.WriteExportMethodDispatch (method.ExportMethodDispatch); + } + + static void WriteExportMethodDispatch (this System.IO.BinaryWriter writer, ExportMethodDispatchData? dispatch) + { + writer.Write (dispatch is not null); + if (dispatch is null) { + return; + } + + writer.Write (dispatch.ManagedMethodName); + writer.Write (dispatch.ParameterTypes.Count); + foreach (var parameterType in dispatch.ParameterTypes) { + writer.WriteTypeRef (parameterType); + } + writer.Write (dispatch.ParameterKinds.Count); + foreach (var parameterKind in dispatch.ParameterKinds) { + writer.Write ((int) parameterKind); + } + writer.WriteTypeRef (dispatch.ReturnType); + writer.Write ((int) dispatch.ReturnKind); + writer.Write (dispatch.IsStatic); + } + + static void WriteUcoConstructor (this System.IO.BinaryWriter writer, UcoConstructorData constructor) + { + writer.Write (constructor.WrapperName); + writer.WriteTypeRef (constructor.TargetType); + writer.Write (constructor.JniSignature); + writer.Write (constructor.HasMatchingManagedCtor); + writer.Write (constructor.ManagedParameterTypes.Count); + foreach (var parameterType in constructor.ManagedParameterTypes) { + writer.WriteTypeRef (parameterType); + } + } + + static void WriteNativeRegistration (this System.IO.BinaryWriter writer, NativeRegistrationData registration) + { + writer.Write (registration.JniMethodName); + writer.Write (registration.JniSignature); + writer.Write (registration.WrapperMethodName); + writer.Write (registration.WrapperTarget.TypeNamespace); + writer.Write (registration.WrapperTarget.TypeName); + writer.Write (registration.WrapperTarget.MethodName); + } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index b127037ae9b..0247909cb38 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; @@ -198,9 +199,16 @@ sealed record TypeRefData /// public bool IsEnum { get; init; } - public string DisplayName => GenericArguments.Count == 0 - ? ManagedTypeName - : $"{ManagedTypeName}<{string.Join (",", GenericArguments.Select (t => t.DisplayName))}>"; + public string DisplayName { + get { + if (ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + return $"{(this with { ManagedTypeName = ManagedTypeName.Substring (0, ManagedTypeName.Length - 2) }).DisplayName}[]"; + } + return GenericArguments.Count == 0 + ? ManagedTypeName + : $"{ManagedTypeName}<{string.Join (",", GenericArguments.Select (t => t.DisplayName))}>"; + } + } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 89fd3f01438..7147075e717 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -674,7 +674,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // Check if this ctor's params are already covered by a base registered ctor bool alreadyCovered = false; foreach (var baseCtor in baseRegisteredCtors) { - if (HaveIdenticalParameterTypes (methodDef, baseCtor.Method)) { + if (HaveIdenticalParameterTypes (methodDef, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)) { alreadyCovered = true; break; } @@ -920,7 +920,7 @@ List CollectBaseRegisteredCtors (TypeDefinition typeDef, AssemblyI if (TryGetMethodRegisterInfo (methodDef, baseIndex, out var registerInfo, out _) && registerInfo is not null && registerInfo.Signature is not null) { - result.Add (new BaseCtorInfo (methodDef, baseIndex, registerInfo)); + result.Add (new BaseCtorInfo (methodDef, baseIndex, registerInfo, baseTypeRef)); } } @@ -980,6 +980,15 @@ bool TryResolveBaseType (TypeDefinition typeDef, AssemblyIndex index, TypeRefDat static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData context) { + if (type.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + var elementType = SubstituteGenericArguments (type with { + ManagedTypeName = type.ManagedTypeName.Substring (0, type.ManagedTypeName.Length - 2), + }, context); + return elementType with { + ManagedTypeName = $"{elementType.ManagedTypeName}[]", + }; + } + if (type.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal) && !type.ManagedTypeName.StartsWith ("!!", StringComparison.Ordinal) && int.TryParse (type.ManagedTypeName.Substring (1), out int parameterIndex) && @@ -1000,7 +1009,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con }; } - readonly record struct BaseCtorInfo (MethodDefinition Method, AssemblyIndex Index, RegisterInfo RegisterInfo); + readonly record struct BaseCtorInfo (MethodDefinition Method, AssemblyIndex Index, RegisterInfo RegisterInfo, TypeRefData DeclaringType); /// /// Walks the base type hierarchy looking for a method with [Register] that matches @@ -1029,7 +1038,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con continue; } - if (!HaveIdenticalParameterTypes (derivedMethod, baseMethodDef)) { + if (!HaveIdenticalParameterTypes (derivedMethod, baseMethodDef, baseIndex, baseTypeRef)) { continue; } @@ -1125,17 +1134,18 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con /// /// Checks if two methods have identical parameter types by comparing their decoded signatures. /// - static bool HaveIdenticalParameterTypes (MethodDefinition method1, MethodDefinition method2) + static bool HaveIdenticalParameterTypes (MethodDefinition derivedMethod, MethodDefinition baseMethod, AssemblyIndex baseIndex, TypeRefData baseTypeRef) { - var sig1 = method1.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - var sig2 = method2.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var derivedSig = derivedMethod.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var baseSig = baseMethod.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: baseIndex); - if (sig1.ParameterTypes.Length != sig2.ParameterTypes.Length) { + if (derivedSig.ParameterTypes.Length != baseSig.ParameterTypes.Length) { return false; } - for (int i = 0; i < sig1.ParameterTypes.Length; i++) { - if (!string.Equals (sig1.ParameterTypes [i], sig2.ParameterTypes [i], StringComparison.Ordinal)) { + for (int i = 0; i < derivedSig.ParameterTypes.Length; i++) { + var baseParameterType = SubstituteGenericArguments (baseSig.ParameterTypes [i], baseTypeRef); + if (!string.Equals (derivedSig.ParameterTypes [i], baseParameterType.DisplayName, StringComparison.Ordinal)) { return false; } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 2363129de49..a390c6bf211 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -744,6 +744,48 @@ public void Generate_IdenticalContent_ProducesIdenticalMVIDs () Assert.Equal (mvid1, mvid2); } + [Fact] + public void Generate_DifferentConstructedCallbackTypes_ProducesDifferentMVIDs () + { + var peer = MakePeerWithActivation ("test/TypeA", "Test.TypeA", "TestAsm"); + var stringPeer = peer with { + MarshalMethods = [ + CreateInheritedGenericCallback (TypeRef ("System.String")), + ], + }; + var objectPeer = peer with { + MarshalMethods = [ + CreateInheritedGenericCallback (TypeRef ("System.Object")), + ], + }; + + using var stream1 = GenerateAssembly (new [] { stringPeer }, "SameName"); + using var stream2 = GenerateAssembly (new [] { objectPeer }, "SameName"); + + using var pe1 = new PEReader (stream1); + using var pe2 = new PEReader (stream2); + var mvid1 = pe1.GetMetadataReader ().GetGuid (pe1.GetMetadataReader ().GetModuleDefinition ().Mvid); + var mvid2 = pe2.GetMetadataReader ().GetGuid (pe2.GetMetadataReader ().GetModuleDefinition ().Mvid); + + Assert.NotEqual (mvid1, mvid2); + + static MarshalMethodInfo CreateInheritedGenericCallback (TypeRefData genericArgument) => new () { + JniName = "setValue", + NativeCallbackName = "n_SetValue_Ljava_lang_Object", + JniSignature = "(Ljava/lang/Object;)V", + ManagedMethodName = "SetValue", + DeclaringTypeName = "Test.GenericBase`1", + DeclaringAssemblyName = "TestAsm", + DeclaringType = new TypeRefData { + ManagedTypeName = "Test.GenericBase`1", + AssemblyName = "TestAsm", + GenericArguments = [ + genericArgument, + ], + }, + }; + } + [Fact] public void Generate_AcwProxy_HasRegisterNativesAndUcoMethods () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index 3e5c024d1e3..fda6d1d9fbb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -153,6 +153,23 @@ public void OverrideAcrossGenericForwardingIntermediateMcwBase_Detected () Assert.Equal ("System.String", argument.ManagedTypeName); } + [Fact] + public void OverrideAcrossGenericBaseWithSubstitutedParameter_Detected () + { + var peer = FindFixtureByJavaName ("my/app/StringValueList"); + var applyValue = Assert.Single (peer.MarshalMethods, m => m.JniName == "applyValue"); + Assert.Equal ("(Ljava/lang/Object;)V", applyValue.JniSignature); + Assert.Equal ("GetApplyValue_Ljava_lang_Object_Handler", applyValue.Connector); + var declaringType = applyValue.DeclaringType; + Assert.NotNull (declaringType); + if (declaringType is null) { + throw new InvalidOperationException ("Expected override declaring type."); + } + Assert.Equal ("MyApp.GenericValueHost`1", declaringType.ManagedTypeName); + var argument = Assert.Single (declaringType.GenericArguments); + Assert.Equal ("System.String", argument.ManagedTypeName); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 398c3365721..8385485727d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1067,6 +1067,41 @@ protected GenericForwardingSelectableList (IntPtr handle, JniHandleOwnership tra public override void SetSelection (int position) { } } + /// + /// Generic base whose registered method uses its type parameter in the managed + /// signature. Override detection must compare this as string when the + /// base is closed over string. + /// + [Register ("my/app/GenericValueHost", DoNotGenerateAcw = true)] + public abstract class GenericValueHost : Java.Lang.Object where T : class + { + protected GenericValueHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("applyValue", "(Ljava/lang/Object;)V", "GetApplyValue_Ljava_lang_Object_Handler")] + public abstract void ApplyValue (T value); + } + + /// + /// Intermediate MCW base that closes a registered generic method parameter. + /// + [Register ("my/app/StringValueContainer", DoNotGenerateAcw = true)] + public abstract class StringValueContainer : GenericValueHost + { + protected StringValueContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method whose base managed signature contains a + /// substituted generic type parameter. + /// + [Register ("my/app/StringValueList")] + public class StringValueList : StringValueContainer + { + protected StringValueList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void ApplyValue (string value) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// From 7e39b8e2f79630ab602e1c105f28fc3fc7ebe1cc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 14:43:45 +0200 Subject: [PATCH 7/8] Address generic typemap review feedback Track value-type generic arguments when decoding TypeRefData so constructed generic TypeSpecs emit VALUETYPE where required. Add element-wise TypeRefData equality for generic arguments and remove the obsolete type-reference resolver helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ExportMethodDispatchEmitter.cs | 2 +- .../Generator/MetadataHelper.cs | 1 + .../Generator/Model/TypeMapAssemblyData.cs | 49 ++++++++++++++++- .../Generator/PEAssemblyBuilder.cs | 9 ++-- .../Scanner/JavaPeerScanner.cs | 31 +---------- .../Scanner/MetadataTypeNameResolver.cs | 4 ++ .../TypeMapAssemblyGeneratorTests.cs | 52 +++++++++++++++++++ .../Scanner/OverrideDetectionTests.cs | 18 +++++++ .../TestFixtures/TestTypes.cs | 38 ++++++++++++++ 9 files changed, 168 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs index e5cd50ef8ba..2eb6ba5f3c1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ExportMethodDispatchEmitter.cs @@ -600,7 +600,7 @@ void EncodeManagedType (SignatureTypeEncoder encoder, TypeRefData managedType) } var typeHandle = ResolveManagedTypeHandle (managedType); - encoder.Type (typeHandle, isValueType: managedType.IsEnum); + encoder.Type (typeHandle, isValueType: managedType.EncodeAsValueType); } void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 6fafb412a4d..c606912621c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -69,6 +69,7 @@ static void WriteTypeRef (this System.IO.BinaryWriter writer, TypeRefData type) { writer.Write (type.ManagedTypeName); writer.Write (type.AssemblyName); + writer.Write (type.IsValueType ? (byte) 1 : (byte) 0); writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); writer.Write (type.GenericArguments.Count); foreach (var argument in type.GenericArguments) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 0247909cb38..f7d2403f6e3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -193,12 +193,21 @@ sealed record TypeRefData public IReadOnlyList GenericArguments { get; init; } = []; /// - /// True if this type — or, for array types, the element type — is an enum. + /// True if this type — or, for array types, the element type — is a value type. /// Used by the IL emitter to encode the type as ELEMENT_TYPE_VALUETYPE /// rather than ELEMENT_TYPE_CLASS in member references and signatures. /// + public bool IsValueType { get; init; } + + /// + /// True if this type — or, for array types, the element type — is an enum. + /// Used by JNI signature generation to map enum values to their underlying + /// primitive ABI type. + /// public bool IsEnum { get; init; } + public bool EncodeAsValueType => IsValueType || IsEnum; + public string DisplayName { get { if (ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { @@ -209,6 +218,44 @@ public string DisplayName { : $"{ManagedTypeName}<{string.Join (",", GenericArguments.Select (t => t.DisplayName))}>"; } } + + public bool Equals (TypeRefData? other) + { + if (ReferenceEquals (this, other)) { + return true; + } + if (other is null) { + return false; + } + if (!string.Equals (ManagedTypeName, other.ManagedTypeName, StringComparison.Ordinal) || + !string.Equals (AssemblyName, other.AssemblyName, StringComparison.Ordinal) || + IsValueType != other.IsValueType || + IsEnum != other.IsEnum || + GenericArguments.Count != other.GenericArguments.Count) { + return false; + } + for (int i = 0; i < GenericArguments.Count; i++) { + if (!GenericArguments [i].Equals (other.GenericArguments [i])) { + return false; + } + } + return true; + } + + public override int GetHashCode () + { + unchecked { + int hash = 17; + hash = hash * 31 + StringComparer.Ordinal.GetHashCode (ManagedTypeName); + hash = hash * 31 + StringComparer.Ordinal.GetHashCode (AssemblyName); + hash = hash * 31 + IsValueType.GetHashCode (); + hash = hash * 31 + IsEnum.GetHashCode (); + foreach (var argument in GenericArguments) { + hash = hash * 31 + argument.GetHashCode (); + } + return hash; + } + } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 0452daf120f..cd5ff6b2b84 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -169,16 +169,17 @@ public EntityHandle ResolveTypeRef (TypeRefData typeRef) static string GetTypeRefCacheKey (TypeRefData typeRef) { + var typeKind = typeRef.EncodeAsValueType ? "valuetype" : "class"; if (typeRef.GenericArguments.Count == 0) { - return $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}"; + return $"{typeKind}:{typeRef.AssemblyName}:{typeRef.ManagedTypeName}"; } - return $"{typeRef.AssemblyName}:{typeRef.ManagedTypeName}<{string.Join (",", typeRef.GenericArguments.Select (GetTypeRefCacheKey))}>"; + return $"{typeKind}:{typeRef.AssemblyName}:{typeRef.ManagedTypeName}<{string.Join (",", typeRef.GenericArguments.Select (GetTypeRefCacheKey))}>"; } void WriteGenericInstantiationSignature (BlobBuilder blob, EntityHandle openType, TypeRefData typeRef) { blob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST - blob.WriteByte (typeRef.IsEnum ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS + blob.WriteByte (typeRef.EncodeAsValueType ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (openType)); blob.WriteCompressedInteger (typeRef.GenericArguments.Count); foreach (var argument in typeRef.GenericArguments) { @@ -233,7 +234,7 @@ public void WriteTypeSignature (BlobBuilder blob, TypeRefData typeRef) } var typeHandle = ResolveTypeRef (typeRef); - blob.WriteByte (typeRef.IsEnum ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS + blob.WriteByte (typeRef.EncodeAsValueType ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeHandle)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 7147075e717..9075ce63f05 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -51,35 +51,6 @@ bool TryResolveType (string typeName, string assemblyName, out TypeDefinitionHan return false; } - /// - /// Resolves a TypeReferenceHandle to (fullName, assemblyName), correctly handling - /// nested types whose ResolutionScope is another TypeReference. - /// - static (string fullName, string assemblyName) ResolveTypeReference (TypeReferenceHandle handle, AssemblyIndex index) - { - var typeRef = index.Reader.GetTypeReference (handle); - var name = index.Reader.GetString (typeRef.Name); - var ns = index.Reader.GetString (typeRef.Namespace); - - var scope = typeRef.ResolutionScope; - switch (scope.Kind) { - case HandleKind.AssemblyReference: { - var asmRef = index.Reader.GetAssemblyReference ((AssemblyReferenceHandle)scope); - var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); - return (fullName, index.Reader.GetString (asmRef.Name)); - } - case HandleKind.TypeReference: { - // Nested type: recurse to get the declaring type's full name and assembly - var (parentFullName, assemblyName) = ResolveTypeReference ((TypeReferenceHandle)scope, index); - return (MetadataTypeNameResolver.JoinNestedTypeName (parentFullName, name), assemblyName); - } - default: { - var fullName = MetadataTypeNameResolver.JoinNamespaceAndName (ns, name); - return (fullName, index.AssemblyName); - } - } - } - /// /// Looks up the [Register] JNI name for a type identified by name + assembly. /// @@ -864,7 +835,7 @@ TypeRefData EnrichTypeRefWithEnumInfo (TypeRefData type) return type; } - return IsEnumOrEnumArray (type.ManagedTypeName, type.AssemblyName) ? type with { IsEnum = true } : type; + return IsEnumOrEnumArray (type.ManagedTypeName, type.AssemblyName) ? type with { IsEnum = true, IsValueType = true } : type; } static bool IsEnumType (TypeDefinition typeDef, AssemblyIndex index) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs index 468fef15f25..fa13b8c5094 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/MetadataTypeNameResolver.cs @@ -8,6 +8,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// static class MetadataTypeNameResolver { + const byte ElementTypeValueType = 0x11; + internal static string JoinNamespaceAndName (string ns, string name) { return ns.Length > 0 ? $"{ns}.{name}" : name; @@ -40,6 +42,7 @@ public static TypeRefData GetTypeRefFromDefinition (MetadataReader reader, TypeD return new TypeRefData { ManagedTypeName = GetTypeFromDefinition (reader, handle, rawTypeKind), AssemblyName = assemblyName, + IsValueType = rawTypeKind == ElementTypeValueType, }; } @@ -64,6 +67,7 @@ public static TypeRefData GetTypeRefFromReference (MetadataReader reader, TypeRe return new TypeRefData { ManagedTypeName = managedTypeName, AssemblyName = assemblyName, + IsValueType = rawTypeKind == ElementTypeValueType, }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index a390c6bf211..cbeddcc7cb3 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -142,6 +142,30 @@ public void Generate_InheritedGenericBaseCallback_UsesConstructedBaseMemberRef ( Assert.Equal (0x0E, blob.ReadByte ()); // ELEMENT_TYPE_STRING } + [Fact] + public void Generate_InheritedGenericBaseCallback_UsesValueTypeGenericArgument () + { + var peer = ScanFixtures ().Single (p => p.JavaName == "my/app/EnumSelectableList"); + using var stream = GenerateAssembly (new [] { peer }); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var member = reader.MemberReferences + .Select (h => reader.GetMemberReference (h)) + .Single (m => reader.GetString (m.Name) == "n_SetSelection_I"); + + Assert.Equal (HandleKind.TypeSpecification, member.Parent.Kind); + var typeSpec = reader.GetTypeSpecification ((TypeSpecificationHandle) member.Parent); + var blob = reader.GetBlobReader (typeSpec.Signature); + + Assert.Equal (0x15, blob.ReadByte ()); // ELEMENT_TYPE_GENERICINST + Assert.Equal (0x12, blob.ReadByte ()); // ELEMENT_TYPE_CLASS + Assert.Equal ("GenericValueTypeSelectionHost`1", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); + Assert.Equal (1, blob.ReadCompressedInteger ()); + Assert.Equal (0x11, blob.ReadByte ()); // ELEMENT_TYPE_VALUETYPE + Assert.Equal ("SelectionMode", GetTypeDefOrRefName (reader, blob.ReadCompressedInteger ())); + } + [Fact] public void Generate_ProxyType_HasCtorAndCreateInstance () { @@ -744,6 +768,34 @@ public void Generate_IdenticalContent_ProducesIdenticalMVIDs () Assert.Equal (mvid1, mvid2); } + [Fact] + public void TypeRefData_Equality_ComparesGenericArgumentsByValue () + { + var left = new TypeRefData { + ManagedTypeName = "Test.GenericBase`1", + AssemblyName = "TestAsm", + GenericArguments = [ + TypeRef ("System.String"), + ], + }; + var right = new TypeRefData { + ManagedTypeName = "Test.GenericBase`1", + AssemblyName = "TestAsm", + GenericArguments = [ + TypeRef ("System.String"), + ], + }; + var different = right with { + GenericArguments = [ + TypeRef ("System.Object"), + ], + }; + + Assert.Equal (left, right); + Assert.Equal (left.GetHashCode (), right.GetHashCode ()); + Assert.NotEqual (left, different); + } + [Fact] public void Generate_DifferentConstructedCallbackTypes_ProducesDifferentMVIDs () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs index fda6d1d9fbb..81e33f417f5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/OverrideDetectionTests.cs @@ -170,6 +170,24 @@ public void OverrideAcrossGenericBaseWithSubstitutedParameter_Detected () Assert.Equal ("System.String", argument.ManagedTypeName); } + [Fact] + public void OverrideAcrossGenericValueTypeArgument_PreservesValueTypeArgument () + { + var peer = FindFixtureByJavaName ("my/app/EnumSelectableList"); + var setSelection = Assert.Single (peer.MarshalMethods, m => m.JniName == "setSelection"); + Assert.Equal ("(I)V", setSelection.JniSignature); + Assert.Equal ("GetSetSelection_IHandler", setSelection.Connector); + var declaringType = setSelection.DeclaringType; + Assert.NotNull (declaringType); + if (declaringType is null) { + throw new InvalidOperationException ("Expected override declaring type."); + } + Assert.Equal ("MyApp.GenericValueTypeSelectionHost`1", declaringType.ManagedTypeName); + var argument = Assert.Single (declaringType.GenericArguments); + Assert.Equal ("MyApp.SelectionMode", argument.ManagedTypeName); + Assert.True (argument.IsValueType); + } + [Fact] public void EmptyConnector_OverrideStillDetected () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 8385485727d..76d2cd61e2f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -1102,6 +1102,44 @@ protected StringValueList (IntPtr handle, JniHandleOwnership transfer) : base (h public override void ApplyValue (string value) { } } + public enum SelectionMode { + Single, + Multiple, + } + + /// + /// Generic base closed over an enum argument so TypeSpec emission must encode + /// the argument as ELEMENT_TYPE_VALUETYPE. + /// + [Register ("my/app/GenericValueTypeSelectionHost", DoNotGenerateAcw = true)] + public abstract class GenericValueTypeSelectionHost : Java.Lang.Object + { + protected GenericValueTypeSelectionHost (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + [Register ("setSelection", "(I)V", "GetSetSelection_IHandler")] + public abstract void SetSelection (int position); + } + + /// + /// Intermediate MCW base that closes a registered generic base over an enum. + /// + [Register ("my/app/EnumSelectionContainer", DoNotGenerateAcw = true)] + public abstract class EnumSelectionContainer : GenericValueTypeSelectionHost + { + protected EnumSelectionContainer (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + } + + /// + /// Overrides a registered method declared on a constructed generic enum base. + /// + [Register ("my/app/EnumSelectableList")] + public class EnumSelectableList : EnumSelectionContainer + { + protected EnumSelectableList (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { } + + public override void SetSelection (int position) { } + } + /// /// Has a ctor with unsigned primitive params to test JNI mapping. /// From 6ef53aaf7dd86d44614542c41a28c4ea116f41d6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 26 Jun 2026 17:29:44 +0200 Subject: [PATCH 8/8] Compare typemap override signatures structurally Decode both inherited and derived method signatures as TypeRefData so override matching compares the shared structured representation instead of parallel string formatters. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 9075ce63f05..b4a17a52f3a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -645,7 +645,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // Check if this ctor's params are already covered by a base registered ctor bool alreadyCovered = false; foreach (var baseCtor in baseRegisteredCtors) { - if (HaveIdenticalParameterTypes (methodDef, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)) { + if (HaveIdenticalParameterTypes (methodDef, index, baseCtor.Method, baseCtor.Index, baseCtor.DeclaringType)) { alreadyCovered = true; break; } @@ -989,7 +989,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con /// UCO wrappers call n_* on the correct base type). /// (RegisterInfo Info, TypeRefData DeclaringType)? FindBaseRegisteredMethodInfo ( - TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod, TypeRefData? currentTypeRef = null) + TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod, AssemblyIndex derivedIndex, TypeRefData? currentTypeRef = null) { if (!TryResolveBaseType (typeDef, index, currentTypeRef, out var baseTypeDef, out _, out var baseIndex, out _, out _, out var baseTypeRef)) { return null; @@ -1009,7 +1009,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con continue; } - if (!HaveIdenticalParameterTypes (derivedMethod, baseMethodDef, baseIndex, baseTypeRef)) { + if (!HaveIdenticalParameterTypes (derivedMethod, derivedIndex, baseMethodDef, baseIndex, baseTypeRef)) { continue; } @@ -1024,13 +1024,13 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con // Keep walking the full base hierarchy so overrides can inherit [Register] // metadata declared above an intermediate MCW base type. - return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod, baseTypeRef); + return FindBaseRegisteredMethodInfo (baseTypeDef, baseIndex, methodName, derivedMethod, derivedIndex, baseTypeRef); } MarshalMethodInfo? FindBaseRegisteredMethod (TypeDefinition typeDef, AssemblyIndex index, string methodName, MethodDefinition derivedMethod) { - var result = FindBaseRegisteredMethodInfo (typeDef, index, methodName, derivedMethod); + var result = FindBaseRegisteredMethodInfo (typeDef, index, methodName, derivedMethod, index); if (result is null || result.Value.Info.Signature is null) { return null; } @@ -1105,9 +1105,9 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con /// /// Checks if two methods have identical parameter types by comparing their decoded signatures. /// - static bool HaveIdenticalParameterTypes (MethodDefinition derivedMethod, MethodDefinition baseMethod, AssemblyIndex baseIndex, TypeRefData baseTypeRef) + static bool HaveIdenticalParameterTypes (MethodDefinition derivedMethod, AssemblyIndex derivedIndex, MethodDefinition baseMethod, AssemblyIndex baseIndex, TypeRefData baseTypeRef) { - var derivedSig = derivedMethod.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var derivedSig = derivedMethod.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: derivedIndex); var baseSig = baseMethod.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: baseIndex); if (derivedSig.ParameterTypes.Length != baseSig.ParameterTypes.Length) { @@ -1116,7 +1116,7 @@ static bool HaveIdenticalParameterTypes (MethodDefinition derivedMethod, MethodD for (int i = 0; i < derivedSig.ParameterTypes.Length; i++) { var baseParameterType = SubstituteGenericArguments (baseSig.ParameterTypes [i], baseTypeRef); - if (!string.Equals (derivedSig.ParameterTypes [i], baseParameterType.DisplayName, StringComparison.Ordinal)) { + if (!derivedSig.ParameterTypes [i].Equals (baseParameterType)) { return false; } }