From a6de122ee60dbe1ac55d3990a75e1ed16dda6861 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 13:20:37 +0200 Subject: [PATCH 1/4] Optimize trimmable typemap generation Reduce scanner and emitter allocations by caching metadata, avoiding unused MCW method scans, and eliminating intermediate buffers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/JniSignatureHelper.cs | 14 +- .../Generator/MetadataHelper.cs | 17 +- .../Generator/ModelBuilder.cs | 6 +- .../Generator/PEAssemblyBuilder.cs | 36 +- .../Generator/TypeMapAssemblyEmitter.cs | 41 ++- .../Scanner/AssemblyIndex.cs | 227 +++++++++++- .../Scanner/JavaPeerInfo.cs | 10 +- .../Scanner/JavaPeerScanner.cs | 336 ++++++++++-------- .../Scanner/SignatureTypeProvider.cs | 56 ++- .../TrimmableTypeMapGenerator.cs | 24 +- .../Tasks/GenerateTrimmableTypeMap.cs | 3 +- .../TrimmableTypeMapGeneratorTests.cs | 31 ++ 12 files changed, 584 insertions(+), 217 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs index 18985fe2dd3..849a4ec78b5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/JniSignatureHelper.cs @@ -174,15 +174,17 @@ public static bool IsAmbiguousCallbackKind (JniParamKind kind) /// public static bool HasAmbiguousCallbackType (string jniSignature) { - if (IsAmbiguousCallbackKind (ParseReturnType (jniSignature))) { - return true; - } - foreach (var kind in ParseParameterTypes (jniSignature)) { - if (IsAmbiguousCallbackKind (kind)) { + int i = 1; + while (i < jniSignature.Length && jniSignature [i] != ')') { + if (IsAmbiguousCallbackKind (ParseSingleType (jniSignature, ref i))) { return true; } } - return false; + if (i >= jniSignature.Length) { + throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'"); + } + i++; + return IsAmbiguousCallbackKind (ParseSingleType (jniSignature, ref i)); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 9160c28fbc3..185c2637dcf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Security.Cryptography; using System.Text; @@ -29,8 +30,8 @@ public static Guid DeterministicMvid (string moduleName, ReadOnlySpan cont public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) { using var sha = SHA256.Create (); - using var stream = new System.IO.MemoryStream (); - using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8); + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); foreach (var entry in data.Entries) { writer.Write (entry.MapKey); writer.Write (entry.ProxyTypeReference); @@ -62,10 +63,10 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) writer.Write (assoc.AliasProxyTypeReference); } writer.Flush (); - return sha.ComputeHash (stream.ToArray ()); + return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); } - static void WriteTypeRef (this System.IO.BinaryWriter writer, TypeRefData type) + static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) { writer.Write (type.ManagedTypeName); writer.Write (type.AssemblyName); @@ -77,7 +78,7 @@ static void WriteTypeRef (this System.IO.BinaryWriter writer, TypeRefData type) } } - static void WriteUcoMethod (this System.IO.BinaryWriter writer, UcoMethodData method) + static void WriteUcoMethod (this BinaryWriter writer, UcoMethodData method) { writer.Write (method.WrapperName); writer.Write (method.CallbackMethodName); @@ -86,7 +87,7 @@ static void WriteUcoMethod (this System.IO.BinaryWriter writer, UcoMethodData me writer.WriteExportMethodDispatch (method.ExportMethodDispatch); } - static void WriteExportMethodDispatch (this System.IO.BinaryWriter writer, ExportMethodDispatchData? dispatch) + static void WriteExportMethodDispatch (this BinaryWriter writer, ExportMethodDispatchData? dispatch) { writer.Write (dispatch is not null); if (dispatch is null) { @@ -107,7 +108,7 @@ static void WriteExportMethodDispatch (this System.IO.BinaryWriter writer, Expor writer.Write (dispatch.IsStatic); } - static void WriteUcoConstructor (this System.IO.BinaryWriter writer, UcoConstructorData constructor) + static void WriteUcoConstructor (this BinaryWriter writer, UcoConstructorData constructor) { writer.Write (constructor.WrapperName); writer.WriteTypeRef (constructor.TargetType); @@ -119,7 +120,7 @@ static void WriteUcoConstructor (this System.IO.BinaryWriter writer, UcoConstruc } } - static void WriteNativeRegistration (this System.IO.BinaryWriter writer, NativeRegistrationData registration) + static void WriteNativeRegistration (this BinaryWriter writer, NativeRegistrationData registration) { writer.Write (registration.JniMethodName); writer.Write (registration.JniSignature); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index ca25d1962d7..82b1dd2d288 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -408,14 +408,14 @@ static void BuildNativeRegistrations (TypeMapAssemblyData model) } foreach (var proxy in model.ProxyTypes) { - var reusedUcoMethods = new HashSet (); + HashSet? reusedUcoMethods = null; foreach (var uco in proxy.UcoMethods) { var wrapperTarget = UcoWrapperTargetData.From (proxy, uco.WrapperName); if (CanReuseUcoWrapper (proxy, uco) && sharedWrapperTargets.TryGetValue (CreateUcoWrapperReuseKey (uco), out var sharedWrapperTarget)) { wrapperTarget = sharedWrapperTarget; - reusedUcoMethods.Add (uco); + (reusedUcoMethods ??= new ()).Add (uco); } proxy.NativeRegistrations.Add (new NativeRegistrationData { JniMethodName = uco.CallbackMethodName, @@ -425,7 +425,7 @@ static void BuildNativeRegistrations (TypeMapAssemblyData model) }); } - if (reusedUcoMethods.Count > 0) { + if (reusedUcoMethods is not null) { proxy.UcoMethods.RemoveAll (uco => reusedUcoMethods.Contains (uco)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 989791c1695..6434c515e27 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -114,6 +114,9 @@ public void WritePE (Stream stream) deterministicIdProvider: DeterministicContentId); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); + if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) { + memoryStream.Capacity = peBlob.Count; + } peBlob.WriteContentTo (stream); } @@ -289,24 +292,20 @@ public void PrepareUtf8Fields (IEnumerable values) foreach (var group in valuesBySize) { var sizedType = GetOrCreateSizedType (group.Key); foreach (string value in group.Value) { - AddUtf8Field (value, group.Key, sizedType); + AddUtf8Field (value, sizedType); } } } - void AddUtf8Field (string value, int size, TypeDefinitionHandle sizedType) + void AddUtf8Field (string value, TypeDefinitionHandle sizedType) { // Encode to null-terminated UTF-8 (all JNI names/signatures are ASCII). - int byteCount = size - 1; - var bytes = new byte [size]; - System.Text.Encoding.UTF8.GetBytes (value, 0, value.Length, bytes, 0); - // bytes[byteCount] is already 0 (null terminator) - _sigBlob.Clear (); new BlobEncoder (_sigBlob).FieldSignature ().Type (sizedType, true); int rva = _mappedFieldData.Count; - _mappedFieldData.WriteBytes (bytes); + _mappedFieldData.WriteUTF8 (value); + _mappedFieldData.WriteByte (0); var fieldHandle = Metadata.AddFieldDefinition ( FieldAttributes.Static | FieldAttributes.Assembly | FieldAttributes.HasFieldRVA | FieldAttributes.InitOnly, @@ -513,6 +512,27 @@ public BlobHandle BuildAttributeBlob (Action writePayload) return Metadata.GetOrAddBlob (_attrBlob); } + public BlobHandle BuildAttributeBlob (string first, string second) + { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); + _attrBlob.WriteSerializedString (first); + _attrBlob.WriteSerializedString (second); + _attrBlob.WriteUInt16 (0x0000); + return Metadata.GetOrAddBlob (_attrBlob); + } + + public BlobHandle BuildAttributeBlob (string first, string second, string third) + { + _attrBlob.Clear (); + _attrBlob.WriteUInt16 (0x0001); + _attrBlob.WriteSerializedString (first); + _attrBlob.WriteSerializedString (second); + _attrBlob.WriteSerializedString (third); + _attrBlob.WriteUInt16 (0x0000); + return Metadata.GetOrAddBlob (_attrBlob); + } + /// /// Emits the IgnoresAccessChecksToAttribute type and applies /// [assembly: IgnoresAccessChecksTo("...")] for each assembly name. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 5b3725406aa..56a160a7914 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; @@ -196,10 +195,7 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) } EmitMemberReferences (); - _pe.PrepareUtf8Fields (model.ProxyTypes - .Where (proxy => proxy.IsAcw) - .SelectMany (proxy => proxy.NativeRegistrations) - .SelectMany (registration => new [] { registration.JniMethodName, registration.JniSignature })); + _pe.PrepareUtf8Fields (EnumerateNativeRegistrationStrings (model.ProxyTypes)); // Track wrapper targets → handles for RegisterNatives. var wrapperHandles = new Dictionary (); @@ -223,6 +219,19 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); } + static IEnumerable EnumerateNativeRegistrationStrings (IReadOnlyList proxies) + { + foreach (var proxy in proxies) { + if (!proxy.IsAcw) { + continue; + } + foreach (var registration in proxy.NativeRegistrations) { + yield return registration.JniMethodName; + yield return registration.JniSignature; + } + } + } + static List OrderProxiesForWrapperTargets (IReadOnlyList proxies) { var proxyByType = new Dictionary<(string Namespace, string TypeName), JavaPeerProxyData> (); @@ -1717,16 +1726,15 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) { var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; - var blob = _pe.BuildAttributeBlob (b => { - b.WriteSerializedString (entry.MapKey); - b.WriteSerializedString (entry.ProxyTypeReference); - if (!entry.IsUnconditional) { - if (entry.TargetTypeReference is null) { - throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.MapKey}'"); - } - b.WriteSerializedString (entry.TargetTypeReference); + BlobHandle blob; + if (entry.IsUnconditional) { + blob = _pe.BuildAttributeBlob (entry.MapKey, entry.ProxyTypeReference); + } else { + if (entry.TargetTypeReference is null) { + throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.MapKey}'"); } - }); + blob = _pe.BuildAttributeBlob (entry.MapKey, entry.ProxyTypeReference, entry.TargetTypeReference); + } _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); } @@ -1734,10 +1742,7 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) { var ctorRef = _typeMapAssociationAttrCtorRef; - var blob = _pe.BuildAttributeBlob (b => { - b.WriteSerializedString (assoc.SourceTypeReference); - b.WriteSerializedString (assoc.AliasProxyTypeReference); - }); + var blob = _pe.BuildAttributeBlob (assoc.SourceTypeReference, assoc.AliasProxyTypeReference); _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 7628ad984ac..30ca13c1b06 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -12,12 +13,25 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// sealed class AssemblyIndex : IDisposable { + const byte ElementTypeValueType = 0x11; + readonly PEReader peReader; readonly CustomAttributeTypeProvider customAttributeTypeProvider; + readonly string? [] typeFullNames; + readonly string? [] typeReferenceFullNames; + readonly string? [] typeReferenceAssemblyNames; + readonly string? [] assemblyReferenceNames; + readonly TypeRefData? [] classTypeDefinitions; + readonly TypeRefData? [] valueTypeDefinitions; + readonly TypeRefData? [] classTypeReferences; + readonly TypeRefData? [] valueTypeReferences; + readonly Dictionary customAttributeNames = new (); + readonly Dictionary constructorParameterCounts = new (); public MetadataReader Reader { get; } public string AssemblyName { get; } public string AssemblyPath { get; } + internal TypeRefSignatureTypeProvider TypeRefSignatureProvider { get; } /// /// Maps full managed type name (e.g., "Android.App.Activity") to its TypeDefinitionHandle. @@ -53,9 +67,18 @@ sealed class AssemblyIndex : IDisposable { this.peReader = peReader; this.customAttributeTypeProvider = new CustomAttributeTypeProvider (reader); + typeFullNames = new string? [reader.TypeDefinitions.Count + 1]; + typeReferenceFullNames = new string? [reader.TypeReferences.Count + 1]; + typeReferenceAssemblyNames = new string? [reader.TypeReferences.Count + 1]; + assemblyReferenceNames = new string? [reader.AssemblyReferences.Count + 1]; + classTypeDefinitions = new TypeRefData? [reader.TypeDefinitions.Count + 1]; + valueTypeDefinitions = new TypeRefData? [reader.TypeDefinitions.Count + 1]; + classTypeReferences = new TypeRefData? [reader.TypeReferences.Count + 1]; + valueTypeReferences = new TypeRefData? [reader.TypeReferences.Count + 1]; Reader = reader; AssemblyName = assemblyName; AssemblyPath = assemblyPath; + TypeRefSignatureProvider = new TypeRefSignatureTypeProvider (this); } public static AssemblyIndex Create (PEReader peReader, string assemblyName, string assemblyPath = "") @@ -95,7 +118,7 @@ void Build () MayUseJniAddNativeMethodRegistrationAttribute = true; } - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, Reader); + var fullName = GetTypeFullName (typeHandle); if (fullName.Length == 0) { continue; } @@ -139,7 +162,7 @@ string GetExportedTypeFullName (ExportedType exportedType) foreach (var caHandle in typeDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); - var attrName = GetCustomAttributeName (ca, Reader); + var attrName = GetCustomAttributeName (ca); if (attrName is null) { continue; @@ -214,6 +237,116 @@ string GetExportedTypeFullName (ExportedType exportedType) return (registerInfo, attrInfo); } + internal string GetTypeFullName (TypeDefinitionHandle handle) + { + int row = MetadataTokens.GetRowNumber (handle); + var fullName = typeFullNames [row]; + if (fullName is not null) { + return fullName; + } + + var typeDef = Reader.GetTypeDefinition (handle); + var name = Reader.GetString (typeDef.Name); + if (typeDef.IsNested) { + fullName = MetadataTypeNameResolver.JoinNestedTypeName (GetTypeFullName (typeDef.GetDeclaringType ()), name); + } else { + fullName = MetadataTypeNameResolver.JoinNamespaceAndName (Reader.GetString (typeDef.Namespace), name); + } + typeFullNames [row] = fullName; + return fullName; + } + + internal TypeRefData GetTypeRef (TypeDefinitionHandle handle, byte rawTypeKind) + { + var cache = rawTypeKind == ElementTypeValueType ? valueTypeDefinitions : classTypeDefinitions; + int row = MetadataTokens.GetRowNumber (handle); + var type = cache [row]; + if (type is not null) { + return type; + } + + type = new TypeRefData { + ManagedTypeName = GetTypeFullName (handle), + AssemblyName = AssemblyName, + IsValueType = rawTypeKind == ElementTypeValueType, + }; + cache [row] = type; + return type; + } + + internal TypeRefData GetTypeRef (TypeReferenceHandle handle, byte rawTypeKind) + { + var cache = rawTypeKind == ElementTypeValueType ? valueTypeReferences : classTypeReferences; + int row = MetadataTokens.GetRowNumber (handle); + var type = cache [row]; + if (type is not null) { + return type; + } + + type = new TypeRefData { + ManagedTypeName = GetTypeReferenceFullName (handle), + AssemblyName = GetTypeReferenceAssemblyName (handle), + IsValueType = rawTypeKind == ElementTypeValueType, + }; + cache [row] = type; + return type; + } + + string GetTypeReferenceFullName (TypeReferenceHandle handle) + { + int row = MetadataTokens.GetRowNumber (handle); + var fullName = typeReferenceFullNames [row]; + if (fullName is not null) { + return fullName; + } + + var typeRef = Reader.GetTypeReference (handle); + var name = Reader.GetString (typeRef.Name); + fullName = typeRef.ResolutionScope.Kind == HandleKind.TypeReference + ? MetadataTypeNameResolver.JoinNestedTypeName (GetTypeReferenceFullName ((TypeReferenceHandle) typeRef.ResolutionScope), name) + : MetadataTypeNameResolver.JoinNamespaceAndName (Reader.GetString (typeRef.Namespace), name); + typeReferenceFullNames [row] = fullName; + return fullName; + } + + string GetTypeReferenceAssemblyName (TypeReferenceHandle handle) + { + int row = MetadataTokens.GetRowNumber (handle); + var assemblyName = typeReferenceAssemblyNames [row]; + if (assemblyName is not null) { + return assemblyName; + } + + var typeRef = Reader.GetTypeReference (handle); + if (typeRef.ResolutionScope.Kind == HandleKind.TypeReference) { + assemblyName = GetTypeReferenceAssemblyName ((TypeReferenceHandle) typeRef.ResolutionScope); + } else if (typeRef.ResolutionScope.Kind == HandleKind.AssemblyReference) { + var assemblyHandle = (AssemblyReferenceHandle) typeRef.ResolutionScope; + int assemblyRow = MetadataTokens.GetRowNumber (assemblyHandle); + assemblyName = assemblyReferenceNames [assemblyRow]; + if (assemblyName is null) { + assemblyName = Reader.GetString (Reader.GetAssemblyReference (assemblyHandle).Name); + assemblyReferenceNames [assemblyRow] = assemblyName; + } + } else { + assemblyName = AssemblyName; + } + + typeReferenceAssemblyNames [row] = assemblyName; + return assemblyName; + } + + internal string? GetCustomAttributeName (CustomAttribute ca) + { + if (customAttributeNames.TryGetValue (ca.Constructor, out var name)) { + return name; + } + + name = GetCustomAttributeName (ca, Reader); + customAttributeNames.Add (ca.Constructor, name); + return name; + } + static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { "ActivityAttribute", "ServiceAttribute", @@ -246,14 +379,14 @@ bool ImplementsJniNameProviderAttribute (CustomAttribute ca) var impl = Reader.GetInterfaceImplementation (implHandle); if (impl.Interface.Kind == HandleKind.TypeReference) { var typeRef = Reader.GetTypeReference ((TypeReferenceHandle)impl.Interface); - if (Reader.GetString (typeRef.Name) == "IJniNameProviderAttribute" && - Reader.GetString (typeRef.Namespace) == "Java.Interop") { + if (Reader.StringComparer.Equals (typeRef.Name, "IJniNameProviderAttribute") && + Reader.StringComparer.Equals (typeRef.Namespace, "Java.Interop")) { return true; } } else if (impl.Interface.Kind == HandleKind.TypeDefinition) { var ifaceDef = Reader.GetTypeDefinition ((TypeDefinitionHandle)impl.Interface); - if (Reader.GetString (ifaceDef.Name) == "IJniNameProviderAttribute" && - Reader.GetString (ifaceDef.Namespace) == "Java.Interop") { + if (Reader.StringComparer.Equals (ifaceDef.Name, "IJniNameProviderAttribute") && + Reader.StringComparer.Equals (ifaceDef.Namespace, "Java.Interop")) { return true; } } @@ -296,16 +429,86 @@ internal static bool IsCustomAttributeMatch (CustomAttribute ca, MetadataReader } static bool IsTypeReferenceMatch (TypeReference typeRef, MetadataReader reader, string typeNamespace, string typeName) => - reader.GetString (typeRef.Name) == typeName && - reader.GetString (typeRef.Namespace) == typeNamespace; + reader.StringComparer.Equals (typeRef.Name, typeName) && + reader.StringComparer.Equals (typeRef.Namespace, typeNamespace); static bool IsTypeDefinitionMatch (TypeDefinition typeDef, MetadataReader reader, string typeNamespace, string typeName) => - reader.GetString (typeDef.Name) == typeName && - reader.GetString (typeDef.Namespace) == typeNamespace; + reader.StringComparer.Equals (typeDef.Name, typeName) && + reader.StringComparer.Equals (typeDef.Namespace, typeNamespace); internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca) { - return ParseRegisterInfo (DecodeAttribute (ca)); + // RegisterAttribute has only string constructor arguments and bool/int/string + // named properties. Reading that stable shape directly avoids the generic + // custom-attribute decoder's arrays and boxed values for every Java method. + var reader = Reader.GetBlobReader (ca.Value); + if (reader.ReadUInt16 () != 1) { + throw new BadImageFormatException ("Invalid custom attribute prolog."); + } + + int parameterCount = GetConstructorParameterCount (ca.Constructor); + if (parameterCount > 3) { + return ParseRegisterInfo (DecodeAttribute (ca)); + } + string jniName = parameterCount > 0 ? reader.ReadSerializedString () ?? "" : ""; + string? signature = parameterCount > 1 ? reader.ReadSerializedString () : null; + string? connector = parameterCount > 2 ? reader.ReadSerializedString () : null; + + bool doNotGenerateAcw = false; + int namedArgumentCount = reader.ReadUInt16 (); + for (int i = 0; i < namedArgumentCount; i++) { + reader.ReadByte (); + var typeCode = (SerializationTypeCode) reader.ReadByte (); + var name = reader.ReadSerializedString (); + switch (typeCode) { + case SerializationTypeCode.Boolean: + bool value = reader.ReadByte () != 0; + if (name == "DoNotGenerateAcw") { + doNotGenerateAcw = value; + } + break; + case SerializationTypeCode.Int32: + reader.ReadInt32 (); + break; + case SerializationTypeCode.String: + reader.ReadSerializedString (); + break; + default: + return ParseRegisterInfo (DecodeAttribute (ca)); + } + } + + return new RegisterInfo { + JniName = jniName, + Signature = signature, + Connector = connector, + DoNotGenerateAcw = doNotGenerateAcw, + }; + } + + int GetConstructorParameterCount (EntityHandle constructor) + { + if (constructorParameterCounts.TryGetValue (constructor, out int count)) { + return count; + } + + BlobHandle signature = constructor.Kind switch { + HandleKind.MemberReference => Reader.GetMemberReference ((MemberReferenceHandle) constructor).Signature, + HandleKind.MethodDefinition => Reader.GetMethodDefinition ((MethodDefinitionHandle) constructor).Signature, + _ => default, + }; + if (signature.IsNil) { + return 0; + } + + var reader = Reader.GetBlobReader (signature); + var header = reader.ReadSignatureHeader (); + if (header.IsGeneric) { + reader.ReadCompressedInteger (); + } + count = reader.ReadCompressedInteger (); + constructorParameterCounts.Add (constructor, count); + return count; } internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) @@ -525,7 +728,7 @@ internal void ScanAssemblyAttributes (AssemblyManifestInfo info) var asmDef = Reader.GetAssemblyDefinition (); foreach (var caHandle in asmDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); - var attrName = GetCustomAttributeName (ca, Reader); + var attrName = GetCustomAttributeName (ca); if (attrName is null || !KnownAssemblyAttributes.Contains (attrName)) { continue; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 1dce5a4e48c..b63b411234e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -165,6 +165,11 @@ public sealed record JavaPeerInfo /// public sealed record MarshalMethodInfo { + internal static readonly TypeRefData DefaultReturnType = new () { + ManagedTypeName = "System.Void", + AssemblyName = "System.Runtime", + }; + /// /// JNI method name, e.g., "onCreate". /// This is the Java method name (without n_ prefix). @@ -273,10 +278,7 @@ public sealed record MarshalMethodInfo /// /// Managed return type, including the defining assembly. /// - internal TypeRefData ManagedReturnType { get; init; } = new () { - ManagedTypeName = "System.Void", - AssemblyName = "System.Runtime", - }; + internal TypeRefData ManagedReturnType { get; init; } = DefaultReturnType; /// /// [ExportParameter] kind applied to the return value, if any. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 0376a54c98b..56f06deb2df 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -7,6 +7,7 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Text; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -24,23 +25,33 @@ enum HashedPackageNamingPolicy { } readonly record struct ResolvabilityResult (bool IsResolvable, string? UnresolvedTypeName, string? UnresolvedAssemblyName); + readonly record struct PublicConstructorInfo (ImmutableArray ParameterTypes, string JniParameterSignature); readonly Dictionary assemblyCache = new (StringComparer.Ordinal); readonly Dictionary<(string typeName, string assemblyName), ActivationCtorInfo> activationCtorCache = new (); - readonly Dictionary<(string AssemblyName, int TypeRow), ResolvabilityResult> resolvabilityCache = new (); + readonly Dictionary resolvabilityCache = new (StringComparer.Ordinal); readonly HashSet<(string AssemblyName, int TypeRow)> resolvabilityVisited = new (); + readonly Dictionary> defaultExportKinds = new (); readonly ITrimmableTypeMapLogger? logger; readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; readonly bool errorOnCustomJavaObject; + readonly bool collectMarshalMethodsForNonAcw; readonly JavaAnnotationParser annotationParser; public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = true) + : this (packageNamingPolicy, logger, frameworkAssemblyNames, errorOnCustomJavaObject, collectMarshalMethodsForNonAcw: true) + { + } + + internal JavaPeerScanner (string? packageNamingPolicy, ITrimmableTypeMapLogger? logger, HashSet? frameworkAssemblyNames, + bool errorOnCustomJavaObject, bool collectMarshalMethodsForNonAcw) { this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy); this.logger = logger; this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet (StringComparer.OrdinalIgnoreCase); this.errorOnCustomJavaObject = errorOnCustomJavaObject; + this.collectMarshalMethodsForNonAcw = collectMarshalMethodsForNonAcw; annotationParser = new JavaAnnotationParser (assemblyCache, ResolveTypeOfArgumentToJniName); } @@ -113,7 +124,7 @@ static bool TryReadNativeCallbackSignature (AssemblyIndex callbackIndex, TypeDef if ((methodDef.Attributes & MethodAttributes.Static) == 0) { continue; } - if (reader.GetString (methodDef.Name) != nativeCallbackName) { + if (!reader.StringComparer.Equals (methodDef.Name, nativeCallbackName)) { continue; } @@ -180,6 +191,7 @@ public List Scan (IEnumerable assemblies) foreach (var assembly in assemblies) { var index = AssemblyIndex.Create (assembly.Reader, assembly.Name, assembly.Path); assemblyCache [index.AssemblyName] = index; + resolvabilityCache [index.AssemblyName] = new ResolvabilityResult? [index.Reader.TypeDefinitions.Count + 1]; } // Key by (managedTypeName, assemblyName) to avoid collisions when two assemblies @@ -264,11 +276,11 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var typeDef = index.Reader.GetTypeDefinition (typeHandle); // Skip module-level types - if (index.Reader.GetString (typeDef.Name) == "") { + if (index.Reader.StringComparer.Equals (typeDef.Name, "")) { continue; } - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var fullName = index.GetTypeFullName (typeHandle); if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) { continue; @@ -315,14 +327,14 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A } else { // No explicit JNI name — check if this type extends a known Java peer. // If so, auto-compute JNI name from the managed type name via CRC64. - if (ExtendsJavaPeer (typeDef, index)) { + if (ExtendsJavaPeer (typeHandle, typeDef, index)) { (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); } else { // A managed class that implements Android.Runtime.IJavaObject but does not // derive from a Java peer (Java.Lang.Object / Java.Lang.Throwable) cannot be // marshaled to Java. Mirror the legacy XAJavaTypeScanner XA4212 diagnostic, // which the managed/llvm-ir typemap paths raise via GenerateJavaStubs. - if (IsCustomJavaObject (typeDef, index)) { + if (IsCustomJavaObject (typeHandle, typeDef, index)) { if (errorOnCustomJavaObject) { logger?.LogCustomJavaObjectError (fullName); } else { @@ -360,7 +372,11 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // Override and interface detection is only for user ACW class types: // - MCW types (DoNotGenerateAcw) already have [Register] on every method // - Interface types don't implement other interfaces' methods in JCWs - var (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); + List? marshalMethods = null; + List? exportFields = null; + if (!doNotGenerateAcw || collectMarshalMethodsForNonAcw) { + (marshalMethods, exportFields) = CollectMarshalMethods (typeDef, index, detectBaseOverrides: !doNotGenerateAcw && !isInterface); + } // Resolve activation constructor var activationCtor = ResolveActivationCtor (fullName, typeDef, index); @@ -394,9 +410,9 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false, IsUnconditional = isUnconditional, CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, - MarshalMethods = marshalMethods, - JavaConstructors = BuildJavaConstructors (marshalMethods, typeDef, index), - JavaFields = exportFields, + MarshalMethods = marshalMethods ?? [], + JavaConstructors = marshalMethods is not null ? BuildJavaConstructors (marshalMethods, typeDef, index) : [], + JavaFields = exportFields ?? [], ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, InvokerActivationCtorStyle = invokerActivationCtorStyle, @@ -431,14 +447,16 @@ bool IsResolvableTypeDefinition ( [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { - var cacheKey = (index.AssemblyName, MetadataTokens.GetRowNumber (typeDefHandle)); + int typeRow = MetadataTokens.GetRowNumber (typeDefHandle); + var cache = resolvabilityCache [index.AssemblyName]; - if (resolvabilityCache.TryGetValue (cacheKey, out var cached)) { + if (cache [typeRow] is ResolvabilityResult cached) { unresolvedTypeName = cached.UnresolvedTypeName; unresolvedAssemblyName = cached.UnresolvedAssemblyName; return cached.IsResolvable; } + var cacheKey = (index.AssemblyName, typeRow); if (!visited.Add (cacheKey)) { unresolvedTypeName = null; unresolvedAssemblyName = null; @@ -448,14 +466,14 @@ bool IsResolvableTypeDefinition ( var typeDef = index.Reader.GetTypeDefinition (typeDefHandle); if (!IsResolvableTypeHandle (typeDef.BaseType, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { - resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); + cache [typeRow] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; } foreach (var interfaceHandle in typeDef.GetInterfaceImplementations ()) { var interfaceImplementation = index.Reader.GetInterfaceImplementation (interfaceHandle); if (!IsResolvableTypeHandle (interfaceImplementation.Interface, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { - resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); + cache [typeRow] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; } } @@ -468,7 +486,7 @@ bool IsResolvableTypeDefinition ( foreach (var constraintHandle in genericParameter.GetConstraints ()) { var constraint = index.Reader.GetGenericParameterConstraint (constraintHandle); if (!IsResolvableTypeHandle (constraint.Type, index, visited, out unresolvedTypeName, out unresolvedAssemblyName)) { - resolvabilityCache [cacheKey] = new (false, unresolvedTypeName, unresolvedAssemblyName); + cache [typeRow] = new (false, unresolvedTypeName, unresolvedAssemblyName); return false; } } @@ -476,7 +494,7 @@ bool IsResolvableTypeDefinition ( unresolvedTypeName = null; unresolvedAssemblyName = null; - resolvabilityCache [cacheKey] = new (true, null, null); + cache [typeRow] = new (true, null, null); return true; } @@ -514,7 +532,7 @@ bool IsResolvableTypeReference ( [NotNullWhen (false)] out string? unresolvedTypeName, [NotNullWhen (false)] out string? unresolvedAssemblyName) { - var typeRef = MetadataTypeNameResolver.GetTypeRefFromReference (index.Reader, handle, index.AssemblyName, rawTypeKind: 0); + var typeRef = index.GetTypeRef (handle, rawTypeKind: 0); var typeName = typeRef.ManagedTypeName; var assemblyName = typeRef.AssemblyName; if (!assemblyCache.TryGetValue (assemblyName, out var resolvedIndex)) { @@ -648,7 +666,7 @@ static void SkipArrayShape (ref BlobReader reader) { var methods = new List (); var fields = new List (); - var registeredMethodKeys = new HashSet (StringComparer.Ordinal); + HashSet? registeredMethodKeys = detectBaseOverrides ? new (StringComparer.Ordinal) : null; // Pass 1: collect methods with [Register], [Export], or [ExportField] directly on them foreach (var methodHandle in typeDef.GetMethods ()) { @@ -668,7 +686,7 @@ static void SkipArrayShape (ref BlobReader reader) // e.g., `[Export("foo")] public override void OnCreate(...)` needs both // the [Register]-driven override entry (Get*Handler connector) AND the // [Export]-driven entry. Skip the dedup key for [Export]/[ExportField]. - if (exportInfo is null) { + if (registeredMethodKeys is not null && exportInfo is null) { var sig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); registeredMethodKeys.Add ($"{index.Reader.GetString (methodDef.Name)}({string.Join (",", sig.ParameterTypes)})"); } @@ -686,8 +704,10 @@ static void SkipArrayShape (ref BlobReader reader) if (!accessors.Getter.IsNil) { var getterDef = index.Reader.GetMethodDefinition (accessors.Getter); AddMarshalMethod (methods, propRegister, getterDef, index); - var sig = getterDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); - registeredMethodKeys.Add ($"{index.Reader.GetString (getterDef.Name)}({string.Join (",", sig.ParameterTypes)})"); + if (registeredMethodKeys is not null) { + var sig = getterDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + registeredMethodKeys.Add ($"{index.Reader.GetString (getterDef.Name)}({string.Join (",", sig.ParameterTypes)})"); + } } } @@ -695,7 +715,7 @@ static void SkipArrayShape (ref BlobReader reader) // Only for user ACW types — MCW types (DoNotGenerateAcw=true) already have // [Register] on every method that matters. Running override detection on them // would incorrectly pick up internal overrides (e.g., JavaObject.equals). - if (detectBaseOverrides) { + if (registeredMethodKeys is not null) { CollectBaseMethodOverrides (typeDef, index, methods, registeredMethodKeys); } @@ -703,12 +723,12 @@ static void SkipArrayShape (ref BlobReader reader) // When a type implements a Java interface (e.g., IOnClickListener), the // implementing method may not have [Register]. The legacy pipeline adds // these via the interface loop in CecilImporter.cs lines 100-120. - if (detectBaseOverrides) { + if (registeredMethodKeys is not null) { CollectInterfaceMethodImplementations (typeDef, index, methods, registeredMethodKeys); } // Pass 5: detect Java constructors that chain from base registered ctors. - if (detectBaseOverrides) { + if (registeredMethodKeys is not null) { CollectBaseConstructorChain (typeDef, index, methods); } @@ -976,9 +996,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // Check each ctor on this type for additional constructors not yet covered foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - var name = index.Reader.GetString (methodDef.Name); - - if (name != ".ctor") { + if (!index.Reader.StringComparer.Equals (methodDef.Name, ".ctor")) { continue; } @@ -1082,7 +1100,7 @@ void CollectBaseConstructorChain (TypeDefinition typeDef, AssemblyIndex index, // emit the correct peer descriptor instead of falling back to // java/lang/Object. var typeDef = index.Reader.GetTypeDefinition (handle); - if (ExtendsJavaPeer (typeDef, index)) { + if (ExtendsJavaPeer (handle, typeDef, index)) { var (jniName, _) = ComputeAutoJniNames (typeDef, index); return $"L{jniName};"; } @@ -1229,8 +1247,7 @@ List CollectBaseRegisteredCtors (TypeDefinition typeDef, AssemblyI 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); - if (name != ".ctor") { + if (!baseIndex.Reader.StringComparer.Equals (methodDef.Name, ".ctor")) { continue; } @@ -1343,9 +1360,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con // Check methods on this base type foreach (var baseMethodHandle in baseTypeDef.GetMethods ()) { var baseMethodDef = baseIndex.Reader.GetMethodDefinition (baseMethodHandle); - var baseName = baseIndex.Reader.GetString (baseMethodDef.Name); - - if (baseName != methodName) { + if (!baseIndex.Reader.StringComparer.Equals (baseMethodDef.Name, methodName)) { continue; } @@ -1422,8 +1437,7 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con } var baseGetterDef = baseIndex.Reader.GetMethodDefinition (baseAccessors.Getter); - var baseGetterName = baseIndex.Reader.GetString (baseGetterDef.Name); - if (baseGetterName != getterName) { + if (!baseIndex.Reader.StringComparer.Equals (baseGetterDef.Name, getterName)) { continue; } @@ -1466,8 +1480,8 @@ static TypeRefData SubstituteGenericArguments (TypeRefData type, TypeRefData con /// static bool HaveIdenticalParameterTypes (MethodDefinition derivedMethod, AssemblyIndex derivedIndex, MethodDefinition baseMethod, AssemblyIndex baseIndex, TypeRefData baseTypeRef) { - var derivedSig = derivedMethod.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: derivedIndex); - var baseSig = baseMethod.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: baseIndex); + var derivedSig = derivedMethod.DecodeSignature (derivedIndex.TypeRefSignatureProvider, genericContext: derivedIndex); + var baseSig = baseMethod.DecodeSignature (baseIndex.TypeRefSignatureProvider, genericContext: baseIndex); if (derivedSig.ParameterTypes.Length != baseSig.ParameterTypes.Length) { return false; @@ -1493,7 +1507,6 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf bool isConstructor = registerInfo.JniName == "" || registerInfo.JniName == ".ctor"; bool isExport = exportInfo is not null; string managedName = index.Reader.GetString (methodDef.Name); - var managedSig = methodDef.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); string jniSignature = registerInfo.Signature ?? "()V"; string declaringTypeName = ""; @@ -1505,16 +1518,19 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf // Only decode TypeRefData signatures for methods that need direct dispatch IL // generation; static n_* callback forwarders already encode from the JNI signature. var managedTypeSig = mayCallManagedMethodDirectly - ? methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index) + ? methodDef.DecodeSignature (index.TypeRefSignatureProvider, index) : default; bool callManagedMethodDirectly = isExport || (mayCallManagedMethodDirectly && SupportsDirectManagedMethodCall (managedTypeSig)); - var parameterKinds = exportInfo?.ParameterKinds ?? CreateDefaultExportKinds (managedSig.ParameterTypes.Length); + var parameterKinds = exportInfo?.ParameterKinds ?? + (callManagedMethodDirectly ? GetDefaultExportKinds (managedTypeSig.ParameterTypes.Length) : []); - var managedParameterTypes = new List (); + IReadOnlyList managedParameterTypes = []; if (callManagedMethodDirectly) { - foreach (var parameterType in managedTypeSig.ParameterTypes) { - managedParameterTypes.Add (EnrichTypeRefWithEnumInfo (parameterType)); + var parameters = new TypeRefData [managedTypeSig.ParameterTypes.Length]; + for (int i = 0; i < parameters.Length; i++) { + parameters [i] = EnrichTypeRefWithEnumInfo (managedTypeSig.ParameterTypes [i]); } + managedParameterTypes = parameters; } string nativeCallbackName = GetNativeCallbackName (registerInfo.Connector, managedName, isConstructor); @@ -1547,10 +1563,9 @@ void AddMarshalMethod (List methods, RegisterInfo registerInf NativeCallbackReturnTypeName = nativeCallbackReturnTypeName, ManagedParameterTypes = managedParameterTypes, ManagedParameterExportKinds = parameterKinds, - ManagedReturnType = callManagedMethodDirectly ? EnrichTypeRefWithEnumInfo (managedTypeSig.ReturnType) : new TypeRefData { - ManagedTypeName = managedSig.ReturnType, - AssemblyName = "System.Runtime", - }, + ManagedReturnType = callManagedMethodDirectly + ? EnrichTypeRefWithEnumInfo (managedTypeSig.ReturnType) + : MarshalMethodInfo.DefaultReturnType, ManagedReturnExportKind = exportInfo?.ReturnKind ?? ExportParameterKindInfo.Unspecified, IsStatic = (methodDef.Attributes & MethodAttributes.Static) == MethodAttributes.Static, IsConstructor = isConstructor, @@ -1627,7 +1642,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 _, out _)) { + if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out var baseTypeHandle, out var baseIndex, out var baseTypeName, out _, out _)) { return null; } @@ -1645,7 +1660,7 @@ static string GetJavaAccess (MethodAttributes access) // Base type may be a Java peer without [Register] that hasn't been scanned yet // (scan order within an assembly is not guaranteed). Resolve it the same way // ScanAssembly does: check ExtendsJavaPeer and compute the auto JNI name. - if (ExtendsJavaPeer (baseTypeDef, baseIndex)) { + if (ExtendsJavaPeer (baseTypeHandle, baseTypeDef, baseIndex)) { var (jniName, _) = ComputeAutoJniNames (baseTypeDef, baseIndex); return jniName; } @@ -1680,7 +1695,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, exportInfo = null; foreach (var caHandle in methodDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + var attrName = index.GetCustomAttributeName (ca); if (attrName == "RegisterAttribute") { registerInfo = index.ParseRegisterAttribute (ca); @@ -1716,7 +1731,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, { foreach (var caHandle in propDef.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + var attrName = index.GetCustomAttributeName (ca); if (attrName == "RegisterAttribute") { return index.ParseRegisterAttribute (ca); @@ -1775,7 +1790,7 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, string resolvedExportName = exportName ?? throw new InvalidOperationException ("Export name should not be null at this point."); // Build JNI signature from method signature - var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, index); var (parameterKinds, returnKind) = GetExportParameterKinds (methodDef, index, sig.ParameterTypes.Length); var jniSig = BuildJniSignatureFromManaged (sig, parameterKinds, returnKind); @@ -1790,18 +1805,21 @@ bool TryGetMethodRegisterInfo (MethodDefinition methodDef, AssemblyIndex index, ); } - static List CreateDefaultExportKinds (int parameterCount) + IReadOnlyList GetDefaultExportKinds (int parameterCount) { - var kinds = new List (parameterCount); - for (int i = 0; i < parameterCount; i++) { - kinds.Add (ExportParameterKindInfo.Unspecified); + if (parameterCount == 0) { + return []; + } + if (!defaultExportKinds.TryGetValue (parameterCount, out var kinds)) { + kinds = new ExportParameterKindInfo [parameterCount]; + defaultExportKinds.Add (parameterCount, kinds); } return kinds; } - static (List parameterKinds, ExportParameterKindInfo returnKind) GetExportParameterKinds (MethodDefinition methodDef, AssemblyIndex index, int parameterCount) + static (IReadOnlyList parameterKinds, ExportParameterKindInfo returnKind) GetExportParameterKinds (MethodDefinition methodDef, AssemblyIndex index, int parameterCount) { - var parameterKinds = CreateDefaultExportKinds (parameterCount); + var parameterKinds = new ExportParameterKindInfo [parameterCount]; var returnKind = ExportParameterKindInfo.Unspecified; foreach (var parameterHandle in methodDef.GetParameters ()) { @@ -1815,7 +1833,7 @@ static List CreateDefaultExportKinds (int parameterCoun returnKind = kind; } else { int parameterIndex = parameter.SequenceNumber - 1; - if (parameterIndex >= 0 && parameterIndex < parameterKinds.Count) { + if (parameterIndex >= 0 && parameterIndex < parameterKinds.Length) { parameterKinds [parameterIndex] = kind; } } @@ -1828,7 +1846,7 @@ static ExportParameterKindInfo GetExportParameterKind (Parameter parameter, Asse { foreach (var caHandle in parameter.GetCustomAttributes ()) { var ca = index.Reader.GetCustomAttribute (caHandle); - var attrName = AssemblyIndex.GetCustomAttributeName (ca, index.Reader); + var attrName = index.GetCustomAttributeName (ca); if (attrName != "ExportParameterAttribute") { continue; } @@ -1880,8 +1898,8 @@ string BuildJniSignatureFromManaged (MethodSignature sig, IReadOnly (RegisterInfo registerInfo, ExportInfo exportInfo) ParseExportFieldAsMethod (CustomAttribute ca, MethodDefinition methodDef, AssemblyIndex index) { var managedName = index.Reader.GetString (methodDef.Name); - var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); - var jniSig = BuildJniSignatureFromManaged (sig, CreateDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); + var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, index); + var jniSig = BuildJniSignatureFromManaged (sig, GetDefaultExportKinds (sig.ParameterTypes.Length), ExportParameterKindInfo.Unspecified); return ( new RegisterInfo { JniName = managedName, Signature = jniSig, Connector = "__export__", DoNotGenerateAcw = false }, @@ -2023,25 +2041,35 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI { foreach (var methodHandle in typeDef.GetMethods ()) { var method = index.Reader.GetMethodDefinition (methodHandle); - var name = index.Reader.GetString (method.Name); - - if (name != ".ctor") { + if (!index.Reader.StringComparer.Equals (method.Name, ".ctor") || + (method.Attributes & MethodAttributes.Static) != 0) { continue; } - var sig = method.DecodeSignature (SignatureTypeProvider.Instance, genericContext: default); + var signature = index.Reader.GetBlobReader (method.Signature); + var header = signature.ReadSignatureHeader (); + if (header.IsGeneric) { + signature.ReadCompressedInteger (); + } + if (signature.ReadCompressedInteger () != 2 || + (SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.Void) { + continue; + } // XI style: (IntPtr, JniHandleOwnership) - if (sig.ParameterTypes.Length == 2 && - sig.ParameterTypes [0] == "System.IntPtr" && - sig.ParameterTypes [1] == "Android.Runtime.JniHandleOwnership") { + var firstParameter = signature; + if ((SignatureTypeCode) signature.ReadByte () == SignatureTypeCode.IntPtr && + IsSignatureType (ref signature, index, "Android.Runtime", "JniHandleOwnership")) { return ActivationCtorStyle.XamarinAndroid; } // JI style: (ref JniObjectReference, JniObjectReferenceOptions) - if (sig.ParameterTypes.Length == 2 && - (sig.ParameterTypes [0] == "Java.Interop.JniObjectReference&" || sig.ParameterTypes [0] == "Java.Interop.JniObjectReference") && - sig.ParameterTypes [1] == "Java.Interop.JniObjectReferenceOptions") { + signature = firstParameter; + if ((SignatureTypeCode) signature.ReadByte () != SignatureTypeCode.ByReference) { + signature = firstParameter; + } + if (IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReference") && + IsSignatureType (ref signature, index, "Java.Interop", "JniObjectReferenceOptions")) { return ActivationCtorStyle.JavaInterop; } } @@ -2049,6 +2077,28 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI return null; } + static bool IsSignatureType (ref BlobReader signature, AssemblyIndex index, string typeNamespace, string typeName) + { + var kind = (SignatureTypeKind) signature.ReadByte (); + if (kind is not (SignatureTypeKind.Class or SignatureTypeKind.ValueType)) { + return false; + } + + var handle = signature.ReadTypeHandle (); + switch (handle.Kind) { + case HandleKind.TypeReference: + var typeRef = index.Reader.GetTypeReference ((TypeReferenceHandle) handle); + return index.Reader.StringComparer.Equals (typeRef.Namespace, typeNamespace) && + index.Reader.StringComparer.Equals (typeRef.Name, typeName); + case HandleKind.TypeDefinition: + var typeDef = index.Reader.GetTypeDefinition ((TypeDefinitionHandle) handle); + return index.Reader.StringComparer.Equals (typeDef.Namespace, typeNamespace) && + index.Reader.StringComparer.Equals (typeDef.Name, typeName); + default: + return false; + } + } + /// /// Resolves a TypeSpecificationHandle (generic instantiation) to the underlying /// type's (fullName, assemblyName) by reading the raw signature blob. @@ -2056,7 +2106,7 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI TypeRefData? ResolveTypeSpecification (TypeSpecificationHandle specHandle, AssemblyIndex index) { var typeSpec = index.Reader.GetTypeSpecification (specHandle); - return typeSpec.DecodeSignature (TypeRefSignatureTypeProvider.Instance, index); + return typeSpec.DecodeSignature (index.TypeRefSignatureProvider, index); } /// @@ -2066,15 +2116,10 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI TypeRefData? ResolveEntityHandle (EntityHandle handle, AssemblyIndex index) { switch (handle.Kind) { - case HandleKind.TypeDefinition: { - var td = index.Reader.GetTypeDefinition ((TypeDefinitionHandle)handle); - return new TypeRefData { - ManagedTypeName = MetadataTypeNameResolver.GetFullName (td, index.Reader), - AssemblyName = index.AssemblyName, - }; - } + case HandleKind.TypeDefinition: + return index.GetTypeRef ((TypeDefinitionHandle)handle, rawTypeKind: 0); case HandleKind.TypeReference: - return MetadataTypeNameResolver.GetTypeRefFromReference (index.Reader, (TypeReferenceHandle)handle, index.AssemblyName, rawTypeKind: 0); + return index.GetTypeRef ((TypeReferenceHandle)handle, rawTypeKind: 0); case HandleKind.TypeSpecification: return ResolveTypeSpecification ((TypeSpecificationHandle)handle, index); default: @@ -2144,11 +2189,11 @@ public void Dispose () assemblyCache.Clear (); } - readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + readonly Dictionary<(string AssemblyName, int TypeRow), bool> extendsJavaPeerCache = new (); const string IJavaObjectFullName = "Android.Runtime.IJavaObject"; - readonly Dictionary implementsIJavaObjectCache = new (StringComparer.Ordinal); + readonly Dictionary<(string AssemblyName, int TypeRow), bool> implementsIJavaObjectCache = new (); /// /// Determines whether a type is a "custom" Java object: a managed class that implements @@ -2156,12 +2201,12 @@ public void Dispose () /// Java.Lang.Throwable). Such types cannot be marshaled and produce XA4212. Interfaces and /// System.Exception subclasses are excluded, matching the legacy XAJavaTypeScanner. /// - bool IsCustomJavaObject (TypeDefinition typeDef, AssemblyIndex index) + bool IsCustomJavaObject (TypeDefinitionHandle typeHandle, TypeDefinition typeDef, AssemblyIndex index) { if ((typeDef.Attributes & TypeAttributes.Interface) != 0) { return false; } - if (!ImplementsIJavaObject (typeDef, index)) { + if (!ImplementsIJavaObject (typeHandle, typeDef, index)) { return false; } if (IsSubclassOfSystemException (typeDef, index)) { @@ -2175,10 +2220,9 @@ bool IsCustomJavaObject (TypeDefinition typeDef, AssemblyIndex index) /// interface that extends it, or via a base class. Results are cached; false-before-recurse /// prevents cycles. /// - bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index) + bool ImplementsIJavaObject (TypeDefinitionHandle typeHandle, TypeDefinition typeDef, AssemblyIndex index) { - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var key = $"{index.AssemblyName}:{fullName}"; + var key = (index.AssemblyName, MetadataTokens.GetRowNumber (typeHandle)); if (implementsIJavaObjectCache.TryGetValue (key, out var cached)) { return cached; @@ -2202,7 +2246,7 @@ bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index) // Recurse into the interface's own base interfaces if (TryResolveType (resolved.ManagedTypeName, resolved.AssemblyName, out var ifaceHandle, out var ifaceIndex)) { var ifaceDef = ifaceIndex.Reader.GetTypeDefinition (ifaceHandle); - if (ImplementsIJavaObject (ifaceDef, ifaceIndex)) { + if (ImplementsIJavaObject (ifaceHandle, ifaceDef, ifaceIndex)) { implementsIJavaObjectCache [key] = true; return true; } @@ -2214,7 +2258,7 @@ bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index) if (baseInfo is not null && TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) { var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); - if (ImplementsIJavaObject (baseDef, baseIndex)) { + if (ImplementsIJavaObject (baseHandle, baseDef, baseIndex)) { implementsIJavaObjectCache [key] = true; return true; } @@ -2247,10 +2291,9 @@ bool IsSubclassOfSystemException (TypeDefinition typeDef, AssemblyIndex index) /// Check if a type extends a known Java peer (has [Register] or component attribute) /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles. /// - bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) + bool ExtendsJavaPeer (TypeDefinitionHandle typeHandle, TypeDefinition typeDef, AssemblyIndex index) { - var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); - var key = $"{index.AssemblyName}:{fullName}"; + var key = (index.AssemblyName, MetadataTokens.GetRowNumber (typeHandle)); if (extendsJavaPeerCache.TryGetValue (key, out var cached)) { return cached; @@ -2283,7 +2326,7 @@ bool ExtendsJavaPeer (TypeDefinition typeDef, AssemblyIndex index) // Recurse up the hierarchy var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); - var result = ExtendsJavaPeer (baseDef, baseIndex); + var result = ExtendsJavaPeer (baseHandle, baseDef, baseIndex); extendsJavaPeerCache [key] = result; return result; } @@ -2473,6 +2516,7 @@ static string ExtractShortName (string fullName) List BuildJavaConstructors (List marshalMethods, TypeDefinition typeDef, AssemblyIndex index) { var ctors = new List (); + List? publicConstructors = null; int ctorIndex = 0; foreach (var mm in marshalMethods) { if (!mm.IsConstructor) { @@ -2481,7 +2525,8 @@ List BuildJavaConstructors (List marshal // Try to find a managed ctor whose signature matches the JNI ctor. // Unsupported managed parameter shapes fail in model building for [Export] // constructors; non-[Export] registrations keep the legacy activation fallback. - var managedParams = TryGetMatchingPublicConstructorParameterTypes (typeDef, mm.JniSignature, index); + publicConstructors ??= GetSupportedPublicConstructors (typeDef, index); + var managedParams = TryGetMatchingPublicConstructorParameterTypes (publicConstructors, mm.JniSignature); ctors.Add (new JavaConstructorInfo { JniSignature = mm.JniSignature, ConstructorIndex = ctorIndex, @@ -2495,73 +2540,68 @@ List BuildJavaConstructors (List marshal return ctors; } - /// - /// Attempts to find a managed instance constructor on - /// whose parameters match the supplied JNI signature, and returns its managed - /// parameter types. Returns when no compatible - /// constructor exists. - /// - IReadOnlyList? TryGetMatchingPublicConstructorParameterTypes (TypeDefinition typeDef, string jniSignature, AssemblyIndex index) + List GetSupportedPublicConstructors (TypeDefinition typeDef, AssemblyIndex index) { - var jniParams = JniSignatureHelper.ParseParameters (jniSignature); + var constructors = new List (); foreach (var methodHandle in typeDef.GetMethods ()) { var methodDef = index.Reader.GetMethodDefinition (methodHandle); - if ((methodDef.Attributes & MethodAttributes.Static) != 0) { - continue; - } - var name = index.Reader.GetString (methodDef.Name); - if (name != ".ctor") { - continue; - } - if ((methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) { - continue; - } - var sig = methodDef.DecodeSignature (TypeRefSignatureTypeProvider.Instance, genericContext: index); - if (sig.ParameterTypes.Length != jniParams.Count) { + if ((methodDef.Attributes & MethodAttributes.Static) != 0 || + (methodDef.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public || + !index.Reader.StringComparer.Equals (methodDef.Name, ".ctor")) { continue; } - // Skip ctors whose managed parameter signatures are not supported by the - // trimmable [Export]-style argument marshaller (generic instantiations, - // by-ref, pointers). Returning null here makes EmitUcoConstructor fall - // back to the legacy `(IntPtr, JniHandleOwnership)` activation ctor, - // which matches the legacy LLVM-IR behaviour for these shapes. - bool unsupportedParam = false; - foreach (var p in sig.ParameterTypes) { - var paramTypeName = p.ManagedTypeName; - if (p.GenericArguments.Count > 0 || paramTypeName.EndsWith ("&", StringComparison.Ordinal) || paramTypeName.EndsWith ("*", StringComparison.Ordinal)) { - unsupportedParam = true; + + var sig = methodDef.DecodeSignature (index.TypeRefSignatureProvider, genericContext: index); + bool unsupported = false; + foreach (var parameter in sig.ParameterTypes) { + var typeName = parameter.ManagedTypeName; + if (parameter.GenericArguments.Count > 0 || + typeName.EndsWith ("&", StringComparison.Ordinal) || + typeName.EndsWith ("*", StringComparison.Ordinal)) { + unsupported = true; break; } } - if (unsupportedParam) { - continue; + if (!unsupported) { + var signature = new StringBuilder (); + signature.Append ('('); + foreach (var parameter in sig.ParameterTypes) { + signature.Append (ManagedTypeToJniDescriptor (parameter)); + } + signature.Append (')'); + constructors.Add (new (sig.ParameterTypes, signature.ToString ())); } - if (!ManagedConstructorParametersMatchJniSignature (sig.ParameterTypes, jniParams)) { + } + return constructors; + } + + /// + /// Attempts to find a managed instance constructor in + /// whose parameters match the supplied JNI signature, and returns its managed + /// parameter types. Returns when no compatible + /// constructor exists. + /// + static IReadOnlyList? TryGetMatchingPublicConstructorParameterTypes ( + List publicConstructors, + string jniSignature) + { + int closeParen = jniSignature.IndexOf (')'); + if (closeParen < 0) { + throw new ArgumentException ($"Malformed JNI signature '{jniSignature}': missing ')'"); + } + int parameterSignatureLength = closeParen + 1; + foreach (var constructor in publicConstructors) { + if (constructor.JniParameterSignature.Length != parameterSignatureLength || + string.CompareOrdinal (jniSignature, 0, constructor.JniParameterSignature, 0, parameterSignatureLength) != 0) { continue; } // If multiple overloads with the same JNI-compatible signature exist, match // the first public constructor in metadata order, like TypeManager.Activate. - return [.. sig.ParameterTypes]; + return [.. constructor.ParameterTypes]; } return null; } - bool ManagedConstructorParametersMatchJniSignature (IReadOnlyList managedParams, IReadOnlyList jniParams) - { - if (managedParams.Count != jniParams.Count) { - return false; - } - - for (int i = 0; i < managedParams.Count; i++) { - var managedDescriptor = ManagedTypeToJniDescriptor (managedParams [i]); - if (!string.Equals (managedDescriptor, jniParams [i].JniType, StringComparison.Ordinal)) { - return false; - } - } - - return true; - } - /// /// Checks a single method for [ExportField] and adds a JavaFieldInfo if found. /// Called inline during Pass 1 to avoid a separate iteration. @@ -2570,7 +2610,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List { - public static readonly TypeRefSignatureTypeProvider Instance = new (); + static readonly TypeRefData VoidType = CreatePrimitiveType ("System.Void"); + static readonly TypeRefData BooleanType = CreatePrimitiveType ("System.Boolean"); + static readonly TypeRefData CharType = CreatePrimitiveType ("System.Char"); + static readonly TypeRefData SByteType = CreatePrimitiveType ("System.SByte"); + static readonly TypeRefData ByteType = CreatePrimitiveType ("System.Byte"); + static readonly TypeRefData Int16Type = CreatePrimitiveType ("System.Int16"); + static readonly TypeRefData UInt16Type = CreatePrimitiveType ("System.UInt16"); + static readonly TypeRefData Int32Type = CreatePrimitiveType ("System.Int32"); + static readonly TypeRefData UInt32Type = CreatePrimitiveType ("System.UInt32"); + static readonly TypeRefData Int64Type = CreatePrimitiveType ("System.Int64"); + static readonly TypeRefData UInt64Type = CreatePrimitiveType ("System.UInt64"); + static readonly TypeRefData SingleType = CreatePrimitiveType ("System.Single"); + static readonly TypeRefData DoubleType = CreatePrimitiveType ("System.Double"); + static readonly TypeRefData StringType = CreatePrimitiveType ("System.String"); + static readonly TypeRefData ObjectType = CreatePrimitiveType ("System.Object"); + static readonly TypeRefData IntPtrType = CreatePrimitiveType ("System.IntPtr"); + static readonly TypeRefData UIntPtrType = CreatePrimitiveType ("System.UIntPtr"); + static readonly TypeRefData TypedReferenceType = CreatePrimitiveType ("System.TypedReference"); + readonly AssemblyIndex index; + + internal TypeRefSignatureTypeProvider (AssemblyIndex index) + { + this.index = index; + } + + public TypeRefData GetPrimitiveType (PrimitiveTypeCode typeCode) => typeCode switch { + PrimitiveTypeCode.Void => VoidType, + PrimitiveTypeCode.Boolean => BooleanType, + PrimitiveTypeCode.Char => CharType, + PrimitiveTypeCode.SByte => SByteType, + PrimitiveTypeCode.Byte => ByteType, + PrimitiveTypeCode.Int16 => Int16Type, + PrimitiveTypeCode.UInt16 => UInt16Type, + PrimitiveTypeCode.Int32 => Int32Type, + PrimitiveTypeCode.UInt32 => UInt32Type, + PrimitiveTypeCode.Int64 => Int64Type, + PrimitiveTypeCode.UInt64 => UInt64Type, + PrimitiveTypeCode.Single => SingleType, + PrimitiveTypeCode.Double => DoubleType, + PrimitiveTypeCode.String => StringType, + PrimitiveTypeCode.Object => ObjectType, + PrimitiveTypeCode.IntPtr => IntPtrType, + PrimitiveTypeCode.UIntPtr => UIntPtrType, + PrimitiveTypeCode.TypedReference => TypedReferenceType, + _ => CreatePrimitiveType (typeCode.ToString ()), + }; - public TypeRefData GetPrimitiveType (PrimitiveTypeCode typeCode) => new () { - ManagedTypeName = SignatureTypeProvider.Instance.GetPrimitiveType (typeCode), + static TypeRefData CreatePrimitiveType (string managedTypeName) => new () { + ManagedTypeName = managedTypeName, AssemblyName = "System.Runtime", }; + // Each provider is owned by the AssemblyIndex for the MetadataReader being decoded. public TypeRefData GetTypeFromDefinition (MetadataReader reader, TypeDefinitionHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeRefFromDefinition (reader, handle, reader.GetString (reader.GetAssemblyDefinition ().Name), rawTypeKind); + => index.GetTypeRef (handle, rawTypeKind); public TypeRefData GetTypeFromReference (MetadataReader reader, TypeReferenceHandle handle, byte rawTypeKind) - => MetadataTypeNameResolver.GetTypeRefFromReference (reader, handle, reader.GetString (reader.GetAssemblyDefinition ().Name), rawTypeKind); + => index.GetTypeRef (handle, rawTypeKind); public TypeRefData GetTypeFromSpecification (MetadataReader reader, AssemblyIndex genericContext, TypeSpecificationHandle handle, byte rawTypeKind) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index be49c266d47..8fb6e85d346 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -26,6 +26,10 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) /// assemblies, generate JCW Java sources, and optionally generate a merged manifest. /// No file IO is performed — all results are returned in memory. /// + /// + /// Set to when callers do not consume method metadata for types + /// that cannot generate Java callable wrappers. + /// public TrimmableTypeMapResult Execute ( IReadOnlyList assemblies, Version systemRuntimeVersion, @@ -35,12 +39,18 @@ public TrimmableTypeMapResult Execute ( XDocument? manifestTemplate = null, string? packageNamingPolicy = null, bool generateTypeMapAssemblies = true, - bool errorOnCustomJavaObject = true) + bool errorOnCustomJavaObject = true, + bool collectMarshalMethodsForNonAcw = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames, errorOnCustomJavaObject); + var (allPeers, assemblyManifestInfo) = ScanAssemblies ( + assemblies, + packageNamingPolicy, + frameworkAssemblyNames, + errorOnCustomJavaObject, + collectMarshalMethodsForNonAcw); if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); @@ -250,9 +260,15 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes IReadOnlyList assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames, - bool errorOnCustomJavaObject = true) + bool errorOnCustomJavaObject, + bool collectMarshalMethodsForNonAcw) { - using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames, errorOnCustomJavaObject); + using var scanner = new JavaPeerScanner ( + packageNamingPolicy, + logger, + frameworkAssemblyNames, + errorOnCustomJavaObject, + collectMarshalMethodsForNonAcw); var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 7cffd69bb6a..1928954aad2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -221,7 +221,8 @@ public override bool RunTask () manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, generateTypeMapAssemblies: GenerateTypeMapAssemblies, - errorOnCustomJavaObject: ErrorOnCustomJavaObject); + errorOnCustomJavaObject: ErrorOnCustomJavaObject, + collectMarshalMethodsForNonAcw: false); if (Log.HasLoggedErrors) { return false; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 567df31d17f..25748d108dc 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -307,6 +307,37 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } + [Fact] + public void Execute_CanSkipUnusedNonAcwMarshalMethods () + { + using var fullReader = CreateTestFixturePEReader (); + using var optimizedReader = CreateTestFixturePEReader (); + var generator = CreateGenerator (); + var full = generator.Execute ([Input ("TestFixtures", fullReader)], new Version (11, 0), new HashSet ()); + var optimized = generator.Execute ( + [Input ("TestFixtures", optimizedReader)], + new Version (11, 0), + new HashSet (), + collectMarshalMethodsForNonAcw: false); + + var fullActivity = Assert.Single (full.AllPeers, peer => peer.JavaName == "android/app/Activity"); + var optimizedActivity = Assert.Single (optimized.AllPeers, peer => peer.JavaName == "android/app/Activity"); + Assert.NotEmpty (fullActivity.MarshalMethods); + Assert.Empty (optimizedActivity.MarshalMethods); + + var fullAcw = Assert.Single (full.AllPeers, peer => peer.JavaName == "my/app/MyHelper"); + var optimizedAcw = Assert.Single (optimized.AllPeers, peer => peer.JavaName == "my/app/MyHelper"); + Assert.Equal ( + fullAcw.MarshalMethods.Select (method => (method.JniName, method.JniSignature, method.ManagedMethodName, method.CallManagedMethodDirectly)), + optimizedAcw.MarshalMethods.Select (method => (method.JniName, method.JniSignature, method.ManagedMethodName, method.CallManagedMethodDirectly))); + Assert.Equal (full.GeneratedJavaSources, optimized.GeneratedJavaSources); + Assert.Equal (full.GeneratedAssemblies.Count, optimized.GeneratedAssemblies.Count); + for (int i = 0; i < full.GeneratedAssemblies.Count; i++) { + Assert.Equal (full.GeneratedAssemblies [i].Name, optimized.GeneratedAssemblies [i].Name); + Assert.Equal (full.GeneratedAssemblies [i].Content.ToArray (), optimized.GeneratedAssemblies [i].Content.ToArray ()); + } + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { From fa9451461a1a3e6050f7461a684949e0a536e4a6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:07:01 +0200 Subject: [PATCH 2/4] Skip unchanged typemap assembly emission Persist versioned model fingerprints so incremental builds only emit typemap PE assemblies whose final model changed, while retaining alias-owner and root-reference invalidation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 136 +++++++++++++++++ .../Generator/TypeMapAssemblyGenerator.cs | 17 ++- .../TrimmableTypeMapGenerator.cs | 46 ++++-- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 3 + .../Tasks/GenerateTrimmableTypeMap.cs | 116 +++++++------- .../Tasks/GenerateTrimmableTypeMapTests.cs | 8 +- .../TrimmableTypeMapGeneratorTests.cs | 143 ++++++++++++++++++ 7 files changed, 400 insertions(+), 69 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 185c2637dcf..54633913dfe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; @@ -7,6 +8,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; static class MetadataHelper { + static readonly Guid GeneratorModuleVersionId = typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; + /// /// Produces a deterministic MVID by hashing the module name together with content-dependent data. /// Assemblies with the same name but different content will have different MVIDs. @@ -66,6 +69,99 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); } + /// + /// Computes a fingerprint of every input that affects a generated per-assembly typemap. + /// Unlike , this is an incremental-build contract, + /// so it includes the generator binary identity and all model fields consumed by the emitter. + /// + public static byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData data, Version systemRuntimeVersion, bool useSharedTypemapUniverse) + { + using var sha = SHA256.Create (); + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (useSharedTypemapUniverse); + writer.Write (data.AssemblyName); + writer.Write (data.ModuleName); + writer.Write (data.Entries.Count); + foreach (var entry in data.Entries) { + writer.Write (entry.MapKey); + writer.Write (entry.ProxyTypeReference); + writer.WriteOptionalString (entry.TargetTypeReference); + } + writer.Write (data.ProxyTypes.Count); + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.JniName); + writer.Write (proxy.Namespace); + writer.WriteTypeRef (proxy.TargetType); + writer.WriteOptionalTypeRef (proxy.InvokerType); + writer.Write (proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.Write ((byte) proxy.InvokerActivationCtorStyle.Value); + } + writer.WriteOptionalActivationCtor (proxy.ActivationCtor); + writer.Write (proxy.IsGenericDefinition); + writer.Write (proxy.CannotRegisterInStaticConstructor); + writer.Write (proxy.IsAcw); + 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); + } + } + writer.Write (data.Associations.Count); + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + writer.Write (data.AliasHolders.Count); + foreach (var holder in data.AliasHolders) { + writer.Write (holder.TypeName); + writer.Write (holder.Namespace); + writer.Write (holder.AliasKeys.Count); + foreach (var aliasKey in holder.AliasKeys) { + writer.Write (aliasKey); + } + } + writer.Write (data.IgnoresAccessChecksTo.Count); + foreach (var assemblyName in data.IgnoresAccessChecksTo) { + writer.Write (assemblyName); + } + writer.Flush (); + return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + } + + /// + /// Computes a fingerprint of every input that affects the root typemap assembly. + /// + public static byte [] ComputeRootIncrementalFingerprint ( + IReadOnlyList perAssemblyTypeMapNames, + Version systemRuntimeVersion, + bool useSharedTypemapUniverse) + { + using var sha = SHA256.Create (); + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (useSharedTypemapUniverse); + writer.Write (perAssemblyTypeMapNames.Count); + foreach (var assemblyName in perAssemblyTypeMapNames) { + writer.Write (assemblyName); + } + writer.Flush (); + return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + } + static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) { writer.Write (type.ManagedTypeName); @@ -78,15 +174,55 @@ static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) } } + static void WriteOptionalTypeRef (this BinaryWriter writer, TypeRefData? type) + { + writer.Write (type is not null); + if (type is not null) { + writer.WriteTypeRef (type); + } + } + + static void WriteOptionalString (this BinaryWriter writer, string? value) + { + writer.Write (value is not null); + if (value is not null) { + writer.Write (value); + } + } + + static void WriteOptionalActivationCtor (this BinaryWriter writer, ActivationCtorData? constructor) + { + writer.Write (constructor is not null); + if (constructor is not null) { + writer.WriteTypeRef (constructor.DeclaringType); + writer.Write (constructor.IsOnLeafType); + writer.Write ((byte) constructor.Style); + } + } + static void WriteUcoMethod (this BinaryWriter writer, UcoMethodData method) { writer.Write (method.WrapperName); writer.Write (method.CallbackMethodName); writer.WriteTypeRef (method.CallbackType); writer.Write (method.JniSignature); + writer.WriteOptionalStrings (method.CallbackParameterTypeNames); + writer.WriteOptionalString (method.CallbackReturnTypeName); writer.WriteExportMethodDispatch (method.ExportMethodDispatch); } + static void WriteOptionalStrings (this BinaryWriter writer, IReadOnlyList? values) + { + writer.Write (values is not null); + if (values is null) { + return; + } + writer.Write (values.Count); + foreach (var value in values) { + writer.Write (value); + } + } + static void WriteExportMethodDispatch (this BinaryWriter writer, ExportMethodDispatchData? dispatch) { writer.Write (dispatch is not null); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 1e449592a43..9e5ae23dd25 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -29,7 +29,22 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var model = CreateModel (peers, assemblyName); + Generate (model, stream, useSharedTypemapUniverse); + } + + internal TypeMapAssemblyData CreateModel (IReadOnlyList peers, string assemblyName) + { + return ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + } + + internal byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + { + return MetadataHelper.ComputeIncrementalFingerprint (model, _systemRuntimeVersion, useSharedTypemapUniverse); + } + + internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse) + { var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 8fb6e85d346..cee0eb3aecf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -30,6 +30,13 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) /// Set to when callers do not consume method metadata for types /// that cannot generate Java callable wrappers. /// + /// + /// Optional incremental-build callback. It receives each output assembly name and a + /// content-model fingerprint, and returns whether that assembly should be emitted. + /// Assemblies skipped by the callback are omitted from + /// . When omitted, all typemap + /// assemblies are generated. + /// public TrimmableTypeMapResult Execute ( IReadOnlyList assemblies, Version systemRuntimeVersion, @@ -40,7 +47,8 @@ public TrimmableTypeMapResult Execute ( string? packageNamingPolicy = null, bool generateTypeMapAssemblies = true, bool errorOnCustomJavaObject = true, - bool collectMarshalMethodsForNonAcw = true) + bool collectMarshalMethodsForNonAcw = true, + Func? shouldGenerateTypeMapAssembly = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -65,7 +73,7 @@ public TrimmableTypeMapResult Execute ( } var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, shouldGenerateTypeMapAssembly) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -275,10 +283,11 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies ( + internal List GenerateTypeMapAssemblies ( List allPeers, Version systemRuntimeVersion, - bool useSharedTypemapUniverse) + bool useSharedTypemapUniverse, + Func? shouldGenerateTypeMapAssembly = null) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -306,18 +315,33 @@ List GenerateTypeMapAssemblies ( foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); + var model = generator.CreateModel (peers, typeMapAssemblyName); + if (shouldGenerateTypeMapAssembly is not null) { + var fingerprint = generator.ComputeIncrementalFingerprint (model, useSharedTypemapUniverse); + if (!shouldGenerateTypeMapAssembly (typeMapAssemblyName, fingerprint)) { + continue; + } + } var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse); + generator.Generate (model, stream, useSharedTypemapUniverse); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } - var rootStream = new MemoryStream (); - var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); - rootStream.Position = 0; - generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); - logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + const string rootAssemblyName = "_Microsoft.Android.TypeMaps"; + bool generateRoot = true; + if (shouldGenerateTypeMapAssembly is not null) { + var rootFingerprint = MetadataHelper.ComputeRootIncrementalFingerprint (perAssemblyNames, systemRuntimeVersion, useSharedTypemapUniverse); + generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); + } + if (generateRoot) { + var rootStream = new MemoryStream (); + var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); + rootStream.Position = 0; + generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); + logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + } logger.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index a2dfc834ca8..9fe59302eae 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -46,6 +46,7 @@ Outputs without making the target run on every build. The stamp is always touched, so the target is correctly skipped when none of its Inputs changed. --> <_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp + <_TrimmableTypeMapFingerprintsFile>$(_TypeMapOutputDirectory)typemap-fingerprints.txt <_TrimmableRemoveRegisterFlag>$(_AndroidStampDirectory)_RemoveRegisterAttribute.stamp <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'CoreCLR' ">_RemoveRegisterAttributeCoreClr <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">_RemoveRegisterAttributeNativeAot @@ -170,6 +171,7 @@ ApplicationJavaClass="$(AndroidApplicationJavaClass)" ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" GeneratedAssembliesListFile="$(_TypeMapAssembliesListFile)" + TypeMapFingerprintsFile="$(_TrimmableTypeMapFingerprintsFile)" AcwMapOutputFile="$(_PreTrimTypeMapAcwMapOutputFile)" ApplicationRegistrationOutputFile="$(_PreTrimTypeMapApplicationRegistrationOutputFile)"> @@ -200,6 +202,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 1928954aad2..78b42b5caf4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -84,6 +84,7 @@ public void LogCustomJavaObjectWarning (string managedTypeName) => public string? ApplicationRegistrationOutputFile { get; set; } public string? GeneratedAssembliesListFile { get; set; } + public string? TypeMapFingerprintsFile { get; set; } public string? ManifestTemplate { get; set; } @@ -173,6 +174,9 @@ public override bool RunTask () var peReaders = new List (); var assemblies = new List (); + var typeMapAssemblyNames = new List (); + var typeMapFingerprints = new SortedDictionary (StringComparer.Ordinal); + var priorTypeMapFingerprints = ReadTypeMapFingerprints (); TrimmableTypeMapResult? result = null; try { foreach (var (path, isFrameworkAssembly) in assemblyInputs) { @@ -222,14 +226,19 @@ public override bool RunTask () packageNamingPolicy: PackageNamingPolicy, generateTypeMapAssemblies: GenerateTypeMapAssemblies, errorOnCustomJavaObject: ErrorOnCustomJavaObject, - collectMarshalMethodsForNonAcw: false); + collectMarshalMethodsForNonAcw: false, + shouldGenerateTypeMapAssembly: TypeMapFingerprintsFile.IsNullOrEmpty () ? null : ShouldGenerateTypeMapAssembly); if (Log.HasLoggedErrors) { return false; } if (GenerateTypeMapAssemblies) { - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); + if (TypeMapFingerprintsFile.IsNullOrEmpty ()) { + typeMapAssemblyNames.AddRange (result.GeneratedAssemblies.Select (assembly => assembly.Name)); + } + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, typeMapAssemblyNames); WriteGeneratedAssembliesListFile (GeneratedAssemblies); + WriteTypeMapFingerprints (typeMapFingerprints); } GeneratedJavaFiles = JavaSourceInputDirectory.IsNullOrEmpty () ? WriteJavaSourcesToDisk (result.GeneratedJavaSources) @@ -286,6 +295,50 @@ public override bool RunTask () } return !Log.HasLoggedErrors; + + bool ShouldGenerateTypeMapAssembly (string assemblyName, byte [] fingerprint) + { + typeMapAssemblyNames.Add (assemblyName); + string fingerprintText = Files.ToHexString (fingerprint); + typeMapFingerprints.Add (assemblyName, fingerprintText); + string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll"); + bool generate = !File.Exists (outputPath) || + !priorTypeMapFingerprints.TryGetValue (assemblyName, out var priorFingerprint) || + !string.Equals (fingerprintText, priorFingerprint, StringComparison.Ordinal); + Log.LogDebugMessage ($" {assemblyName}: {(generate ? "changed, generating" : "unchanged, skipping emission")}"); + return generate; + } + } + + Dictionary ReadTypeMapFingerprints () + { + var fingerprints = new Dictionary (StringComparer.Ordinal); + if (TypeMapFingerprintsFile.IsNullOrEmpty () || !File.Exists (TypeMapFingerprintsFile)) { + return fingerprints; + } + foreach (var line in File.ReadLines (TypeMapFingerprintsFile)) { + int separator = line.IndexOf ('\t'); + if (separator <= 0 || separator == line.Length - 1) { + continue; + } + fingerprints [line.Substring (0, separator)] = line.Substring (separator + 1); + } + return fingerprints; + } + + void WriteTypeMapFingerprints (IReadOnlyDictionary fingerprints) + { + if (TypeMapFingerprintsFile.IsNullOrEmpty ()) { + return; + } + var directory = Path.GetDirectoryName (TypeMapFingerprintsFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + var text = fingerprints.Count == 0 + ? "" + : string.Join (Environment.NewLine, fingerprints.Select (entry => $"{entry.Key}\t{entry.Value}")) + Environment.NewLine; + Files.CopyIfStringChanged (text, TypeMapFingerprintsFile); } static bool IsFrameworkAssemblyItem (ITaskItem item) => @@ -332,68 +385,21 @@ ITaskItem [] CopyJavaSourcesFromInputDirectory (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyNames) { - // Build a map from assembly name -> source path for timestamp comparison - var sourcePathByName = new Dictionary (StringComparer.Ordinal); - foreach (var path in assemblyPaths) { - var name = Path.GetFileNameWithoutExtension (path); - sourcePathByName [name] = path; - } - + var generatedByName = assemblies.ToDictionary (assembly => assembly.Name, StringComparer.Ordinal); var items = new List (); - bool anyRegenerated = false; - - foreach (var assembly in assemblies) { - if (assembly.Name == "_Microsoft.Android.TypeMaps") { - continue; // Handle root assembly separately below - } - - string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); - // Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") - string originalName = assembly.Name; - if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { - originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); - } - - if (IsUpToDate (outputPath, originalName, sourcePathByName)) { - Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); - } else { + foreach (var assemblyName in assemblyNames) { + string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll"); + if (generatedByName.TryGetValue (assemblyName, out var assembly)) { Files.CopyIfStreamChanged (assembly.Content, outputPath); - anyRegenerated = true; - Log.LogDebugMessage ($" {assembly.Name}: written"); + Log.LogDebugMessage ($" {assemblyName}: written"); } - items.Add (new TaskItem (outputPath)); } - - // Root assembly — regenerate if any per-assembly typemap changed - var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); - if (rootAssembly is not null) { - string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); - if (anyRegenerated || !File.Exists (rootOutputPath)) { - Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); - Log.LogDebugMessage ($" Root: written"); - } else { - Log.LogDebugMessage ($" Root: up to date, skipping"); - } - items.Add (new TaskItem (rootOutputPath)); - } - return items.ToArray (); } - static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) - { - if (!File.Exists (outputPath)) { - return false; - } - if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { - return false; - } - return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); - } - ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) { var items = new List (); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index e4b82397080..2fe8f68229c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -101,13 +101,16 @@ public void Execute_SecondRun_OutputsAreUpToDate () .First (p => p.Contains ("_Mono.Android.TypeMap.dll")); var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - // Second run: same inputs — outputs should not be rewritten (CopyIfStreamChanged) - var task2 = CreateTask (assemblies, outputDir, javaDir); + // Second run: the persisted model fingerprint should avoid PE emission entirely. + var messages = new List (); + var task2 = CreateTask (assemblies, outputDir, javaDir, messages: messages); Assert.IsTrue (task2.Execute (), "Second run should succeed."); var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); Assert.AreEqual (firstWriteTime, secondWriteTime, "Typemap assembly should NOT be rewritten when content hasn't changed."); + Assert.IsTrue (messages.Any (message => message.Message?.Contains ("_Mono.Android.TypeMap: unchanged, skipping emission", StringComparison.Ordinal) == true), + "Second run should skip typemap PE emission based on the persisted model fingerprint."); } [Test] @@ -407,6 +410,7 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, TargetFrameworkVersion = tfv, + TypeMapFingerprintsFile = Path.Combine (outputDir, "typemap-fingerprints.txt"), }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 25748d108dc..a84a40d02ca 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -338,6 +338,128 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () } } + [Fact] + public void GenerateTypeMapAssemblies_UnchangedFingerprintsSkipAllEmission () + { + var peers = new List { + CreatePeer ("MyApp", "MyApp.MainActivity", "my/app/MainActivity"), + CreatePeer ("MyLibrary", "MyLibrary.Widget", "my/library/Widget"), + }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + var second = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints [name].SequenceEqual (fingerprint)); + + Assert.Empty (second); + } + + [Fact] + public void Execute_IncrementalCallbackPreservesGeneratedBytes () + { + using var fullReader = CreateTestFixturePEReader (); + using var incrementalReader = CreateTestFixturePEReader (); + var generator = CreateGenerator (); + var full = generator.Execute ( + [Input ("TestFixtures", fullReader)], + new Version (11, 0), + new HashSet ()); + var incremental = generator.Execute ( + [Input ("TestFixtures", incrementalReader)], + new Version (11, 0), + new HashSet (), + shouldGenerateTypeMapAssembly: (_, _) => true); + + Assert.Equal (full.GeneratedAssemblies.Count, incremental.GeneratedAssemblies.Count); + for (int i = 0; i < full.GeneratedAssemblies.Count; i++) { + Assert.Equal (full.GeneratedAssemblies [i].Name, incremental.GeneratedAssemblies [i].Name); + Assert.Equal (full.GeneratedAssemblies [i].Content.ToArray (), incremental.GeneratedAssemblies [i].Content.ToArray ()); + } + DisposeGeneratedAssemblies (full.GeneratedAssemblies); + DisposeGeneratedAssemblies (incremental.GeneratedAssemblies); + } + + [Fact] + public void GenerateTypeMapAssemblies_ChangedCrossAssemblyAliasRegeneratesOwner () + { + var owner = CreatePeer ("Owner", "Owner.JavaObject", "java/lang/Object"); + var alias = CreatePeer ("Alias", "Alias.JavaObject", "java/lang/Object") with { + IsFromJniTypeSignature = true, + }; + var unrelatedAliasPeer = CreatePeer ("Alias", "Alias.Widget", "alias/Widget"); + var peers = new List { owner, alias, unrelatedAliasPeer }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + peers [1] = alias with { + ManagedTypeName = "Alias.ChangedJavaObject", + ManagedTypeShortName = "ChangedJavaObject", + }; + var regenerated = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints [name].SequenceEqual (fingerprint)); + + var assembly = Assert.Single (regenerated); + Assert.Equal ("_Owner.TypeMap", assembly.Name); + DisposeGeneratedAssemblies (regenerated); + } + + [Fact] + public void GenerateTypeMapAssemblies_ChangedAssemblySetRegeneratesRoot () + { + var initialPeers = new List { + CreatePeer ("MyApp", "MyApp.MainActivity", "my/app/MainActivity"), + }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + initialPeers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + var peersWithLibrary = new List (initialPeers) { + CreatePeer ("MyLibrary", "MyLibrary.Widget", "my/library/Widget"), + }; + var regenerated = generator.GenerateTypeMapAssemblies ( + peersWithLibrary, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints.TryGetValue (name, out var prior) || !prior.SequenceEqual (fingerprint)); + + Assert.Equal ( + ["_MyLibrary.TypeMap", "_Microsoft.Android.TypeMaps"], + regenerated.Select (assembly => assembly.Name)); + DisposeGeneratedAssemblies (regenerated); + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { @@ -354,6 +476,27 @@ public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstru Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); } + static JavaPeerInfo CreatePeer (string assemblyName, string managedTypeName, string javaName) + { + int separator = managedTypeName.LastIndexOf ('.'); + return new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = managedTypeName, + ManagedTypeNamespace = separator < 0 ? "" : managedTypeName.Substring (0, separator), + ManagedTypeShortName = separator < 0 ? managedTypeName : managedTypeName.Substring (separator + 1), + AssemblyName = assemblyName, + DoNotGenerateAcw = true, + }; + } + + static void DisposeGeneratedAssemblies (IEnumerable assemblies) + { + foreach (var assembly in assemblies) { + assembly.Content.Dispose (); + } + } + [Fact] public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendants () { From 51e0fdf677d17da24290d3e8043897f64069d0aa Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:07:01 +0200 Subject: [PATCH 3/4] Skip unchanged typemap assembly emission Persist versioned model fingerprints so incremental builds only emit typemap PE assemblies whose final model changed, while retaining alias-owner and root-reference invalidation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/MetadataHelper.cs | 136 +++++++++++++++++ .../Generator/TypeMapAssemblyGenerator.cs | 17 ++- .../TrimmableTypeMapGenerator.cs | 46 ++++-- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 3 + .../Tasks/GenerateTrimmableTypeMap.cs | 116 +++++++------- .../Tasks/GenerateTrimmableTypeMapTests.cs | 8 +- .../TrimmableTypeMapGeneratorTests.cs | 143 ++++++++++++++++++ 7 files changed, 400 insertions(+), 69 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 185c2637dcf..54633913dfe 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using System.Text; @@ -7,6 +8,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; static class MetadataHelper { + static readonly Guid GeneratorModuleVersionId = typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; + /// /// Produces a deterministic MVID by hashing the module name together with content-dependent data. /// Assemblies with the same name but different content will have different MVIDs. @@ -66,6 +69,99 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); } + /// + /// Computes a fingerprint of every input that affects a generated per-assembly typemap. + /// Unlike , this is an incremental-build contract, + /// so it includes the generator binary identity and all model fields consumed by the emitter. + /// + public static byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData data, Version systemRuntimeVersion, bool useSharedTypemapUniverse) + { + using var sha = SHA256.Create (); + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (useSharedTypemapUniverse); + writer.Write (data.AssemblyName); + writer.Write (data.ModuleName); + writer.Write (data.Entries.Count); + foreach (var entry in data.Entries) { + writer.Write (entry.MapKey); + writer.Write (entry.ProxyTypeReference); + writer.WriteOptionalString (entry.TargetTypeReference); + } + writer.Write (data.ProxyTypes.Count); + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + writer.Write (proxy.JniName); + writer.Write (proxy.Namespace); + writer.WriteTypeRef (proxy.TargetType); + writer.WriteOptionalTypeRef (proxy.InvokerType); + writer.Write (proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.Write ((byte) proxy.InvokerActivationCtorStyle.Value); + } + writer.WriteOptionalActivationCtor (proxy.ActivationCtor); + writer.Write (proxy.IsGenericDefinition); + writer.Write (proxy.CannotRegisterInStaticConstructor); + writer.Write (proxy.IsAcw); + 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); + } + } + writer.Write (data.Associations.Count); + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + writer.Write (data.AliasHolders.Count); + foreach (var holder in data.AliasHolders) { + writer.Write (holder.TypeName); + writer.Write (holder.Namespace); + writer.Write (holder.AliasKeys.Count); + foreach (var aliasKey in holder.AliasKeys) { + writer.Write (aliasKey); + } + } + writer.Write (data.IgnoresAccessChecksTo.Count); + foreach (var assemblyName in data.IgnoresAccessChecksTo) { + writer.Write (assemblyName); + } + writer.Flush (); + return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + } + + /// + /// Computes a fingerprint of every input that affects the root typemap assembly. + /// + public static byte [] ComputeRootIncrementalFingerprint ( + IReadOnlyList perAssemblyTypeMapNames, + Version systemRuntimeVersion, + bool useSharedTypemapUniverse) + { + using var sha = SHA256.Create (); + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (useSharedTypemapUniverse); + writer.Write (perAssemblyTypeMapNames.Count); + foreach (var assemblyName in perAssemblyTypeMapNames) { + writer.Write (assemblyName); + } + writer.Flush (); + return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + } + static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) { writer.Write (type.ManagedTypeName); @@ -78,15 +174,55 @@ static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) } } + static void WriteOptionalTypeRef (this BinaryWriter writer, TypeRefData? type) + { + writer.Write (type is not null); + if (type is not null) { + writer.WriteTypeRef (type); + } + } + + static void WriteOptionalString (this BinaryWriter writer, string? value) + { + writer.Write (value is not null); + if (value is not null) { + writer.Write (value); + } + } + + static void WriteOptionalActivationCtor (this BinaryWriter writer, ActivationCtorData? constructor) + { + writer.Write (constructor is not null); + if (constructor is not null) { + writer.WriteTypeRef (constructor.DeclaringType); + writer.Write (constructor.IsOnLeafType); + writer.Write ((byte) constructor.Style); + } + } + static void WriteUcoMethod (this BinaryWriter writer, UcoMethodData method) { writer.Write (method.WrapperName); writer.Write (method.CallbackMethodName); writer.WriteTypeRef (method.CallbackType); writer.Write (method.JniSignature); + writer.WriteOptionalStrings (method.CallbackParameterTypeNames); + writer.WriteOptionalString (method.CallbackReturnTypeName); writer.WriteExportMethodDispatch (method.ExportMethodDispatch); } + static void WriteOptionalStrings (this BinaryWriter writer, IReadOnlyList? values) + { + writer.Write (values is not null); + if (values is null) { + return; + } + writer.Write (values.Count); + foreach (var value in values) { + writer.Write (value); + } + } + static void WriteExportMethodDispatch (this BinaryWriter writer, ExportMethodDispatchData? dispatch) { writer.Write (dispatch is not null); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 1e449592a43..9e5ae23dd25 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -29,7 +29,22 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var model = CreateModel (peers, assemblyName); + Generate (model, stream, useSharedTypemapUniverse); + } + + internal TypeMapAssemblyData CreateModel (IReadOnlyList peers, string assemblyName) + { + return ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + } + + internal byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + { + return MetadataHelper.ComputeIncrementalFingerprint (model, _systemRuntimeVersion, useSharedTypemapUniverse); + } + + internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse) + { var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 176caf7e79d..60c98619437 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -30,6 +30,13 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) /// Set to when callers do not consume method metadata for types /// that cannot generate Java callable wrappers. /// + /// + /// Optional incremental-build callback. It receives each output assembly name and a + /// content-model fingerprint, and returns whether that assembly should be emitted. + /// Assemblies skipped by the callback are omitted from + /// . When omitted, all typemap + /// assemblies are generated. + /// public TrimmableTypeMapResult Execute ( IReadOnlyList assemblies, Version systemRuntimeVersion, @@ -40,7 +47,8 @@ public TrimmableTypeMapResult Execute ( string? packageNamingPolicy = null, bool generateTypeMapAssemblies = true, bool errorOnCustomJavaObject = true, - bool collectMarshalMethodsForNonAcw = true) + bool collectMarshalMethodsForNonAcw = true, + Func? shouldGenerateTypeMapAssembly = null) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -65,7 +73,7 @@ public TrimmableTypeMapResult Execute ( } var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, shouldGenerateTypeMapAssembly) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -284,10 +292,11 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies ( + internal List GenerateTypeMapAssemblies ( List allPeers, Version systemRuntimeVersion, - bool useSharedTypemapUniverse) + bool useSharedTypemapUniverse, + Func? shouldGenerateTypeMapAssembly = null) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -315,18 +324,33 @@ List GenerateTypeMapAssemblies ( foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); + var model = generator.CreateModel (peers, typeMapAssemblyName); + if (shouldGenerateTypeMapAssembly is not null) { + var fingerprint = generator.ComputeIncrementalFingerprint (model, useSharedTypemapUniverse); + if (!shouldGenerateTypeMapAssembly (typeMapAssemblyName, fingerprint)) { + continue; + } + } var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse); + generator.Generate (model, stream, useSharedTypemapUniverse); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } - var rootStream = new MemoryStream (); - var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); - rootStream.Position = 0; - generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); - logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + const string rootAssemblyName = "_Microsoft.Android.TypeMaps"; + bool generateRoot = true; + if (shouldGenerateTypeMapAssembly is not null) { + var rootFingerprint = MetadataHelper.ComputeRootIncrementalFingerprint (perAssemblyNames, systemRuntimeVersion, useSharedTypemapUniverse); + generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); + } + if (generateRoot) { + var rootStream = new MemoryStream (); + var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); + rootStream.Position = 0; + generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); + logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); + } logger.LogGeneratedTypeMapAssembliesInfo (generatedAssemblies.Count); return generatedAssemblies; } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index a2dfc834ca8..9fe59302eae 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -46,6 +46,7 @@ Outputs without making the target run on every build. The stamp is always touched, so the target is correctly skipped when none of its Inputs changed. --> <_TrimmableTypeMapOutputStamp>$(_TypeMapOutputDirectory)_GenerateTrimmableTypeMap.stamp + <_TrimmableTypeMapFingerprintsFile>$(_TypeMapOutputDirectory)typemap-fingerprints.txt <_TrimmableRemoveRegisterFlag>$(_AndroidStampDirectory)_RemoveRegisterAttribute.stamp <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'CoreCLR' ">_RemoveRegisterAttributeCoreClr <_TrimmableRemoveRegisterTarget Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">_RemoveRegisterAttributeNativeAot @@ -170,6 +171,7 @@ ApplicationJavaClass="$(AndroidApplicationJavaClass)" ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" GeneratedAssembliesListFile="$(_TypeMapAssembliesListFile)" + TypeMapFingerprintsFile="$(_TrimmableTypeMapFingerprintsFile)" AcwMapOutputFile="$(_PreTrimTypeMapAcwMapOutputFile)" ApplicationRegistrationOutputFile="$(_PreTrimTypeMapApplicationRegistrationOutputFile)"> @@ -200,6 +202,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 1928954aad2..78b42b5caf4 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -84,6 +84,7 @@ public void LogCustomJavaObjectWarning (string managedTypeName) => public string? ApplicationRegistrationOutputFile { get; set; } public string? GeneratedAssembliesListFile { get; set; } + public string? TypeMapFingerprintsFile { get; set; } public string? ManifestTemplate { get; set; } @@ -173,6 +174,9 @@ public override bool RunTask () var peReaders = new List (); var assemblies = new List (); + var typeMapAssemblyNames = new List (); + var typeMapFingerprints = new SortedDictionary (StringComparer.Ordinal); + var priorTypeMapFingerprints = ReadTypeMapFingerprints (); TrimmableTypeMapResult? result = null; try { foreach (var (path, isFrameworkAssembly) in assemblyInputs) { @@ -222,14 +226,19 @@ public override bool RunTask () packageNamingPolicy: PackageNamingPolicy, generateTypeMapAssemblies: GenerateTypeMapAssemblies, errorOnCustomJavaObject: ErrorOnCustomJavaObject, - collectMarshalMethodsForNonAcw: false); + collectMarshalMethodsForNonAcw: false, + shouldGenerateTypeMapAssembly: TypeMapFingerprintsFile.IsNullOrEmpty () ? null : ShouldGenerateTypeMapAssembly); if (Log.HasLoggedErrors) { return false; } if (GenerateTypeMapAssemblies) { - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); + if (TypeMapFingerprintsFile.IsNullOrEmpty ()) { + typeMapAssemblyNames.AddRange (result.GeneratedAssemblies.Select (assembly => assembly.Name)); + } + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, typeMapAssemblyNames); WriteGeneratedAssembliesListFile (GeneratedAssemblies); + WriteTypeMapFingerprints (typeMapFingerprints); } GeneratedJavaFiles = JavaSourceInputDirectory.IsNullOrEmpty () ? WriteJavaSourcesToDisk (result.GeneratedJavaSources) @@ -286,6 +295,50 @@ public override bool RunTask () } return !Log.HasLoggedErrors; + + bool ShouldGenerateTypeMapAssembly (string assemblyName, byte [] fingerprint) + { + typeMapAssemblyNames.Add (assemblyName); + string fingerprintText = Files.ToHexString (fingerprint); + typeMapFingerprints.Add (assemblyName, fingerprintText); + string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll"); + bool generate = !File.Exists (outputPath) || + !priorTypeMapFingerprints.TryGetValue (assemblyName, out var priorFingerprint) || + !string.Equals (fingerprintText, priorFingerprint, StringComparison.Ordinal); + Log.LogDebugMessage ($" {assemblyName}: {(generate ? "changed, generating" : "unchanged, skipping emission")}"); + return generate; + } + } + + Dictionary ReadTypeMapFingerprints () + { + var fingerprints = new Dictionary (StringComparer.Ordinal); + if (TypeMapFingerprintsFile.IsNullOrEmpty () || !File.Exists (TypeMapFingerprintsFile)) { + return fingerprints; + } + foreach (var line in File.ReadLines (TypeMapFingerprintsFile)) { + int separator = line.IndexOf ('\t'); + if (separator <= 0 || separator == line.Length - 1) { + continue; + } + fingerprints [line.Substring (0, separator)] = line.Substring (separator + 1); + } + return fingerprints; + } + + void WriteTypeMapFingerprints (IReadOnlyDictionary fingerprints) + { + if (TypeMapFingerprintsFile.IsNullOrEmpty ()) { + return; + } + var directory = Path.GetDirectoryName (TypeMapFingerprintsFile); + if (!directory.IsNullOrEmpty ()) { + Directory.CreateDirectory (directory); + } + var text = fingerprints.Count == 0 + ? "" + : string.Join (Environment.NewLine, fingerprints.Select (entry => $"{entry.Key}\t{entry.Value}")) + Environment.NewLine; + Files.CopyIfStringChanged (text, TypeMapFingerprintsFile); } static bool IsFrameworkAssemblyItem (ITaskItem item) => @@ -332,68 +385,21 @@ ITaskItem [] CopyJavaSourcesFromInputDirectory (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyNames) { - // Build a map from assembly name -> source path for timestamp comparison - var sourcePathByName = new Dictionary (StringComparer.Ordinal); - foreach (var path in assemblyPaths) { - var name = Path.GetFileNameWithoutExtension (path); - sourcePathByName [name] = path; - } - + var generatedByName = assemblies.ToDictionary (assembly => assembly.Name, StringComparer.Ordinal); var items = new List (); - bool anyRegenerated = false; - - foreach (var assembly in assemblies) { - if (assembly.Name == "_Microsoft.Android.TypeMaps") { - continue; // Handle root assembly separately below - } - - string outputPath = Path.Combine (OutputDirectory, assembly.Name + ".dll"); - // Extract the original assembly name from the typemap name (e.g., "_Foo.TypeMap" -> "Foo") - string originalName = assembly.Name; - if (originalName.StartsWith ("_", StringComparison.Ordinal) && originalName.EndsWith (".TypeMap", StringComparison.Ordinal)) { - originalName = originalName.Substring (1, originalName.Length - ".TypeMap".Length - 1); - } - - if (IsUpToDate (outputPath, originalName, sourcePathByName)) { - Log.LogDebugMessage ($" {assembly.Name}: up to date, skipping"); - } else { + foreach (var assemblyName in assemblyNames) { + string outputPath = Path.Combine (OutputDirectory, assemblyName + ".dll"); + if (generatedByName.TryGetValue (assemblyName, out var assembly)) { Files.CopyIfStreamChanged (assembly.Content, outputPath); - anyRegenerated = true; - Log.LogDebugMessage ($" {assembly.Name}: written"); + Log.LogDebugMessage ($" {assemblyName}: written"); } - items.Add (new TaskItem (outputPath)); } - - // Root assembly — regenerate if any per-assembly typemap changed - var rootAssembly = assemblies.FirstOrDefault (a => a.Name == "_Microsoft.Android.TypeMaps"); - if (rootAssembly is not null) { - string rootOutputPath = Path.Combine (OutputDirectory, rootAssembly.Name + ".dll"); - if (anyRegenerated || !File.Exists (rootOutputPath)) { - Files.CopyIfStreamChanged (rootAssembly.Content, rootOutputPath); - Log.LogDebugMessage ($" Root: written"); - } else { - Log.LogDebugMessage ($" Root: up to date, skipping"); - } - items.Add (new TaskItem (rootOutputPath)); - } - return items.ToArray (); } - static bool IsUpToDate (string outputPath, string assemblyName, Dictionary sourcePathByName) - { - if (!File.Exists (outputPath)) { - return false; - } - if (!sourcePathByName.TryGetValue (assemblyName, out var sourcePath)) { - return false; - } - return File.GetLastWriteTimeUtc (outputPath) >= File.GetLastWriteTimeUtc (sourcePath); - } - ITaskItem [] WriteJavaSourcesToDisk (IReadOnlyList javaSources) { var items = new List (); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index e4b82397080..2fe8f68229c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -101,13 +101,16 @@ public void Execute_SecondRun_OutputsAreUpToDate () .First (p => p.Contains ("_Mono.Android.TypeMap.dll")); var firstWriteTime = File.GetLastWriteTimeUtc (typeMapPath); - // Second run: same inputs — outputs should not be rewritten (CopyIfStreamChanged) - var task2 = CreateTask (assemblies, outputDir, javaDir); + // Second run: the persisted model fingerprint should avoid PE emission entirely. + var messages = new List (); + var task2 = CreateTask (assemblies, outputDir, javaDir, messages: messages); Assert.IsTrue (task2.Execute (), "Second run should succeed."); var secondWriteTime = File.GetLastWriteTimeUtc (typeMapPath); Assert.AreEqual (firstWriteTime, secondWriteTime, "Typemap assembly should NOT be rewritten when content hasn't changed."); + Assert.IsTrue (messages.Any (message => message.Message?.Contains ("_Mono.Android.TypeMap: unchanged, skipping emission", StringComparison.Ordinal) == true), + "Second run should skip typemap PE emission based on the persisted model fingerprint."); } [Test] @@ -407,6 +410,7 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, OutputDirectory = outputDir, JavaSourceOutputDirectory = javaDir, TargetFrameworkVersion = tfv, + TypeMapFingerprintsFile = Path.Combine (outputDir, "typemap-fingerprints.txt"), }; } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 89075a5832d..cc6a0ec36b9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -340,6 +340,128 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () } } + [Fact] + public void GenerateTypeMapAssemblies_UnchangedFingerprintsSkipAllEmission () + { + var peers = new List { + CreatePeer ("MyApp", "MyApp.MainActivity", "my/app/MainActivity"), + CreatePeer ("MyLibrary", "MyLibrary.Widget", "my/library/Widget"), + }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + var second = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints [name].SequenceEqual (fingerprint)); + + Assert.Empty (second); + } + + [Fact] + public void Execute_IncrementalCallbackPreservesGeneratedBytes () + { + using var fullReader = CreateTestFixturePEReader (); + using var incrementalReader = CreateTestFixturePEReader (); + var generator = CreateGenerator (); + var full = generator.Execute ( + [Input ("TestFixtures", fullReader)], + new Version (11, 0), + new HashSet ()); + var incremental = generator.Execute ( + [Input ("TestFixtures", incrementalReader)], + new Version (11, 0), + new HashSet (), + shouldGenerateTypeMapAssembly: (_, _) => true); + + Assert.Equal (full.GeneratedAssemblies.Count, incremental.GeneratedAssemblies.Count); + for (int i = 0; i < full.GeneratedAssemblies.Count; i++) { + Assert.Equal (full.GeneratedAssemblies [i].Name, incremental.GeneratedAssemblies [i].Name); + Assert.Equal (full.GeneratedAssemblies [i].Content.ToArray (), incremental.GeneratedAssemblies [i].Content.ToArray ()); + } + DisposeGeneratedAssemblies (full.GeneratedAssemblies); + DisposeGeneratedAssemblies (incremental.GeneratedAssemblies); + } + + [Fact] + public void GenerateTypeMapAssemblies_ChangedCrossAssemblyAliasRegeneratesOwner () + { + var owner = CreatePeer ("Owner", "Owner.JavaObject", "java/lang/Object"); + var alias = CreatePeer ("Alias", "Alias.JavaObject", "java/lang/Object") with { + IsFromJniTypeSignature = true, + }; + var unrelatedAliasPeer = CreatePeer ("Alias", "Alias.Widget", "alias/Widget"); + var peers = new List { owner, alias, unrelatedAliasPeer }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + peers [1] = alias with { + ManagedTypeName = "Alias.ChangedJavaObject", + ManagedTypeShortName = "ChangedJavaObject", + }; + var regenerated = generator.GenerateTypeMapAssemblies ( + peers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints [name].SequenceEqual (fingerprint)); + + var assembly = Assert.Single (regenerated); + Assert.Equal ("_Owner.TypeMap", assembly.Name); + DisposeGeneratedAssemblies (regenerated); + } + + [Fact] + public void GenerateTypeMapAssemblies_ChangedAssemblySetRegeneratesRoot () + { + var initialPeers = new List { + CreatePeer ("MyApp", "MyApp.MainActivity", "my/app/MainActivity"), + }; + var fingerprints = new Dictionary (StringComparer.Ordinal); + var generator = CreateGenerator (); + var first = generator.GenerateTypeMapAssemblies ( + initialPeers, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => { + fingerprints.Add (name, fingerprint); + return true; + }); + DisposeGeneratedAssemblies (first); + + var peersWithLibrary = new List (initialPeers) { + CreatePeer ("MyLibrary", "MyLibrary.Widget", "my/library/Widget"), + }; + var regenerated = generator.GenerateTypeMapAssemblies ( + peersWithLibrary, + new Version (11, 0), + useSharedTypemapUniverse: true, + (name, fingerprint) => !fingerprints.TryGetValue (name, out var prior) || !prior.SequenceEqual (fingerprint)); + + Assert.Equal ( + ["_MyLibrary.TypeMap", "_Microsoft.Android.TypeMaps"], + regenerated.Select (assembly => assembly.Name)); + DisposeGeneratedAssemblies (regenerated); + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { @@ -356,6 +478,27 @@ public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstru Assert.Contains ("my.app.IntermediateInstrumentation", result.ApplicationRegistrationTypes); } + static JavaPeerInfo CreatePeer (string assemblyName, string managedTypeName, string javaName) + { + int separator = managedTypeName.LastIndexOf ('.'); + return new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = managedTypeName, + ManagedTypeNamespace = separator < 0 ? "" : managedTypeName.Substring (0, separator), + ManagedTypeShortName = separator < 0 ? managedTypeName : managedTypeName.Substring (separator + 1), + AssemblyName = assemblyName, + DoNotGenerateAcw = true, + }; + } + + static void DisposeGeneratedAssemblies (IEnumerable assemblies) + { + foreach (var assembly in assemblies) { + assembly.Content.Dispose (); + } + } + [Fact] public void CollectApplicationRegistrationTypes_ExcludesLegacyFrameworkDescendants () { From 633d8c77c85f053618ab9a0785d7d22e17d7e02a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 21:23:19 +0200 Subject: [PATCH 4/4] Preserve typemap fingerprints incrementally Keep the fingerprint cache registered during no-op builds and treat unreadable or malformed cache contents as a regeneration signal. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 18 ++++++++---- .../Tasks/GenerateTrimmableTypeMapTests.cs | 29 +++++++++++++++++++ .../TrimmableTypeMapBuildTests.cs | 16 ++++++++++ 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 9fe59302eae..416e9d8eb27 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -277,6 +277,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 78b42b5caf4..cf9614f94c9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -310,18 +310,24 @@ bool ShouldGenerateTypeMapAssembly (string assemblyName, byte [] fingerprint) } } - Dictionary ReadTypeMapFingerprints () + internal Dictionary ReadTypeMapFingerprints () { var fingerprints = new Dictionary (StringComparer.Ordinal); if (TypeMapFingerprintsFile.IsNullOrEmpty () || !File.Exists (TypeMapFingerprintsFile)) { return fingerprints; } - foreach (var line in File.ReadLines (TypeMapFingerprintsFile)) { - int separator = line.IndexOf ('\t'); - if (separator <= 0 || separator == line.Length - 1) { - continue; + try { + foreach (var line in File.ReadLines (TypeMapFingerprintsFile)) { + int separator = line.IndexOf ('\t'); + if (separator <= 0 || separator == line.Length - 1) { + Log.LogDebugMessage ($"Ignoring invalid trimmable typemap fingerprint cache '{TypeMapFingerprintsFile}'."); + return new Dictionary (StringComparer.Ordinal); + } + fingerprints [line.Substring (0, separator)] = line.Substring (separator + 1); } - fingerprints [line.Substring (0, separator)] = line.Substring (separator + 1); + } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { + Log.LogDebugMessage ($"Could not read trimmable typemap fingerprint cache '{TypeMapFingerprintsFile}': {ex.Message}"); + fingerprints.Clear (); } return fingerprints; } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 2fe8f68229c..db956a6a4cd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -113,6 +113,35 @@ public void Execute_SecondRun_OutputsAreUpToDate () "Second run should skip typemap PE emission based on the persisted model fingerprint."); } + [Test] + public void ReadTypeMapFingerprints_UnreadableCache_Regenerates () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + var fingerprintsFile = Path.Combine (outputDir, "typemap-fingerprints.txt"); + Directory.CreateDirectory (outputDir); + File.WriteAllText (fingerprintsFile, "_Existing.TypeMap\tfingerprint"); + + using var fingerprintsLock = File.Open (fingerprintsFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None); + var task = CreateTask ([], outputDir, javaDir); + + Assert.IsEmpty (task.ReadTypeMapFingerprints (), "An unreadable incremental cache should regenerate every typemap assembly."); + } + + [Test] + public void ReadTypeMapFingerprints_InvalidCache_Regenerates () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + Directory.CreateDirectory (outputDir); + File.WriteAllText (Path.Combine (outputDir, "typemap-fingerprints.txt"), "invalid"); + var task = CreateTask ([], outputDir, javaDir); + + Assert.IsEmpty (task.ReadTypeMapFingerprints (), "An invalid incremental cache should regenerate every typemap assembly."); + } + [Test] public void Execute_MissingJavaSource_DoesNotPruneExistingOutput () { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b5512e330fe..59283a6a8e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -76,6 +76,10 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease AssertTrimmableTypeMapOutputs (intermediateDir); var typemapDlls = Directory.GetFiles (intermediateDir, "*.dll"); Assert.IsNotEmpty (typemapDlls, "First build should have generated typemap DLL(s)."); + var typemapFingerprints = Path.Combine (intermediateDir, "typemap-fingerprints.txt"); + FileAssert.Exists (typemapFingerprints, "First build should persist typemap fingerprints."); + var typemapFingerprintContent = File.ReadAllText (typemapFingerprints); + var typemapWriteTimes = typemapDlls.ToDictionary (path => path, File.GetLastWriteTimeUtc); string scanDgml = ""; DateTime scanDgmlTimestamp = default; @@ -106,6 +110,18 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease foreach (var typemapDll in typemapDlls) { FileAssert.Exists (typemapDll, $"No-op builds should preserve generated typemap assembly {typemapDll} when _GenerateTrimmableTypeMap is skipped."); } + + FileAssert.Exists (typemapFingerprints, "IncrementalClean should preserve typemap fingerprints on a no-op build."); + Assert.AreEqual (typemapFingerprintContent, File.ReadAllText (typemapFingerprints), "A no-op build should not change typemap fingerprints."); + + proj.MainActivity += Environment.NewLine + "// Force trimmable typemap regeneration."; + proj.Touch ("MainActivity.cs"); + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true, saveProject: false), "Changed-input build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); + foreach (var typemapDll in typemapDlls) { + Assert.AreEqual (typemapWriteTimes [typemapDll], File.GetLastWriteTimeUtc (typemapDll), + $"A source change that does not affect the typemap model should skip PE emission for {typemapDll}."); + } } [Test]