diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs index 198cb9c97fd..a8fcc8d771f 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeManagerTests.cs @@ -161,6 +161,20 @@ static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeywo AssertArrayTypesFromSignature (manager, "[[D", typeof (JavaObjectArray>)); AssertArrayTypesFromSignature (manager, "[[Ljava/lang/String;", typeof (JavaObjectArray>)); + // Nullable counterparts of the primitive value types: the boxed reference signatures + // java/lang/ map to Nullable (see JniBuiltinSimpleReferenceToType / + // GetBuiltInTypeForSimpleReference). Unlike the primitive keyword arrays above, these are + // non-keyword reference arrays, so the element type is the boxed Nullable and the array + // family is T?[] and JavaObjectArray (no JavaArray/JavaPrimitiveArray). + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Boolean;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Byte;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Character;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Short;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Integer;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Long;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Float;", typeof (JavaObjectArray)); + AssertArrayTypesFromSignature (manager, "[Ljava/lang/Double;", typeof (JavaObjectArray)); + // Yes, these look weird... // Assume: class II {} Assert.AreEqual (null, GetType ("II")); diff --git a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index 9120d400345..5bdbd9fc9e0 100644 --- a/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/external/Java.Interop/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -219,18 +219,16 @@ public unsafe void RegisterNativeMethods_JniNativeMethod () static int NativeAdd (IntPtr jnienv, IntPtr self, int a, int b) => a + b; [Test] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniNativeMethodRegistration[] registration path.")] - public unsafe void RegisterNativeMethods_JniNativeMethodRegistration () + public unsafe void RegisterNativeMethods_JniNativeMethod_UnmanagedCallersOnly () { using (var nativeClass = new JniType ("net/dot/jni/test/RegisterNativesTestType")) { - // Test the JniNativeMethodRegistration[] registration path (the one that marshals to blittable) - var methods = new JniNativeMethodRegistration [1]; - methods [0] = new JniNativeMethodRegistration ( - "add", - "(II)I", - new AddDelegate (ManagedAdd)); - - JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods, methods.Length); + Span methods = stackalloc JniNativeMethod [1]; + fixed (byte* namePtr = "add"u8) + fixed (byte* sigPtr = "(II)I"u8) { + methods [0] = new JniNativeMethod (namePtr, sigPtr, + (IntPtr) (delegate* unmanaged) &ManagedAdd); + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods); + } // Call the native method from Java to verify registration worked var ctor = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "", "()V"); @@ -248,26 +246,26 @@ public unsafe void RegisterNativeMethods_JniNativeMethodRegistration () } } - delegate int AddDelegate (IntPtr jnienv, IntPtr self, int a, int b); - + [UnmanagedCallersOnly] static int ManagedAdd (IntPtr jnienv, IntPtr self, int a, int b) => a + b; [Test] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniNativeMethodRegistration[] registration path with many methods.")] - public unsafe void RegisterNativeMethods_JniNativeMethodRegistration_ManyMethods () + public unsafe void RegisterNativeMethods_JniNativeMethod_UnmanagedCallersOnly_ManyMethods () { using (var nativeClass = new JniType ("net/dot/jni/test/RegisterNativesTestType")) { - // Test the heap allocation path (> 32 methods) to ensure the marshalling loop handles it - var methods = new JniNativeMethodRegistration [40]; - for (int i = 0; i < methods.Length; ++i) { - methods [i] = new JniNativeMethodRegistration ( - "add", - "(II)I", - new AddDelegate (ManagedAdd)); - } + // Keep coverage for a large registration set while using the NativeAOT-compatible + // blittable overload instead of the delegate-marshaling overload. + var methods = new JniNativeMethod [40]; + fixed (byte* namePtr = "add"u8) + fixed (byte* sigPtr = "(II)I"u8) { + for (int i = 0; i < methods.Length; ++i) { + methods [i] = new JniNativeMethod (namePtr, sigPtr, + (IntPtr) (delegate* unmanaged) &ManagedAdd); + } - // This should not crash even with > 32 methods - JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods, methods.Length); + // This should not crash even with > 32 methods. + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods); + } // Verify the registration worked by calling the method var ctor = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "", "()V"); @@ -286,4 +284,3 @@ public unsafe void RegisterNativeMethods_JniNativeMethodRegistration_ManyMethods } } } - diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs index 82b5651a25c..f738548ca8e 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ComponentElementBuilder.cs @@ -44,8 +44,7 @@ static class ComponentElementBuilder element.Add (CreateIntentFilterElement (intentFilter)); } - // Add element from a [Layout] attribute, if present - if (component.LayoutProperties is not null) { + if (component.Kind == ComponentKind.Activity && component.LayoutProperties is not null) { var layout = CreateLayoutElement (component.LayoutProperties); if (layout is not null) { element.Add (layout); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs index 1a4fb259ded..9a1b21f8653 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ManifestGenerator.cs @@ -516,9 +516,13 @@ internal static void ApplyPlaceholders (XDocument doc, string? placeholders, str if (!placeholders.IsNullOrEmpty ()) { foreach (var entry in placeholders.Split (PlaceholderSeparators, StringSplitOptions.RemoveEmptyEntries)) { var eqIndex = entry.IndexOf ('='); - if (eqIndex > 0) { + if (eqIndex >= 0) { var key = entry.Substring (0, eqIndex).Trim (); - var value = entry.Substring (eqIndex + 1).Trim (); + // Normalize '\' to the platform directory separator to match the legacy pipeline, + // where the substituted manifest is re-encoded by aapt2 (which rewrites backslashes + // to '/' on Unix). The trimmable generator writes the merged manifest directly, so we + // apply the same normalization here to keep placeholder values byte-for-byte identical. + var value = entry.Substring (eqIndex + 1).Trim ().Replace ('\\', Path.DirectorySeparatorChar); replacements ["${" + key + "}"] = value; } else if (eqIndex < 0) { // An entry without '=' is not a valid key=value pair. Mirror the legacy diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index c372463c6b6..50575b5b2be 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -31,6 +31,21 @@ static class ModelBuilder new ("D", "Double", "System.Double", ["Java.Interop.JavaDoubleArray"]), ]; + // Nullable value types map to the boxed java/lang/ references (java/lang/Boolean etc.) via + // TrimmableTypeMapTypeManager.GetBuiltInTypeForSimpleReference. Like System.String they are neither + // scanned Java peers nor primitives, so their array proxies are emitted explicitly. Only the eight + // types with a boxed java/lang mapping are included (java/lang/Byte -> sbyte?, not byte?, etc.). + static readonly (string Name, string ManagedTypeName) [] NullableArrayProxies = [ + ("Boolean", "System.Boolean"), + ("SByte", "System.SByte"), + ("Char", "System.Char"), + ("Int16", "System.Int16"), + ("Int32", "System.Int32"), + ("Int64", "System.Int64"), + ("Single", "System.Single"), + ("Double", "System.Double"), + ]; + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -55,7 +70,7 @@ static class ModelBuilder /// Emit per-rank array TypeMap entries + __ArrayMapRank{N} sentinels /// for ranks 1... 0 disables array entry emission. /// - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0, int maxReferenceArrayRank = 0) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -66,13 +81,20 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (maxArrayRank < 0) { throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); } + if (maxReferenceArrayRank < 0) { + throw new ArgumentOutOfRangeException (nameof (maxReferenceArrayRank), maxReferenceArrayRank, "Must be >= 0."); + } assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); var model = new TypeMapAssemblyData { AssemblyName = assemblyName, ModuleName = Path.GetFileName (outputPath), - MaxArrayRank = maxArrayRank, + // The per-assembly __ArrayMapRank{N} anchor count must be uniform across every typemap + // assembly (the root generator builds a rectangular [assembly][rank] matrix), so the model + // always carries the overall maximum rank. Primitive and reference element types may + // populate different rank ranges within that shared anchor set. + MaxArrayRank = Math.Max (maxArrayRank, maxReferenceArrayRank), }; // Invoker types are NOT emitted as separate proxies or TypeMap entries. @@ -102,19 +124,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri string jniName = kvp.Key; var peersForName = kvp.Value; - // Sort aliases by managed type name for deterministic proxy naming if (peersForName.Count > 1) { - peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); + peersForName.Sort (CompareAliasesForRuntimeResolution); } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); - if (maxArrayRank > 0) { - EmitArrayEntries (model, jniName, peersForName, maxArrayRank); + // Java peer types are reference types: emit array proxies only up to the reference rank. + if (maxReferenceArrayRank > 0) { + EmitArrayEntries (model, jniName, peersForName, maxReferenceArrayRank); } } - if (maxArrayRank > 0 && string.Equals (assemblyName, "_Java.Interop.TypeMap", StringComparison.Ordinal)) { + if (string.Equals (assemblyName, "_Java.Interop.TypeMap", StringComparison.Ordinal)) { EmitPrimitiveArrayEntries (model, maxArrayRank); } @@ -141,6 +163,19 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri return model; } + static int CompareAliasesForRuntimeResolution (JavaPeerInfo a, JavaPeerInfo b) + { + // Keep alias [0] aligned with the native java→managed map, which processes Mono.Android first. + bool aMonoAndroid = string.Equals (a.AssemblyName, "Mono.Android", StringComparison.Ordinal); + bool bMonoAndroid = string.Equals (b.AssemblyName, "Mono.Android", StringComparison.Ordinal); + if (aMonoAndroid != bMonoAndroid) { + return aMonoAndroid ? -1 : 1; + } + + int result = StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName); + return result != 0 ? result : StringComparer.Ordinal.Compare (a.AssemblyName, b.AssemblyName); + } + static void EmitPeers (TypeMapAssemblyData model, string jniName, List peersForName, string assemblyName, HashSet usedProxyNames) { @@ -541,6 +576,26 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; + static string AssemblyQualify (TypeRefData type) + { + if (type.GenericArguments.Count == 0) { + return AssemblyQualify (type.ManagedTypeName, type.AssemblyName); + } + + var builder = new StringBuilder (); + builder.Append (type.ManagedTypeName); + builder.Append ("[["); + for (int i = 0; i < type.GenericArguments.Count; i++) { + if (i > 0) { + builder.Append ("],["); + } + builder.Append (AssemblyQualify (type.GenericArguments [i])); + } + builder.Append ("]], "); + builder.Append (type.AssemblyName); + return builder.ToString (); + } + static string AddArrayRank (string typeReference, int rank) { if (rank == 0) { @@ -569,7 +624,7 @@ static string MakeNestedJavaObjectArrayTypeReference (string elementTypeReferenc static IReadOnlyList GetArrayTypeReferences (ArrayProxyData proxy) { - var elementType = AssemblyQualify (proxy.ElementType.ManagedTypeName, proxy.ElementType.AssemblyName); + var elementType = AssemblyQualify (proxy.ElementType); if (proxy.Primitive is null) { var rankOneTypes = new [] { MakeGenericTypeReference ("Java.Interop.JavaObjectArray`1", "Java.Interop", elementType), @@ -585,7 +640,7 @@ static IReadOnlyList GetArrayTypeReferences (ArrayProxyData proxy) MakeGenericTypeReference ("Java.Interop.JavaPrimitiveArray`1", "Java.Interop", elementType), ]; foreach (var concreteArrayType in proxy.Primitive.ConcreteArrayTypes) { - rankOnePrimitiveTypes.Add (AssemblyQualify (concreteArrayType.ManagedTypeName, concreteArrayType.AssemblyName)); + rankOnePrimitiveTypes.Add (AssemblyQualify (concreteArrayType)); } return ExpandRankOneTypes (rankOnePrimitiveTypes, proxy.Rank); } @@ -616,7 +671,7 @@ static void AddArrayProxyAssociations (TypeMapAssemblyData model, ArrayProxyData } static string GetArrayProxyMapKey (TypeRefData elementType) - => AssemblyQualify (elementType.ManagedTypeName, elementType.AssemblyName); + => AssemblyQualify (elementType); /// /// Emits per-rank array TypeMap entries for one peer, anchored to the per-assembly @@ -665,6 +720,13 @@ static void EmitArrayEntriesForPeer (TypeMapAssemblyData model, JavaPeerInfo pee } } + // Emits array proxies for the built-in element types that are not scanned Java peers: + // * the keyword primitives (int/bool/...) up to maxArrayRank (jagged/multidim primitive arrays + // like int[][][] are cheap — a small fixed set of element types), + // * System.String up to maxArrayRank (also a built-in element type, so multidimensional string + // arrays like String[][] stay resolvable), + // * the boxed Nullable value types up to maxArrayRank (also built-in element types mapping to + // java/lang/; a small fixed set, so multidim boxed arrays stay resolvable too). static void EmitPrimitiveArrayEntries (TypeMapAssemblyData model, int maxArrayRank) { foreach (var primitive in PrimitiveArrayProxies) { @@ -694,6 +756,74 @@ static void EmitPrimitiveArrayEntries (TypeMapAssemblyData model, int maxArrayRa AddArrayProxyAssociations (model, proxy, proxyReference); } } + + // System.String maps to java/lang/String but is a built-in element type injected at runtime by + // TrimmableTypeMapTypeManager.GetBuiltInTypeForSimpleReference, so it is neither a scanned Java + // peer (EmitArrayEntriesForPeer) nor a keyword primitive. Emit its array proxies here (Primitive + // is null, so it gets the reference-array family) so GetTypes ("[Ljava/lang/String;") yields + // System.String[] / JavaObjectArray / JavaArray on NativeAOT, matching CoreCLR. + // String is a single, fixed built-in element type (not part of the scanned-peer explosion), so — + // like the keyword primitives — it goes up to maxArrayRank. This keeps multidimensional string + // arrays (String[][], "[[Ljava/lang/String;") resolvable, matching the other type-map backends. + for (int rank = 1; rank <= maxArrayRank; rank++) { + var proxy = new ArrayProxyData { + TypeName = ManagedTypeNameToArrayProxyTypeName ("System.String", rank), + ElementType = new TypeRefData { + ManagedTypeName = "System.String", + AssemblyName = "System.Runtime", + }, + Rank = rank, + }; + model.ArrayProxyTypes.Add (proxy); + var proxyReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", model.AssemblyName); + model.Entries.Add (new TypeMapAttributeData { + MapKey = GetArrayProxyMapKey (proxy.ElementType), + ProxyTypeReference = proxyReference, + TargetTypeReference = proxyReference, + AnchorRank = rank, + }); + AddArrayProxyAssociations (model, proxy, proxyReference); + } + + // Nullable counterparts of the primitive value types map to the boxed java/lang/ + // references and, like System.String, are built-in element mappings (no scanned peer, no + // primitive proxy). Emit reference-array proxies (Primitive is null) so GetTypes + // ("[Ljava/lang/Boolean;") yields bool?[] / JavaObjectArray on NativeAOT. The element + // key uses the normalized generic form (simple assembly names) that + // TrimmableTypeMap.BuildManagedTypeKey produces at runtime for Nullable, while the model + // keeps the generic instantiation structured so the emitter writes a real Nullable type + // signature. Like the primitives and System.String, these are a small fixed set of built-in + // element types (not the scanned-peer explosion), so they go up to maxArrayRank and keep + // multidim boxed arrays resolvable. + foreach (var nullablePrimitive in NullableArrayProxies) { + for (int rank = 1; rank <= maxArrayRank; rank++) { + var proxy = new ArrayProxyData { + TypeName = $"Nullable_{nullablePrimitive.Name}_ArrayProxy{rank}", + ElementType = new TypeRefData { + ManagedTypeName = "System.Nullable`1", + AssemblyName = "System.Runtime", + GenericArguments = [ + new TypeRefData { + ManagedTypeName = nullablePrimitive.ManagedTypeName, + AssemblyName = "System.Runtime", + IsValueType = true, + }, + ], + IsValueType = true, + }, + Rank = rank, + }; + model.ArrayProxyTypes.Add (proxy); + var proxyReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", model.AssemblyName); + model.Entries.Add (new TypeMapAttributeData { + MapKey = GetArrayProxyMapKey (proxy.ElementType), + ProxyTypeReference = proxyReference, + TargetTypeReference = proxyReference, + AnchorRank = rank, + }); + AddArrayProxyAssociations (model, proxy, proxyReference); + } + } } static string Brackets (int rank) => rank switch { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index c6f60a7d592..bb28663cc61 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; +using System.Security.Cryptography; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -107,12 +109,28 @@ public void WritePE (Stream stream) new PEHeaderBuilder (imageCharacteristics: Characteristics.Dll), new MetadataRootBuilder (Metadata), ILBuilder, - mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null); + mappedFieldData: _mappedFieldData.Count > 0 ? _mappedFieldData : null, + // ManagedPEBuilder otherwise uses a time-based id, changing bytes on every regeneration. + deterministicIdProvider: DeterministicContentId); var peBlob = new BlobBuilder (); peBuilder.Serialize (peBlob); peBlob.WriteContentTo (stream); } + static BlobContentId DeterministicContentId (IEnumerable content) + { + using var hash = IncrementalHash.CreateHash (HashAlgorithmName.SHA256); + foreach (var blob in content) { + var segment = blob.GetBytes (); + if (segment.Count == 0) { + continue; + } + Debug.Assert (segment.Array is not null); + hash.AppendData (segment.Array, segment.Offset, segment.Count); + } + return BlobContentId.FromHash (hash.GetHashAndReset ()); + } + /// /// Adds (or retrieves from cache) an assembly reference. /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 48ca89f45bc..292189fe3b6 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -27,10 +27,11 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// - /// Max rank for per-rank array TypeMap entries. 0 disables. - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) + /// Max rank for per-rank built-in element array TypeMap entries. 0 disables. + /// Max rank for per-rank scanned Java peer reference-type array TypeMap entries. 0 disables. + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0, int maxReferenceArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, maxReferenceArrayRank); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 993cb949a1c..636b22e25b0 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -13,6 +13,7 @@ public interface ITrimmableTypeMapLogger void LogRootingManifestReferencedTypeInfo (string javaTypeName, string managedTypeName); void LogManifestReferencedTypeNotFoundWarning (string javaTypeName); void LogLibraryManifestMergeWarning (string message); + void LogInvalidManifestPlaceholderWarning (string placeholders); void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 7544d61b588..3f55ef799d5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -164,6 +164,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen // with the wrong AttributeName. List? intentFilters = null; List? metaData = null; + Dictionary? layoutProperties = null; foreach (var caHandle in typeDef.GetCustomAttributes ()) { var ca = Reader.GetCustomAttribute (caHandle); @@ -214,6 +215,8 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen metaData ??= new List (); var (mdName, mdProps) = ParseNameAndProperties (ca); metaData.Add (CreateMetaDataInfo (mdName, mdProps)); + } else if (attrName == "LayoutAttribute") { + layoutProperties = ParseLayoutAttribute (ca); } else if (attrInfo is null && ImplementsJniNameProviderAttribute (ca)) { // Custom attribute implementing IJniNameProviderAttribute (e.g., user-defined [CustomJniName]) var name = TryGetNameProperty (ca); @@ -232,6 +235,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen if (metaData is not null) { attrInfo.MetaData.AddRange (metaData); } + if (layoutProperties is not null) { + attrInfo.LayoutProperties = layoutProperties; + } } return (registerInfo, attrInfo); @@ -425,6 +431,18 @@ RegisterInfo ParseRegisterInfo (CustomAttributeValue value) return null; } + Dictionary ParseLayoutAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + var properties = new Dictionary (StringComparer.Ordinal); + foreach (var named in value.NamedArguments) { + if (named.Name is not null) { + properties [named.Name] = named.Value; + } + } + return properties; + } + IntentFilterInfo ParseIntentFilterAttribute (CustomAttribute ca) { var value = DecodeAttribute (ca); @@ -712,6 +730,12 @@ class TypeAttributeInfo (string attributeName) /// Metadata entries declared on this type via [MetaData] attributes. /// public List MetaData { get; } = []; + + /// + /// Named property values from a [Layout] attribute on this type, or null if none. + /// Maps to the <layout> child element of the component in the manifest. + /// + public Dictionary? LayoutProperties { get; set; } } sealed class ApplicationAttributeInfo () : TypeAttributeInfo ("ApplicationAttribute") diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 7eff64e8b8f..f239f509f54 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -274,6 +274,10 @@ static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string } } + // ManagedPeer depends on reflection-based registration; the trimmable path uses IAndroidCallableWrapper. + static bool IsUnsupportedByTrimmableTypeMap (string managedFullName, string assemblyName) => + managedFullName == "Java.Interop.ManagedPeer" && assemblyName == "Java.Interop"; + void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { foreach (var typeHandle in index.Reader.TypeDefinitions) { @@ -286,6 +290,10 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + if (IsUnsupportedByTrimmableTypeMap (fullName, index.AssemblyName)) { + continue; + } + // Temporarily allow [JniAddNativeMethodRegistrationAttribute] while we investigate // which scenarios fail later in the trimmable typemap pipeline. // if (index.MayUseJniAddNativeMethodRegistrationAttribute && @@ -2528,6 +2536,7 @@ void CollectExportField (MethodDefinition methodDef, AssemblyIndex index, List= 0."); } + if (maxReferenceArrayRank < 0) { + throw new ArgumentOutOfRangeException (nameof (maxReferenceArrayRank), maxReferenceArrayRank, "Must be >= 0."); + } var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); if (allPeers.Count == 0) { @@ -56,7 +60,7 @@ public TrimmableTypeMapResult Execute ( PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, maxReferenceArrayRank) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -155,6 +159,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes // Other codes (e.g. unresolvable type properties) are not yet assigned XA codes // and are intentionally not surfaced here. }, + WarnInvalidPlaceholder = placeholders => logger.LogInvalidManifestPlaceholderWarning (placeholders), LibraryManifests = config.LibraryManifests ?? [], }; @@ -178,7 +183,8 @@ List GenerateTypeMapAssemblies ( List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, - int maxArrayRank) + int maxArrayRank, + int maxReferenceArrayRank) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -207,14 +213,16 @@ List GenerateTypeMapAssemblies ( string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank, maxReferenceArrayRank); 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, maxArrayRank: maxArrayRank); + // The root generator builds the [assembly][rank] anchor matrix, so it needs the overall maximum + // rank across primitive and reference element types. + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: Math.Max (maxArrayRank, maxReferenceArrayRank)); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index 2421f41d264..7c086dfefe8 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -42,7 +42,7 @@ public sealed class JavaPeerAliasesAttribute : Attribute [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] public abstract class JavaPeerProxy : Attribute { - private protected JavaPeerProxy (string jniName, Type targetType) + protected JavaPeerProxy (string jniName, Type targetType) { ArgumentNullException.ThrowIfNull (jniName); ArgumentNullException.ThrowIfNull (targetType); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b5b34ea2b09..5fb831b43f8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -6,6 +6,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; +using System.Text; using System.Threading; using Android.Runtime; using Java.Interop; @@ -543,25 +544,58 @@ internal bool TryGetArrayProxy (Type elementType, int additionalRank, [NotNullWh static bool TryGetManagedTypeKey (Type type, [NotNullWhen (true)] out string? key) { - var fullName = type.FullName; - if (fullName is null) { - key = null; - return false; - } + key = BuildManagedTypeKey (type); + return key is not null; + } + // Builds the array-proxy map key for a managed type. Closed generic types use a normalized, + // version-independent form so the key matches the one emitted by the trimmable typemap generator, + // which references types by simple assembly name (no Version/Culture/PublicKeyToken). Without + // normalization a closed generic like Nullable would key on Type.FullName, whose type + // arguments carry the full versioned assembly-qualified name and would never match. + static string? BuildManagedTypeKey (Type type) + { var assemblyName = GetAssemblyNameForManagedTypeKey (type); if (assemblyName is null) { - key = null; - return false; + return null; } - key = $"{fullName}, {assemblyName}"; - return true; + if (type.IsGenericType && !type.IsGenericTypeDefinition) { + var definitionName = type.GetGenericTypeDefinition ().FullName; + if (definitionName is null) { + return null; + } + var arguments = type.GetGenericArguments (); + var builder = new StringBuilder (definitionName); + builder.Append ("[["); + for (int i = 0; i < arguments.Length; i++) { + if (i > 0) { + builder.Append ("],["); + } + var argumentKey = BuildManagedTypeKey (arguments [i]); + if (argumentKey is null) { + return null; + } + builder.Append (argumentKey); + } + builder.Append ("]], "); + builder.Append (assemblyName); + return builder.ToString (); + } + + var fullName = type.FullName; + if (fullName is null) { + return null; + } + return $"{fullName}, {assemblyName}"; } static string? GetAssemblyNameForManagedTypeKey (Type type) { - if (type.IsPrimitive || type == typeof (string)) { + // Primitives, string, and Nullable are surfaced through the System.Runtime reference + // assembly; the trimmable typemap generator emits their keys with "System.Runtime", so + // normalize to it here (the runtime implementation assembly is System.Private.CoreLib). + if (type.IsPrimitive || type == typeof (string) || Nullable.GetUnderlyingType (type) is not null) { return "System.Runtime"; } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets index efb8c209f7b..c02e81bf37f 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.AssemblyResolution.targets @@ -86,6 +86,36 @@ _ResolveAssemblies MSBuild target. + + + + <_TypeMapRuntimeOnlyAssembly Include="@(RuntimePackAsset)" + Condition=" '%(RuntimePackAsset.Extension)' == '.dll' and '%(RuntimePackAsset.AssetType)' == 'runtime' and $([System.String]::Copy('%(RuntimePackAsset.NuGetPackageId)').StartsWith('Microsoft.Android.Runtime')) " /> + + <_TypeMapRuntimeOnlyAssembly Remove="@(ReferencePath)" MatchOnMetadata="Filename" /> + + + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index e145c311485..53f153a8720 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -17,7 +17,7 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. <_AndroidRuntimePackRuntime>NativeAOT <_AndroidUseWorkloadNativeLinker Condition=" '$(_AndroidUseWorkloadNativeLinker)' == '' ">true <_AndroidJcwCodegenTarget Condition=" '$(_AndroidJcwCodegenTarget)' == '' ">JavaInterop1 - <_AndroidTypeMapImplementation Condition=" '$(_AndroidTypeMapImplementation)' == '' ">managed + <_AndroidTypeMapImplementation Condition=" '$(_AndroidTypeMapImplementation)' == '' ">trimmable true diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets index 1beb4ab9505..8981130964c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.CoreCLR.targets @@ -80,6 +80,7 @@ TargetFrameworkVersion="$(TargetFrameworkVersion)" PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)" MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)" + MaxReferenceArrayRank="$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)" GenerateTypeMapAssemblies="false" CleanJavaSourceOutputDirectory="true" AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt" diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index fb655661a24..2d1201e2cfc 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -5,13 +5,22 @@ + + <_AndroidTrimmableTypemapTrimJavaCode Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == '' ">false <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider r8 d8 True True - true + + true <_UseTrimmableNativeAotProguardConfiguration Condition=" '$(_UseTrimmableNativeAotProguardConfiguration)' == '' ">true + <_TrimmableNativeAotProguardConfigurationInputsStamp>$(_AndroidStampDirectory)_GenerateTrimmableTypeMapProguardConfiguration.inputs <_CompileToDalvikDependsOnTargets>$(_CompileToDalvikDependsOnTargets);_GenerateTrimmableTypeMapProguardConfiguration @@ -27,6 +36,15 @@ <_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(PrivateSdkAssemblies->'_%(Filename).TypeMap')" /> <_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(ReferencePath->'_%(Filename).TypeMap')" Condition=" '%(ReferencePath.FrameworkAssembly)' == 'true' " /> + + <_TrimmableTypeMapFrameworkIlcAssemblyNames Include="@(RuntimePackAsset->'_%(Filename).TypeMap')" + Condition=" '%(RuntimePackAsset.Extension)' == '.dll' and '%(RuntimePackAsset.AssetType)' == 'runtime' " /> + + + <_TrimmableTypeMapResolveRid Condition=" '$(RuntimeIdentifiers)' != '' ">$([System.String]::Copy('$(RuntimeIdentifiers)').Split(';')[0]) + <_TrimmableTypeMapResolveRid Condition=" '$(_TrimmableTypeMapResolveRid)' == '' ">$(RuntimeIdentifier) + + <_TrimmableTypeMapResolveProperties>_ComputeFilesToPublishForRuntimeIdentifiers=true;SelfContained=true;DesignTimeBuild=$(DesignTimeBuild);AppendRuntimeIdentifierToOutputPath=true;ResolveAssemblyReferencesFindRelatedSatellites=false;SkipCompilerExecution=true;_OuterIntermediateAssembly=@(IntermediateAssembly);_OuterOutputPath=$(OutputPath);_OuterIntermediateOutputPath=$(IntermediateOutputPath);_AndroidNdkDirectory=$(_AndroidNdkDirectory) + + + <_TrimmableTypeMapResolveProject Include="$(MSBuildProjectFile)" + AdditionalProperties="RuntimeIdentifier=$(_TrimmableTypeMapResolveRid);$(_TrimmableTypeMapResolveProperties)" /> + + + + + + + + + + + + + + + + + + Condition=" '$(_AndroidTrimmableTypemapTrimJavaCode)' == 'true' and '$(PublishTrimmed)' == 'true' and '$(_ProguardProjectConfiguration)' != '' "> <_TrimmableNativeAotRuntimeIdentifiers Remove="@(_TrimmableNativeAotRuntimeIdentifiers)" /> <_TrimmableNativeAotDgmlFiles Remove="@(_TrimmableNativeAotDgmlFiles)" /> + <_TrimmableNativeAotCodegenDgmlFiles Remove="@(_TrimmableNativeAotCodegenDgmlFiles)" /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifier)" Condition=" '$(RuntimeIdentifier)' != '' " /> <_TrimmableNativeAotRuntimeIdentifiers Include="$(RuntimeIdentifiers)" Condition=" '$(RuntimeIdentifier)' == '' and '$(RuntimeIdentifiers)' != '' " /> - - <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' == '' " /> - <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' != '' " /> - <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" Condition=" '$(RuntimeIdentifier)' == '' and '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' " /> + + <_TrimmableNativeAotDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml" + Condition=" Exists('$(NativeIntermediateOutputPath)$(TargetName).scan.dgml.xml') " /> + <_TrimmableNativeAotDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml" + Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' and Exists('$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).scan.dgml.xml') " /> + + <_TrimmableNativeAotCodegenDgmlFiles Include="$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml" + Condition=" Exists('$(NativeIntermediateOutputPath)$(TargetName).codegen.dgml.xml') " /> + <_TrimmableNativeAotCodegenDgmlFiles Include="$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).codegen.dgml.xml" + Condition=" '@(_TrimmableNativeAotRuntimeIdentifiers)' != '' and Exists('$(IntermediateOutputPath)%(_TrimmableNativeAotRuntimeIdentifiers.Identity)\native\$(TargetName).codegen.dgml.xml') " /> + <_TrimmableNativeAotDgmlFiles Include="@(_TrimmableNativeAotCodegenDgmlFiles)" + Condition=" '@(_TrimmableNativeAotDgmlFiles->Count())' == '0' " /> + + + + + + + + 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 5090ba7fa6a..ab5f665947c 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 @@ -44,11 +44,23 @@ stays incremental while still reacting to post-trim JCW regeneration. --> <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' and '$(_AndroidRuntime)' == 'CoreCLR' and '$(PublishTrimmed)' == 'true' ">$(_PostTrimTrimmableTypeMapJavaStamp) <_TrimmableJavaSourceStamp Condition=" '$(_TrimmableJavaSourceStamp)' == '' ">$(_TrimmableTypeMapOutputStamp) + + <_GenerateTrimmableTypeMapDependsOn Condition=" '$(_AndroidRuntime)' == 'NativeAOT' ">$(IlcDynamicBuildPropertyDependencies) + + <_GenerateTrimmableTypeMapDependsOn>$(_GenerateTrimmableTypeMapDependsOn);_GetLibraryImports <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and ('$(PublishAot)' == 'true' or '$(DynamicCodeSupport)' == 'false') ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 + + <_AndroidTrimmableTypeMapMaxReferenceArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)' == '' and ('$(PublishAot)' == 'true' or '$(DynamicCodeSupport)' == 'false') ">1 + <_AndroidTrimmableTypeMapMaxReferenceArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)' == '' ">0 _RecordTrimmableTypeMapFileWrites; $(IncrementalCleanDependsOn); @@ -58,6 +70,8 @@ + + <_TypeMapInputAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" /> <_TypeMapFrameworkAssemblies Include="@(ResolvedFrameworkAssemblies)" /> <_TypeMapFrameworkAssemblies Include="@(PrivateSdkAssemblies)" /> <_TypeMapFrameworkAssemblies Include="@(FrameworkAssemblies)" /> + <_TypeMapFrameworkAssemblies Include="@(_AndroidTrimmableTypeMapExtraFrameworkAssembly)" /> <_TypeMapInputAssemblies Include="$(IntermediateOutputPath)$(TargetFileName)" Condition="Exists('$(IntermediateOutputPath)$(TargetFileName)')" /> @@ -383,6 +404,15 @@ SkipUnchangedFiles="true" Condition="Exists('$(_TypeMapBaseOutputDir)AndroidManifest.xml')" /> + + + @@ -392,6 +422,8 @@ + @@ -418,4 +450,41 @@ + + + + + <_ResourceDesignerReferenceToRoot Include="@(ReferencePath)" + Condition=" '%(Filename)' == '_Microsoft.Android.Resource.Designer' " /> + + + + diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs index 9afb10b3404..436b06d7602 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateAdditionalProviderSources.cs @@ -70,18 +70,8 @@ void Generate (NativeCodeGenStateObject codeGenState) Xamarin.Android.Tasks.AndroidRuntime.CoreCLR => true, _ => false, }; - string providerTemplateFile = isMonoVM ? - "MonoRuntimeProvider.Bundled.java" : - "NativeAotRuntimeProvider.java"; - string providerTemplate = GetResource (providerTemplateFile); - foreach (var provider in AdditionalProviderSources) { - var contents = providerTemplate.Replace (isMonoVM ? "MonoRuntimeProvider" : "NativeAotRuntimeProvider", provider); - var real_provider = isMonoVM ? - Path.Combine (OutputDirectory, "src", "mono", provider + ".java") : - Path.Combine (OutputDirectory, "src", "net", "dot", "jni", "nativeaot", provider + ".java"); - Files.CopyIfStringChanged (contents, real_provider); - } + WriteAdditionalRuntimeProviderSources (OutputDirectory, isMonoVM, AdditionalProviderSources); // For NativeAOT, generate JavaInteropRuntime.java and NativeAotEnvironmentVars.java if (androidRuntime == Xamarin.Android.Tasks.AndroidRuntime.NativeAOT) { @@ -122,6 +112,29 @@ static string GetResource (string resource) return reader.ReadToEnd (); } + /// + /// Writes the additional per-process runtime provider Java sources (e.g. NativeAotRuntimeProvider_1.java) + /// by cloning the runtime provider template for each name. Shared between the legacy (ILLink) and + /// trimmable build paths so both emit the extra providers a multi-process app declares in its manifest. + /// + internal static void WriteAdditionalRuntimeProviderSources (string outputDirectory, bool isMonoVM, string [] additionalProviderSources) + { + if (additionalProviderSources.Length == 0) { + return; + } + string providerTemplateFile = isMonoVM ? + "MonoRuntimeProvider.Bundled.java" : + "NativeAotRuntimeProvider.java"; + string providerTemplate = GetResource (providerTemplateFile); + foreach (var provider in additionalProviderSources) { + var contents = providerTemplate.Replace (isMonoVM ? "MonoRuntimeProvider" : "NativeAotRuntimeProvider", provider); + var realProvider = isMonoVM ? + Path.Combine (outputDirectory, "src", "mono", provider + ".java") : + Path.Combine (outputDirectory, "src", "net", "dot", "jni", "nativeaot", provider + ".java"); + Files.CopyIfStringChanged (contents, realProvider); + } + } + void SaveResource (string resource, string filename, string destDir, Func applyTemplate) { string template = GetResource (resource); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs index ba2505f71f3..39c4f4e3eb0 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotBootstrapSources.cs @@ -29,11 +29,19 @@ public sealed class GenerateNativeAotBootstrapSources : AndroidTask public bool EnableSGenConcurrent { get; set; } + // Names of the extra per-process runtime providers (e.g. NativeAotRuntimeProvider_1) that the + // manifest declares for components with a non-default android:process; their Java sources must be + // generated too. On the legacy path GenerateAdditionalProviderSources writes these; the trimmable + // path has no such task, so the bootstrap step handles them here. + public string [] AdditionalProviderSources { get; set; } = []; + public override bool RunTask () { GenerateAdditionalProviderSources.GenerateNativeAotBootstrapFiles ( Log, OutputDirectory, TargetName, Environments, EnableSGenConcurrent); + GenerateAdditionalProviderSources.WriteAdditionalRuntimeProviderSources (OutputDirectory, isMonoVM: false, AdditionalProviderSources); + return !Log.HasLoggedErrors; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs index 44ae925d4f4..08e768d315f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeAotProguardConfiguration.cs @@ -15,7 +15,6 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask public override string TaskPrefix => "GNAPC"; - [Required] public ITaskItem [] NativeAotDgmlFiles { get; set; } = []; [Required] @@ -24,6 +23,12 @@ public class GenerateNativeAotProguardConfiguration : AndroidTask [Required] public string OutputFile { get; set; } = ""; + // When false, the ILC DGML is not consulted (it may not have been generated at all) and a + // -keep rule is emitted for every Java Callable Wrapper in the ACW map, so R8 keeps them all + // instead of shrinking the unused ones. This trades a small amount of dex size for skipping the + // very large DGML files and the DGML parsing/scan, which dominate NativeAOT build time. + public bool TrimJavaCallableWrappers { get; set; } = true; + public override bool RunTask () { var dir = Path.GetDirectoryName (OutputFile); @@ -31,22 +36,27 @@ public override bool RunTask () Directory.CreateDirectory (dir); } - if (NativeAotDgmlFiles.Length == 0) { - Log.LogCodedError ("XA4319", Properties.Resources.XA4319); - return !Log.HasLoggedErrors; - } if (!File.Exists (AcwMapFile)) { Log.LogCodedError ("XA4320", Properties.Resources.XA4320, AcwMapFile); return !Log.HasLoggedErrors; } - foreach (var dgmlFile in NativeAotDgmlFiles) { - if (!File.Exists (dgmlFile.ItemSpec)) { - Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); + + HashSet? retainedTypeKeys = null; + if (TrimJavaCallableWrappers) { + if (NativeAotDgmlFiles.Length == 0) { + Log.LogCodedError ("XA4319", Properties.Resources.XA4319); return !Log.HasLoggedErrors; } + foreach (var dgmlFile in NativeAotDgmlFiles) { + if (!File.Exists (dgmlFile.ItemSpec)) { + Log.LogCodedError ("XA4321", Properties.Resources.XA4321, dgmlFile.ItemSpec); + return !Log.HasLoggedErrors; + } + } + retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); } - var retainedTypeKeys = LoadRetainedTypeKeysFromDgml (); + // A null retainedTypeKeys means "keep every ACW" (Java trimming disabled). var javaTypes = LoadJavaTypesFromAcwMap (retainedTypeKeys); using var writer = new StringWriter (); @@ -56,13 +66,17 @@ public override bool RunTask () } Files.CopyIfStringChanged (writer.ToString (), OutputFile); - Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); + if (TrimJavaCallableWrappers) { + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT trimmable typemap ProGuard rules from {1} DGML file(s).", javaTypes.Count, NativeAotDgmlFiles.Length); + } else { + Log.LogMessage (MessageImportance.Low, "Generated {0} NativeAOT ProGuard rules keeping all ACWs (Java Callable Wrapper trimming is disabled).", javaTypes.Count); + } return !Log.HasLoggedErrors; } - List LoadJavaTypesFromAcwMap (HashSet retainedTypeKeys) + List LoadJavaTypesFromAcwMap (HashSet? retainedTypeKeys) { - var javaTypes = new List (retainedTypeKeys.Count); + var javaTypes = new List (retainedTypeKeys?.Count ?? 0); var seenJavaTypes = new HashSet (StringComparer.Ordinal); foreach (var line in File.ReadLines (AcwMapFile)) { var separator = line.IndexOf (";", StringComparison.Ordinal); @@ -71,7 +85,7 @@ List LoadJavaTypesFromAcwMap (HashSet retainedTypeKeys) } var managedTypeName = line.Substring (0, separator); var javaTypeName = line.Substring (separator + 1); - if (retainedTypeKeys.Contains (managedTypeName) && seenJavaTypes.Add (javaTypeName)) { + if ((retainedTypeKeys == null || retainedTypeKeys.Contains (managedTypeName)) && seenJavaTypes.Add (javaTypeName)) { javaTypes.Add (javaTypeName); } } diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 6c7d18b5823..04e5fd280fc 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -46,6 +46,8 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => log.LogCodedWarning ("XA4250", Properties.Resources.XA4250, javaTypeName); public void LogLibraryManifestMergeWarning (string message) => log.LogCodedWarning ("XA4302", Properties.Resources.XA4302, message); + public void LogInvalidManifestPlaceholderWarning (string placeholders) => + log.LogCodedWarning ("XA1010", Properties.Resources.XA1010, placeholders); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, @@ -102,11 +104,21 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN /// /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} - /// sentinels and TypeMap entries. 0 disables. Set via - /// $(_AndroidTrimmableTypeMapMaxArrayRank). + /// sentinels and built-in element TypeMap entries: primitive arrays + /// (e.g. int[][][]), System.String, and boxed Nullable<T>. + /// 0 disables. Set via $(_AndroidTrimmableTypeMapMaxArrayRank). /// public int MaxArrayRank { get; set; } + /// + /// Maximum array rank for scanned Java peer reference-type TypeMap entries. + /// Kept lower than because jagged arrays for arbitrary scanned + /// reference types are rare and each nesting level multiplies the generated + /// JavaObjectArray<T> closure. 0 disables. Set via + /// $(_AndroidTrimmableTypeMapMaxReferenceArrayRank). + /// + public int MaxReferenceArrayRank { get; set; } + public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -213,6 +225,7 @@ public override bool RunTask () manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, maxArrayRank: MaxArrayRank, + maxReferenceArrayRank: MaxReferenceArrayRank, generateTypeMapAssemblies: GenerateTypeMapAssemblies); if (GenerateTypeMapAssemblies) { diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs index a525bab3a8c..7cf23a04b7b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/R8.cs @@ -66,6 +66,7 @@ IEnumerable GetUserJavaTypes () yield break; } var seen = new HashSet (StringComparer.Ordinal); + var javaTypes = new List (); foreach (var item in JavaSourceFiles) { var path = item.ItemSpec; if (path.IsNullOrEmpty () || !File.Exists (path)) { @@ -77,9 +78,13 @@ IEnumerable GetUserJavaTypes () typeName = $"{package}.{typeName}"; } if (seen.Add (typeName)) { - yield return typeName; + javaTypes.Add (typeName); } } + javaTypes.Sort (StringComparer.Ordinal); + foreach (var javaType in javaTypes) { + yield return javaType; + } } internal static string? ReadJavaPackage (string path) @@ -150,7 +155,16 @@ protected override string CreateResponseFile () if (EnableShrinking) { if (UseTrimmableNativeAotProguardConfiguration && !ProguardGeneratedApplicationConfiguration.IsNullOrEmpty ()) { - File.WriteAllText (ProguardGeneratedApplicationConfiguration, "# ACW keep rules are generated from NativeAOT ILC metadata.\n"); + // ACW keep rules come from the DGML/acw-map-driven proguard_project_references.cfg on + // the trimmable path. User-authored AndroidJavaSource (Bind != true) has no managed peer + // and is absent from that map, so keep it here explicitly; otherwise R8 shrinks it away + // (e.g. dropping large unreferenced sources so an app that needs multidex no longer does). + using (var appcfg = File.CreateText (ProguardGeneratedApplicationConfiguration)) { + appcfg.WriteLine ("# ACW keep rules are generated from NativeAOT ILC metadata."); + foreach (var java in GetUserJavaTypes ()) { + appcfg.WriteLine ($"-keep class {java} {{ *; }}"); + } + } } else if (!AcwMapFile.IsNullOrEmpty ()) { var acwMap = MonoAndroidHelper.LoadMapFile (BuildEngine4, Path.GetFullPath (AcwMapFile), StringComparer.OrdinalIgnoreCase); var javaTypes = new List (acwMap.Values.Count); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs index 52d6a4bb8a5..b445f7cb60e 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BindingBuildTest.cs @@ -819,6 +819,10 @@ public void BindingWithAndroidJavaSource ([Values (AndroidRuntime.CoreCLR, Andro if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } + + if (IgnoreOnNativeAot (runtime, "R8 shrinks bound library Java types out of classes.dex on the trimmable typemap path (missing proguard keeps). Tracked by https://github.com/dotnet/android/issues/11774.")) { + return; + } var path = Path.Combine ("temp", TestName); var lib = new XamarinAndroidBindingProject () { IsRelease = isRelease, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index fabd888d41e..651538de7df 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -137,6 +137,9 @@ public void DotNetBuild (string runtimeIdentifiers, bool isRelease, bool aot, bo if (isRelease) { expectedFiles.Add ($"{proj.PackageName}.aab"); expectedFiles.Add ($"{proj.PackageName}-Signed.aab"); + if (runtime == AndroidRuntime.NativeAOT) { + expectedFiles.Add ("mapping.txt"); + } } else { expectedFiles.Add ($"{proj.PackageName}.apk"); expectedFiles.Add ($"{proj.PackageName}-Signed.apk.idsig"); @@ -1801,20 +1804,7 @@ public void XA4310 ([Values ("apk", "aab")] string packageFormat, [Values (Andro StringAssertEx.Contains ("error XA4310", builder.LastBuildOutput, "Error should be XA4310"); StringAssertEx.Contains ("`DoesNotExist`", builder.LastBuildOutput, "Error should include the name of the nonexistent file"); - if (runtime != AndroidRuntime.NativeAOT) { - builder.AssertHasNoWarnings (); - return; - } - - // NativeAOT currently (Nov 2025) produces the following warning - // warning IL3053: Assembly 'Mono.Android' produced AOT analysis warnings. - string expectedWarning = "warning IL3053:"; - Assert.IsNotNull ( - builder.LastBuildOutput - .SkipWhile (x => !x.StartsWith ("Build FAILED.", StringComparison.Ordinal)) - .FirstOrDefault (x => x.Contains (expectedWarning)), - $"Build output should contain '{expectedWarning}'." - ); + builder.AssertHasNoWarnings (); } } @@ -1828,6 +1818,9 @@ public void CheckLintErrorsAndWarnings ([Values (AndroidRuntime.CoreCLR, Android if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } + if (IgnoreOnNativeAot (runtime, "the trimmable typemap generates additional Java Callable Wrappers that trip XA0102 lint warnings (e.g. CustomX509TrustManager, MissingApplicationIcon). Tracked by https://github.com/dotnet/android/issues/11774.")) { + return; + } var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index ea79e9bd883..aa725c506c2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -101,133 +101,6 @@ public void BasicApplicationPublishReadyToRun ([Values] bool isComposite, [Value StringAssert.Contains ("@uncompressed_assemblies_data_buffer = dso_local local_unnamed_addr global [0 x i8] zeroinitializer, align 1", compressedAssembliesSourceText); } - [Test] - public void NativeAOT () - { - var proj = new XamarinAndroidApplicationProject { - IsRelease = true, - ProjectName = "Hello", - }; - proj.SetRuntime (AndroidRuntime.NativeAOT); - proj.SetProperty ("_ExtraTrimmerArgs", "--verbose"); - - // Required for java/util/ArrayList assertion below - proj.MainActivity = proj.DefaultMainActivity - .Replace ("//${AFTER_ONCREATE}", "new Android.Runtime.JavaList (); new Android.Runtime.JavaList ();"); - - using var b = CreateApkBuilder (); - Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - b.Output.AssertTargetIsNotSkipped ("_PrepareLinking"); - - string [] mono_classes = [ - "Lmono/MonoRuntimeProvider;", - ]; - string[] mono_files = [ - "lib/arm64-v8a/libmonosgen-2.0.so", - "lib/x86_64/libmonosgen-2.0.so", - ]; - string [] nativeaot_files = [ - $"lib/arm64-v8a/lib{proj.ProjectName}.so", - $"lib/x86_64/lib{proj.ProjectName}.so", - ]; - - var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath); - var output = Path.Combine (Root, b.ProjectDirectory, proj.OutputPath); - - var linkedMonoAndroidAssembly = Path.Combine (intermediate, "android-arm64", "linked", "Mono.Android.dll"); - FileAssert.Exists (linkedMonoAndroidAssembly); - var javaClassNames = new List (); - var types = new List (); - - using (var assembly = AssemblyDefinition.ReadAssembly (linkedMonoAndroidAssembly)) { - var typeName = "Android.App.Activity"; - var methodName = "GetOnCreate_Landroid_os_Bundle_Handler"; - var type = assembly.MainModule.GetType (typeName); - Assert.IsNotNull (type, $"{linkedMonoAndroidAssembly} should contain {typeName}"); - var method = type.Methods.FirstOrDefault (m => m.Name == methodName); - Assert.IsNotNull (method, $"{linkedMonoAndroidAssembly} should contain {typeName}.{methodName}"); - - type = assembly.MainModule.Types.FirstOrDefault (t => t.Name == "ManagedTypeMapping"); - Assert.IsNotNull (type, $"{linkedMonoAndroidAssembly} should contain ManagedTypeMapping"); - method = type.Methods.FirstOrDefault (m => m.Name == "GetJniNameByTypeNameHashIndex"); - Assert.IsNotNull (method, $"{type.Name} should contain GetJniNameByTypeNameHashIndex"); - - foreach (var i in method.Body.Instructions) { - if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldstr) - continue; - if (i.Operand is not string javaName) - continue; - if (i.Next.OpCode != Mono.Cecil.Cil.OpCodes.Ret) - continue; - javaClassNames.Add (javaName); - } - - method = type.Methods.FirstOrDefault (m => m.Name == "GetTypeByJniNameHashIndex"); - Assert.IsNotNull (method, $"{type.Name} should contain GetTypeByJniNameHashIndex"); - - foreach (var i in method.Body.Instructions) { - if (i.OpCode != Mono.Cecil.Cil.OpCodes.Ldtoken) - continue; - if (i.Operand is not TypeReference typeReference) - continue; - if (i.Next?.OpCode != Mono.Cecil.Cil.OpCodes.Call) - continue; - if (i.Next.Next?.OpCode != Mono.Cecil.Cil.OpCodes.Ret) - continue; - types.Add (typeReference); - } - - // Basic types - AssertTypeMap ("java/lang/Object", "Java.Lang.Object"); - AssertTypeMap ("java/lang/String", "Java.Lang.String"); - AssertTypeMap ("[Ljava/lang/Object;", "Java.Interop.JavaArray`1"); - AssertTypeMap ("java/util/ArrayList", "Android.Runtime.JavaList"); - AssertTypeMap ("android/app/Activity", "Android.App.Activity"); - AssertTypeMap ("android/widget/Button", "Android.Widget.Button"); - Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, - "Duplicate typemap entry for java/util/ArrayList => Android.Runtime.JavaList`1"), - "Should get log message about duplicate Android.Runtime.JavaList`1!"); - - // Special *Invoker case - AssertTypeMap ("android/view/View$OnClickListener", "Android.Views.View/IOnClickListener"); - Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, - "Duplicate typemap entry for android/view/View$OnClickListener => Android.Views.View/IOnClickListenerInvoker"), - "Should get log message about duplicate IOnClickListenerInvoker!"); - } - - // Verify that Java stubs for Mono.Android.dll were generated, instead of using mono.android.jar/dex - var onLayoutChangeListenerImplementor = Path.Combine (intermediate, "android", "src", "mono", "android", "view", "View_OnClickListenerImplementor.java"); - FileAssert.Exists (onLayoutChangeListenerImplementor); - - var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex"); - FileAssert.Exists (dexFile); - foreach (var className in mono_classes) { - Assert.IsFalse (DexUtils.ContainsClassWithMethod (className, "", "()V", dexFile, AndroidSdkPath), $"`{dexFile}` should *not* include `{className}`!"); - } - - var apkFile = Path.Combine (output, $"{proj.PackageName}-Signed.apk"); - FileAssert.Exists (apkFile); - using var zip = ZipHelper.OpenZip (apkFile); - foreach (var mono_file in mono_files) { - Assert.IsFalse (zip.ContainsEntry (mono_file, caseSensitive: true), $"APK must *not* contain `{mono_file}`."); - } - foreach (var nativeaot_file in nativeaot_files) { - Assert.IsTrue (zip.ContainsEntry (nativeaot_file, caseSensitive: true), $"APK must contain `{nativeaot_file}`."); - } - - void AssertTypeMap(string javaName, string managedName) - { - var javaNameIndex = javaClassNames.FindIndex (name => name == javaName); - var typeIndex = types.FindIndex (td => td.ToString() == managedName); - - if (javaNameIndex < 0) { - Assert.Fail ($"TypeMapping should contain \"{javaName}\"!"); - } else if (typeIndex < 0) { - Assert.Fail ($"TypeMapping should contain \"{managedName}\"!"); - } - } - } - [Test] public void BuildBasicApplicationThenMoveIt ([Values] bool isRelease, [Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { @@ -280,6 +153,10 @@ public void BuildReleaseArm64 ([Values] bool forms, [Values (AndroidRuntime.Core return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } + var proj = forms ? new XamarinFormsAndroidApplicationProject () : new XamarinAndroidApplicationProject (); @@ -481,38 +358,7 @@ public void BuildHasNoWarnings (bool isRelease, bool multidex, string packageFor using (var b = CreateApkBuilder ()) { Assert.IsTrue (b.Build (proj), "Build should have succeeded."); - if (runtime == AndroidRuntime.NativeAOT) { - // NativeAOT currently (Jul 2026) produces 2 `ILC : AOT analysis warning IL3050` - // warnings: a single distinct warning (the reflection-backed ManagedTypeManager - // constructor, which chains to the [RequiresDynamicCode] ReflectionJniTypeManager - // base and is therefore not Native AOT compatible), surfaced twice in the MSBuild - // summary (once per publish target context). Even though this test expects no - // warnings and the above likely make the app not work correctly at run time, it is - // still worth running this test under NativeAOT to test for the absence of other - // warnings. - int numberOfExpectedWarnings = 2; - - // MSBuild prints a " N Warning(s)" summary line near the end of the build; parse N so the - // assertion can report the actual count instead of a bare "Expected: True But was: False". - var warningSummaryLine = b.LastBuildOutput.LastOrDefault (x => x.TrimEnd ().EndsWith ("Warning(s)", StringComparison.Ordinal)); - int actualNumberOfWarnings = -1; - if (warningSummaryLine != null) { - var summary = warningSummaryLine.Trim (); - var firstSpace = summary.IndexOf (' '); - if (firstSpace > 0) { - int.TryParse (summary.Substring (0, firstSpace), out actualNumberOfWarnings); - } - } - - Assert.AreEqual (numberOfExpectedWarnings, actualNumberOfWarnings, - $"{b.BuildLogFile} should have exactly {numberOfExpectedWarnings} MSBuild warnings for NativeAOT, but found {actualNumberOfWarnings}."); - - const string expectedWarningIL3050 = "ILC : AOT analysis warning IL3050:"; - var warnings = b.LastBuildOutput.SkipWhile (x => !x.StartsWith ("Build succeeded.", StringComparison.Ordinal)).Where (x => x.Contains (expectedWarningIL3050, StringComparison.Ordinal)); - Assert.IsTrue (warnings.Count () == numberOfExpectedWarnings, $"Expected {numberOfExpectedWarnings} 'IL3050' warnings, found {warnings.Count ()}"); - } else { - b.AssertHasNoWarnings (); - } + b.AssertHasNoWarnings (); Assert.IsFalse (StringAssertEx.ContainsText (b.LastBuildOutput, "Warning: end of file not at end of a line"), "Should not get a warning from the task."); var lockFile = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath, ".__lock"); @@ -1636,9 +1482,17 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r } var toolbar_class = "androidx.appcompat.widget.Toolbar"; - var proguardProjectPrimary = Path.Combine (intermediate, "proguard", "proguard_project_primary.cfg"); - FileAssert.Exists (proguardProjectPrimary); - Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectPrimary), $"-keep class {proj.JavaPackageName}.MainActivity"), $"`{proj.JavaPackageName}.MainActivity` should exist in `proguard_project_primary.cfg`!"); + IEnumerable proguardProjectConfigurations = [Path.Combine (intermediate, "proguard", + runtime == AndroidRuntime.NativeAOT ? "proguard_project_references.cfg" : "proguard_project_primary.cfg")]; + if (runtime == AndroidRuntime.NativeAOT && string.IsNullOrEmpty (rid)) { + proguardProjectConfigurations = Directory.GetFiles (intermediate, "proguard_project_references.cfg", SearchOption.AllDirectories); + Assert.AreEqual (1, proguardProjectConfigurations.Count (), "A multi-RID NativeAOT build should generate one references proguard config for the single R8 invocation."); + } + foreach (var proguardProjectConfiguration in proguardProjectConfigurations) { + FileAssert.Exists (proguardProjectConfiguration); + Assert.IsTrue (StringAssertEx.ContainsText (File.ReadAllLines (proguardProjectConfiguration), $"-keep class {proj.JavaPackageName}.MainActivity"), + $"`{proj.JavaPackageName}.MainActivity` should exist in `{proguardProjectConfiguration}`!"); + } var aapt_rules = Path.Combine (intermediate, "aapt_rules.txt"); FileAssert.Exists (aapt_rules); @@ -1666,6 +1520,36 @@ public void BuildProguardEnabledProject ([Values ("", "android-arm64")] string r } } + [Test] + public void NativeAotKeepsRuntimeAcwJavaTypesUnderR8 () + { + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) { + return; + } + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + LinkTool = "r8", + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + using (var b = CreateApkBuilder ()) { + Assert.IsTrue (b.Build (proj), "Build should have succeeded."); + + var intermediate = Path.Combine (Root, b.ProjectDirectory, proj.IntermediateOutputPath); + var dexFile = Path.Combine (intermediate, "android", "bin", "classes.dex"); + FileAssert.Exists (dexFile); + + // Regression test: the trimmable NativeAOT path generates its ACW keep rules from the + // ILC DGML into proguard_project_references.cfg. If that file is not passed to R8, R8 + // tree-shakes the runtime ACW/JCW classes out of classes.dex and the app crashes at + // startup inside JavaInteropRuntime.init with a ClassNotFoundException for the + // UncaughtExceptionMarshaler Java Callable Wrapper. The JCW class name is CRC-hashed + // (e.g. `scrc64...UncaughtExceptionMarshaler`), so match on the type name suffix. + Assert.IsTrue (DexUtils.ContainsClass ("UncaughtExceptionMarshaler;", dexFile, AndroidSdkPath), + $"`{dexFile}` should include the UncaughtExceptionMarshaler ACW kept by the generated NativeAOT ProGuard rules."); + } + } + XamarinAndroidApplicationProject CreateMultiDexRequiredApplication (string debugConfigurationName = "Debug", string releaseConfigurationName = "Release") { var proj = new XamarinAndroidApplicationProject (debugConfigurationName, releaseConfigurationName); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs index 0ee843db8ae..cf764341d4f 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildWithLibraryTests.cs @@ -321,6 +321,10 @@ public void ProjectDependencies ([Values] bool projectReference, [Values (Androi return; } + if (IgnoreOnNativeAot (runtime, "the trimmable typemap trims Java Callable Wrappers for library types that are never instantiated, so the unused LibraryB JCWs are intentionally absent from classes.dex.")) { + return; + } + // Setup dependencies App A -> Lib B -> Lib C var path = Path.Combine ("temp", TestName); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs index 0c17988fe71..3a22b0880a9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/IncrementalBuildTest.cs @@ -606,6 +606,9 @@ public void AppProjectTargetsDoNotBreak ([Values (AndroidRuntime.CoreCLR, Androi if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } var targets = new List<(string target, bool ignoreOnNAOT)> { ("_GeneratePackageManagerJava", true), // TODO: NativeAOT doesn't skip this target on 3rd attempt, check if that's ok? ("_ResolveLibraryProjectImports", false), @@ -947,6 +950,9 @@ public void LinkAssembliesNoShrink ([Values (AndroidRuntime.CoreCLR, AndroidRunt if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } var proj = new XamarinFormsAndroidApplicationProject { IsRelease = isRelease, }; @@ -1535,6 +1541,10 @@ public void ChangePackageNamingPolicy ([Values (AndroidRuntime.CoreCLR, AndroidR return; } + if (IgnoreOnNativeAot (runtime, "the 'Lowercase' $(AndroidPackageNamingPolicy) is intentionally unsupported with the trimmable typemap (only Crc64 and LowercaseCrc64 are supported).")) { + return; + } + var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, }; diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs index 4ec821d3baa..122208b3356 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/ManifestTest.cs @@ -1227,8 +1227,15 @@ public void ExportedErrorMessage ([Values (AndroidRuntime.CoreCLR, AndroidRuntim b.ThrowOnBuildFailure = false; Assert.IsFalse (b.Build (proj), "Build should have failed"); var extension = IsWindows ? ".exe" : ""; - uint errorLine = runtime == AndroidRuntime.NativeAOT ? 11u : 12u; - Assert.IsTrue (b.LastBuildOutput.ContainsText ($"AndroidManifest.xml({errorLine},5): java{extension} error AMM0000:"), "Should receive AMM0000 error"); + if (runtime == AndroidRuntime.NativeAOT) { + // The trimmable manifest generator emits the merged components in a different + // (but valid) order than the legacy path, so the offending lands on a + // different manifest line. Assert the coded AMM0000 error itself rather than the + // exact line/column, which is an implementation detail of the manifest layout. + Assert.IsTrue (b.LastBuildOutput.ContainsText ($"java{extension} error AMM0000:"), "Should receive AMM0000 error"); + } else { + Assert.IsTrue (b.LastBuildOutput.ContainsText ($"AndroidManifest.xml(12,5): java{extension} error AMM0000:"), "Should receive AMM0000 error"); + } Assert.IsTrue (b.LastBuildOutput.ContainsText ("Apps targeting Android 12 and higher are required to specify an explicit value for `android:exported`"), "Should receive AMM0000 error"); } 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 7a6f121832d..5ecd668649e 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 @@ -271,6 +271,7 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, AcwMapFile = acwMapFile, OutputFile = outputFile, + TrimJavaCallableWrappers = true, }; Assert.IsTrue (task.Execute (), "Task should succeed."); @@ -282,6 +283,75 @@ public void Execute_GenerateNativeAotProguardConfiguration_UsesDgmlTypeMetadata StringAssert.DoesNotContain ("other.Type", proguard); } + [Test] + public void Execute_GenerateNativeAotProguardConfiguration_KeepsAllWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Duplicate.Type, My.Assembly;my.app.Duplicate + Other.Type;other.Type + """); + + // No DGML is provided: with trimming disabled the task must keep every ACW from the map + // rather than shrinking to the DGML-retained subset. + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed without a DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class my.app.Duplicate { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } + + [Test] + public void Execute_GenerateNativeAotProguardConfiguration_IgnoresDgmlWhenTrimmingDisabled () + { + var path = Path.Combine (Root, "temp", TestName); + var dgmlFile = Path.Combine (path, "app.scan.dgml.xml"); + var acwMapFile = Path.Combine (path, "acw-map.txt"); + var outputFile = Path.Combine (path, "proguard", "proguard_project_references.cfg"); + Directory.CreateDirectory (path); + File.WriteAllText (dgmlFile, """ + + + + + + + + """); + File.WriteAllText (acwMapFile, """ + UnnamedProject.MainActivity, UnnamedProject;crc64a1.MainActivity + Android.App.Activity, Mono.Android;android.app.Activity + Other.Type;other.Type + """); + + var task = new GenerateNativeAotProguardConfiguration { + BuildEngine = new MockBuildEngine (TestContext.Out), + NativeAotDgmlFiles = new [] { new TaskItem (dgmlFile) }, + AcwMapFile = acwMapFile, + OutputFile = outputFile, + TrimJavaCallableWrappers = false, + }; + + Assert.IsTrue (task.Execute (), "Task should succeed and ignore the DGML when trimming is disabled."); + var proguard = File.ReadAllText (outputFile); + StringAssert.Contains ("-keep class crc64a1.MainActivity { *; }", proguard); + StringAssert.Contains ("-keep class android.app.Activity { *; }", proguard); + StringAssert.Contains ("-keep class other.Type { *; }", proguard); + } + GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, string javaDir, IList? messages = null, IList? warnings = null, string tfv = "v11.0") { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs index 0c797f3fd43..bde17a65eca 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LinkerTests.cs @@ -395,6 +395,10 @@ public void AndroidAddKeepAlives (bool isRelease, bool setAndroidAddKeepAlivesTr return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } + if (runtime == AndroidRuntime.CoreCLR && isRelease && !setAndroidAddKeepAlivesTrue && setLinkModeNone && shouldAddKeepAlives) { // This currently fails with the following exception: // @@ -512,6 +516,10 @@ public void AndroidUseNegotiateAuthentication ([Values (true, false, null)] bool return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } + var proj = new XamarinAndroidApplicationProject { IsRelease = true }; proj.SetRuntime (runtime); proj.AddReferences ("System.Net.Http"); @@ -549,6 +557,9 @@ public void PreserveIX509TrustManagerSubclasses ([Values] bool hasServerCertific if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease }; proj.SetRuntime (runtime); proj.AddReferences ("System.Net.Http"); @@ -589,6 +600,10 @@ public void PreserveServices ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.Na return; } + if (IgnoreNativeAotLinkedAssemblyChecks (runtime)) { + return; + } + var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease, TrimModeRelease = TrimMode.Full, @@ -735,6 +750,10 @@ public void WarnWithReferenceToPreserveAttribute ([Values (AndroidRuntime.CoreCL return; } + if (IgnoreOnNativeAot (runtime, "ILC does not run illink, so the obsolete-PreserveAttribute IL6001 warning is not emitted.")) { + return; + } + var proj = new XamarinAndroidApplicationProject { IsRelease = isRelease }; proj.SetRuntime (runtime); proj.AddReferences ("System.Net.Http"); 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 431225f683c..248f2f828a6 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 @@ -67,6 +67,38 @@ public void Build_WithTrimmableTypeMap_IncrementalBuild ([Values] bool isRelease } } + [Test] + public void Build_WithTrimmableTypeMap_KeepsNativeAotRuntimeHostAcws () + { + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (AndroidRuntime.NativeAOT, release: isRelease)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + LinkTool = "r8", + }; + proj.SetRuntime (AndroidRuntime.NativeAOT); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var dexFile = builder.Output.GetIntermediaryPath (Path.Combine ("android", "bin", "classes.dex")); + FileAssert.Exists (dexFile); + + // Regression test: the NativeAOT runtime host assembly (Microsoft.Android.Runtime.NativeAOT) is + // resolved only in the per-RID inner build, so the RID-independent outer-build trimmable typemap + // generator never scanned it. Its only Java Callable Wrapper type, UncaughtExceptionMarshaler, + // therefore had no JCW and no typemap entry -> the runtime ACW is absent from classes.dex and the + // app crashes at startup in JavaInteropRuntime.init (setDefaultUncaughtExceptionHandler). A + // reference assembly for the host is now shipped in the SDK pack and fed to the generator. The JCW + // name is CRC-hashed (e.g. `scrc64...UncaughtExceptionMarshaler`), so match on the type name suffix. + Assert.IsTrue (DexUtils.ContainsClass ("UncaughtExceptionMarshaler;", dexFile, AndroidSdkPath), + $"`{dexFile}` should include the UncaughtExceptionMarshaler runtime ACW."); + } + [Test] public void Build_WithTrimmableTypeMap_DeletesStaleGeneratedJavaSourcesAndCopies () { @@ -304,6 +336,33 @@ public void Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap () builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); } + [Test] + public void Build_WithTrimmableTypeMap_ReferenceArrayRankChangeRegeneratesTypeMap () + { + if (IgnoreUnsupportedConfiguration (AndroidRuntime.CoreCLR, release: true)) { + return; + } + + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + proj.SetProperty ("_AndroidTrimmableTypeMapMaxArrayRank", "0"); + proj.SetProperty ("_AndroidTrimmableTypeMapMaxReferenceArrayRank", "0"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); + + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "Second build should have succeeded."); + builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMap"); + + proj.SetProperty ("_AndroidTrimmableTypeMapMaxReferenceArrayRank", "1"); + Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "Reference array rank change build should have succeeded."); + builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); + } + [Test] public void Build_WithTrimmableTypeMap_DoesNotHitCopyIfChangedMismatch ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] AndroidRuntime runtime) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs index 5f3757da1a7..1f639becb1c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/BaseTest.cs @@ -627,6 +627,34 @@ protected bool IgnoreUnsupportedConfiguration (AndroidRuntime runtime, bool aot return false; } + // NativeAOT trims with ILC and does not emit illink's `obj///linked/` output. + // Tests that inspect the `linked/` directory (e.g. to verify trimming or type-map behavior) + // therefore cannot run as-is on NativeAOT. + // TODO: add DGML-based counterparts to verify these behaviors on NativeAOT (follow-up issue). + protected bool IgnoreNativeAotLinkedAssemblyChecks (AndroidRuntime runtime) + { + if (runtime == AndroidRuntime.NativeAOT) { + Assert.Ignore ("NativeAOT does not produce illink's `linked/` output; skipping `linked/` assembly inspection (DGML counterpart tracked as a follow-up)."); + return true; + } + + return false; + } + + // Some behaviors differ fundamentally between NativeAOT (ILC) and CoreCLR/MonoVM + // (e.g. ILC does not run illink, and the trimmable typemap uses CRC-only package naming), + // so certain test cases cannot apply as-is on NativeAOT. Use this to skip such a case + // with an explicit reason. + protected bool IgnoreOnNativeAot (AndroidRuntime runtime, string reason) + { + if (runtime == AndroidRuntime.NativeAOT) { + Assert.Ignore ($"NativeAOT: {reason}"); + return true; + } + + return false; + } + [SetUp] public void TestSetup () { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs index 7a7af12254a..97664008c2b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/XASdkTests.cs @@ -326,12 +326,7 @@ public void DotNetPublish ([Values] bool isRelease, [ValueSource (nameof(Get_Dot // NOTE: Preview API levels emit XA4211 if (!preview) { - if (runtime != AndroidRuntime.NativeAOT) { - dotnet.AssertHasNoWarnings (); - } else { - // NativeAOT currently issues 1 warning - dotnet.AssertHasSomeWarnings (1); - } + dotnet.AssertHasNoWarnings (); } // Only check latest TFM, as previous or preview TFMs will come from NuGet diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 1b4b676cb37..112ba88ffbd 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -993,6 +993,7 @@ because xbuild doesn't support framework reference assemblies. <_PropertyCacheItems Include="AndroidUseDesignerAssembly=$(AndroidUseDesignerAssembly)" /> <_PropertyCacheItems Include="_AndroidTypeMapImplementation=$(_AndroidTypeMapImplementation)" /> <_PropertyCacheItems Include="_AndroidTrimmableTypeMapMaxArrayRank=$(_AndroidTrimmableTypeMapMaxArrayRank)" /> + <_PropertyCacheItems Include="_AndroidTrimmableTypeMapMaxReferenceArrayRank=$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)" /> <_PropertyCacheItems Include="_AndroidUseMarshalMethods=$(_AndroidUseMarshalMethods)" /> <_PropertyCacheItems Include="_AndroidJcwCodegenTarget=$(_AndroidJcwCodegenTarget)" /> <_PropertyCacheItems Include="_AndroidAssemblyStoreCompressionLevel=$(_AndroidAssemblyStoreCompressionLevel)" /> @@ -1474,10 +1475,22 @@ because xbuild doesn't support framework reference assemblies. _RunAotForAllRIDs (which dispatches inner builds that AOT from linked/) sees the updated assemblies and re-AOTs them. When marshal methods are NOT enabled (NativeAOT, CoreCLR, etc.), write to afterlink/ so linked/ stays unchanged and IlcCompile incrementalism is preserved. --> - + + <_AfterILLinkAdditionalStepsInputs Remove="@(_AfterILLinkAdditionalStepsInputs)" /> + <_AfterILLinkAdditionalStepsInputs Include="$(_AndroidLinkFlag)" + Condition=" '$(_AndroidTypeMapImplementation)' != 'trimmable' or '$(_AndroidRuntime)' != 'NativeAOT' " /> + <_AfterILLinkAdditionalStepsInputs Include="@(ResolvedAssemblies);$(_AndroidBuildPropertiesCache)" + Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' and '$(_AndroidRuntime)' == 'NativeAOT' " /> + + + + @@ -1500,6 +1513,9 @@ because xbuild doesn't support framework reference assemblies. TargetName="$(TargetName)"> + + + <_AfterILLinkDestFiles Remove="@(_AfterILLinkDestFiles)" /> @@ -1513,7 +1529,7 @@ because xbuild doesn't support framework reference assemblies. so no redirection is needed, but the target still runs to trigger _RunAfterILLinkAdditionalSteps. --> + Condition="'$(PublishTrimmed)' == 'true' and ('$(_AndroidTypeMapImplementation)' != 'trimmable' or '$(_AndroidRuntime)' == 'NativeAOT')"> <_OrigResolvedAssemblies Include="@(ResolvedAssemblies)" /> diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index 241f116a183..818b751a3da 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -1554,6 +1554,10 @@ public void SkiaSharpCanvasBasedAppRuns ([Values] bool isRelease, [Values] bool return; } + if (IgnoreOnNativeAot (runtime, "the legacy resource-designer fix (FixLegacyResourceDesignerStep, which emits XA8000 for the unresolved SkiaSharp @styleable/SKCanvasView) is intentionally not run on the trimmable typemap path, which is the NativeAOT default.")) { + return; + } + var app = new XamarinAndroidApplicationProject (packageName: PackageUtils.MakePackageName (runtime, "SkiaSharpCanvasTest")) { IsRelease = isRelease, PackageReferences = { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs index 56c1b2fa3f6..ef0224aaecb 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/ManifestGeneratorTests.cs @@ -105,6 +105,25 @@ public void Placeholders_AllValid_DoesNotWarn () Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label")); } + [Fact] + public void Placeholders_EmptyKey_ReplacesEmptyTokenWithoutWarning () + { + var gen = CreateDefaultGenerator (); + var warnings = new List (); + gen.WarnInvalidPlaceholder = warnings.Add; + gen.ManifestPlaceholders = "=val1"; + var template = ParseTemplate (""" + + + + + + """); + var doc = GenerateAndLoad (gen, template: template); + Assert.Empty (warnings); + Assert.Equal ("val1", (string?) doc.Root?.Element ("application")?.Attribute (AndroidNs + "label")); + } + [Fact] public void Package_PlaceholderToken_ReplacedWithResolvedPackageName () { @@ -385,6 +404,24 @@ public void Activity_LayoutAttributeElement () Assert.Equal ("400dp", (string?)layout?.Attribute (AndroidNs + "minHeight")); } + [Fact] + public void Service_LayoutAttributeElement_Ignored () + { + var gen = CreateDefaultGenerator (); + var peer = CreatePeer ("com/example/app/MyService", new ComponentInfo { + Kind = ComponentKind.Service, + LayoutProperties = new Dictionary { + ["DefaultWidth"] = "500dp", + }, + }); + + var doc = GenerateAndLoad (gen, [peer]); + var service = doc.Root?.Element ("application")?.Element ("service"); + + Assert.NotNull (service); + Assert.Null (service?.Element ("layout")); + } + [Fact] public void Activity_AllExtendedProperties () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index a17715f8d68..fb9829ca648 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -38,6 +38,8 @@ public 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) => warnings?.Add (message); + public void LogInvalidManifestPlaceholderWarning (string placeholders) => + warnings?.Add ($"Invalid $(AndroidManifestPlaceholders) '{placeholders}'."); public void LogUnresolvableJavaPeerSkippedWarning ( string managedTypeName, string assemblyName, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 0cf84e0146d..2adad8caa93 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -3,6 +3,7 @@ using System.IO; using System.Linq; using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; using System.Reflection.PortableExecutable; using Xunit; @@ -16,10 +17,10 @@ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string return ModelBuilder.Build (peers, outputPath, assemblyName); } - static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3) + static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3, int? maxReferenceArrayRank = null) { var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); - return ModelBuilder.Build (peers, outputPath, assemblyName, maxArrayRank); + return ModelBuilder.Build (peers, outputPath, assemblyName, maxArrayRank, maxReferenceArrayRank ?? maxArrayRank); } public class BasicStructure @@ -113,6 +114,23 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); } + [Fact] + public void Build_AliasGroup_MonoAndroidPeerSortsFirst () + { + // Java.Interop first proves Mono.Android still becomes alias [0], matching runtime lookup. + var peers = new List { + MakeMcwPeer ("java/lang/Object", "Java.Interop.JavaObject", "Java.Interop") with { IsFromJniTypeSignature = true }, + MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Mono.Android"), + }; + + var model = BuildModel (peers, "MonoAndroid"); + + Assert.Equal ("java/lang/Object[0]", model.Entries [0].MapKey); + Assert.Contains ("Java.Lang.Object", model.Entries [0].ProxyTypeReference); + Assert.Equal ("java/lang/Object[1]", model.Entries [1].MapKey); + Assert.Contains ("Java.Interop.JavaObject", model.Entries [1].ProxyTypeReference); + } + [Fact] public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivation () { @@ -1180,7 +1198,8 @@ public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAss var primitiveEntries = model.Entries .Where (e => e.MapKey.StartsWith ("System.", StringComparison.Ordinal) && e.AnchorRank is not null) .ToList (); - Assert.Equal (36, primitiveEntries.Count); + // 12 primitives × 3 ranks + System.String × 3 ranks + 8 Nullable × 3 ranks. + Assert.Equal (63, primitiveEntries.Count); var sbyteRank1 = primitiveEntries.Single (e => e.MapKey == "System.SByte, System.Runtime" && e.AnchorRank == 1); Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy1, _Java.Interop.TypeMap", sbyteRank1.ProxyTypeReference); @@ -1216,6 +1235,84 @@ public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAss a.SourceTypeReference == concreteArrayTypeReference && a.AliasProxyTypeReference == rank1.ProxyTypeReference); } + + // System.String is a built-in reference mapping (java/lang/String), emitted with the + // reference-array family (JavaObjectArray, JavaArray, T[]) and no primitive concrete + // arrays or JavaPrimitiveArray. + var stringRank1 = primitiveEntries.Single (e => e.MapKey == "System.String, System.Runtime" && e.AnchorRank == 1); + Assert.Equal ("_TypeMap.ArrayProxies.System_String_ArrayProxy1, _Java.Interop.TypeMap", stringRank1.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.System_String_ArrayProxy1, _Java.Interop.TypeMap", stringRank1.TargetTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "System.String[], System.Runtime" && + a.AliasProxyTypeReference == stringRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[System.String, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == stringRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[System.String, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == stringRank1.ProxyTypeReference); + Assert.DoesNotContain (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaPrimitiveArray`1[[System.String, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == stringRank1.ProxyTypeReference); + + // Nullable boxed mappings (java/lang/Boolean etc.) use the normalized generic key + // (simple assembly names) that TrimmableTypeMap.BuildManagedTypeKey produces at runtime, and + // get the reference-array family. + var nullableBoolRank1 = primitiveEntries.Single (e => + e.MapKey == "System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime" && e.AnchorRank == 1); + Assert.Equal ("_TypeMap.ArrayProxies.Nullable_Boolean_ArrayProxy1, _Java.Interop.TypeMap", nullableBoolRank1.ProxyTypeReference); + var nullableBoolProxy = model.ArrayProxyTypes.Single (p => p.TypeName == "Nullable_Boolean_ArrayProxy1"); + Assert.Equal ("System.Nullable`1", nullableBoolProxy.ElementType.ManagedTypeName); + Assert.Equal ("System.Runtime", nullableBoolProxy.ElementType.AssemblyName); + Assert.True (nullableBoolProxy.ElementType.IsValueType); + var nullableBoolArgument = Assert.Single (nullableBoolProxy.ElementType.GenericArguments); + Assert.Equal ("System.Boolean", nullableBoolArgument.ManagedTypeName); + Assert.Equal ("System.Runtime", nullableBoolArgument.AssemblyName); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "System.Nullable`1[[System.Boolean, System.Runtime]][], System.Runtime" && + a.AliasProxyTypeReference == nullableBoolRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == nullableBoolRank1.ProxyTypeReference); + } + + [Fact] + public void Build_ReferenceRankLowerThanPrimitiveRank_PeersGetReferenceRankOnly () + { + // Split ranks: reference types (Java peers) capped at rank 1, primitives at rank 3. + var peer = MakeMcwPeer ("com/example/Foo", "Com.Example.Foo", "App"); + var model = BuildModelWithArrays (new [] { peer }, maxArrayRank: 3, maxReferenceArrayRank: 1); + + // The peer (a reference type) gets exactly one array entry, at rank 1. + var peerEntries = model.Entries.Where (e => e.MapKey == "Com.Example.Foo, App" && e.AnchorRank is not null).ToList (); + Assert.Single (peerEntries); + Assert.Equal (1, peerEntries [0].AnchorRank); + + // The anchor set is still rank 3 (uniform across assemblies for the root matrix). + Assert.Equal (3, model.MaxArrayRank); + } + + [Fact] + public void Build_ReferenceRankLowerThanPrimitiveRank_PrimitivesRank3ReferencesRank1 () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Java.Interop"); + var model = BuildModelWithArrays (new [] { peer }, assemblyName: "_Java.Interop.TypeMap", maxArrayRank: 3, maxReferenceArrayRank: 1); + + // Primitives keep ranks 1..3: 12 primitives × 3. + var primitiveInt = model.Entries.Where (e => e.MapKey == "System.Int32, System.Runtime" && e.AnchorRank is not null).Select (e => e.AnchorRank).OrderBy (r => r).ToList (); + Assert.Equal (new int? [] { 1, 2, 3 }, primitiveInt); + + // System.String is a built-in element type (like the primitives), so it keeps ranks 1..3. + var stringRanks = model.Entries.Where (e => e.MapKey == "System.String, System.Runtime" && e.AnchorRank is not null).Select (e => e.AnchorRank).OrderBy (r => r).ToList (); + Assert.Equal (new int? [] { 1, 2, 3 }, stringRanks); + + // Nullable is a built-in element type too (boxed java/lang/Boolean), so it keeps ranks 1..3. + var nullableRanks = model.Entries.Where (e => e.MapKey == "System.Nullable`1[[System.Boolean, System.Runtime]], System.Runtime" && e.AnchorRank is not null).Select (e => e.AnchorRank).OrderBy (r => r).ToList (); + Assert.Equal (new int? [] { 1, 2, 3 }, nullableRanks); + + // Totals: (12 primitives + String + 8 nullables) × 3 = 21 × 3 = 63. + var builtinEntries = model.Entries.Count (e => e.MapKey.StartsWith ("System.", StringComparison.Ordinal) && e.AnchorRank is not null); + Assert.Equal (63, builtinEntries); } [Fact] @@ -1321,6 +1418,20 @@ public void FullPipeline_PrimitiveAliasArrayEntries_EmitWithoutConcreteArrayType Assert.Contains ("Primitive_Byte_ArrayProxy1", typeDefNames); Assert.Contains ("Primitive_Byte_ArrayProxy2", typeDefNames); Assert.Contains ("Primitive_UInt32_ArrayProxy1", typeDefNames); + Assert.Contains ("Nullable_Boolean_ArrayProxy1", typeDefNames); + + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + .ToList (); + Assert.Contains ("Nullable`1", typeRefNames); + Assert.DoesNotContain (typeRefNames, name => name.Contains ("[[", StringComparison.Ordinal)); + + var typeSpecNames = Enumerable.Range (1, reader.GetTableRowCount (TableIndex.TypeSpec)) + .Select (MetadataTokens.TypeSpecificationHandle) + .Select (h => reader.GetTypeSpecification (h).DecodeSignature (SignatureTypeProvider.Instance, genericContext: null)) + .ToList (); + Assert.Contains ("System.Nullable`1[]", typeSpecNames); + Assert.Contains ("Java.Interop.JavaObjectArray`1>", typeSpecNames); var assocAttrs = ReadAllTypeMapAssociationAttributeBlobs (reader); Assert.Contains (assocAttrs, a => @@ -1341,7 +1452,7 @@ public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () { var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var outputPath = Path.Combine (Path.GetTempPath (), "ArrBlobs.dll"); - var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", maxArrayRank: 3); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", maxArrayRank: 3, maxReferenceArrayRank: 3); EmitAndVerify (model, "ArrBlobs", (pe, reader) => { var arrayAttrs = ReadAllTypeMapAttributeBlobs (reader) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 8b4abf0d84f..b32127fbf73 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -256,7 +256,7 @@ public void TrimmableJavaProxyObject_ObjectMethodsUseJavaIdentitySemantics () } [Test] - public void TryGetArrayProxy_ObjectLeaf_ReturnsAllRankTypes () + public void TryGetArrayProxy_ObjectLeaf_ReturnsRankOneTypes () { AssumeTrimmableTypeMapEnabled (); AssumeGeneratedArrayProxiesEnabled (); @@ -265,6 +265,14 @@ public void TryGetArrayProxy_ObjectLeaf_ReturnsAllRankTypes () CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray)); CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Lang.Object[])); + } + + [Test] + [Ignore ("Higher-rank reference array proxies are not generated by default; re-enable when the rank-2 object-array plan lands.")] + public void TryGetArrayProxy_ObjectLeaf_ReturnsRankTwoTypes () + { + AssumeTrimmableTypeMapEnabled (); + AssumeGeneratedArrayProxiesEnabled (); Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (Java.Lang.Object), additionalRank: 2, out var jaggedObjectArrayProxy)); CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray>));