From a6de122ee60dbe1ac55d3990a75e1ed16dda6861 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 13:20:37 +0200 Subject: [PATCH 01/13] 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 02/13] 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 03/13] 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 04/13] 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] From 3ebb677e9f8b7548a7616e691fd5a6b3790e4c11 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:43:34 +0200 Subject: [PATCH 05/13] Reduce typemap signature encoder allocations Cache fixed activation and CreateInstance signatures so repeated PE emission avoids nested signature encoder delegates while preserving emitted bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 24 +++++++- .../Generator/TypeMapAssemblyEmitter.cs | 56 ++++++++++++------- .../TypeMapAssemblyGeneratorTests.cs | 43 ++++++++++++++ 3 files changed, 101 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 6434c515e27..001a834a201 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -162,9 +162,12 @@ public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Act { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); - return Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), Metadata.GetOrAddBlob (_sigBlob)); + return AddMemberRef (parent, name, Metadata.GetOrAddBlob (_sigBlob)); } + public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, BlobHandle signature) + => Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), signature); + /// /// Resolves a to a TypeReference/TypeSpecification handle, with caching. /// @@ -382,6 +385,15 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL) => EmitBody (name, attrs, encodeSig, emitIL, encodeLocals: null, useBranches: false); + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL) + => EmitBody (name, attrs, signature, emitIL, encodeLocals: null, useBranches: false); + + public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL, + Action? encodeLocals) + => EmitBody (name, attrs, signature, emitIL, encodeLocals, useBranches: false); + /// /// Emits a method body and definition with optional local variable declarations. /// @@ -408,7 +420,13 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, encodeSig (new BlobEncoder (_sigBlob)); // Capture the sig blob handle before emitIL, because emitIL callbacks // may call AddMemberRef which clears and repopulates _sigBlob. - var sigBlobHandle = Metadata.GetOrAddBlob (_sigBlob); + return EmitBody (name, attrs, Metadata.GetOrAddBlob (_sigBlob), emitIL, encodeLocals, useBranches); + } + + MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, + BlobHandle signature, Action emitIL, + Action? encodeLocals, bool useBranches) + { StandaloneSignatureHandle localSigHandle = default; if (encodeLocals != null) { @@ -433,7 +451,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, return Metadata.AddMethodDefinition ( attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), - sigBlobHandle, + signature, bodyOffset, MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 56a160a7914..2aa48fb0e0f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -137,6 +137,9 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + BlobHandle _activationCtorSignature; + BlobHandle _createInstanceSignature; + EntityHandle _anchorTypeHandle; ExportMethodDispatchEmitter? _exportMethodDispatchEmitter; @@ -950,12 +953,7 @@ void EmitCreateInstanceBody (Action emitIL) { _pe.EmitBody ("CreateInstance", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), + GetCreateInstanceSignature (), emitIL); } @@ -963,25 +961,45 @@ void EmitCreateInstanceBodyWithLocals (Action encodeLocals, Action< { _pe.EmitBody ("CreateInstance", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Type ().Type (_iJavaPeerableRef, false), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - }), + GetCreateInstanceSignature (), emitIL, encodeLocals); } MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) { - return _pe.AddMemberRef (declaringTypeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().IntPtr (); - p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); - })); + return _pe.AddMemberRef (declaringTypeRef, ".ctor", GetActivationCtorSignature ()); + } + + BlobHandle GetActivationCtorSignature () + { + if (_activationCtorSignature.IsNil) { + var blob = new BlobBuilder (8); + blob.WriteByte ((byte) SignatureAttributes.Instance); + blob.WriteCompressedInteger (2); + blob.WriteByte ((byte) SignatureTypeCode.Void); + blob.WriteByte ((byte) SignatureTypeCode.IntPtr); + blob.WriteByte ((byte) SignatureTypeKind.ValueType); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniHandleOwnershipRef)); + _activationCtorSignature = _pe.Metadata.GetOrAddBlob (blob); + } + return _activationCtorSignature; + } + + BlobHandle GetCreateInstanceSignature () + { + if (_createInstanceSignature.IsNil) { + var blob = new BlobBuilder (8); + blob.WriteByte ((byte) SignatureAttributes.Instance); + blob.WriteCompressedInteger (2); + blob.WriteByte ((byte) SignatureTypeKind.Class); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_iJavaPeerableRef)); + blob.WriteByte ((byte) SignatureTypeCode.IntPtr); + blob.WriteByte ((byte) SignatureTypeKind.ValueType); + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniHandleOwnershipRef)); + _createInstanceSignature = _pe.Metadata.GetOrAddBlob (blob); + } + return _createInstanceSignature; } MemberReferenceHandle AddManagedCtorRef (EntityHandle declaringTypeRef, IReadOnlyList parameterTypes) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 48e55b3edc5..f205fc5236a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -393,6 +393,49 @@ public void EmitBody_ILCallbackCallsAddMemberRef_SignatureNotCorrupted () Assert.Equal ("System.Int32", paramType); } + [Fact] + public void EmitBody_PreencodedSignature_PreservesMethodSignature () + { + var pe = new PEAssemblyBuilder (new Version (11, 0, 0, 0)); + pe.EmitPreamble ("PreencodedSigTest", "PreencodedSigTest.dll"); + var objectRef = pe.Metadata.AddTypeReference (pe.SystemRuntimeRef, + pe.Metadata.GetOrAddString ("System"), pe.Metadata.GetOrAddString ("Object")); + pe.Metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Class, + pe.Metadata.GetOrAddString ("Test"), + pe.Metadata.GetOrAddString ("MyType"), + objectRef, + MetadataTokens.FieldDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (pe.Metadata.GetRowCount (TableIndex.MethodDef) + 1)); + var signature = new BlobBuilder (); + signature.WriteByte ((byte) SignatureAttributes.Instance); + signature.WriteCompressedInteger (1); + signature.WriteByte ((byte) SignatureTypeCode.String); + signature.WriteByte ((byte) SignatureTypeCode.Int32); + + pe.EmitBody ( + "PreencodedMethod", + MethodAttributes.Public, + pe.Metadata.GetOrAddBlob (signature), + encoder => { + encoder.OpCode (ILOpCode.Ldnull); + encoder.Return (returnsValue: true); + }); + using var stream = new MemoryStream (); + pe.WritePE (stream); + stream.Position = 0; + using var peReader = new PEReader (stream); + var reader = peReader.GetMetadataReader (); + var method = reader.TypeDefinitions + .SelectMany (handle => reader.GetTypeDefinition (handle).GetMethods ()) + .Select (handle => reader.GetMethodDefinition (handle)) + .Single (method => reader.GetString (method.Name) == "PreencodedMethod"); + var decoded = method.DecodeSignature (SignatureTypeProvider.Instance, null); + + Assert.Equal ("System.String", decoded.ReturnType); + Assert.Equal ("System.Int32", Assert.Single (decoded.ParameterTypes)); + } + [Fact] public void Generate_JiStyleInvoker_FirstParamIsByRef () { From e8ac902e91c3a588e29c9af1d134cd17011cd2de Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 14:45:20 +0200 Subject: [PATCH 06/13] Reuse metadata signature encoder Build the cached signatures through the existing SRM encoder so the optimization stays small and preserves the established encoding path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/PEAssemblyBuilder.cs | 15 ++++----- .../Generator/TypeMapAssemblyEmitter.cs | 31 +++++++++---------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 001a834a201..ec4da021cf7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -159,15 +159,18 @@ public AssemblyReferenceHandle FindOrAddAssemblyRef (string assemblyName) /// Adds a member reference using the reusable signature blob builder. /// public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, Action encodeSig) + => AddMemberRef (parent, name, GetOrAddSignature (encodeSig)); + + public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, BlobHandle signature) + => Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), signature); + + public BlobHandle GetOrAddSignature (Action encodeSig) { _sigBlob.Clear (); encodeSig (new BlobEncoder (_sigBlob)); - return AddMemberRef (parent, name, Metadata.GetOrAddBlob (_sigBlob)); + return Metadata.GetOrAddBlob (_sigBlob); } - public MemberReferenceHandle AddMemberRef (EntityHandle parent, string name, BlobHandle signature) - => Metadata.AddMemberReference (parent, Metadata.GetOrAddString (name), signature); - /// /// Resolves a to a TypeReference/TypeSpecification handle, with caching. /// @@ -416,11 +419,9 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, Action encodeSig, Action emitIL, Action? encodeLocals, bool useBranches) { - _sigBlob.Clear (); - encodeSig (new BlobEncoder (_sigBlob)); // Capture the sig blob handle before emitIL, because emitIL callbacks // may call AddMemberRef which clears and repopulates _sigBlob. - return EmitBody (name, attrs, Metadata.GetOrAddBlob (_sigBlob), emitIL, encodeLocals, useBranches); + return EmitBody (name, attrs, GetOrAddSignature (encodeSig), emitIL, encodeLocals, useBranches); } MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 2aa48fb0e0f..812a533653e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -974,14 +974,13 @@ MemberReferenceHandle AddActivationCtorRef (EntityHandle declaringTypeRef) BlobHandle GetActivationCtorSignature () { if (_activationCtorSignature.IsNil) { - var blob = new BlobBuilder (8); - blob.WriteByte ((byte) SignatureAttributes.Instance); - blob.WriteCompressedInteger (2); - blob.WriteByte ((byte) SignatureTypeCode.Void); - blob.WriteByte ((byte) SignatureTypeCode.IntPtr); - blob.WriteByte ((byte) SignatureTypeKind.ValueType); - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniHandleOwnershipRef)); - _activationCtorSignature = _pe.Metadata.GetOrAddBlob (blob); + _activationCtorSignature = _pe.GetOrAddSignature ( + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); } return _activationCtorSignature; } @@ -989,15 +988,13 @@ BlobHandle GetActivationCtorSignature () BlobHandle GetCreateInstanceSignature () { if (_createInstanceSignature.IsNil) { - var blob = new BlobBuilder (8); - blob.WriteByte ((byte) SignatureAttributes.Instance); - blob.WriteCompressedInteger (2); - blob.WriteByte ((byte) SignatureTypeKind.Class); - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_iJavaPeerableRef)); - blob.WriteByte ((byte) SignatureTypeCode.IntPtr); - blob.WriteByte ((byte) SignatureTypeKind.ValueType); - blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (_jniHandleOwnershipRef)); - _createInstanceSignature = _pe.Metadata.GetOrAddBlob (blob); + _createInstanceSignature = _pe.GetOrAddSignature ( + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Type ().Type (_iJavaPeerableRef, false), + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); + })); } return _createInstanceSignature; } From e0552291449d0ee28cad8a7ef570bf4a203b5225 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 15:42:12 +0200 Subject: [PATCH 07/13] Compute typemap fingerprints in a single streaming pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The content fingerprint (which seeds the deterministic MVID) and the incremental-build fingerprint each walked the whole model into a BinaryWriter over a MemoryStream, then hashed the buffer. On a real Mono.Android workload those two buffered walks were the single largest allocation source in the generator. Serialise both fingerprints from one walk, streaming the fields straight into SHA-256 through a small reusable buffer. Fields shared by the two fingerprints are UTF-8 encoded once and appended to both hashes, so the byte stream seen by each hash — and therefore every fingerprint value and generated MVID — is unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/FingerprintWriter.cs | 174 ++++++++ .../Generator/MetadataHelper.cs | 321 ++++++++------- .../Generator/TypeMapAssemblyEmitter.cs | 17 +- .../Generator/TypeMapAssemblyGenerator.cs | 14 +- .../TrimmableTypeMapGenerator.cs | 8 +- .../Generator/FingerprintWriterTests.cs | 374 ++++++++++++++++++ 6 files changed, 735 insertions(+), 173 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs new file mode 100644 index 00000000000..304afa72da0 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs @@ -0,0 +1,174 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// Streams model fields into one or two SHA-256 hashes without materialising the whole +/// serialised model in memory. +/// +/// +/// +/// The byte stream produced for each sink is identical to what +/// would have written into a : +/// strings are UTF-8 encoded with a 7-bit encoded byte-length prefix, is one +/// byte, and is four bytes little-endian. Keeping the encoding identical means +/// the resulting fingerprints — and therefore the deterministic MVIDs derived from them — are +/// unchanged. +/// +/// +/// Two sinks are supported so the content fingerprint (which seeds the MVID) and the +/// incremental-build fingerprint can be produced from a single walk over the model. Fields shared +/// by both fingerprints are UTF-8 encoded once and appended to both sinks; fields belonging to +/// only one fingerprint are appended to that sink alone. +/// +/// +sealed class FingerprintWriter : IDisposable +{ + /// Selects which fingerprint(s) a write applies to. + [Flags] + public enum Sink + { + Content = 1, + Incremental = 2, + Both = Content | Incremental, + } + + // Large enough to absorb the small field writes that dominate the model walk without + // paying per-field hash update costs, small enough to stay off the large object heap. + const int BufferSize = 8 * 1024; + + readonly IncrementalHash contentHash; + readonly IncrementalHash? incrementalHash; + readonly byte [] contentBuffer; + readonly byte []? incrementalBuffer; + byte [] scratch = new byte [512]; + int contentPosition; + int incrementalPosition; + + public FingerprintWriter (bool includeIncremental) + { + contentHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + contentBuffer = new byte [BufferSize]; + if (includeIncremental) { + incrementalHash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + incrementalBuffer = new byte [BufferSize]; + } + } + + public void WriteString (Sink sink, string value) + { + int byteCount = Encoding.UTF8.GetByteCount (value); + EnsureScratch (byteCount + 5); + int offset = Write7BitEncodedInt (scratch, 0, byteCount); + Encoding.UTF8.GetBytes (value, 0, value.Length, scratch, offset); + Write (sink, scratch, 0, offset + byteCount); + } + + public void WriteOptionalString (Sink sink, string? value) + { + WriteBoolean (sink, value is not null); + if (value is not null) { + WriteString (sink, value); + } + } + + public void WriteBoolean (Sink sink, bool value) => WriteByte (sink, value ? (byte) 1 : (byte) 0); + + public void WriteByte (Sink sink, byte value) + { + scratch [0] = value; + Write (sink, scratch, 0, 1); + } + + public void WriteInt32 (Sink sink, int value) + { + scratch [0] = (byte) value; + scratch [1] = (byte) (value >> 8); + scratch [2] = (byte) (value >> 16); + scratch [3] = (byte) (value >> 24); + Write (sink, scratch, 0, 4); + } + + public void WriteRaw (Sink sink, byte [] value) => Write (sink, value, 0, value.Length); + + public byte [] GetContentFingerprint () + { + FlushContent (); + return contentHash.GetHashAndReset (); + } + + public byte [] GetIncrementalFingerprint () + { + if (incrementalHash is null || incrementalBuffer is null) { + throw new InvalidOperationException ("The incremental fingerprint was not requested."); + } + if (incrementalPosition > 0) { + incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); + incrementalPosition = 0; + } + return incrementalHash.GetHashAndReset (); + } + + public void Dispose () + { + contentHash.Dispose (); + incrementalHash?.Dispose (); + } + + void EnsureScratch (int required) + { + if (scratch.Length < required) { + scratch = new byte [Math.Max (required, scratch.Length * 2)]; + } + } + + void Write (Sink sink, byte [] data, int offset, int count) + { + if ((sink & Sink.Content) != 0) { + if (count > contentBuffer.Length - contentPosition) { + FlushContent (); + } + if (count > contentBuffer.Length) { + contentHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, contentBuffer, contentPosition, count); + contentPosition += count; + } + } + if ((sink & Sink.Incremental) != 0 && incrementalHash is not null && incrementalBuffer is not null) { + if (count > incrementalBuffer.Length - incrementalPosition) { + if (incrementalPosition > 0) { + incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); + incrementalPosition = 0; + } + } + if (count > incrementalBuffer.Length) { + incrementalHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); + incrementalPosition += count; + } + } + } + + void FlushContent () + { + if (contentPosition > 0) { + contentHash.AppendData (contentBuffer, 0, contentPosition); + contentPosition = 0; + } + } + + static int Write7BitEncodedInt (byte [] destination, int offset, int value) + { + uint remaining = (uint) value; + while (remaining > 0x7Fu) { + destination [offset++] = (byte) (remaining | ~0x7Fu); + remaining >>= 7; + } + destination [offset++] = (byte) remaining; + return offset; + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 54633913dfe..1cc2c2b7e4a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -1,11 +1,22 @@ using System; using System.Collections.Generic; -using System.IO; using System.Security.Cryptography; using System.Text; +using Sink = Microsoft.Android.Sdk.TrimmableTypeMap.FingerprintWriter.Sink; namespace Microsoft.Android.Sdk.TrimmableTypeMap; +/// +/// Fingerprints computed from a model in a single walk. +/// +/// +/// Content fingerprint that seeds the deterministic MVID of the emitted assembly. +/// +/// +/// Incremental-build fingerprint, or when it was not requested. +/// +readonly record struct ModelFingerprints (byte [] Content, byte []? Incremental); + static class MetadataHelper { static readonly Guid GeneratorModuleVersionId = typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; @@ -28,116 +39,119 @@ public static Guid DeterministicMvid (string moduleName, ReadOnlySpan cont } /// - /// Computes a content fingerprint for the given . + /// Computes the content fingerprint — and optionally the incremental-build fingerprint — for + /// in a single walk over the model. /// - public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) + /// + /// The content fingerprint covers only the model data that changes the emitted assembly's + /// contents. The incremental fingerprint is an incremental-build contract, so it additionally + /// covers the generator binary identity, the emitter configuration, and every model field the + /// emitter consumes. Both are serialised from the same walk: fields shared by the two + /// fingerprints are UTF-8 encoded once and appended to both hashes. + /// + public static ModelFingerprints ComputeFingerprints ( + TypeMapAssemblyData data, + Version systemRuntimeVersion, + bool useSharedTypemapUniverse, + bool includeIncremental) { - using var sha = SHA256.Create (); - using var stream = new MemoryStream (); - using var writer = new BinaryWriter (stream, Encoding.UTF8); + using var writer = new FingerprintWriter (includeIncremental); + var incremental = Sink.Incremental; + var both = includeIncremental ? Sink.Both : Sink.Content; + + if (includeIncremental) { + writer.WriteRaw (incremental, GeneratorModuleVersionId.ToByteArray ()); + writer.WriteString (incremental, systemRuntimeVersion.ToString ()); + writer.WriteBoolean (incremental, useSharedTypemapUniverse); + writer.WriteString (incremental, data.AssemblyName); + writer.WriteString (incremental, data.ModuleName); + writer.WriteInt32 (incremental, data.Entries.Count); + } + foreach (var entry in data.Entries) { - writer.Write (entry.MapKey); - writer.Write (entry.ProxyTypeReference); - writer.Write (entry.TargetTypeReference ?? ""); + writer.WriteString (both, entry.MapKey); + writer.WriteString (both, entry.ProxyTypeReference); + writer.WriteString (Sink.Content, entry.TargetTypeReference ?? ""); + if (includeIncremental) { + writer.WriteOptionalString (incremental, entry.TargetTypeReference); + } + } + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.ProxyTypes.Count); } foreach (var proxy in data.ProxyTypes) { - writer.Write (proxy.TypeName); - writer.WriteTypeRef (proxy.TargetType); - writer.Write ((byte)(proxy.ActivationCtor?.Style ?? 0)); - if (proxy.ActivationCtor is not null) { - writer.WriteTypeRef (proxy.ActivationCtor.DeclaringType); + writer.WriteString (both, proxy.TypeName); + if (includeIncremental) { + writer.WriteString (incremental, proxy.JniName); + writer.WriteString (incremental, proxy.Namespace); } - writer.Write ((byte)(proxy.InvokerActivationCtorStyle ?? 0)); - writer.Write (proxy.UcoMethods.Count); - foreach (var method in proxy.UcoMethods) { - writer.WriteUcoMethod (method); + WriteTypeRef (writer, both, proxy.TargetType); + if (includeIncremental) { + WriteOptionalTypeRef (writer, incremental, proxy.InvokerType); } - writer.Write (proxy.UcoConstructors.Count); - foreach (var constructor in proxy.UcoConstructors) { - writer.WriteUcoConstructor (constructor); + writer.WriteByte (Sink.Content, (byte) (proxy.ActivationCtor?.Style ?? 0)); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.WriteByte (incremental, (byte) proxy.InvokerActivationCtorStyle.Value); + } + writer.WriteBoolean (incremental, proxy.ActivationCtor is not null); } - writer.Write (proxy.NativeRegistrations.Count); - foreach (var registration in proxy.NativeRegistrations) { - writer.WriteNativeRegistration (registration); + if (proxy.ActivationCtor is not null) { + WriteTypeRef (writer, both, proxy.ActivationCtor.DeclaringType); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.ActivationCtor.IsOnLeafType); + writer.WriteByte (incremental, (byte) proxy.ActivationCtor.Style); + } } - } - foreach (var assoc in data.Associations) { - writer.Write (assoc.SourceTypeReference); - writer.Write (assoc.AliasProxyTypeReference); - } - writer.Flush (); - 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.WriteByte (Sink.Content, (byte) (proxy.InvokerActivationCtorStyle ?? 0)); + if (includeIncremental) { + writer.WriteBoolean (incremental, proxy.IsGenericDefinition); + writer.WriteBoolean (incremental, proxy.CannotRegisterInStaticConstructor); + writer.WriteBoolean (incremental, proxy.IsAcw); } - writer.WriteOptionalActivationCtor (proxy.ActivationCtor); - writer.Write (proxy.IsGenericDefinition); - writer.Write (proxy.CannotRegisterInStaticConstructor); - writer.Write (proxy.IsAcw); - writer.Write (proxy.UcoMethods.Count); + writer.WriteInt32 (both, proxy.UcoMethods.Count); foreach (var method in proxy.UcoMethods) { - writer.WriteUcoMethod (method); + WriteUcoMethod (writer, both, method); } - writer.Write (proxy.UcoConstructors.Count); + writer.WriteInt32 (both, proxy.UcoConstructors.Count); foreach (var constructor in proxy.UcoConstructors) { - writer.WriteUcoConstructor (constructor); + WriteUcoConstructor (writer, both, constructor); } - writer.Write (proxy.NativeRegistrations.Count); + writer.WriteInt32 (both, proxy.NativeRegistrations.Count); foreach (var registration in proxy.NativeRegistrations) { - writer.WriteNativeRegistration (registration); + WriteNativeRegistration (writer, both, registration); } } - writer.Write (data.Associations.Count); + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.Associations.Count); + } foreach (var assoc in data.Associations) { - writer.Write (assoc.SourceTypeReference); - writer.Write (assoc.AliasProxyTypeReference); + writer.WriteString (both, assoc.SourceTypeReference); + writer.WriteString (both, 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); + + if (includeIncremental) { + writer.WriteInt32 (incremental, data.AliasHolders.Count); + foreach (var holder in data.AliasHolders) { + writer.WriteString (incremental, holder.TypeName); + writer.WriteString (incremental, holder.Namespace); + writer.WriteInt32 (incremental, holder.AliasKeys.Count); + foreach (var aliasKey in holder.AliasKeys) { + writer.WriteString (incremental, aliasKey); + } + } + writer.WriteInt32 (incremental, data.IgnoresAccessChecksTo.Count); + foreach (var assemblyName in data.IgnoresAccessChecksTo) { + writer.WriteString (incremental, assemblyName); } } - 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)); + + return new ModelFingerprints ( + writer.GetContentFingerprint (), + includeIncremental ? writer.GetIncrementalFingerprint () : null); } /// @@ -148,121 +162,100 @@ public static byte [] ComputeRootIncrementalFingerprint ( 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); + using var writer = new FingerprintWriter (includeIncremental: false); + writer.WriteRaw (Sink.Content, GeneratorModuleVersionId.ToByteArray ()); + writer.WriteString (Sink.Content, systemRuntimeVersion.ToString ()); + writer.WriteBoolean (Sink.Content, useSharedTypemapUniverse); + writer.WriteInt32 (Sink.Content, perAssemblyTypeMapNames.Count); foreach (var assemblyName in perAssemblyTypeMapNames) { - writer.Write (assemblyName); + writer.WriteString (Sink.Content, assemblyName); } - writer.Flush (); - return sha.ComputeHash (stream.GetBuffer (), 0, checked ((int) stream.Length)); + return writer.GetContentFingerprint (); } - static void WriteTypeRef (this BinaryWriter writer, TypeRefData type) + static void WriteTypeRef (FingerprintWriter writer, Sink sink, TypeRefData type) { - writer.Write (type.ManagedTypeName); - writer.Write (type.AssemblyName); - writer.Write (type.IsValueType ? (byte) 1 : (byte) 0); - writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); - writer.Write (type.GenericArguments.Count); + writer.WriteString (sink, type.ManagedTypeName); + writer.WriteString (sink, type.AssemblyName); + writer.WriteByte (sink, type.IsValueType ? (byte) 1 : (byte) 0); + writer.WriteByte (sink, type.IsEnum ? (byte) 1 : (byte) 0); + writer.WriteInt32 (sink, type.GenericArguments.Count); foreach (var argument in type.GenericArguments) { - writer.WriteTypeRef (argument); + WriteTypeRef (writer, sink, argument); } } - static void WriteOptionalTypeRef (this BinaryWriter writer, TypeRefData? type) + static void WriteOptionalTypeRef (FingerprintWriter writer, Sink sink, TypeRefData? type) { - writer.Write (type is not null); + writer.WriteBoolean (sink, 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); + WriteTypeRef (writer, sink, type); } } - static void WriteUcoMethod (this BinaryWriter writer, UcoMethodData method) + static void WriteUcoMethod (FingerprintWriter writer, Sink sink, 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); + writer.WriteString (sink, method.WrapperName); + writer.WriteString (sink, method.CallbackMethodName); + WriteTypeRef (writer, sink, method.CallbackType); + writer.WriteString (sink, method.JniSignature); + WriteOptionalStrings (writer, sink, method.CallbackParameterTypeNames); + writer.WriteOptionalString (sink, method.CallbackReturnTypeName); + WriteExportMethodDispatch (writer, sink, method.ExportMethodDispatch); } - static void WriteOptionalStrings (this BinaryWriter writer, IReadOnlyList? values) + static void WriteOptionalStrings (FingerprintWriter writer, Sink sink, IReadOnlyList? values) { - writer.Write (values is not null); + writer.WriteBoolean (sink, values is not null); if (values is null) { return; } - writer.Write (values.Count); + writer.WriteInt32 (sink, values.Count); foreach (var value in values) { - writer.Write (value); + writer.WriteString (sink, value); } } - static void WriteExportMethodDispatch (this BinaryWriter writer, ExportMethodDispatchData? dispatch) + static void WriteExportMethodDispatch (FingerprintWriter writer, Sink sink, ExportMethodDispatchData? dispatch) { - writer.Write (dispatch is not null); + writer.WriteBoolean (sink, dispatch is not null); if (dispatch is null) { return; } - writer.Write (dispatch.ManagedMethodName); - writer.Write (dispatch.ParameterTypes.Count); + writer.WriteString (sink, dispatch.ManagedMethodName); + writer.WriteInt32 (sink, dispatch.ParameterTypes.Count); foreach (var parameterType in dispatch.ParameterTypes) { - writer.WriteTypeRef (parameterType); + WriteTypeRef (writer, sink, parameterType); } - writer.Write (dispatch.ParameterKinds.Count); + writer.WriteInt32 (sink, dispatch.ParameterKinds.Count); foreach (var parameterKind in dispatch.ParameterKinds) { - writer.Write ((int) parameterKind); + writer.WriteInt32 (sink, (int) parameterKind); } - writer.WriteTypeRef (dispatch.ReturnType); - writer.Write ((int) dispatch.ReturnKind); - writer.Write (dispatch.IsStatic); + WriteTypeRef (writer, sink, dispatch.ReturnType); + writer.WriteInt32 (sink, (int) dispatch.ReturnKind); + writer.WriteBoolean (sink, dispatch.IsStatic); } - static void WriteUcoConstructor (this BinaryWriter writer, UcoConstructorData constructor) + static void WriteUcoConstructor (FingerprintWriter writer, Sink sink, UcoConstructorData constructor) { - writer.Write (constructor.WrapperName); - writer.WriteTypeRef (constructor.TargetType); - writer.Write (constructor.JniSignature); - writer.Write (constructor.HasMatchingManagedCtor); - writer.Write (constructor.ManagedParameterTypes.Count); + writer.WriteString (sink, constructor.WrapperName); + WriteTypeRef (writer, sink, constructor.TargetType); + writer.WriteString (sink, constructor.JniSignature); + writer.WriteBoolean (sink, constructor.HasMatchingManagedCtor); + writer.WriteInt32 (sink, constructor.ManagedParameterTypes.Count); foreach (var parameterType in constructor.ManagedParameterTypes) { - writer.WriteTypeRef (parameterType); + WriteTypeRef (writer, sink, parameterType); } } - static void WriteNativeRegistration (this BinaryWriter writer, NativeRegistrationData registration) + static void WriteNativeRegistration (FingerprintWriter writer, Sink sink, NativeRegistrationData registration) { - writer.Write (registration.JniMethodName); - writer.Write (registration.JniSignature); - writer.Write (registration.WrapperMethodName); - writer.Write (registration.WrapperTarget.TypeNamespace); - writer.Write (registration.WrapperTarget.TypeName); - writer.Write (registration.WrapperTarget.MethodName); + writer.WriteString (sink, registration.JniMethodName); + writer.WriteString (sink, registration.JniSignature); + writer.WriteString (sink, registration.WrapperMethodName); + writer.WriteString (sink, registration.WrapperTarget.TypeNamespace); + writer.WriteString (sink, registration.WrapperTarget.TypeName); + writer.WriteString (sink, registration.WrapperTarget.MethodName); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 812a533653e..9c66d57c9e3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -168,6 +168,14 @@ public TypeMapAssemblyEmitter (Version systemRuntimeVersion) /// share a single typemap universe. When false, emits a per-assembly __TypeMapAnchor. /// public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse = false) + => Emit (model, stream, useSharedTypemapUniverse, contentFingerprint: null); + + /// + /// Pre-computed content fingerprint seeding the deterministic MVID. When + /// it is computed here; callers that already walked the model should pass it in to avoid a + /// second walk. + /// + internal void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse, byte []? contentFingerprint) { if (model is null) { throw new ArgumentNullException (nameof (model)); @@ -176,13 +184,16 @@ public void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedTypema throw new ArgumentNullException (nameof (stream)); } - EmitCore (model, useSharedTypemapUniverse); + EmitCore (model, useSharedTypemapUniverse, contentFingerprint); _pe.WritePE (stream); } - void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint) { - _pe.EmitPreamble (model.AssemblyName, model.ModuleName, MetadataHelper.ComputeContentFingerprint (model)); + contentFingerprint ??= MetadataHelper + .ComputeFingerprints (model, _systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental: false) + .Content; + _pe.EmitPreamble (model.AssemblyName, model.ModuleName, contentFingerprint); _javaInteropRef = _pe.AddAssemblyRef ("Java.Interop", new Version (0, 0, 0, 0)); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 9e5ae23dd25..9475d1cad9f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -38,15 +38,21 @@ internal TypeMapAssemblyData CreateModel (IReadOnlyList peers, str return ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); } - internal byte [] ComputeIncrementalFingerprint (TypeMapAssemblyData model, bool useSharedTypemapUniverse) + /// + /// Computes the content fingerprint — and, when is + /// , the incremental-build fingerprint — in a single walk over the model. + /// The content fingerprint should be passed back to + /// so the model is not walked twice. + /// + internal ModelFingerprints ComputeFingerprints (TypeMapAssemblyData model, bool useSharedTypemapUniverse, bool includeIncremental) { - return MetadataHelper.ComputeIncrementalFingerprint (model, _systemRuntimeVersion, useSharedTypemapUniverse); + return MetadataHelper.ComputeFingerprints (model, _systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental); } - internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse) + internal void Generate (TypeMapAssemblyData model, Stream stream, bool useSharedTypemapUniverse, byte []? contentFingerprint = null) { var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); - emitter.Emit (model, stream, useSharedTypemapUniverse); + emitter.Emit (model, stream, useSharedTypemapUniverse, contentFingerprint); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 60c98619437..dd2296230d3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -325,14 +325,18 @@ internal List GenerateTypeMapAssemblies ( string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var model = generator.CreateModel (peers, typeMapAssemblyName); + // Both fingerprints come out of a single walk over the model: the incremental one + // gates emission, the content one seeds the emitted assembly's deterministic MVID. + var fingerprints = generator.ComputeFingerprints (model, useSharedTypemapUniverse, + includeIncremental: shouldGenerateTypeMapAssembly is not null); if (shouldGenerateTypeMapAssembly is not null) { - var fingerprint = generator.ComputeIncrementalFingerprint (model, useSharedTypemapUniverse); + var fingerprint = fingerprints.Incremental ?? throw new InvalidOperationException ("Incremental fingerprint was requested but not produced."); if (!shouldGenerateTypeMapAssembly (typeMapAssemblyName, fingerprint)) { continue; } } var stream = new MemoryStream (); - generator.Generate (model, stream, useSharedTypemapUniverse); + generator.Generate (model, stream, useSharedTypemapUniverse, fingerprints.Content); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs new file mode 100644 index 00000000000..b6d7ee4c0e8 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Cryptography; +using System.Text; +using Xunit; +using Sink = Microsoft.Android.Sdk.TrimmableTypeMap.FingerprintWriter.Sink; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +/// +/// The fingerprints are computed by streaming model fields straight into SHA-256 instead of +/// buffering a serialisation in a . These +/// tests pin that rewrite to the previous byte stream: the content fingerprint seeds the emitted +/// assembly's deterministic MVID and the incremental fingerprint is an incremental-build contract, +/// so neither may drift. +/// +public class FingerprintWriterTests : FixtureTestBase +{ + [Theory] + [InlineData ("")] + [InlineData ("a")] + [InlineData ("Java.Lang.Object")] + [InlineData ("caf\u00e9 \u00fcber \u65e5\u672c\u8a9e")] + public void WriteString_MatchesBinaryWriter (string value) + { + AssertSameBytes (writer => writer.Write (value), writer => writer.WriteString (Sink.Content, value)); + } + + [Fact] + public void WriteString_LongerThanInternalBuffers_MatchesBinaryWriter () + { + // Exercises both the scratch-buffer growth and the direct-append path for values that + // cannot fit into the sink buffer. + foreach (int length in new [] { 100, 511, 512, 513, 8191, 8192, 8193, 40000 }) { + var value = new string ('x', length); + AssertSameBytes (writer => writer.Write (value), writer => writer.WriteString (Sink.Content, value)); + } + } + + [Fact] + public void WritePrimitives_MatchBinaryWriter () + { + AssertSameBytes (writer => writer.Write (true), writer => writer.WriteBoolean (Sink.Content, true)); + AssertSameBytes (writer => writer.Write (false), writer => writer.WriteBoolean (Sink.Content, false)); + AssertSameBytes (writer => writer.Write ((byte) 0xAB), writer => writer.WriteByte (Sink.Content, 0xAB)); + AssertSameBytes (writer => writer.Write (int.MinValue), writer => writer.WriteInt32 (Sink.Content, int.MinValue)); + AssertSameBytes (writer => writer.Write (0x12345678), writer => writer.WriteInt32 (Sink.Content, 0x12345678)); + AssertSameBytes (writer => writer.Write (false), writer => writer.WriteOptionalString (Sink.Content, null)); + AssertSameBytes ( + writer => { writer.Write (true); writer.Write ("value"); }, + writer => writer.WriteOptionalString (Sink.Content, "value")); + AssertSameBytes ( + writer => writer.Write (GeneratorModuleVersionId.ToByteArray ()), + writer => writer.WriteRaw (Sink.Content, GeneratorModuleVersionId.ToByteArray ())); + } + + [Fact] + public void SinksAreIndependent () + { + using var writer = new FingerprintWriter (includeIncremental: true); + writer.WriteString (Sink.Both, "shared"); + writer.WriteString (Sink.Incremental, "incremental-only"); + writer.WriteInt32 (Sink.Content, 7); + + Assert.Equal (Sha256 (BinaryWriterBytes (w => { w.Write ("shared"); w.Write (7); })), writer.GetContentFingerprint ()); + Assert.Equal (Sha256 (BinaryWriterBytes (w => { w.Write ("shared"); w.Write ("incremental-only"); })), writer.GetIncrementalFingerprint ()); + } + + [Fact] + public void GetIncrementalFingerprint_WhenNotRequested_Throws () + { + using var writer = new FingerprintWriter (includeIncremental: false); + Assert.Throws (() => writer.GetIncrementalFingerprint ()); + } + + [Theory] + [InlineData (true)] + [InlineData (false)] + public void ComputeFingerprints_MatchesLegacyBufferedSerialization (bool useSharedTypemapUniverse) + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + // The fixtures must exercise every part of the walk that the two fingerprints share. + Assert.NotEmpty (model.Entries); + Assert.NotEmpty (model.ProxyTypes); + + var fingerprints = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse, includeIncremental: true); + + Assert.Equal (LegacyContentFingerprint (model), fingerprints.Content); + Assert.Equal (LegacyIncrementalFingerprint (model, systemRuntimeVersion, useSharedTypemapUniverse), fingerprints.Incremental); + } + + [Fact] + public void ComputeFingerprints_WithoutIncremental_ProducesSameContentFingerprint () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + var withIncremental = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: true); + var contentOnly = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: false); + + Assert.Equal (withIncremental.Content, contentOnly.Content); + Assert.Null (contentOnly.Incremental); + Assert.NotNull (withIncremental.Incremental); + } + + [Fact] + public void ComputeFingerprints_ContentIgnoresEmitterConfiguration () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + var model = new TypeMapAssemblyGenerator (systemRuntimeVersion) + .CreateModel (ScanFixtures (), "_TestFixtures.TypeMap"); + + var shared = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: true, includeIncremental: true); + var perAssembly = MetadataHelper.ComputeFingerprints (model, systemRuntimeVersion, useSharedTypemapUniverse: false, includeIncremental: true); + + // The content fingerprint only covers the model, the incremental one also covers config. + Assert.Equal (shared.Content, perAssembly.Content); + Assert.NotEqual (shared.Incremental, perAssembly.Incremental); + } + + [Fact] + public void ComputeRootIncrementalFingerprint_MatchesLegacyBufferedSerialization () + { + var systemRuntimeVersion = new Version (11, 0, 0, 0); + string [] names = ["_A.TypeMap", "_B.TypeMap"]; + + var expected = Sha256 (BinaryWriterBytes (writer => { + writer.Write (GeneratorModuleVersionId.ToByteArray ()); + writer.Write (systemRuntimeVersion.ToString ()); + writer.Write (true); + writer.Write (names.Length); + foreach (var name in names) { + writer.Write (name); + } + })); + + Assert.Equal (expected, MetadataHelper.ComputeRootIncrementalFingerprint (names, systemRuntimeVersion, useSharedTypemapUniverse: true)); + } + + static Guid GeneratorModuleVersionId => typeof (TypeMapAssemblyGenerator).Module.ModuleVersionId; + + static void AssertSameBytes (Action expected, Action actual) + { + Assert.Equal (Sha256 (BinaryWriterBytes (expected)), WriterBytes (actual)); + } + + static byte [] WriterBytes (Action write) + { + using var writer = new FingerprintWriter (includeIncremental: false); + write (writer); + return writer.GetContentFingerprint (); + } + + static byte [] BinaryWriterBytes (Action write) + { + using var stream = new MemoryStream (); + using var writer = new BinaryWriter (stream, Encoding.UTF8); + write (writer); + writer.Flush (); + return stream.ToArray (); + } + + static byte [] Sha256 (byte [] bytes) + { + using var sha = SHA256.Create (); + return sha.ComputeHash (bytes); + } + + // The implementations below are the buffered BinaryWriter serialisation used before the + // streaming rewrite. They are intentionally verbatim copies so the tests fail if the new + // walk changes the byte stream in any way. + + static byte [] LegacyContentFingerprint (TypeMapAssemblyData data) => Sha256 (BinaryWriterBytes (writer => { + foreach (var entry in data.Entries) { + writer.Write (entry.MapKey); + writer.Write (entry.ProxyTypeReference); + writer.Write (entry.TargetTypeReference ?? ""); + } + foreach (var proxy in data.ProxyTypes) { + writer.Write (proxy.TypeName); + LegacyWriteTypeRef (writer, proxy.TargetType); + writer.Write ((byte) (proxy.ActivationCtor?.Style ?? 0)); + if (proxy.ActivationCtor is not null) { + LegacyWriteTypeRef (writer, proxy.ActivationCtor.DeclaringType); + } + writer.Write ((byte) (proxy.InvokerActivationCtorStyle ?? 0)); + writer.Write (proxy.UcoMethods.Count); + foreach (var method in proxy.UcoMethods) { + LegacyWriteUcoMethod (writer, method); + } + writer.Write (proxy.UcoConstructors.Count); + foreach (var constructor in proxy.UcoConstructors) { + LegacyWriteUcoConstructor (writer, constructor); + } + writer.Write (proxy.NativeRegistrations.Count); + foreach (var registration in proxy.NativeRegistrations) { + LegacyWriteNativeRegistration (writer, registration); + } + } + foreach (var assoc in data.Associations) { + writer.Write (assoc.SourceTypeReference); + writer.Write (assoc.AliasProxyTypeReference); + } + })); + + static byte [] LegacyIncrementalFingerprint (TypeMapAssemblyData data, Version systemRuntimeVersion, bool useSharedTypemapUniverse) => + Sha256 (BinaryWriterBytes (writer => { + 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); + LegacyWriteOptionalString (writer, 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); + LegacyWriteTypeRef (writer, proxy.TargetType); + LegacyWriteOptionalTypeRef (writer, proxy.InvokerType); + writer.Write (proxy.InvokerActivationCtorStyle.HasValue); + if (proxy.InvokerActivationCtorStyle.HasValue) { + writer.Write ((byte) proxy.InvokerActivationCtorStyle.Value); + } + LegacyWriteOptionalActivationCtor (writer, 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) { + LegacyWriteUcoMethod (writer, method); + } + writer.Write (proxy.UcoConstructors.Count); + foreach (var constructor in proxy.UcoConstructors) { + LegacyWriteUcoConstructor (writer, constructor); + } + writer.Write (proxy.NativeRegistrations.Count); + foreach (var registration in proxy.NativeRegistrations) { + LegacyWriteNativeRegistration (writer, 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); + } + })); + + static void LegacyWriteTypeRef (BinaryWriter writer, TypeRefData type) + { + writer.Write (type.ManagedTypeName); + writer.Write (type.AssemblyName); + writer.Write (type.IsValueType ? (byte) 1 : (byte) 0); + writer.Write (type.IsEnum ? (byte) 1 : (byte) 0); + writer.Write (type.GenericArguments.Count); + foreach (var argument in type.GenericArguments) { + LegacyWriteTypeRef (writer, argument); + } + } + + static void LegacyWriteOptionalTypeRef (BinaryWriter writer, TypeRefData? type) + { + writer.Write (type is not null); + if (type is not null) { + LegacyWriteTypeRef (writer, type); + } + } + + static void LegacyWriteOptionalString (BinaryWriter writer, string? value) + { + writer.Write (value is not null); + if (value is not null) { + writer.Write (value); + } + } + + static void LegacyWriteOptionalActivationCtor (BinaryWriter writer, ActivationCtorData? constructor) + { + writer.Write (constructor is not null); + if (constructor is not null) { + LegacyWriteTypeRef (writer, constructor.DeclaringType); + writer.Write (constructor.IsOnLeafType); + writer.Write ((byte) constructor.Style); + } + } + + static void LegacyWriteUcoMethod (BinaryWriter writer, UcoMethodData method) + { + writer.Write (method.WrapperName); + writer.Write (method.CallbackMethodName); + LegacyWriteTypeRef (writer, method.CallbackType); + writer.Write (method.JniSignature); + LegacyWriteOptionalStrings (writer, method.CallbackParameterTypeNames); + LegacyWriteOptionalString (writer, method.CallbackReturnTypeName); + LegacyWriteExportMethodDispatch (writer, method.ExportMethodDispatch); + } + + static void LegacyWriteOptionalStrings (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 LegacyWriteExportMethodDispatch (BinaryWriter writer, ExportMethodDispatchData? dispatch) + { + writer.Write (dispatch is not null); + if (dispatch is null) { + return; + } + writer.Write (dispatch.ManagedMethodName); + writer.Write (dispatch.ParameterTypes.Count); + foreach (var parameterType in dispatch.ParameterTypes) { + LegacyWriteTypeRef (writer, parameterType); + } + writer.Write (dispatch.ParameterKinds.Count); + foreach (var parameterKind in dispatch.ParameterKinds) { + writer.Write ((int) parameterKind); + } + LegacyWriteTypeRef (writer, dispatch.ReturnType); + writer.Write ((int) dispatch.ReturnKind); + writer.Write (dispatch.IsStatic); + } + + static void LegacyWriteUcoConstructor (BinaryWriter writer, UcoConstructorData constructor) + { + writer.Write (constructor.WrapperName); + LegacyWriteTypeRef (writer, constructor.TargetType); + writer.Write (constructor.JniSignature); + writer.Write (constructor.HasMatchingManagedCtor); + writer.Write (constructor.ManagedParameterTypes.Count); + foreach (var parameterType in constructor.ManagedParameterTypes) { + LegacyWriteTypeRef (writer, parameterType); + } + } + + static void LegacyWriteNativeRegistration (BinaryWriter writer, NativeRegistrationData registration) + { + writer.Write (registration.JniMethodName); + writer.Write (registration.JniSignature); + writer.Write (registration.WrapperMethodName); + writer.Write (registration.WrapperTarget.TypeNamespace); + writer.Write (registration.WrapperTarget.TypeName); + writer.Write (registration.WrapperTarget.MethodName); + } +} From d5ee47fae6d531b6fd0c540a5b4c84e8283dade6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 16:00:51 +0200 Subject: [PATCH 08/13] Return generated typemap assemblies without a second buffer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Emitting a typemap assembly serialised the PE image into a BlobBuilder and then copied every byte again into a MemoryStream, so each generated assembly was held twice while it was produced. Return a read-only, seekable stream over the chunks the serialiser already produced instead. GeneratedAssembly.Content is now typed as Stream, which is all the build task needs — it hashes the stream and copies it to disk through Files.CopyIfStreamChanged, so atomic last-known-good replacement is unchanged, as are the generated bytes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/BlobBuilderStream.cs | 141 ++++++++++++++ .../Generator/PEAssemblyBuilder.cs | 21 ++- .../Generator/TypeMapAssemblyEmitter.cs | 15 ++ .../Generator/TypeMapAssemblyGenerator.cs | 10 + .../TrimmableTypeMapGenerator.cs | 4 +- .../TrimmableTypeMapTypes.cs | 6 +- .../Generator/BlobBuilderStreamTests.cs | 172 ++++++++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 12 +- 8 files changed, 371 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs new file mode 100644 index 00000000000..9a8a3f706f0 --- /dev/null +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap; + +/// +/// A read-only, seekable over the chunks a already +/// holds, so a serialised PE image can be hashed and copied to disk without being duplicated into +/// a second contiguous buffer. +/// +/// +/// Only the chunk arrays are retained — the itself and the metadata +/// graph that produced it stay collectible — so the live byte count matches what a +/// copy would have held, without the transient second copy. +/// +sealed class BlobBuilderStream : Stream +{ + readonly ArraySegment [] segments; + readonly long [] segmentStarts; + readonly long length; + long position; + int cursor; + + public BlobBuilderStream (BlobBuilder builder) + { + _ = builder ?? throw new ArgumentNullException (nameof (builder)); + + var collected = new List> (); + foreach (var blob in builder.GetBlobs ()) { + var bytes = blob.GetBytes (); + if (bytes.Count == 0 || bytes.Array is null) { + continue; + } + collected.Add (bytes); + } + + segments = collected.ToArray (); + segmentStarts = new long [segments.Length + 1]; + long total = 0; + for (int i = 0; i < segments.Length; i++) { + segmentStarts [i] = total; + total += segments [i].Count; + } + segmentStarts [segments.Length] = total; + length = total; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => length; + + public override long Position { + get => position; + set { + if (value < 0) { + throw new ArgumentOutOfRangeException (nameof (value)); + } + position = value; + } + } + + public override void Flush () + { + } + + public override int Read (byte [] buffer, int offset, int count) + { + if (buffer is null) { + throw new ArgumentNullException (nameof (buffer)); + } + if (offset < 0 || count < 0 || buffer.Length - offset < count) { + throw new ArgumentOutOfRangeException (nameof (count)); + } + + int copied = 0; + while (count > 0 && position < length) { + int index = FindSegment (position); + var segment = segments [index]; + int within = (int) (position - segmentStarts [index]); + int available = segment.Count - within; + int toCopy = Math.Min (available, count); + if (segment.Array is null) { + break; + } + Buffer.BlockCopy (segment.Array, segment.Offset + within, buffer, offset, toCopy); + position += toCopy; + offset += toCopy; + count -= toCopy; + copied += toCopy; + } + return copied; + } + + public override long Seek (long offset, SeekOrigin origin) + { + long target = origin switch { + SeekOrigin.Begin => offset, + SeekOrigin.Current => position + offset, + SeekOrigin.End => length + offset, + _ => throw new ArgumentOutOfRangeException (nameof (origin)), + }; + if (target < 0) { + throw new IOException ("Cannot seek before the beginning of the stream."); + } + position = target; + return position; + } + + public override void SetLength (long value) => throw new NotSupportedException (); + + public override void Write (byte [] buffer, int offset, int count) => throw new NotSupportedException (); + + int FindSegment (long offset) + { + // Reads are overwhelmingly sequential, so try the last used chunk first. + if (cursor < segments.Length && offset >= segmentStarts [cursor] && offset < segmentStarts [cursor + 1]) { + return cursor; + } + + int low = 0; + int high = segments.Length - 1; + while (low <= high) { + int middle = low + ((high - low) / 2); + if (offset < segmentStarts [middle]) { + high = middle - 1; + } else if (offset >= segmentStarts [middle + 1]) { + low = middle + 1; + } else { + cursor = middle; + return middle; + } + } + throw new ArgumentOutOfRangeException (nameof (offset)); + } +} diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index ec4da021cf7..1149154817a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -104,6 +104,22 @@ public void EmitPreamble (string assemblyName, string moduleName, ReadOnlySpan. /// public void WritePE (Stream stream) + { + var peBlob = SerializePE (); + if (stream is MemoryStream memoryStream && memoryStream.Length == 0 && memoryStream.Capacity < peBlob.Count) { + memoryStream.Capacity = peBlob.Count; + } + peBlob.WriteContentTo (stream); + } + + /// + /// Serialises the metadata + IL into a PE DLL and returns a read-only stream over the + /// serialised bytes. Unlike the image is not copied into a + /// second contiguous buffer. + /// + public Stream CreatePEStream () => new BlobBuilderStream (SerializePE ()); + + BlobBuilder SerializePE () { var peBuilder = new ManagedPEBuilder ( new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), @@ -114,10 +130,7 @@ 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); + return peBlob; } static BlobContentId DeterministicContentId (IEnumerable content) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 9c66d57c9e3..d441c0e4026 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -188,6 +188,21 @@ internal void Emit (TypeMapAssemblyData model, Stream stream, bool useSharedType _pe.WritePE (stream); } + /// + /// Emits a PE assembly from the given model and returns a read-only stream over the serialised + /// image, avoiding the copy into a second buffer that + /// performs. + /// + internal Stream EmitToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint) + { + if (model is null) { + throw new ArgumentNullException (nameof (model)); + } + + EmitCore (model, useSharedTypemapUniverse, contentFingerprint); + return _pe.CreatePEStream (); + } + void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint) { contentFingerprint ??= MetadataHelper diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 9475d1cad9f..00db5979451 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -55,6 +55,16 @@ internal void Generate (TypeMapAssemblyData model, Stream stream, bool useShared emitter.Emit (model, stream, useSharedTypemapUniverse, contentFingerprint); } + /// + /// Generates the PE assembly and returns a read-only stream over the serialised image without + /// copying it into a second buffer. + /// + internal Stream GenerateToStream (TypeMapAssemblyData model, bool useSharedTypemapUniverse, byte []? contentFingerprint = null) + { + var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); + return emitter.EmitToStream (model, useSharedTypemapUniverse, contentFingerprint); + } + /// /// Emits an empty typemap assembly (containing no type map entries) with the given /// , writing it to . Used to satisfy diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index dd2296230d3..c77f11dbf4c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -335,9 +335,7 @@ internal List GenerateTypeMapAssemblies ( continue; } } - var stream = new MemoryStream (); - generator.Generate (model, stream, useSharedTypemapUniverse, fingerprints.Content); - stream.Position = 0; + var stream = generator.GenerateToStream (model, useSharedTypemapUniverse, fingerprints.Content); generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index d2a3c1e7122..c88adac74a5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -20,7 +20,11 @@ public record TrimmableTypeMapResult ( ApplicationRegistrationTypes ?? []; } -public record GeneratedAssembly (string Name, MemoryStream Content); +/// +/// A generated typemap assembly. is a read-only, seekable stream +/// positioned at the start of the serialised PE image; callers own it and should dispose it. +/// +public record GeneratedAssembly (string Name, Stream Content); public record GeneratedJavaSource (string RelativePath, string Content); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs new file mode 100644 index 00000000000..8ad4daaebe4 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Xunit; + +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +/// +/// Generated typemap assemblies are surfaced as a read-only stream over the chunks the PE +/// serialiser already produced instead of being copied into a . +/// These tests pin the stream semantics the build task relies on: seek to the start, hash the +/// whole image, then copy it to disk. +/// +public class BlobBuilderStreamTests : FixtureTestBase +{ + static BlobBuilder MultiChunkBuilder (byte [] content, int chunkSize) + { + var builder = new BlobBuilder (chunkSize); + for (int offset = 0; offset < content.Length; offset += chunkSize) { + int count = Math.Min (chunkSize, content.Length - offset); + builder.WriteBytes (content, offset, count); + if (offset + count < content.Length) { + // Force a new chunk so the stream has to walk several segments. + builder.LinkSuffix (MultiChunkBuilderTail (content, offset + count, chunkSize)); + return builder; + } + } + return builder; + } + + static BlobBuilder MultiChunkBuilderTail (byte [] content, int start, int chunkSize) + { + var tail = new BlobBuilder (chunkSize); + tail.WriteBytes (content, start, content.Length - start); + return tail; + } + + static byte [] Sequence (int length) + { + var bytes = new byte [length]; + for (int i = 0; i < length; i++) { + bytes [i] = (byte) (i % 251); + } + return bytes; + } + + [Theory] + [InlineData (0)] + [InlineData (1)] + [InlineData (63)] + [InlineData (64)] + [InlineData (1000)] + public void ReadsBackTheExactContent (int length) + { + var content = Sequence (length); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 16)); + + Assert.Equal (length, stream.Length); + Assert.Equal (0, stream.Position); + Assert.True (stream.CanRead); + Assert.True (stream.CanSeek); + Assert.False (stream.CanWrite); + + using var copy = new MemoryStream (); + stream.CopyTo (copy); + Assert.Equal (content, copy.ToArray ()); + Assert.Equal (length, stream.Position); + Assert.Equal (0, stream.Read (new byte [1], 0, 1)); + } + + [Fact] + public void SupportsRepeatedSequentialAndRandomAccess () + { + var content = Sequence (5000); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 37)); + + // The build task hashes the stream and then rewinds it to copy it to disk. + using var first = new MemoryStream (); + stream.CopyTo (first); + stream.Position = 0; + using var second = new MemoryStream (); + stream.CopyTo (second); + Assert.Equal (first.ToArray (), second.ToArray ()); + + Assert.Equal (4000, stream.Seek (4000, SeekOrigin.Begin)); + var buffer = new byte [10]; + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (4000).Take (10), buffer); + + Assert.Equal (10, stream.Seek (-4000, SeekOrigin.Current)); + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (10).Take (10), buffer); + + Assert.Equal (4990, stream.Seek (-10, SeekOrigin.End)); + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Skip (4990).Take (10), buffer); + + // Backwards seeks must invalidate the sequential-read cursor. + stream.Position = 0; + Assert.Equal (10, stream.Read (buffer, 0, 10)); + Assert.Equal (content.Take (10), buffer); + } + + [Fact] + public void PartialReadsSpanChunkBoundaries () + { + var content = Sequence (300); + using var stream = new BlobBuilderStream (MultiChunkBuilder (content, 7)); + using var copy = new MemoryStream (); + + var buffer = new byte [11]; + int read; + while ((read = stream.Read (buffer, 0, buffer.Length)) > 0) { + copy.Write (buffer, 0, read); + } + Assert.Equal (content, copy.ToArray ()); + } + + [Fact] + public void RejectsWrites () + { + using var stream = new BlobBuilderStream (MultiChunkBuilder (Sequence (16), 8)); + Assert.Throws (() => stream.Write (new byte [1], 0, 1)); + Assert.Throws (() => stream.SetLength (0)); + } + + [Fact] + public void GeneratedAssemblyStreamIsAReadableImage () + { + var generator = new TrimmableTypeMapGenerator (new CollectingLogger ()); + using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var reader = peReader.GetMetadataReader (); + var result = generator.Execute ( + [new AssemblyInput (reader.GetString (reader.GetAssemblyDefinition ().Name), TestFixtureAssemblyPath, peReader)], + new Version (11, 0), + new HashSet ()); + + Assert.NotEmpty (result.GeneratedAssemblies); + foreach (var assembly in result.GeneratedAssemblies) { + Assert.Equal (0, assembly.Content.Position); + Assert.True (assembly.Content.Length > 0); + using var generatedReader = new PEReader (assembly.Content); + var metadata = generatedReader.GetMetadataReader (); + Assert.Equal (assembly.Name, metadata.GetString (metadata.GetAssemblyDefinition ().Name)); + } + } + + sealed class CollectingLogger : ITrimmableTypeMapLogger + { + public void LogNoJavaPeerTypesFound () { } + public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } + public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } + public void LogDeferredRegistrationTypesInfo (int typeCount) { } + public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } + public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } + public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } + public void LogGeneratedJcwFilesInfo (int sourceCount) { } + public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } + public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } + public void LogLibraryManifestMergeWarning (string message) { } + public void LogInvalidManifestPlaceholderWarning (string placeholders) { } + public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, + string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) { } + public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } + public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } + public void LogCustomJavaObjectError (string managedTypeName) { } + public void LogCustomJavaObjectWarning (string managedTypeName) { } + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index cc6a0ec36b9..42e985cb438 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -336,7 +336,7 @@ public void Execute_CanSkipUnusedNonAcwMarshalMethods () 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 ()); + Assert.Equal (ReadAllBytes (full.GeneratedAssemblies [i].Content), ReadAllBytes (optimized.GeneratedAssemblies [i].Content)); } } @@ -387,7 +387,7 @@ public void Execute_IncrementalCallbackPreservesGeneratedBytes () 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 ()); + Assert.Equal (ReadAllBytes (full.GeneratedAssemblies [i].Content), ReadAllBytes (incremental.GeneratedAssemblies [i].Content)); } DisposeGeneratedAssemblies (full.GeneratedAssemblies); DisposeGeneratedAssemblies (incremental.GeneratedAssemblies); @@ -492,6 +492,14 @@ static JavaPeerInfo CreatePeer (string assemblyName, string managedTypeName, str }; } + static byte [] ReadAllBytes (Stream stream) + { + stream.Position = 0; + using var buffer = new MemoryStream (); + stream.CopyTo (buffer); + return buffer.ToArray (); + } + static void DisposeGeneratedAssemblies (IEnumerable assemblies) { foreach (var assembly in assemblies) { From 3eb5249739c1394b5964810b58af33e5aeb3e555 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 31 Aug 2026 21:39:20 +0200 Subject: [PATCH 09/13] Address code review feedback Fail fast when a caller writes to the incremental fingerprint sink that was never requested. Dropping the write silently would produce a fingerprint that looks valid but covers less than it claims to. The incremental sink's buffer flush is now factored out to mirror the content sink's, so the two paths are symmetric. Validate the arguments of BlobBuilderStream.Read the way Stream implementations conventionally do: name the parameter that actually failed, and throw ArgumentException when the range overflows the buffer. Document why GeneratedAssembly.Content is typed as Stream rather than MemoryStream, so the narrowed member surface reads as the intentional trade for the removed buffer copy that it is. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/BlobBuilderStream.cs | 8 +++- .../Generator/FingerprintWriter.cs | 45 +++++++++++-------- .../TrimmableTypeMapTypes.cs | 10 +++++ .../Generator/BlobBuilderStreamTests.cs | 12 +++++ .../Generator/FingerprintWriterTests.cs | 38 ++++++++++++++++ 5 files changed, 94 insertions(+), 19 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs index 9a8a3f706f0..088a7d8f845 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs @@ -74,9 +74,15 @@ public override int Read (byte [] buffer, int offset, int count) if (buffer is null) { throw new ArgumentNullException (nameof (buffer)); } - if (offset < 0 || count < 0 || buffer.Length - offset < count) { + if (offset < 0) { + throw new ArgumentOutOfRangeException (nameof (offset)); + } + if (count < 0) { throw new ArgumentOutOfRangeException (nameof (count)); } + if (buffer.Length - offset < count) { + throw new ArgumentException ("The buffer is too small for the requested range."); + } int copied = 0; while (count > 0 && position < length) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs index 304afa72da0..ff5621db3e7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs @@ -101,13 +101,10 @@ public byte [] GetContentFingerprint () public byte [] GetIncrementalFingerprint () { - if (incrementalHash is null || incrementalBuffer is null) { + if (incrementalHash is null) { throw new InvalidOperationException ("The incremental fingerprint was not requested."); } - if (incrementalPosition > 0) { - incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); - incrementalPosition = 0; - } + FlushIncremental (); return incrementalHash.GetHashAndReset (); } @@ -137,19 +134,23 @@ void Write (Sink sink, byte [] data, int offset, int count) contentPosition += count; } } - if ((sink & Sink.Incremental) != 0 && incrementalHash is not null && incrementalBuffer is not null) { - if (count > incrementalBuffer.Length - incrementalPosition) { - if (incrementalPosition > 0) { - incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); - incrementalPosition = 0; - } - } - if (count > incrementalBuffer.Length) { - incrementalHash.AppendData (data, offset, count); - } else { - Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); - incrementalPosition += count; - } + if ((sink & Sink.Incremental) == 0) { + return; + } + if (incrementalHash is null || incrementalBuffer is null) { + // Silently dropping the write would produce a fingerprint that looks valid but + // covers less than it claims to, so fail fast on the caller's mistake instead. + throw new InvalidOperationException ( + $"Cannot write to {nameof (Sink.Incremental)} because the incremental fingerprint was not requested."); + } + if (count > incrementalBuffer.Length - incrementalPosition) { + FlushIncremental (); + } + if (count > incrementalBuffer.Length) { + incrementalHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); + incrementalPosition += count; } } @@ -161,6 +162,14 @@ void FlushContent () } } + void FlushIncremental () + { + if (incrementalPosition > 0 && incrementalHash is not null && incrementalBuffer is not null) { + incrementalHash.AppendData (incrementalBuffer, 0, incrementalPosition); + incrementalPosition = 0; + } + } + static int Write7BitEncodedInt (byte [] destination, int offset, int value) { uint remaining = (uint) value; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index c88adac74a5..e0d8e0aa4f5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -24,6 +24,16 @@ public record TrimmableTypeMapResult ( /// A generated typemap assembly. is a read-only, seekable stream /// positioned at the start of the serialised PE image; callers own it and should dispose it. /// +/// +/// is deliberately typed as rather than +/// : the emitter hands back a view over the buffers the PE serialiser +/// already produced, so the image is never copied into a second contiguous buffer. Consumers +/// should treat it as a forward-reading stream — hash it, rewind, and copy it — rather than +/// reaching for members such as ToArray or GetBuffer. +/// This assembly is build-time SDK infrastructure rather than a library third parties compile +/// against, and the in-tree consumer (GenerateTrimmableTypeMap) only ever streams the +/// content to disk, so the narrowed member surface is an intentional trade for the removed copy. +/// public record GeneratedAssembly (string Name, Stream Content); public record GeneratedJavaSource (string RelativePath, string Content); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs index 8ad4daaebe4..d9b60994bcf 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -127,6 +127,18 @@ public void RejectsWrites () Assert.Throws (() => stream.SetLength (0)); } + [Fact] + public void ValidatesReadArguments () + { + using var stream = new BlobBuilderStream (MultiChunkBuilder (Sequence (32), 8)); + var buffer = new byte [16]; + + Assert.Throws (() => stream.Read (buffer, -1, 1)); + Assert.Throws (() => stream.Read (buffer, 0, -1)); + // Range overflows the buffer: ArgumentException, matching Stream.Read conventions. + Assert.Throws (() => stream.Read (buffer, 8, 16)); + } + [Fact] public void GeneratedAssemblyStreamIsAReadableImage () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs index b6d7ee4c0e8..79d63bcd8e1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs @@ -74,6 +74,44 @@ public void GetIncrementalFingerprint_WhenNotRequested_Throws () Assert.Throws (() => writer.GetIncrementalFingerprint ()); } + [Fact] + public void WritingToIncrementalSink_WhenNotRequested_Throws () + { + // Silently dropping the write would yield a fingerprint that looks valid but covers + // less than it claims to, so the writer must fail fast instead. + foreach (var sink in new [] { Sink.Incremental, Sink.Both }) { + using var writer = new FingerprintWriter (includeIncremental: false); + Assert.Throws (() => writer.WriteString (sink, "value")); + Assert.Throws (() => writer.WriteBoolean (sink, true)); + Assert.Throws (() => writer.WriteInt32 (sink, 1)); + Assert.Throws (() => writer.WriteRaw (sink, [1, 2, 3])); + } + } + + [Fact] + public void IncrementalSink_FlushesBufferedDataSpanningMultipleFlushes () + { + // Exercises the incremental sink's buffer-flush path across the 8 KB boundary, both for + // values that fit in the buffer and for one larger than it. + using var writer = new FingerprintWriter (includeIncremental: true); + var chunk = new string ('y', 3000); + var oversized = new string ('z', 20000); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, chunk); + writer.WriteString (Sink.Incremental, oversized); + writer.WriteString (Sink.Incremental, chunk); + + var expected = Sha256 (BinaryWriterBytes (w => { + w.Write (chunk); + w.Write (chunk); + w.Write (chunk); + w.Write (oversized); + w.Write (chunk); + })); + Assert.Equal (expected, writer.GetIncrementalFingerprint ()); + } + [Theory] [InlineData (true)] [InlineData (false)] From 37b87f960079b9c6709fb90774e923411c4eb19e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 3 Sep 2026 13:37:26 +0200 Subject: [PATCH 10/13] Fix typemap incremental test source update Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs | 2 ++ 1 file changed, 2 insertions(+) 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 59283a6a8e3..3ed5da9d41c 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 @@ -65,6 +65,7 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, }; + proj.MainActivity = proj.DefaultMainActivity; proj.SetRuntime (runtime); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); bool trimNativeAotJavaCode = isRelease && runtime == AndroidRuntime.NativeAOT; @@ -132,6 +133,7 @@ public void Build_WithTrimmableTypeMap_MissingJavaListPreservesGeneratedJava () } var proj = new XamarinAndroidApplicationProject (); + proj.MainActivity = proj.DefaultMainActivity; proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("AndroidTypeMapImplementation", "trimmable"); From efaed8852cf7c4663931ea2ebf0374e1e353921b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 3 Sep 2026 21:57:58 +0200 Subject: [PATCH 11/13] Update test logger for main Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/BlobBuilderStreamTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs index d9b60994bcf..1d40c90b255 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -178,6 +178,11 @@ public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, strin string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) { } public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } + public void LogDuplicateJavaTypeError (string javaName) { } + public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) { } + public void LogExportFieldWithParametersError () { } + public void LogExportFieldOnGenericTypeError () { } + public void LogExportFieldReturnsVoidError () { } public void LogCustomJavaObjectError (string managedTypeName) { } public void LogCustomJavaObjectWarning (string managedTypeName) { } } From 26fc62415b1d0fa446e8a6fa38f8d855da593628 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 4 Sep 2026 14:09:26 +0200 Subject: [PATCH 12/13] Improve typemap streaming implementation Stream root typemap output, make invalid multi-sink fingerprint writes atomic, and consolidate test logger stubs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/BlobBuilderStream.cs | 5 +- .../Generator/FingerprintWriter.cs | 47 ++++++++++++------- .../Generator/MetadataHelper.cs | 2 + .../Generator/PEAssemblyBuilder.cs | 1 - .../Generator/RootTypeMapAssemblyGenerator.cs | 19 +++++++- .../Generator/TypeMapAssemblyEmitter.cs | 2 + .../TrimmableTypeMapGenerator.cs | 5 +- .../Generator/BlobBuilderStreamTests.cs | 29 +----------- .../Generator/FingerprintWriterTests.cs | 1 + .../TrimmableTypeMapGeneratorTests.cs | 46 +++++++++--------- .../NoOpTrimmableTypeMapLogger.cs | 32 +++++++++++++ 11 files changed, 112 insertions(+), 77 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs index 088a7d8f845..5eb2ba20924 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/BlobBuilderStream.cs @@ -14,6 +14,8 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// Only the chunk arrays are retained — the itself and the metadata /// graph that produced it stay collectible — so the live byte count matches what a /// copy would have held, without the transient second copy. +/// The source builder must not be cleared, freed, or reused after this stream is created because +/// the stream retains references to its chunk arrays. /// sealed class BlobBuilderStream : Stream { @@ -91,9 +93,6 @@ public override int Read (byte [] buffer, int offset, int count) int within = (int) (position - segmentStarts [index]); int available = segment.Count - within; int toCopy = Math.Min (available, count); - if (segment.Array is null) { - break; - } Buffer.BlockCopy (segment.Array, segment.Offset + within, buffer, offset, toCopy); position += toCopy; offset += toCopy; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs index ff5621db3e7..f9bd9e541c3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/FingerprintWriter.cs @@ -123,37 +123,48 @@ void EnsureScratch (int required) void Write (Sink sink, byte [] data, int offset, int count) { - if ((sink & Sink.Content) != 0) { - if (count > contentBuffer.Length - contentPosition) { - FlushContent (); - } - if (count > contentBuffer.Length) { - contentHash.AppendData (data, offset, count); - } else { - Buffer.BlockCopy (data, offset, contentBuffer, contentPosition, count); - contentPosition += count; - } - } if ((sink & Sink.Incremental) == 0) { + if ((sink & Sink.Content) != 0) { + WriteContent (data, offset, count); + } return; } - if (incrementalHash is null || incrementalBuffer is null) { - // Silently dropping the write would produce a fingerprint that looks valid but - // covers less than it claims to, so fail fast on the caller's mistake instead. + + // Validate before writing either sink so a rejected Sink.Both write cannot leave the + // content fingerprint in a partially updated state. + if (incrementalHash is not IncrementalHash incrementalHashForWrite || + incrementalBuffer is not byte [] incrementalBufferForWrite) { throw new InvalidOperationException ( $"Cannot write to {nameof (Sink.Incremental)} because the incremental fingerprint was not requested."); } - if (count > incrementalBuffer.Length - incrementalPosition) { + + if ((sink & Sink.Content) != 0) { + WriteContent (data, offset, count); + } + if (count > incrementalBufferForWrite.Length - incrementalPosition) { FlushIncremental (); } - if (count > incrementalBuffer.Length) { - incrementalHash.AppendData (data, offset, count); + if (count > incrementalBufferForWrite.Length) { + incrementalHashForWrite.AppendData (data, offset, count); } else { - Buffer.BlockCopy (data, offset, incrementalBuffer, incrementalPosition, count); + Buffer.BlockCopy (data, offset, incrementalBufferForWrite, incrementalPosition, count); incrementalPosition += count; } } + void WriteContent (byte [] data, int offset, int count) + { + if (count > contentBuffer.Length - contentPosition) { + FlushContent (); + } + if (count > contentBuffer.Length) { + contentHash.AppendData (data, offset, count); + } else { + Buffer.BlockCopy (data, offset, contentBuffer, contentPosition, count); + contentPosition += count; + } + } + void FlushContent () { if (contentPosition > 0) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 1cc2c2b7e4a..5d840c25313 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -162,6 +162,8 @@ public static byte [] ComputeRootIncrementalFingerprint ( Version systemRuntimeVersion, bool useSharedTypemapUniverse) { + // This method needs only one hash. The content sink is used as the writer's always-present + // sink; the returned value is still solely the incremental-build fingerprint for the root. using var writer = new FingerprintWriter (includeIncremental: false); writer.WriteRaw (Sink.Content, GeneratorModuleVersionId.ToByteArray ()); writer.WriteString (Sink.Content, systemRuntimeVersion.ToString ()); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 1149154817a..2f69667e2f9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -441,7 +441,6 @@ MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, BlobHandle signature, Action emitIL, Action? encodeLocals, bool useBranches) { - StandaloneSignatureHandle localSigHandle = default; if (encodeLocals != null) { var localSigBlob = new BlobBuilder (32); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 683464daa4c..56ebc34b512 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -83,6 +83,23 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha throw new ArgumentNullException (nameof (stream)); } + CreatePEBuilder (perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName, moduleName).WritePE (stream); + } + + /// + /// Generates the root typemap assembly and returns a read-only stream over the serialised image. + /// + internal Stream GenerateToStream (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, string? assemblyName = null, string? moduleName = null) + { + return CreatePEBuilder (perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName, moduleName).CreatePEStream (); + } + + PEAssemblyBuilder CreatePEBuilder (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, string? assemblyName, string? moduleName) + { + if (perAssemblyTypeMapNames is null) { + throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); + } + assemblyName ??= DefaultAssemblyName; moduleName ??= assemblyName + ".dll"; @@ -130,7 +147,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha // Emit TypeMapLoader class with Initialize() method EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, assemblyName); - pe.WritePE (stream); + return pe; } static void EmitSharedUniverseAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 785e50b3470..35c5bc17baf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -138,6 +138,8 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; + // These handles belong to this emitter's readonly PE metadata builder and must not be shared + // across emitter instances. BlobHandle _activationCtorSignature; BlobHandle _createInstanceSignature; diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index dc42a35a7e8..0b700521a9c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Reflection.PortableExecutable; using System.Xml.Linq; @@ -382,10 +381,8 @@ internal List GenerateTypeMapAssemblies ( generateRoot = shouldGenerateTypeMapAssembly (rootAssemblyName, rootFingerprint); } if (generateRoot) { - var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); - rootStream.Position = 0; + var rootStream = rootGenerator.GenerateToStream (perAssemblyNames, useSharedTypemapUniverse); generatedAssemblies.Add (new GeneratedAssembly (rootAssemblyName, rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs index 1d40c90b255..2a8f79d05bd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/BlobBuilderStreamTests.cs @@ -142,7 +142,7 @@ public void ValidatesReadArguments () [Fact] public void GeneratedAssemblyStreamIsAReadableImage () { - var generator = new TrimmableTypeMapGenerator (new CollectingLogger ()); + var generator = new TrimmableTypeMapGenerator (new NoOpTrimmableTypeMapLogger ()); using var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var reader = peReader.GetMetadataReader (); var result = generator.Execute ( @@ -152,6 +152,7 @@ [new AssemblyInput (reader.GetString (reader.GetAssemblyDefinition ().Name), Tes Assert.NotEmpty (result.GeneratedAssemblies); foreach (var assembly in result.GeneratedAssemblies) { + Assert.IsType (assembly.Content); Assert.Equal (0, assembly.Content.Position); Assert.True (assembly.Content.Length > 0); using var generatedReader = new PEReader (assembly.Content); @@ -160,30 +161,4 @@ [new AssemblyInput (reader.GetString (reader.GetAssemblyDefinition ().Name), Tes } } - sealed class CollectingLogger : ITrimmableTypeMapLogger - { - public void LogNoJavaPeerTypesFound () { } - public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } - public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } - public void LogDeferredRegistrationTypesInfo (int typeCount) { } - public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } - public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } - public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } - public void LogGeneratedJcwFilesInfo (int sourceCount) { } - public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } - public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } - public void LogLibraryManifestMergeWarning (string message) { } - public void LogInvalidManifestPlaceholderWarning (string placeholders) { } - public void LogUnresolvableJavaPeerSkippedWarning (string managedTypeName, string assemblyName, - string unresolvedTypeName, string unresolvedAssemblyName, string unresolvedAssemblyPath) { } - public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } - public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } - public void LogDuplicateJavaTypeError (string javaName) { } - public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) { } - public void LogExportFieldWithParametersError () { } - public void LogExportFieldOnGenericTypeError () { } - public void LogExportFieldReturnsVoidError () { } - public void LogCustomJavaObjectError (string managedTypeName) { } - public void LogCustomJavaObjectWarning (string managedTypeName) { } - } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs index 79d63bcd8e1..0aa09df882b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FingerprintWriterTests.cs @@ -85,6 +85,7 @@ public void WritingToIncrementalSink_WhenNotRequested_Throws () Assert.Throws (() => writer.WriteBoolean (sink, true)); Assert.Throws (() => writer.WriteInt32 (sink, 1)); Assert.Throws (() => writer.WriteRaw (sink, [1, 2, 3])); + Assert.Equal (Sha256 ([]), writer.GetContentFingerprint ()); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 25381672f13..097889f9166 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -14,33 +14,33 @@ public class TrimmableTypeMapGeneratorTests : FixtureTestBase { readonly List logMessages = new (); - sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : ITrimmableTypeMapLogger + sealed class TestTrimmableTypeMapLogger (List logMessages, List? warnings = null) : NoOpTrimmableTypeMapLogger { - public void LogNoJavaPeerTypesFound () => + public override void LogNoJavaPeerTypesFound () => logMessages.Add ("No Java peer types found, skipping typemap generation."); - public void LogJavaPeerScanInfo (int assemblyCount, int peerCount) => + public override void LogJavaPeerScanInfo (int assemblyCount, int peerCount) => logMessages.Add ($"Scanned {assemblyCount} assemblies, found {peerCount} Java peer types."); - public void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) => + public override void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) => logMessages.Add ($"Generating JCW files for {jcwPeerCount} types (filtered from {totalPeerCount} total)."); - public void LogDeferredRegistrationTypesInfo (int typeCount) => + public override void LogDeferredRegistrationTypesInfo (int typeCount) => logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); - public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => + public override void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => logMessages.Add ($" {assemblyName}: {typeCount} types"); - public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => + public override void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); - public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => + public override void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => logMessages.Add ($"Generated {assemblyCount} typemap assemblies."); - public void LogGeneratedJcwFilesInfo (int sourceCount) => + public override void LogGeneratedJcwFilesInfo (int sourceCount) => logMessages.Add ($"Generated {sourceCount} JCW Java source files."); - public void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => + public override void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) => logMessages.Add ($"Rooting manifest-referenced type '{javaTypeName}' ({managedTypeName}) as unconditional."); - public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => + public override void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => warnings?.Add ($"Manifest-referenced type '{javaTypeName}' was not found in any scanned assembly. It may be a framework type."); - public void LogLibraryManifestMergeWarning (string message) => + public override void LogLibraryManifestMergeWarning (string message) => warnings?.Add (message); - public void LogInvalidManifestPlaceholderWarning (string placeholders) => + public override void LogInvalidManifestPlaceholderWarning (string placeholders) => warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'."); - public void LogUnresolvableJavaPeerSkippedWarning ( + public override void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, string unresolvedTypeName, @@ -49,23 +49,23 @@ public void LogUnresolvableJavaPeerSkippedWarning ( warnings?.Add ( $"Skipping Java peer '{managedTypeName}' from '{assemblyName}' because referenced type " + $"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); - public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => + public override void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); - public void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => + public override void LogInvalidJavaNameError (string javaName, string invalidIdentifier) => logMessages.Add ($"XA4258: Java name '{javaName}' contains reserved Java identifier '{invalidIdentifier}'."); - public void LogDuplicateJavaTypeError (string javaName) => + public override void LogDuplicateJavaTypeError (string javaName) => logMessages.Add ($"XA4215: The Java type `{javaName}` is generated by more than one managed type."); - public void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => + public override void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) => logMessages.Add ($"XA4215: `{javaName}` generated by: {managedTypeName}"); - public void LogExportFieldWithParametersError () => + public override void LogExportFieldWithParametersError () => logMessages.Add ("XA4205: [ExportField] can only be used on methods with 0 parameters."); - public void LogExportFieldReturnsVoidError () => + public override void LogExportFieldReturnsVoidError () => logMessages.Add ("XA4208: [ExportField] cannot be used on a method returning 'void'."); - public void LogExportFieldOnGenericTypeError () => + public override void LogExportFieldOnGenericTypeError () => logMessages.Add ("XA4207: [ExportField] cannot be used on a generic type."); - public void LogCustomJavaObjectError (string managedTypeName) => + public override void LogCustomJavaObjectError (string managedTypeName) => logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); - public void LogCustomJavaObjectWarning (string managedTypeName) => + public override void LogCustomJavaObjectWarning (string managedTypeName) => warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs new file mode 100644 index 00000000000..ba25b770d77 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/NoOpTrimmableTypeMapLogger.cs @@ -0,0 +1,32 @@ +namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; + +class NoOpTrimmableTypeMapLogger : ITrimmableTypeMapLogger +{ + public virtual void LogNoJavaPeerTypesFound () { } + public virtual void LogJavaPeerScanInfo (int assemblyCount, int peerCount) { } + public virtual void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount) { } + public virtual void LogDeferredRegistrationTypesInfo (int typeCount) { } + public virtual void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) { } + public virtual void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) { } + public virtual void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) { } + public virtual void LogGeneratedJcwFilesInfo (int sourceCount) { } + public virtual void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName) { } + public virtual void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) { } + public virtual void LogLibraryManifestMergeWarning (string message) { } + public virtual void LogInvalidManifestPlaceholderWarning (string placeholders) { } + public virtual void LogUnresolvableJavaPeerSkippedWarning ( + string managedTypeName, + string assemblyName, + string unresolvedTypeName, + string unresolvedAssemblyName, + string unresolvedAssemblyPath) { } + public virtual void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) { } + public virtual void LogInvalidJavaNameError (string javaName, string invalidIdentifier) { } + public virtual void LogDuplicateJavaTypeError (string javaName) { } + public virtual void LogDuplicateJavaTypeDetailsError (string javaName, string managedTypeName) { } + public virtual void LogExportFieldWithParametersError () { } + public virtual void LogExportFieldOnGenericTypeError () { } + public virtual void LogExportFieldReturnsVoidError () { } + public virtual void LogCustomJavaObjectError (string managedTypeName) { } + public virtual void LogCustomJavaObjectWarning (string managedTypeName) { } +} From e9e0b28ae9b74bf662f7885d02e7b328fa1e1902 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 8 Sep 2026 23:00:52 +0200 Subject: [PATCH 13/13] Relocate typemap JNI rewrite tests Move typemap-generated JNI rewrite coverage to the net11 task test project after the typemap library retargeting. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../JniAssemblyRewriterTypeMapTests.cs | 112 ++++++++++++++++++ ...Microsoft.Android.Build.Tasks.Tests.csproj | 3 + .../Properties/AssemblyInfo.cs | 1 + .../JniRemapping/JniAssemblyRewriterTests.cs | 80 ------------- 4 files changed, 116 insertions(+), 80 deletions(-) create mode 100644 src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs new file mode 100644 index 00000000000..e561aa0e4b0 --- /dev/null +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/JniAssemblyRewriterTypeMapTests.cs @@ -0,0 +1,112 @@ +extern alias xamarinbuildtasks; + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using Microsoft.Android.Sdk.TrimmableTypeMap; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NUnit.Framework; +using FieldRvaTable = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.FieldRvaTable; +using JniAssemblyRewriter = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniAssemblyRewriter; +using JniRewriteResult = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.JniRewriteResult; +using R8Mapping = xamarinbuildtasks::Xamarin.Android.Tasks.JniRemapping.R8Mapping; + +namespace Xamarin.Android.Build.Tests; + +[TestFixture] +[Parallelizable (ParallelScope.Children)] +public class JniAssemblyRewriterTypeMapTests +{ + [Test] + public void RewritesGeneratedTypeMapWithOwnerSpecificMethodNames () + { + byte [] source = GenerateTypeMapWithSharedMethodName (); + var warnings = new List (); + + JniRewriteResult result = Rewrite (source, Mapping ( + "test.First -> a.b.First:\n" + + " void n_Run() -> a\n" + + "test.Second -> a.b.Second:\n" + + " void n_Run() -> b\n"), warnings); + + CollectionAssert.AreEquivalent (new [] { "a", "b", "()V" }, ReadUtf8Values (result.Image)); + CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); + } + + [Test] + public void RewritesGeneratedTypeMapWithMappedAndUnmappedMethodNames () + { + byte [] source = GenerateTypeMapWithSharedMethodName (); + var warnings = new List (); + + JniRewriteResult result = Rewrite (source, Mapping ( + "test.First -> a.b.First:\n" + + " void n_Run() -> a\n" + + "test.Second -> test.Second:\n"), warnings); + + CollectionAssert.AreEquivalent (new [] { "a", "n_Run", "()V" }, ReadUtf8Values (result.Image)); + CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); + } + + static JniRewriteResult Rewrite (byte [] sourceImage, R8Mapping mapping, IList warnings) + { + var engine = new MockBuildEngine (TestContext.Out, warnings: warnings); + var log = new TaskLoggingHelper (engine, nameof (JniAssemblyRewriterTypeMapTests)); + return JniAssemblyRewriter.Rewrite (sourceImage, mapping, log); + } + + static R8Mapping Mapping (string text) => R8Mapping.Parse (new StringReader (text)); + + static byte [] GenerateTypeMapWithSharedMethodName () + { + var peers = new [] { + CreatePeer ("test/First", "Test.First"), + CreatePeer ("test/Second", "Test.Second"), + }; + using var stream = new MemoryStream (); + new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)).Generate (peers, stream, "OwnerSpecificNames"); + return stream.ToArray (); + } + + static JavaPeerInfo CreatePeer (string javaName, string managedName) + { + int separator = managedName.LastIndexOf ('.'); + return new JavaPeerInfo { + JavaName = javaName, + CompatJniName = javaName, + ManagedTypeName = managedName, + ManagedTypeNamespace = managedName.Substring (0, separator), + ManagedTypeShortName = managedName.Substring (separator + 1), + AssemblyName = "TestAsm", + DoNotGenerateAcw = false, + ActivationCtor = new ActivationCtorInfo { + DeclaringTypeName = managedName, + DeclaringAssemblyName = "TestAsm", + Style = ActivationCtorStyle.XamarinAndroid, + }, + MarshalMethods = [ + new MarshalMethodInfo { + JniName = "run", + NativeCallbackName = "n_Run", + JniSignature = "()V", + ManagedMethodName = "Run", + }, + ], + }; + } + + static string [] ReadUtf8Values (byte [] image) + { + using var peReader = new PEReader (ImmutableArray.Create (image)); + MetadataReader reader = peReader.GetMetadataReader (); + return FieldRvaTable.Read (peReader, reader).Entries + .Select (entry => entry.Utf8Value) + .OfType () + .ToArray (); + } +} diff --git a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj index 8e92f0383b8..0245cc843b8 100644 --- a/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj +++ b/src/Microsoft.Android.Build.Tasks/Tests/Microsoft.Android.Build.Tasks.Tests/Microsoft.Android.Build.Tasks.Tests.csproj @@ -19,6 +19,9 @@ + + xamarinbuildtasks + diff --git a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs index e66435ccc40..7dedecd1006 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/AssemblyInfo.cs @@ -19,5 +19,6 @@ [assembly: Guid ("42bb9aea-f4d8-4b43-8956-8e1fee857697")] [assembly: InternalsVisibleTo ("Xamarin.Android.Build.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo ("Microsoft.Android.Build.Tasks.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("MSBuildDeviceIntegration, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] [assembly: InternalsVisibleTo ("Microsoft.Android.Sdk.TrimmableTypeMap.IntegrationTests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniAssemblyRewriterTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniAssemblyRewriterTests.cs index 21c93763bdc..f05d4f16851 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniAssemblyRewriterTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/JniRemapping/JniAssemblyRewriterTests.cs @@ -7,7 +7,6 @@ using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; -using Microsoft.Android.Sdk.TrimmableTypeMap; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NUnit.Framework; @@ -891,85 +890,6 @@ public void FailsWhenASharedUtf8DatumNeedsTwoDifferentNames () StringAssert.Contains ("shared", exception.Message.ToLowerInvariant ()); } - [Test] - public void RewritesGeneratedTypeMapWithOwnerSpecificMethodNames () - { - byte [] source = GenerateTypeMapWithSharedMethodName (); - var warnings = new List (); - - JniRewriteResult result = Rewrite (source, Mapping ( - "test.First -> a.b.First:\n" + - " void n_Run() -> a\n" + - "test.Second -> a.b.Second:\n" + - " void n_Run() -> b\n"), warnings); - - CollectionAssert.AreEquivalent (new [] { "a", "b", "()V" }, ReadUtf8Values (result.Image)); - CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); - } - - [Test] - public void RewritesGeneratedTypeMapWithMappedAndUnmappedMethodNames () - { - byte [] source = GenerateTypeMapWithSharedMethodName (); - var warnings = new List (); - - JniRewriteResult result = Rewrite (source, Mapping ( - "test.First -> a.b.First:\n" + - " void n_Run() -> a\n" + - "test.Second -> test.Second:\n"), warnings); - - CollectionAssert.AreEquivalent (new [] { "a", "n_Run", "()V" }, ReadUtf8Values (result.Image)); - CollectionAssert.DoesNotContain (warnings.Select (warning => warning.Code).ToArray (), "XA4326"); - } - - static byte [] GenerateTypeMapWithSharedMethodName () - { - var peers = new [] { - CreatePeer ("test/First", "Test.First"), - CreatePeer ("test/Second", "Test.Second"), - }; - using var stream = new MemoryStream (); - new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)).Generate (peers, stream, "OwnerSpecificNames"); - return stream.ToArray (); - - static JavaPeerInfo CreatePeer (string javaName, string managedName) - { - int separator = managedName.LastIndexOf ('.'); - return new JavaPeerInfo { - JavaName = javaName, - CompatJniName = javaName, - ManagedTypeName = managedName, - ManagedTypeNamespace = managedName.Substring (0, separator), - ManagedTypeShortName = managedName.Substring (separator + 1), - AssemblyName = "TestAsm", - DoNotGenerateAcw = false, - ActivationCtor = new ActivationCtorInfo { - DeclaringTypeName = managedName, - DeclaringAssemblyName = "TestAsm", - Style = ActivationCtorStyle.XamarinAndroid, - }, - MarshalMethods = [ - new MarshalMethodInfo { - JniName = "run", - NativeCallbackName = "n_Run", - JniSignature = "()V", - ManagedMethodName = "Run", - }, - ], - }; - } - } - - static string [] ReadUtf8Values (byte [] image) - { - using var peReader = new PEReader (ImmutableArray.Create (image)); - MetadataReader reader = peReader.GetMetadataReader (); - return FieldRvaTable.Read (peReader, reader).Entries - .Select (entry => entry.Utf8Value) - .OfType () - .ToArray (); - } - [Test] public void FailsWhenASharedUtf8DatumMustRemainUnmappedForOneProxy () {