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/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index c372463c6b6..6bbf04d2c49 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. @@ -109,12 +131,13 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri 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); } @@ -541,6 +564,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 +612,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 +628,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 +659,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 +708,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 +744,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/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/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index f59e99e0334..99fcb4cf42b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -35,6 +35,7 @@ public TrimmableTypeMapResult Execute ( XDocument? manifestTemplate = null, string? packageNamingPolicy = null, int maxArrayRank = 0, + int maxReferenceArrayRank = 0, bool generateTypeMapAssemblies = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); @@ -43,6 +44,9 @@ public TrimmableTypeMapResult Execute ( if (maxArrayRank < 0) { throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 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); @@ -178,7 +182,8 @@ List GenerateTypeMapAssemblies ( List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, - int maxArrayRank) + int maxArrayRank, + int maxReferenceArrayRank) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -207,14 +212,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.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.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 5090ba7fa6a..997a38ba646 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 @@ -49,6 +49,12 @@ defaults to 0 otherwise, where dynamic code can use Array.CreateInstance directly. --> <_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); @@ -140,6 +146,7 @@ EmbedAssemblies="$(EmbedAssembliesIntoApk)" PackageNamingPolicy="$(_TrimmableTypeMapPackageNamingPolicy)" MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)" + MaxReferenceArrayRank="$(_AndroidTrimmableTypeMapMaxReferenceArrayRank)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 6c7d18b5823..493a9daca71 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -102,11 +102,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 +223,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/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 431225f683c..b943a41e3df 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 @@ -304,6 +304,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/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 1b4b676cb37..c20d49c88c4 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)" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 0cf84e0146d..3956e1fc7ad 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 @@ -1180,7 +1181,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 +1218,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 +1401,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 +1435,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>));