diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs index 93f8ce5c5fc..95ada73504f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/MetadataHelper.cs @@ -32,7 +32,7 @@ public static byte [] ComputeContentFingerprint (TypeMapAssemblyData data) using var stream = new System.IO.MemoryStream (); using var writer = new System.IO.BinaryWriter (stream, Encoding.UTF8); foreach (var entry in data.Entries) { - writer.Write (entry.JniName); + writer.Write (entry.MapKey); writer.Write (entry.ProxyTypeReference); writer.Write (entry.TargetTypeReference ?? ""); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index a90f0d7dbeb..d81ac1acb8a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; namespace Microsoft.Android.Sdk.TrimmableTypeMap; @@ -40,6 +39,11 @@ sealed class TypeMapAssemblyData /// public List AliasHolders { get; } = new (); + /// + /// Array proxy types to emit — one per JNI element name and rank. + /// + public List ArrayProxyTypes { get; } = new (); + /// /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} /// sentinel TypeDefs and TypeMap entries. 0 disables. @@ -62,9 +66,10 @@ sealed class TypeMapAssemblyData sealed record TypeMapAttributeData { /// - /// JNI type name, e.g., "android/app/Activity". + /// Type map key, e.g., "android/app/Activity" for peer entries or + /// "Android.App.Activity, Mono.Android" for array proxy entries. /// - public required string JniName { get; init; } + public required string MapKey { get; init; } /// /// Assembly-qualified proxy type reference string. @@ -91,6 +96,30 @@ sealed record TypeMapAttributeData public int? AnchorRank { get; init; } } +/// +/// A generated array proxy type used by per-rank array TypeMap entries. +/// +sealed record ArrayProxyData +{ + public required string TypeName { get; init; } + + public string Namespace { get; init; } = "_TypeMap.ArrayProxies"; + + public required TypeRefData ElementType { get; init; } + + public required int Rank { get; init; } + + public PrimitiveArrayProxyData? Primitive { get; init; } +} + +/// +/// Additional primitive array metadata for . +/// +sealed record PrimitiveArrayProxyData +{ + public required TypeRefData ConcreteArrayType { get; init; } +} + /// /// A proxy type to generate in the TypeMap assembly (subclass of JavaPeerProxy). /// @@ -390,7 +419,7 @@ sealed record ActivationCtorData /// /// One [assembly: TypeMapAssociation(typeof(Source), typeof(AliasProxy))] entry. -/// Links a managed type to the alias holder that owns the alias group. +/// Links a managed type to an alias holder, generated proxy, or generated array proxy. /// sealed record TypeMapAssociationData { @@ -403,6 +432,12 @@ sealed record TypeMapAssociationData /// Assembly-qualified proxy type reference (the alias holder). /// public required string AliasProxyTypeReference { get; init; } + + /// + /// 1-based array rank when this association should use a __ArrayMapRank{value} + /// sentinel as its TGroup instead of the default model anchor. + /// + public int? AnchorRank { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4671394e936..2c6891178c3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,6 +16,17 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; + static readonly PrimitiveArrayProxyInfo [] PrimitiveArrayProxies = [ + new ("Z", "Boolean", "System.Boolean", "Java.Interop.JavaBooleanArray"), + new ("B", "SByte", "System.SByte", "Java.Interop.JavaSByteArray"), + new ("C", "Char", "System.Char", "Java.Interop.JavaCharArray"), + new ("S", "Int16", "System.Int16", "Java.Interop.JavaInt16Array"), + new ("I", "Int32", "System.Int32", "Java.Interop.JavaInt32Array"), + new ("J", "Int64", "System.Int64", "Java.Interop.JavaInt64Array"), + new ("F", "Single", "System.Single", "Java.Interop.JavaSingleArray"), + new ("D", "Double", "System.Double", "Java.Interop.JavaDoubleArray"), + ]; + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -99,6 +110,10 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } } + if (maxArrayRank > 0 && string.Equals (assemblyName, "_Java.Interop.TypeMap", StringComparison.Ordinal)) { + EmitPrimitiveArrayEntries (model, maxArrayRank); + } + BuildNativeRegistrations (model); // Compute IgnoresAccessChecksTo from cross-assembly references @@ -190,7 +205,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, bool aliasBaseUnconditional = EssentialRuntimeTypes.Contains (jniName) || peersForName.Any (IsUnconditionalEntry); model.Entries.Add (new TypeMapAttributeData { - JniName = jniName, + MapKey = jniName, ProxyTypeReference = holderRef, TargetTypeReference = aliasBaseUnconditional ? null : holderRef, }); @@ -253,13 +268,26 @@ static void AddIfCrossAssembly (SortedSet set, string? asmName, string o static string ManagedTypeNameToProxyTypeName (string managedTypeName) { var builder = new StringBuilder (managedTypeName.Length + ProxyTypeSuffix.Length); + AppendSafeManagedTypeName (builder, managedTypeName); + builder.Append (ProxyTypeSuffix); + return builder.ToString (); + } + + static string ManagedTypeNameToArrayProxyTypeName (string managedTypeName, int rank) + { + var builder = new StringBuilder (managedTypeName.Length + 20); + AppendSafeManagedTypeName (builder, managedTypeName); + builder.Append ("_ArrayProxy"); + builder.Append (rank); + return builder.ToString (); + } + + static void AppendSafeManagedTypeName (StringBuilder builder, string managedTypeName) + { for (int i = 0; i < managedTypeName.Length; i++) { char c = managedTypeName [i]; builder.Append (c == '.' || c == '+' || c == '`' ? '_' : c); } - - builder.Append (ProxyTypeSuffix); - return builder.ToString (); } static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, HashSet usedProxyNames, bool isAcw) @@ -493,7 +521,7 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr } return new TypeMapAttributeData { - JniName = jniName, + MapKey = jniName, ProxyTypeReference = proxyRef, TargetTypeReference = targetRef, }; @@ -502,17 +530,88 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; + static string AddArrayRank (string typeReference, int rank) + { + if (rank == 0) { + return typeReference; + } + + int assemblySeparator = typeReference.LastIndexOf (", ", StringComparison.Ordinal); + if (assemblySeparator < 0) { + throw new InvalidOperationException ($"Assembly-qualified type reference '{typeReference}' does not contain an assembly name."); + } + + return typeReference.Substring (0, assemblySeparator) + Brackets (rank) + typeReference.Substring (assemblySeparator); + } + + static string MakeGenericTypeReference (string openTypeName, string openTypeAssembly, string argumentTypeReference) + => $"{openTypeName}[[{argumentTypeReference}]], {openTypeAssembly}"; + + static string MakeNestedJavaObjectArrayTypeReference (string elementTypeReference, int rank) + { + var result = elementTypeReference; + for (int i = 0; i < rank; i++) { + result = MakeGenericTypeReference ("Java.Interop.JavaObjectArray`1", "Java.Interop", result); + } + return result; + } + + static IReadOnlyList GetArrayTypeReferences (ArrayProxyData proxy) + { + var elementType = AssemblyQualify (proxy.ElementType.ManagedTypeName, proxy.ElementType.AssemblyName); + if (proxy.Primitive is null) { + var rankOneTypes = new [] { + MakeGenericTypeReference ("Java.Interop.JavaObjectArray`1", "Java.Interop", elementType), + MakeGenericTypeReference ("Java.Interop.JavaArray`1", "Java.Interop", elementType), + AddArrayRank (elementType, 1), + }; + return ExpandRankOneTypes (rankOneTypes, proxy.Rank); + } + + var rankOnePrimitiveTypes = new [] { + AddArrayRank (elementType, 1), + MakeGenericTypeReference ("Java.Interop.JavaArray`1", "Java.Interop", elementType), + MakeGenericTypeReference ("Java.Interop.JavaPrimitiveArray`1", "Java.Interop", elementType), + AssemblyQualify (proxy.Primitive.ConcreteArrayType.ManagedTypeName, proxy.Primitive.ConcreteArrayType.AssemblyName), + }; + return ExpandRankOneTypes (rankOnePrimitiveTypes, proxy.Rank); + } + + static IReadOnlyList ExpandRankOneTypes (IReadOnlyList rankOneTypes, int rank) + { + if (rank == 1) { + return rankOneTypes; + } + + var result = new List (rankOneTypes.Count * 2); + foreach (var type in rankOneTypes) { + result.Add (MakeNestedJavaObjectArrayTypeReference (type, rank - 1)); + result.Add (AddArrayRank (type, rank - 1)); + } + return result; + } + + static void AddArrayProxyAssociations (TypeMapAssemblyData model, ArrayProxyData proxy, string proxyReference) + { + foreach (var typeReference in GetArrayTypeReferences (proxy)) { + model.Associations.Add (new TypeMapAssociationData { + SourceTypeReference = typeReference, + AliasProxyTypeReference = proxyReference, + AnchorRank = proxy.Rank, + }); + } + } + + static string GetArrayProxyMapKey (TypeRefData elementType) + => AssemblyQualify (elementType.ManagedTypeName, elementType.AssemblyName); + /// /// Emits per-rank array TypeMap entries for one peer, anchored to the per-assembly - /// __ArrayMapRank{N} sentinels. Keys are bare element JNI names (rank is encoded - /// by the sentinel anchor, not by JNI array prefixes). Skips open generics, primitive JNI - /// keyword keys (handled by the legacy primitive-array path), and alias groups. + /// __ArrayMapRank{N} sentinels. Keys are managed element type names (rank is encoded + /// by the sentinel anchor, not by JNI array prefixes). Skips open generics and alias groups. /// static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List peersForName, int maxArrayRank) { - if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) { - return; - } if (peersForName.Count != 1) { return; } @@ -524,15 +623,60 @@ static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I' || c == 'J' || c == 'F' || c == 'D' || c == 'V'; + + readonly record struct PrimitiveArrayProxyInfo ( + string JniName, + string Name, + string ManagedTypeName, + string ConcreteArrayTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index 57e57b7a835..92c68ddda5f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -158,6 +158,50 @@ public EntityHandle ResolveTypeRef (TypeRefData typeRef) return result; } + public void WriteTypeSignature (BlobBuilder blob, TypeRefData typeRef) + { + if (typeRef.ManagedTypeName.EndsWith ("[]", StringComparison.Ordinal)) { + blob.WriteByte (0x1D); // ELEMENT_TYPE_SZARRAY + WriteTypeSignature (blob, typeRef with { + ManagedTypeName = typeRef.ManagedTypeName.Substring (0, typeRef.ManagedTypeName.Length - 2), + }); + return; + } + + switch (typeRef.ManagedTypeName) { + case "System.Boolean": blob.WriteByte (0x02); return; + case "System.Char": blob.WriteByte (0x03); return; + case "System.SByte": blob.WriteByte (0x04); return; + case "System.Byte": blob.WriteByte (0x05); return; + case "System.Int16": blob.WriteByte (0x06); return; + case "System.UInt16": blob.WriteByte (0x07); return; + case "System.Int32": blob.WriteByte (0x08); return; + case "System.UInt32": blob.WriteByte (0x09); return; + case "System.Int64": blob.WriteByte (0x0A); return; + case "System.UInt64": blob.WriteByte (0x0B); return; + case "System.Single": blob.WriteByte (0x0C); return; + case "System.Double": blob.WriteByte (0x0D); return; + case "System.String": blob.WriteByte (0x0E); return; + case "System.Object": blob.WriteByte (0x1C); return; + case "System.IntPtr": blob.WriteByte (0x18); return; + } + + if (typeRef.ManagedTypeName.StartsWith ("!!", StringComparison.Ordinal)) { + blob.WriteByte (0x1E); // ELEMENT_TYPE_MVAR + blob.WriteCompressedInteger (int.Parse (typeRef.ManagedTypeName.Substring (2), System.Globalization.CultureInfo.InvariantCulture)); + return; + } + if (typeRef.ManagedTypeName.StartsWith ("!", StringComparison.Ordinal)) { + blob.WriteByte (0x13); // ELEMENT_TYPE_VAR + blob.WriteCompressedInteger (int.Parse (typeRef.ManagedTypeName.Substring (1), System.Globalization.CultureInfo.InvariantCulture)); + return; + } + + var typeHandle = ResolveTypeRef (typeRef); + blob.WriteByte (typeRef.IsEnum ? (byte) 0x11 : (byte) 0x12); // VALUETYPE or CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (typeHandle)); + } + TypeReferenceHandle MakeTypeRefForManagedName (EntityHandle scope, string managedTypeName) { int plusIndex = managedTypeName.IndexOf ('+'); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index b54c2f535ec..ad272b49a85 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -305,7 +305,7 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByAssemblyAndRank-or-null) encoder.LoadLocal (0); encoder.LoadLocal (1); - EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); + EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); encoder.Return (); }, @@ -450,7 +450,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByAssemblyAndRank-or-null) encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true); encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); - EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); + EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); encoder.Return (); }); @@ -534,7 +534,7 @@ static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder /// static void EmitArrayMapsByAssemblyAndRankOrNull (PEAssemblyBuilder pe, TrackedInstructionEncoder encoder, IReadOnlyList perAssemblyTypeMapNames, - MemberReferenceHandle getExternalMemberRef, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, int maxArrayRank) { @@ -549,14 +549,22 @@ static void EmitArrayMapsByAssemblyAndRankOrNull (PEAssemblyBuilder pe, TrackedI var asmRef = pe.FindOrAddAssemblyRef (perAssemblyTypeMapNames [i]); encoder.OpCode (ILOpCode.Dup); encoder.LoadConstantI4 (i); - EmitArrayMapsByRank (pe, encoder, asmRef, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + EmitArrayMapsByRank (pe, encoder, asmRef, getExternalMemberRef, getProxyMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Stelem_ref); } } + /// + /// Pushes a fresh IReadOnlyDictionary<string, Type>[maxArrayRank] for a single + /// assembly, where slot r holds the external (JNI-name -> proxy ) + /// array map for the __ArrayMapRank{r + 1} group anchor. At runtime only this external + /// map is consulted (see TrimmableTypeMap.TryGetArrayProxy), which resolves a proxy + /// by JNI name and then reads its array-proxy attribute; the proxy typemap + /// dictionary itself is never indexed. + /// static void EmitArrayMapsByRank (PEAssemblyBuilder pe, TrackedInstructionEncoder encoder, AssemblyReferenceHandle assemblyRef, - MemberReferenceHandle getExternalMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) { encoder.LoadConstantI4 (maxArrayRank); encoder.NewArray (externalDictTypeSpec); @@ -564,10 +572,20 @@ static void EmitArrayMapsByRank (PEAssemblyBuilder pe, TrackedInstructionEncoder var rankRef = pe.Metadata.AddTypeReference (assemblyRef, default, pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); var rankSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + var proxyRankSpec = MakeGenericMethodSpec (pe, getProxyMemberRef, rankRef); + // Store the external map for this rank into the array. encoder.OpCode (ILOpCode.Dup); encoder.LoadConstantI4 (r); encoder.Call (rankSpec, parameterCount: 0, returnsValue: true); encoder.OpCode (ILOpCode.Stelem_ref); + // Unlike the external map on the line above (stored via Stelem_ref), this + // GetOrCreateProxyTypeMapping<__ArrayMapRank{r + 1}> () call has its result + // immediately Popped: the proxy map for arrays is never stored or indexed at + // runtime. We still emit the call purely for its side effect — requesting the proxy + // typemap group roots that group's per-rank [TypeMapAssociation] entries, so the + // trimmer/ILC keeps the proxy types that the external map above references by JNI name. + encoder.Call (proxyRankSpec, parameterCount: 0, returnsValue: true); + encoder.OpCode (ILOpCode.Pop); } } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8ab18800ea5..fc7a10031c7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -80,6 +80,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _javaPeerProxyRef; TypeReferenceHandle _javaPeerProxyNonGenericRef; + TypeReferenceHandle _javaArrayProxyRef; TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; TypeReferenceHandle _jniObjectReferenceRef; @@ -88,6 +89,9 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _jniEnvRef; TypeReferenceHandle _javaLangObjectRef; + TypeReferenceHandle _javaObjectArrayOpenRef; + TypeReferenceHandle _javaArrayOpenRef; + TypeReferenceHandle _javaPrimitiveArrayOpenRef; TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _systemArrayRef; TypeReferenceHandle _runtimeTypeHandleRef; @@ -115,7 +119,9 @@ sealed class TypeMapAssemblyEmitter BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; MemberReferenceHandle _typeMapAttrCtorRef3Arg; + MemberReferenceHandle _javaArrayProxyCtorRef; MemberReferenceHandle _typeMapAssociationAttrCtorRef; + TypeReferenceHandle _typeMapAssociationAttrOpenRef; // RegisterNatives with JniNativeMethod TypeReferenceHandle _jniNativeMethodRef; @@ -145,8 +151,10 @@ sealed class TypeMapAssemblyEmitter // aren't emitted. EntityHandle [] _rankAnchorHandles = []; - // Per-anchor TypeMap(string, Type, Type) ctor refs, lazily built. + // Per-anchor TypeMap ctor refs, lazily built. + readonly Dictionary _typeMapAttr2ArgCtorRefByAnchor = new (); readonly Dictionary _typeMapAttr3ArgCtorRefByAnchor = new (); + readonly Dictionary _typeMapAssociationAttrCtorRefByAnchor = new (); // Cached open TypeMapAttribute`1 ref shared across closed TypeSpecs. TypeReferenceHandle _typeMapAttrOpenRef; @@ -214,6 +222,10 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) EmitAliasHolderType (holder); } + foreach (var arrayProxy in model.ArrayProxyTypes) { + EmitArrayProxyType (arrayProxy); + } + foreach (var entry in model.Entries) { EmitTypeMapAttribute (entry); } @@ -278,6 +290,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy`1")); _javaPeerProxyNonGenericRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPeerProxy")); + _javaArrayProxyRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaArrayProxy")); _iJavaPeerableRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("IJavaPeerable")); _jniHandleOwnershipRef = metadata.AddTypeReference (_pe.MonoAndroidRef, @@ -286,6 +300,12 @@ void EmitTypeReferences () metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); _javaLangObjectRef = metadata.AddTypeReference (_pe.MonoAndroidRef, metadata.GetOrAddString ("Java.Lang"), metadata.GetOrAddString ("Object")); + _javaObjectArrayOpenRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaObjectArray`1")); + _javaArrayOpenRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaArray`1")); + _javaPrimitiveArrayOpenRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JavaPrimitiveArray`1")); _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference")); _jniObjectReferenceTypeRef = metadata.AddTypeReference (_javaInteropRef, @@ -395,6 +415,9 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().String ())); + _javaArrayProxyCtorRef = _pe.AddMemberRef (_javaArrayProxyRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + // JniObjectReference..ctor(IntPtr handle, JniObjectReferenceType type) // Note: The C# constructor has a default parameter (type = Invalid), but in IL there is only // the 2-parameter overload. We must emit both parameters explicitly. @@ -556,26 +579,39 @@ void EmitTypeMapAttributeCtorRef () metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAttribute`1")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, _anchorTypeHandle); + // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional. + _typeMapAttrCtorRef2Arg = AddTypeMapAttr2ArgCtorRef (_anchorTypeHandle); + _typeMapAttr2ArgCtorRefByAnchor [_anchorTypeHandle] = _typeMapAttrCtorRef2Arg; + + // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable. + // Cache by anchor so rank-anchored entries can build their own closed ctor on demand. + _typeMapAttrCtorRef3Arg = AddTypeMapAttr3ArgCtorRef (_anchorTypeHandle); + _typeMapAttr3ArgCtorRefByAnchor [_anchorTypeHandle] = _typeMapAttrCtorRef3Arg; + } + + /// Cached 2-arg TypeMap<TGroup> ctor ref for the given anchor, built on first use. + MemberReferenceHandle GetOrAddTypeMapAttr2ArgCtorRef (EntityHandle anchor) + { + if (_typeMapAttr2ArgCtorRefByAnchor.TryGetValue (anchor, out var cached)) { + return cached; + } + var ctorRef = AddTypeMapAttr2ArgCtorRef (anchor); + _typeMapAttr2ArgCtorRefByAnchor [anchor] = ctorRef; + return ctorRef; + } - // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional. Default anchor only; - // rank-anchored entries are always conditional (3-arg) so no per-rank 2-arg ctor is - // needed today. - _typeMapAttrCtorRef2Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + MemberReferenceHandle AddTypeMapAttr2ArgCtorRef (EntityHandle anchor) + { + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, anchor); + return _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), p => { p.AddParameter ().Type ().String (); p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - - // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable. - // Cache by anchor so rank-anchored entries can build their own closed ctor on demand. - _typeMapAttrCtorRef3Arg = AddTypeMapAttr3ArgCtorRef (_anchorTypeHandle); - _typeMapAttr3ArgCtorRefByAnchor [_anchorTypeHandle] = _typeMapAttrCtorRef3Arg; } - /// Cached 3-arg TypeMap<TGroup> ctor ref for the given anchor, built on first use. MemberReferenceHandle GetOrAddTypeMapAttr3ArgCtorRef (EntityHandle anchor) { if (_typeMapAttr3ArgCtorRefByAnchor.TryGetValue (anchor, out var cached)) { @@ -602,10 +638,10 @@ MemberReferenceHandle AddTypeMapAttr3ArgCtorRef (EntityHandle anchor) void EmitTypeMapAssociationAttributeCtorRef () { var metadata = _pe.Metadata; - var typeMapAssociationAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + _typeMapAssociationAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAssociationAttribute`1")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAssociationAttrOpenRef, _anchorTypeHandle); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAssociationAttrOpenRef, _anchorTypeHandle); _typeMapAssociationAttrCtorRef = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, @@ -614,6 +650,24 @@ void EmitTypeMapAssociationAttributeCtorRef () p.AddParameter ().Type ().Type (_systemTypeRef, false); p.AddParameter ().Type ().Type (_systemTypeRef, false); })); + _typeMapAssociationAttrCtorRefByAnchor [_anchorTypeHandle] = _typeMapAssociationAttrCtorRef; + } + + MemberReferenceHandle GetOrAddTypeMapAssociationAttrCtorRef (EntityHandle anchor) + { + if (_typeMapAssociationAttrCtorRefByAnchor.TryGetValue (anchor, out var cached)) { + return cached; + } + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAssociationAttrOpenRef, anchor); + var ctorRef = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_systemTypeRef, false); + })); + _typeMapAssociationAttrCtorRefByAnchor [anchor] = ctorRef; + return ctorRef; } ExportMethodDispatchEmitterContext CreateExportMethodDispatchEmitterContext () @@ -775,6 +829,69 @@ void EmitAliasHolderType (AliasHolderData holder) EmitJavaPeerAliasesAttribute (typeDefHandle, holder.AliasKeys); } + void EmitArrayProxyType (ArrayProxyData proxy) + { + var metadata = _pe.Metadata; + var typeDefHandle = metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, + metadata.GetOrAddString (proxy.Namespace), + metadata.GetOrAddString (proxy.TypeName), + _javaArrayProxyRef, + MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), + MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + + var selfAttrCtorDef = _pe.EmitBody (".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), + encoder => { + encoder.OpCode (ILOpCode.Ldarg_0); + encoder.Call (_javaArrayProxyCtorRef, parameterCount: 0, isInstance: true); + encoder.Return (); + }); + + metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorDef, _pe.BuildAttributeBlob (b => { })); + + EmitArrayProxyGetArrayTypes (proxy); + EmitArrayProxyCreateManagedArray (proxy); + } + + void EmitArrayProxyGetArrayTypes (ArrayProxyData proxy) + { + var arrayTypes = GetArrayProxyTypes (proxy); + _pe.EmitBody ("GetArrayTypes", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, + rt => rt.Type ().SZArray ().Type (_systemTypeRef, false), + p => { }), + encoder => { + encoder.LoadConstantI4 (arrayTypes.Count); + encoder.NewArray (_systemTypeRef); + for (int i = 0; i < arrayTypes.Count; i++) { + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (i); + encoder.LoadToken (ResolveRuntimeTypeSpec (arrayTypes [i])); + encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + encoder.OpCode (ILOpCode.Stelem_ref); + } + encoder.Return (returnsValue: true); + }); + } + + void EmitArrayProxyCreateManagedArray (ArrayProxyData proxy) + { + var elementType = AddSzArrayRank (new NamedRuntimeTypeSpec (proxy.ElementType), proxy.Rank - 1); + _pe.EmitBody ("CreateManagedArray", + MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + rt => rt.Type ().Type (_systemArrayRef, false), + p => p.AddParameter ().Type ().Int32 ()), + encoder => { + encoder.LoadArgument (1); + encoder.NewArray (ResolveRuntimeTypeSpec (elementType)); + encoder.Return (returnsValue: true); + }); + } + void EmitJavaPeerAliasesAttributeCtorRef () { // JavaPeerAliasesAttribute(params string[] aliases) — in Mono.Android, Java.Interop namespace @@ -1476,6 +1593,101 @@ void EmitManagedConstructorArgument (TrackedInstructionEncoder encoder, TypeRefD encoder.CastClass (managedTypeHandle); } + IReadOnlyList GetArrayProxyTypes (ArrayProxyData proxy) + { + var elementType = new NamedRuntimeTypeSpec (proxy.ElementType); + if (proxy.Primitive is null) { + var rankOneObjectTypes = new RuntimeTypeSpec [] { + new GenericRuntimeTypeSpec (_javaObjectArrayOpenRef, elementType), + new GenericRuntimeTypeSpec (_javaArrayOpenRef, elementType), + AddSzArrayRank (elementType, 1), + }; + return ExpandRankOneTypes (rankOneObjectTypes, proxy.Rank); + } + + var rankOneTypes = new RuntimeTypeSpec [] { + AddSzArrayRank (elementType, 1), + new GenericRuntimeTypeSpec (_javaArrayOpenRef, elementType), + new GenericRuntimeTypeSpec (_javaPrimitiveArrayOpenRef, elementType), + new NamedRuntimeTypeSpec (proxy.Primitive.ConcreteArrayType), + }; + + return ExpandRankOneTypes (rankOneTypes, proxy.Rank); + } + + IReadOnlyList ExpandRankOneTypes (IReadOnlyList rankOneTypes, int rank) + { + if (rank == 1) { + return rankOneTypes; + } + + var result = new List (rankOneTypes.Count * 2); + foreach (var type in rankOneTypes) { + result.Add (MakeNestedJavaObjectArrayType (type, rank - 1)); + result.Add (AddSzArrayRank (type, rank - 1)); + } + return result; + } + + static RuntimeTypeSpec AddSzArrayRank (RuntimeTypeSpec elementType, int rank) + { + var result = elementType; + for (int i = 0; i < rank; i++) { + result = new SzArrayRuntimeTypeSpec (result); + } + return result; + } + + RuntimeTypeSpec MakeNestedJavaObjectArrayType (RuntimeTypeSpec elementType, int rank) + { + var result = elementType; + for (int i = 0; i < rank; i++) { + result = new GenericRuntimeTypeSpec (_javaObjectArrayOpenRef, result); + } + return result; + } + + EntityHandle ResolveRuntimeTypeSpec (RuntimeTypeSpec type) + { + if (type is NamedRuntimeTypeSpec namedType) { + return _pe.ResolveTypeRef (namedType.Type); + } + + var blob = new BlobBuilder (64); + EncodeRuntimeTypeSpec (blob, type); + return _pe.Metadata.AddTypeSpecification (_pe.Metadata.GetOrAddBlob (blob)); + } + + void EncodeRuntimeTypeSpec (BlobBuilder blob, RuntimeTypeSpec type) + { + switch (type) { + case NamedRuntimeTypeSpec namedType: + _pe.WriteTypeSignature (blob, namedType.Type); + break; + case SzArrayRuntimeTypeSpec arrayType: + blob.WriteByte (0x1D); // ELEMENT_TYPE_SZARRAY + EncodeRuntimeTypeSpec (blob, arrayType.ElementType); + break; + case GenericRuntimeTypeSpec genericType: + blob.WriteByte (0x15); // ELEMENT_TYPE_GENERICINST + blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS + blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (genericType.OpenType)); + blob.WriteCompressedInteger (1); // generic arity = 1 + EncodeRuntimeTypeSpec (blob, genericType.Argument); + break; + default: + throw new InvalidOperationException ($"Unsupported runtime type spec '{type.GetType ()}'."); + } + } + + abstract record RuntimeTypeSpec; + + sealed record NamedRuntimeTypeSpec (TypeRefData Type) : RuntimeTypeSpec; + + sealed record SzArrayRuntimeTypeSpec (RuntimeTypeSpec ElementType) : RuntimeTypeSpec; + + sealed record GenericRuntimeTypeSpec (EntityHandle OpenType, RuntimeTypeSpec Argument) : RuntimeTypeSpec; + EntityHandle ResolveManagedTypeHandle (string managedType, string defaultAssemblyName) { if (TryGetSzArrayElementType (managedType, out var elementType)) { @@ -1731,12 +1943,12 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) if (entry.AnchorRank is int rank) { if (entry.IsUnconditional) { throw new InvalidOperationException ( - $"Rank-anchored TypeMap entries must be conditional (3-arg). Entry '{entry.JniName}' rank={rank}."); + $"Rank-anchored TypeMap entries must be conditional (3-arg). Entry '{entry.MapKey}' rank={rank}."); } int anchorIndex = rank - 1; if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length) { throw new InvalidOperationException ( - $"No rank-{rank} anchor available for entry '{entry.JniName}'. " + + $"No rank-{rank} anchor available for entry '{entry.MapKey}'. " + $"Ensure TypeMapAssemblyData.MaxArrayRank was >= {rank} before emit."); } ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [anchorIndex]); @@ -1745,11 +1957,11 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) } var blob = _pe.BuildAttributeBlob (b => { - b.WriteSerializedString (entry.JniName); + b.WriteSerializedString (entry.MapKey); b.WriteSerializedString (entry.ProxyTypeReference); if (!entry.IsUnconditional) { if (entry.TargetTypeReference is null) { - throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.JniName}'"); + throw new InvalidOperationException ($"TargetTypeReference must not be null for conditional entry '{entry.MapKey}'"); } b.WriteSerializedString (entry.TargetTypeReference); } @@ -1759,11 +1971,22 @@ void EmitTypeMapAttribute (TypeMapAttributeData entry) void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) { + var ctorRef = _typeMapAssociationAttrCtorRef; + if (assoc.AnchorRank is int rank) { + int anchorIndex = rank - 1; + if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length) { + throw new InvalidOperationException ( + $"No rank-{rank} anchor available for association '{assoc.SourceTypeReference}'. " + + $"Ensure TypeMapAssemblyData.MaxArrayRank was >= {rank} before emit."); + } + ctorRef = GetOrAddTypeMapAssociationAttrCtorRef (_rankAnchorHandles [anchorIndex]); + } + var blob = _pe.BuildAttributeBlob (b => { b.WriteSerializedString (assoc.SourceTypeReference); b.WriteSerializedString (assoc.AliasProxyTypeReference); }); - _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); + _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blob); } /// diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index ebfc81cc39b..6b2fa369ad0 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -27,20 +27,22 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { - if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { + if (RuntimeFeature.IsCoreClrRuntime) { // CoreCLR runtime type loader can construct any T[] dynamically. // IsDynamicCodeSupported is a [FeatureGuard] so this branch is // dead-coded under PublishAot. return Array.CreateInstance (elementType, length); } - // NativeAOT: resolve via per-rank typemap + Array.CreateInstanceFromArrayType. - if (TrimmableTypeMap.Instance.TryGetArrayType (elementType, out var arrayType)) { - return Array.CreateInstanceFromArrayType (arrayType, length); + if (RuntimeFeature.IsNativeAotRuntime) { + // NativeAOT: resolve via per-rank typemap + generated array proxy. + if (TrimmableTypeMap.Instance.TryGetArrayProxy (elementType, additionalRank: 1, out var arrayProxy)) { + return arrayProxy.CreateManagedArray (length); + } } throw new NotSupportedException ( - $"No TrimmableTypeMap array entry for element type '{elementType}'. " + + $"No TrimmableTypeMap array proxy entry for element type '{elementType}'. " + $"Array lookups use the element type within the per-rank __ArrayMapRank{GetArrayRank (elementType)} typemap group; " + $"ensure the mapping is emitted for that rank (for example by increasing _AndroidTrimmableTypeMapMaxArrayRank) or report an issue."); } @@ -64,13 +66,6 @@ static int GetArrayRank (Type elementType) return rank; } - static Type MakeArrayType (Type type) => - // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 - // IL3050 disabled in source: if someone uses NativeAOT, they will get the warning. - #pragma warning disable IL3050 - type.MakeArrayType (); - #pragma warning restore IL3050 - internal static IntPtr IdentityHash (IntPtr v) { return JniEnvironment.References.GetIdentityHashCode (new JniObjectReference (v)); @@ -577,9 +572,9 @@ public static unsafe IntPtr NewString (char[]? text, int length) return JniEnvironment.Strings.NewString (s, length).Handle; } - static void AssertCompatibleArrayTypes (Type sourceType, IntPtr destArray) + static void AssertCompatibleArrayTypes (Type srcElementType, IntPtr destArray) { - IntPtr grefSource = FindClass (sourceType); + IntPtr grefSource = FindArrayClassByElementType (srcElementType); IntPtr lrefDest = GetObjectClass (destArray); try { if (!IsAssignableFrom (grefSource, lrefDest)) { @@ -592,9 +587,9 @@ static void AssertCompatibleArrayTypes (Type sourceType, IntPtr destArray) } } - static void AssertCompatibleArrayTypes (IntPtr sourceArray, Type destType) + static void AssertCompatibleArrayTypes (IntPtr sourceArray, Type destElementType) { - IntPtr grefDest = FindClass (destType); + IntPtr grefDest = FindArrayClassByElementType (destElementType); IntPtr lrefSource = GetObjectClass (sourceArray); try { if (!IsAssignableFrom (lrefSource, grefDest)) { @@ -607,12 +602,19 @@ static void AssertCompatibleArrayTypes (IntPtr sourceArray, Type destType) } } + static IntPtr FindArrayClassByElementType (Type elementType) + { + int rank = JavaNativeTypeManager.GetArrayInfo (elementType, out elementType) + 1; + var typeSignature = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (elementType).AddArrayRank (rank); + return FindClass (typeSignature.Name); + } + public static void CopyArray (IntPtr src, bool[] dest) { if (dest == null) throw new ArgumentNullException ("dest"); - AssertCompatibleArrayTypes (src, typeof (bool[])); + AssertCompatibleArrayTypes (src, destElementType: typeof (bool)); _GetBooleanArrayRegion (src, 0, dest.Length, dest); } @@ -804,7 +806,7 @@ public static void CopyArray (IntPtr src, Array dest, Type? elementType = null) throw new ArgumentNullException ("dest"); if (elementType != null && elementType.IsValueType) - AssertCompatibleArrayTypes (src, MakeArrayType (elementType)); + AssertCompatibleArrayTypes (src, destElementType: elementType); if (elementType != null && elementType.IsArray) { for (int i = 0; i < dest.Length; ++i) { @@ -840,7 +842,7 @@ public static void CopyArray (IntPtr src, T[] dest) throw new ArgumentNullException ("dest"); if (typeof (T).IsValueType) - AssertCompatibleArrayTypes (src, typeof (T[])); + AssertCompatibleArrayTypes (src, destElementType: typeof (T)); if (typeof (T).IsArray) { CopyArray (src, dest, typeof (T)); @@ -858,7 +860,7 @@ public static unsafe void CopyArray (bool[] src, IntPtr dest) if (src == null) throw new ArgumentNullException ("src"); - AssertCompatibleArrayTypes (typeof (bool[]), dest); + AssertCompatibleArrayTypes (srcElementType: typeof (bool), dest); fixed (bool* p = src) JniEnvironment.Arrays.SetBooleanArrayRegion (new JniObjectReference (dest), 0, src.Length, p); @@ -947,7 +949,7 @@ public static void CopyArray (Array source, Type elementType, IntPtr dest) throw new ArgumentNullException ("elementType"); if (elementType.IsValueType) - AssertCompatibleArrayTypes (MakeArrayType (elementType), dest); + AssertCompatibleArrayTypes (srcElementType: elementType, dest); Action converter = GetConverter (CopyManagedToNativeArray, elementType, dest); @@ -1072,7 +1074,7 @@ public static void CopyArray (T[] src, IntPtr dest) return null; if (element_type != null && element_type.IsValueType) - AssertCompatibleArrayTypes (array_ptr, MakeArrayType (element_type)); + AssertCompatibleArrayTypes (array_ptr, destElementType: element_type); int cnt = _GetArrayLength (array_ptr); @@ -1119,7 +1121,7 @@ static int _GetArrayLength (IntPtr array_ptr) return null; if (typeof (T).IsValueType) - AssertCompatibleArrayTypes (array_ptr, typeof (T[])); + AssertCompatibleArrayTypes (array_ptr, destElementType: typeof (T)); int cnt = _GetArrayLength (array_ptr); T[] ret = new T [cnt]; diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ba46b155c00..9fb9de7429a 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -56,6 +56,19 @@ static class JavaConvert { } }, }; + static readonly Dictionary ScalarContainerFactories = new Dictionary { + { typeof (bool), JavaPeerContainerFactory.Instance }, + { typeof (byte), JavaPeerContainerFactory.Instance }, + { typeof (sbyte), JavaPeerContainerFactory.Instance }, + { typeof (char), JavaPeerContainerFactory.Instance }, + { typeof (short), JavaPeerContainerFactory.Instance }, + { typeof (int), JavaPeerContainerFactory.Instance }, + { typeof (long), JavaPeerContainerFactory.Instance }, + { typeof (float), JavaPeerContainerFactory.Instance }, + { typeof (double), JavaPeerContainerFactory.Instance }, + { typeof (string), JavaPeerContainerFactory.Instance }, + }; + static Func? GetJniHandleConverter (Type? target) { // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 @@ -115,25 +128,19 @@ params Type [] typeArguments /// static Func? TryGetFactoryBasedConverter (Type target) { - if (!target.IsGenericType) - return null; - - var genericDef = target.GetGenericTypeDefinition (); - var typeArgs = target.GetGenericArguments (); - - if (genericDef == typeof (IList<>) && typeArgs.Length == 1) { - var factory = TryGetContainerFactory (typeArgs [0]); + if (TryGetSingleGenericArgument (target, typeof (IList<>), typeof (JavaList<>), out var listElementType)) { + var factory = TryGetContainerFactory (listElementType); if (factory != null) return (h, t) => factory.CreateList (h, t); } - if (genericDef == typeof (ICollection<>) && typeArgs.Length == 1) { - var factory = TryGetContainerFactory (typeArgs [0]); + if (TryGetSingleGenericArgument (target, typeof (ICollection<>), typeof (JavaCollection<>), out var collectionElementType)) { + var factory = TryGetContainerFactory (collectionElementType); if (factory != null) return (h, t) => factory.CreateCollection (h, t); } - if (genericDef == typeof (IDictionary<,>) && typeArgs.Length == 2) { + if (TryGetDictionaryArguments (target, out var typeArgs)) { var keyFactory = TryGetContainerFactory (typeArgs [0]); var valueFactory = TryGetContainerFactory (typeArgs [1]); if (keyFactory != null && valueFactory != null) @@ -141,18 +148,45 @@ params Type [] typeArguments } return null; - } - static JavaPeerContainerFactory? TryGetContainerFactory (Type elementType) - { - if (!typeof (IJavaPeerable).IsAssignableFrom (elementType)) - return null; + static bool TryGetSingleGenericArgument (Type target, Type interfaceType, Type wrapperType, [NotNullWhen (true)] out Type? argument) + { + if (target.IsGenericType && !target.IsGenericTypeDefinition) { + var genericDef = target.GetGenericTypeDefinition (); + if (genericDef == interfaceType || genericDef == wrapperType) { + argument = target.GetGenericArguments () [0]; + return true; + } + } - if (RuntimeFeature.TrimmableTypeMap) { - return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + argument = null; + return false; } - return null; + static bool TryGetDictionaryArguments (Type target, [NotNullWhen (true)] out Type []? arguments) + { + if (target.IsGenericType && !target.IsGenericTypeDefinition) { + var genericDef = target.GetGenericTypeDefinition (); + if (genericDef == typeof (IDictionary<,>) || genericDef == typeof (JavaDictionary<,>)) { + arguments = target.GetGenericArguments (); + return true; + } + } + + arguments = null; + return false; + } + + static JavaPeerContainerFactory? TryGetContainerFactory (Type elementType) + { + if (ScalarContainerFactories.TryGetValue (elementType, out var scalarFactory)) + return scalarFactory; + + if (typeof (IJavaPeerable).IsAssignableFrom (elementType)) + return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + + return null; + } } static Func GetJniHandleConverterForType ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index 04995ea206c..c17728d5259 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -4,7 +4,6 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Android.Runtime; namespace Java.Interop @@ -15,6 +14,8 @@ namespace Java.Interop /// public abstract class JavaPeerContainerFactory { + private protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + /// /// Creates a typed JavaList<T> from a JNI handle. /// @@ -36,9 +37,8 @@ public abstract class JavaPeerContainerFactory /// Visitor callback invoked by the value factory's . /// Override in to provide both type parameters. /// - internal virtual IDictionary? CreateDictionaryWithValueFactory ( + internal virtual IDictionary? CreateDictionaryWithValueFactory<[DynamicallyAccessedMembers (Constructors)] TValue> ( JavaPeerContainerFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) - where TValue : class, IJavaPeerable => null; /// @@ -47,20 +47,15 @@ public abstract class JavaPeerContainerFactory public static JavaPeerContainerFactory Create< [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T - > () where T : class, IJavaPeerable + > () => JavaPeerContainerFactory.Instance; } /// /// Typed container factory. All creation uses direct new expressions — fully AOT-safe. /// - /// The Java peer element type. - public sealed class JavaPeerContainerFactory< - // TODO (https://github.com/dotnet/android/issues/10794): Remove this DAM annotation — it preserves too much reflection metadata on all types in the typemap. - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - T - > : JavaPeerContainerFactory - where T : class, IJavaPeerable + /// The container element type. + public sealed class JavaPeerContainerFactory<[DynamicallyAccessedMembers (Constructors)] T> : JavaPeerContainerFactory { internal static readonly JavaPeerContainerFactory Instance = new (); @@ -75,10 +70,8 @@ internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnershi internal override IDictionary? CreateDictionary (JavaPeerContainerFactory keyFactory, IntPtr handle, JniHandleOwnership transfer) => keyFactory.CreateDictionaryWithValueFactory (this, handle, transfer); - #pragma warning disable IL2091 // DynamicallyAccessedMembers on base method type parameter cannot be repeated on override in C# - internal override IDictionary? CreateDictionaryWithValueFactory ( + internal override IDictionary? CreateDictionaryWithValueFactory<[DynamicallyAccessedMembers (Constructors)] TValue> ( JavaPeerContainerFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) => new Android.Runtime.JavaDictionary (handle, transfer); - #pragma warning restore IL2091 } } diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index fec5337d347..f5fb4c90d38 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -157,4 +157,38 @@ protected JavaPeerProxy ( public override JavaPeerContainerFactory GetContainerFactory () => JavaPeerContainerFactory.Instance; } + + /// + /// Base attribute class for generated array-proxy types that enable AOT-safe construction + /// of managed arrays for a specific element type and rank, without + /// or other reflection-based allocation. + /// + /// + /// Like , each generated array proxy is applied to its own holder + /// type (self-application pattern), so the runtime can retrieve it via + /// GetCustomAttribute<JavaArrayProxy>() and invoke it without + /// Activator.CreateInstance(). The per-rank TypeMap groups + /// (__ArrayMapRank{N}) map a JNI name to the holder type carrying this attribute; + /// TrimmableTypeMap.TryGetArrayProxy resolves the holder and returns the attribute. + /// + [AttributeUsage (AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + public abstract class JavaArrayProxy : Attribute + { + /// + /// Gets the .NET array and wrapper types associated with this proxy (for example + /// T[], JavaArray<T>, and the matching JavaObjectArray<T> / + /// JavaPrimitiveArray<T> wrappers). Emitting these tokens + /// roots the types so the trimmer/ILC keeps them available for marshaling. + /// + /// The array and wrapper types handled by this proxy. + public abstract Type[] GetArrayTypes (); + + /// + /// Creates a new managed array of this proxy's element type and rank using a rooted + /// newarr, which is AOT-safe unlike . + /// + /// The length of the outermost array dimension. + /// A new array of the proxy's element type with the requested length. + public abstract Array CreateManagedArray (int length); + } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs index 0374570f12a..5c5a5b71864 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs @@ -42,15 +42,15 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr return false; } - public bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType) + public bool TryGetArrayProxyType (string managedTypeKey, int rankIndex, [NotNullWhen (true)] out Type? proxyType) { foreach (var universe in _universes) { - if (universe.TryGetArrayType (jniName, rankIndex, out arrayType)) { + if (universe.TryGetArrayProxyType (managedTypeKey, rankIndex, out proxyType)) { return true; } } - arrayType = null; + proxyType = null; return false; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs index 8aa230d273f..1aa51fd1362 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ITypeMap.cs @@ -26,7 +26,7 @@ interface ITypeMap bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType); /// - /// Resolves a JNI leaf name and 0-based array rank index to a managed array type. + /// Resolves a managed element type key and 0-based array rank index to a generated array proxy type. /// - bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType); + bool TryGetArrayProxyType (string managedTypeKey, int rankIndex, [NotNullWhen (true)] out Type? proxyType); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs new file mode 100644 index 00000000000..77cd34fc30f --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/PrimitiveArrayInfo.cs @@ -0,0 +1,224 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +static class PrimitiveArrayInfo +{ + delegate TArray PrimitiveArrayFactory (ref JniObjectReference reference, JniObjectReferenceOptions options); + + abstract class Handler + { + public abstract bool TryGetArrayTypes (Type elementType, [NotNullWhen (true)] out Type[]? arrayTypes); + + public abstract bool TryGetTypeSignature (Type type, out JniTypeSignature signature); + + public abstract bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType, + [NotNullWhen (true)] out object? value); + + public abstract bool TryCreateObjectReference (object value, out JniObjectReference reference); + } + + sealed class Handler : Handler + where T : struct + where TArray : JavaArray + { + readonly string jniSimpleReference; + readonly PrimitiveArrayFactory createFromReference; + readonly Func, TArray> createCopy; + + public Handler (string jniSimpleReference, PrimitiveArrayFactory createFromReference, Func, TArray> createCopy) + { + this.jniSimpleReference = jniSimpleReference; + this.createFromReference = createFromReference; + this.createCopy = createCopy; + } + + public override bool TryGetArrayTypes (Type elementType, [NotNullWhen (true)] out Type[]? arrayTypes) + { + if (typeof (T) != elementType) { + arrayTypes = null; + return false; + } + + arrayTypes = [typeof (T[]), typeof (JavaArray), typeof (JavaPrimitiveArray), typeof (TArray)]; + return true; + } + + public override bool TryGetTypeSignature (Type type, out JniTypeSignature signature) + { + if (IsArrayType (type)) { + signature = new JniTypeSignature (jniSimpleReference, arrayRank: 1, keyword: true); + return true; + } + + signature = default; + return false; + } + + public override bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType, + [NotNullWhen (true)] out object? value) + { + if (!IsTargetType (targetType)) { + value = null; + return false; + } + + var array = createFromReference (ref reference, options); + if (targetType == typeof (T[]) || IsCompatibleListType (targetType)) { + try { + value = array.ToArray (); + return true; + } finally { + array.Dispose (); + } + } + + value = array; + return true; + } + + public override bool TryCreateObjectReference (object value, out JniObjectReference reference) + { + if (value is TArray array) { + reference = array.PeerReference.IsValid + ? array.PeerReference.NewLocalRef () + : new JniObjectReference (); + return true; + } + + if (value is not IList list) { + reference = new JniObjectReference (); + return false; + } + + var marshaledArray = createCopy (list); + try { + reference = marshaledArray.PeerReference.IsValid + ? marshaledArray.PeerReference.NewLocalRef () + : new JniObjectReference (); + return true; + } finally { + marshaledArray.Dispose (); + } + } + + bool IsTargetType (Type targetType) + { + return IsArrayType (targetType) || + IsCompatibleListType (targetType); + } + + static bool IsArrayType (Type targetType) + { + return targetType == typeof (JavaArray) || + targetType == typeof (JavaPrimitiveArray) || + targetType == typeof (TArray) || + targetType == typeof (T[]); + } + + static bool IsCompatibleListType (Type targetType) + { + return targetType.IsGenericType && + targetType.GetGenericTypeDefinition () == typeof (IList<>) && + targetType.IsAssignableFrom (typeof (IList)); + } + } + + static readonly Handler[] Handlers = [ + new Handler ( + "Z", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaBooleanArray (ref h, o), + list => new JavaBooleanArray (list)), + new Handler ( + "B", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSByteArray (ref h, o), + list => new JavaSByteArray (list)), + new Handler ( + "C", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaCharArray (ref h, o), + list => new JavaCharArray (list)), + new Handler ( + "S", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt16Array (ref h, o), + list => new JavaInt16Array (list)), + new Handler ( + "I", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt32Array (ref h, o), + list => new JavaInt32Array (list)), + new Handler ( + "J", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt64Array (ref h, o), + list => new JavaInt64Array (list)), + new Handler ( + "F", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSingleArray (ref h, o), + list => new JavaSingleArray (list)), + new Handler ( + "D", + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaDoubleArray (ref h, o), + list => new JavaDoubleArray (list)), + ]; + + public static bool TryGetArrayTypes (Type elementType, [NotNullWhen (true)] out Type[]? arrayTypes) + { + foreach (var handler in Handlers) { + if (handler.TryGetArrayTypes (elementType, out arrayTypes)) { + return true; + } + } + + arrayTypes = null; + return false; + } + + public static bool TryGetTypeSignature (Type type, out JniTypeSignature signature) + { + foreach (var handler in Handlers) { + if (handler.TryGetTypeSignature (type, out signature)) { + return true; + } + } + + signature = default; + return false; + } + + public static bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type targetType, + [NotNullWhen (true)] out object? value) + { + foreach (var handler in Handlers) { + if (handler.TryCreateWrapper (ref reference, options, targetType, out value)) { + return true; + } + } + + value = null; + return false; + } + + public static bool TryCreateObjectReference (object value, out JniObjectReference reference) + { + foreach (var handler in Handlers) { + if (handler.TryCreateObjectReference (value, out reference)) { + return true; + } + } + + reference = new JniObjectReference (); + return false; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs index a6afdba2bab..2046ec1910e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs @@ -102,17 +102,17 @@ public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? pr return false; } - public bool TryGetArrayType (string jniName, int rankIndex, [NotNullWhen (true)] out Type? arrayType) + public bool TryGetArrayProxyType (string managedTypeKey, int rankIndex, [NotNullWhen (true)] out Type? proxyType) { foreach (var arrayMapsByRank in _arrayMapsByUniverseAndRank) { if ((uint)rankIndex < (uint)arrayMapsByRank.Length && arrayMapsByRank [rankIndex] is { } dict && - dict.TryGetValue (jniName, out arrayType)) { + dict.TryGetValue (managedTypeKey, out proxyType)) { return true; } } - arrayType = null; + proxyType = null; return false; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 915daa0e248..145d253050d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -21,6 +21,7 @@ public class TrimmableTypeMap { static readonly Lock s_initLock = new (); static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); + static readonly JavaArrayProxy s_noArrayProxySentinel = new MissingJavaArrayProxy (); static TrimmableTypeMap? s_instance; static bool s_nativeMethodsRegistered; static JniMethodInfo? s_classGetInterfacesMethod; @@ -31,6 +32,7 @@ public class TrimmableTypeMap readonly ITypeMap _typeMap; readonly ConcurrentDictionary _proxyCache = new (); + readonly ConcurrentDictionary _arrayProxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); readonly ConcurrentDictionary<(string ClassName, Type TargetType), JavaPeerProxy> _interfaceProxyCache = new (); @@ -509,64 +511,70 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType) return GetProxyForManagedType (type)?.GetContainerFactory (); } - /// AOT-safe lookup of the closed managed array type for the given element type. - internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) + /// Lookup of the generated array proxy after adding array rank to the given element type. + internal bool TryGetArrayProxy (Type elementType, int additionalRank, [NotNullWhen (true)] out JavaArrayProxy? arrayProxy) { - arrayType = null; - - // Walk array nesting to the leaf; rankIndex = depth = (rank - 1). - // Reject multi-dim arrays (byte[,]) — JNI only supports szarrays. - var leaf = elementType; - int rankIndex = 0; - while (leaf.IsArray) { - if (!leaf.IsSZArray) { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero (additionalRank); + + var leafType = elementType; + int rankIndex = additionalRank - 1; + while (leafType.IsArray) { + if (!leafType.IsSZArray) { + arrayProxy = null; return false; } - var next = leaf.GetElementType (); + var next = leafType.GetElementType (); if (next is null) { + arrayProxy = null; return false; } - leaf = next; + leafType = next; rankIndex++; } - bool isPrimitiveLeaf = leaf.IsPrimitive; - string? leafJniName = isPrimitiveLeaf - ? TryGetPrimitiveJniName (leaf, out var p) ? p : null - : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; - - if (leafJniName is not null && _typeMap.TryGetArrayType (leafJniName, rankIndex, out arrayType)) { - return true; + if (!TryGetManagedTypeKey (leafType, out var managedTypeKey)) { + arrayProxy = null; + return false; } - if (isPrimitiveLeaf) { - arrayType = MakePrimitiveArrayType (elementType); - return true; + if (_typeMap.TryGetArrayProxyType (managedTypeKey, rankIndex, out var proxyType)) { + var proxy = _arrayProxyCache.GetOrAdd (proxyType, static type => + type.GetCustomAttribute (inherit: false) ?? s_noArrayProxySentinel); + if (!ReferenceEquals (proxy, s_noArrayProxySentinel)) { + arrayProxy = proxy; + return true; + } } + arrayProxy = null; return false; } - static Type MakePrimitiveArrayType (Type elementType) + static bool TryGetManagedTypeKey (Type type, [NotNullWhen (true)] out string? key) { -#pragma warning disable IL3050 // Primitive array types are runtime intrinsic; no generated generic code is needed. - return elementType.MakeArrayType (); -#pragma warning restore IL3050 + var fullName = type.FullName; + if (fullName is null) { + key = null; + return false; + } + + var assemblyName = GetAssemblyNameForManagedTypeKey (type); + if (assemblyName is null) { + key = null; + return false; + } + + key = $"{fullName}, {assemblyName}"; + return true; } - /// JNI single-letter encoding for primitive element types. - static bool TryGetPrimitiveJniName (Type primitive, [NotNullWhen (true)] out string? jni) + static string? GetAssemblyNameForManagedTypeKey (Type type) { - if (primitive == typeof (bool)) { jni = "Z"; return true; } - if (primitive == typeof (byte)) { jni = "B"; return true; } - if (primitive == typeof (char)) { jni = "C"; return true; } - if (primitive == typeof (short)) { jni = "S"; return true; } - if (primitive == typeof (int)) { jni = "I"; return true; } - if (primitive == typeof (long)) { jni = "J"; return true; } - if (primitive == typeof (float)) { jni = "F"; return true; } - if (primitive == typeof (double)) { jni = "D"; return true; } - jni = null; - return false; + if (type.IsPrimitive || type == typeof (string)) { + return "System.Runtime"; + } + + return type.Assembly.GetName ().Name; } [UnmanagedCallersOnly] @@ -612,4 +620,11 @@ public MissingJavaPeerProxy () : base ("", typeof (Java.Lang.Object), n public override IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) => null; } + sealed class MissingJavaArrayProxy : JavaArrayProxy + { + public override Type[] GetArrayTypes () => []; + + public override Array CreateManagedArray (int length) => throw new NotSupportedException (); + } + } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index f8013325628..638b4c19d43 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -367,6 +367,7 @@ + diff --git a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt index a42c999affc..75505207238 100644 --- a/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-35/PublicAPI.Unshipped.txt @@ -25,3 +25,6 @@ REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.SetupRequest(System.Net REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.WriteRequestContentToOutput(System.Net.Http.HttpRequestMessage! request, Java.Net.HttpURLConnection! httpConnection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Java.Interop.JavaArrayProxy +abstract Java.Interop.JavaArrayProxy.CreateManagedArray(int length) -> System.Array! +abstract Java.Interop.JavaArrayProxy.GetArrayTypes() -> System.Type![]! diff --git a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt index a42c999affc..75505207238 100644 --- a/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36.1/PublicAPI.Unshipped.txt @@ -25,3 +25,6 @@ REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.SetupRequest(System.Net REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.WriteRequestContentToOutput(System.Net.Http.HttpRequestMessage! request, Java.Net.HttpURLConnection! httpConnection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Java.Interop.JavaArrayProxy +abstract Java.Interop.JavaArrayProxy.CreateManagedArray(int length) -> System.Array! +abstract Java.Interop.JavaArrayProxy.GetArrayTypes() -> System.Type![]! diff --git a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt index 43ea83561b1..4def2ad9232 100644 --- a/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-36/PublicAPI.Unshipped.txt @@ -4401,3 +4401,6 @@ virtual Java.Util.IdentityHashMap.Remove(Java.Lang.Object? key, Java.Lang.Object virtual Java.Util.IdentityHashMap.Replace(Java.Lang.Object? key, Java.Lang.Object? oldValue, Java.Lang.Object? newValue) -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Java.Interop.JavaArrayProxy +abstract Java.Interop.JavaArrayProxy.CreateManagedArray(int length) -> System.Array! +abstract Java.Interop.JavaArrayProxy.GetArrayTypes() -> System.Type![]! diff --git a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt index a42c999affc..75505207238 100644 --- a/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt +++ b/src/Mono.Android/PublicAPI/API-37/PublicAPI.Unshipped.txt @@ -25,3 +25,6 @@ REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.SetupRequest(System.Net REMOVED virtual Xamarin.Android.Net.AndroidClientHandler.WriteRequestContentToOutput(System.Net.Http.HttpRequestMessage! request, Java.Net.HttpURLConnection! httpConnection, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.get -> bool Android.App.ApplicationAttribute.EnableOnBackInvokedCallback.set -> void +Java.Interop.JavaArrayProxy +abstract Java.Interop.JavaArrayProxy.CreateManagedArray(int length) -> System.Array! +abstract Java.Interop.JavaArrayProxy.GetArrayTypes() -> System.Type![]! 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 6d92d889def..2c9f84617b0 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 @@ -404,19 +404,26 @@ public void BuildHasNoWarnings (bool isRelease, bool multidex, string packageFor Assert.IsTrue (b.Build (proj), "Build should have succeeded."); if (runtime == AndroidRuntime.NativeAOT) { - // NativeAOT currently (Nov 2025) produces 10 `ILC : AOT analysis warning IL3050` warnings for various + // NativeAOT currently (June 2026) produces 8 `ILC : AOT analysis warning IL3050` warnings for various // bits of code. 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 = 10; + int numberOfExpectedWarnings = 8; + + // 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.IsTrue ( - StringAssertEx.ContainsText ( - b.LastBuildOutput, - $" {numberOfExpectedWarnings} Warning(s)" - ), - $"{b.BuildLogFile} should have exactly {numberOfExpectedWarnings} MSBuild warnings for NativeAOT." - ); + 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)); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index b3eba523be1..15179bf8d9c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -575,10 +575,10 @@ public void MergeCrossAssemblyAliases_SameManagedName_ProducesCorrectAliasGroup // 3 indexed entries + 1 base entry = 4 Assert.Equal (4, model.Entries.Count); - Assert.Equal ("java/lang/Throwable[0]", model.Entries [0].JniName); - Assert.Equal ("java/lang/Throwable[1]", model.Entries [1].JniName); - Assert.Equal ("java/lang/Throwable[2]", model.Entries [2].JniName); - Assert.Equal ("java/lang/Throwable", model.Entries [3].JniName); + Assert.Equal ("java/lang/Throwable[0]", model.Entries [0].MapKey); + Assert.Equal ("java/lang/Throwable[1]", model.Entries [1].MapKey); + Assert.Equal ("java/lang/Throwable[2]", model.Entries [2].MapKey); + Assert.Equal ("java/lang/Throwable", model.Entries [3].MapKey); // Exactly 1 alias holder Assert.Single (model.AliasHolders); @@ -592,7 +592,7 @@ public void MergeCrossAssemblyAliases_SameManagedName_ProducesCorrectAliasGroup Assert.Equal (3, model.Associations.Count); // The bare "java/lang/Throwable" key appears exactly once — no duplicates - Assert.Single (model.Entries, e => e.JniName == "java/lang/Throwable"); + Assert.Single (model.Entries, e => e.MapKey == "java/lang/Throwable"); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 0a19aa073ac..9c1a9f9865c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -56,8 +56,8 @@ public void Build_CreatesOneEntryPerPeer () var model = BuildModel (peers); Assert.Equal (2, model.Entries.Count); - Assert.Equal ("android/app/Activity", model.Entries [0].JniName); - Assert.Equal ("java/lang/Object", model.Entries [1].JniName); + Assert.Equal ("android/app/Activity", model.Entries [0].MapKey); + Assert.Equal ("java/lang/Object", model.Entries [1].MapKey); } [Fact] @@ -71,11 +71,11 @@ public void Build_DuplicateJniNames_CreatesAliasEntries () var model = BuildModel (peers); // Three entries: "test/Dup[0]", "test/Dup[1]", and the base "test/Dup" → alias holder Assert.Equal (3, model.Entries.Count); - Assert.Equal ("test/Dup[0]", model.Entries [0].JniName); + Assert.Equal ("test/Dup[0]", model.Entries [0].MapKey); Assert.Contains ("Test.First", model.Entries [0].ProxyTypeReference); - Assert.Equal ("test/Dup[1]", model.Entries [1].JniName); + Assert.Equal ("test/Dup[1]", model.Entries [1].MapKey); Assert.Contains ("Test.Second", model.Entries [1].ProxyTypeReference); - Assert.Equal ("test/Dup", model.Entries [2].JniName); + Assert.Equal ("test/Dup", model.Entries [2].MapKey); // Both peers get associations to the alias holder Assert.Equal (2, model.Associations.Count); @@ -97,10 +97,10 @@ public void Build_ThreeWayAlias_CreatesCorrectIndexedEntries () var model = BuildModel (peers, "TripleAlias"); // 3 indexed entries + 1 base entry → alias holder = 4 Assert.Equal (4, model.Entries.Count); - Assert.Equal ("test/Triple[0]", model.Entries [0].JniName); - Assert.Equal ("test/Triple[1]", model.Entries [1].JniName); - Assert.Equal ("test/Triple[2]", model.Entries [2].JniName); - Assert.Equal ("test/Triple", model.Entries [3].JniName); + Assert.Equal ("test/Triple[0]", model.Entries [0].MapKey); + Assert.Equal ("test/Triple[1]", model.Entries [1].MapKey); + Assert.Equal ("test/Triple[2]", model.Entries [2].MapKey); + Assert.Equal ("test/Triple", model.Entries [3].MapKey); // All three peers get associations to the alias holder Assert.Equal (3, model.Associations.Count); @@ -124,9 +124,9 @@ public void Build_AliasWithMixedActivation_PrimaryNoActivation_AliasHasActivatio var model = BuildModel (peers, "MixedAlias"); // 2 indexed entries + 1 base entry → alias holder = 3 Assert.Equal (3, model.Entries.Count); - Assert.Equal ("test/Mixed[0]", model.Entries [0].JniName); - Assert.Equal ("test/Mixed[1]", model.Entries [1].JniName); - Assert.Equal ("test/Mixed", model.Entries [2].JniName); + Assert.Equal ("test/Mixed[0]", model.Entries [0].MapKey); + Assert.Equal ("test/Mixed[1]", model.Entries [1].MapKey); + Assert.Equal ("test/Mixed", model.Entries [2].MapKey); // Only the alias peer with activation gets a proxy Assert.Single (model.ProxyTypes); @@ -147,7 +147,7 @@ public void Build_AllMcwAliasGroup_BaseEntryIsConditional () }; var model = BuildModel (peers); - var baseEntry = model.Entries.Single (e => e.JniName == "test/AllMcw"); + var baseEntry = model.Entries.Single (e => e.MapKey == "test/AllMcw"); Assert.False (baseEntry.IsUnconditional, "All-MCW alias group base entry should be conditional"); Assert.NotNull (baseEntry.TargetTypeReference); } @@ -163,7 +163,7 @@ public void Build_MixedAcwMcwAliasGroup_BaseEntryIsUnconditional () }; var model = BuildModel (peers); - var baseEntry = model.Entries.Single (e => e.JniName == "test/Mixed"); + var baseEntry = model.Entries.Single (e => e.MapKey == "test/Mixed"); Assert.True (baseEntry.IsUnconditional, "Mixed alias group with ACW should have unconditional base entry"); Assert.Null (baseEntry.TargetTypeReference); } @@ -179,7 +179,7 @@ public void Build_EssentialTypeAliasGroup_BaseEntryIsUnconditional () }; var model = BuildModel (peers); - var baseEntry = model.Entries.Single (e => e.JniName == "java/lang/Object"); + var baseEntry = model.Entries.Single (e => e.MapKey == "java/lang/Object"); Assert.True (baseEntry.IsUnconditional, "Essential type alias group should have unconditional base entry"); Assert.Null (baseEntry.TargetTypeReference); } @@ -211,7 +211,7 @@ public void Build_UserAcwType_IsUnconditional () var peer = MakeAcwPeer ("my/app/Main", "MyApp.MainActivity", "App"); var model = BuildModel (new [] { peer }); - var mainEntry = model.Entries.First (e => e.JniName == "my/app/Main"); + var mainEntry = model.Entries.First (e => e.MapKey == "my/app/Main"); Assert.True (mainEntry.IsUnconditional); Assert.Null (mainEntry.TargetTypeReference); } @@ -224,7 +224,7 @@ public void Build_FrameworkAcwType_IsTrimmable () }; var model = BuildModel (new [] { peer }); - var entry = model.Entries.First (e => e.JniName == "mono/android/view/View_OnClickListenerImplementor"); + var entry = model.Entries.First (e => e.MapKey == "mono/android/view/View_OnClickListenerImplementor"); Assert.False (entry.IsUnconditional); Assert.Equal ("Android.Views.View+IOnClickListenerImplementor, Mono.Android", entry.TargetTypeReference); } @@ -389,7 +389,7 @@ public void Build_FromScannedFixtures_ProducesValidModel () Assert.NotEmpty (model.Entries); Assert.NotEmpty (model.ProxyTypes); - Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.JniName))); + Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.MapKey))); Assert.All (model.Entries, e => Assert.False (string.IsNullOrEmpty (e.ProxyTypeReference))); } @@ -437,9 +437,9 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) return model.ProxyTypes.FirstOrDefault (p => p.TypeName == proxyTypeName); } - static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string jniName) + static TypeMapAttributeData? FindEntry (TypeMapAssemblyData model, string mapKey) { - return model.Entries.FirstOrDefault (e => e.JniName == jniName); + return model.Entries.FirstOrDefault (e => e.MapKey == mapKey); } public class FixtureMcwTypes @@ -556,7 +556,7 @@ public void Fixture_InterfaceAndInvoker_ShareJniName_InvokerSeparated () // Invoker is excluded from TypeMap entries/proxies. It still gets a // managed→proxy association so its JniPeerMembers can resolve the JNI name. Assert.Single (model.Entries); - Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].JniName); + Assert.Equal ("android/view/View$OnClickListener", model.Entries [0].MapKey); // Only the interface proxy exists; the invoker type is also referenced // as a TypeRef in the interface proxy's InvokerType property. @@ -616,10 +616,10 @@ public void Fixture_AliasTarget_ProducesIndexedEntries () // 3 indexed entries + 1 base entry → alias holder = 4 Assert.Equal (4, model.Entries.Count); - Assert.Equal ("test/AliasTarget[0]", model.Entries [0].JniName); - Assert.Equal ("test/AliasTarget[1]", model.Entries [1].JniName); - Assert.Equal ("test/AliasTarget[2]", model.Entries [2].JniName); - Assert.Equal ("test/AliasTarget", model.Entries [3].JniName); + Assert.Equal ("test/AliasTarget[0]", model.Entries [0].MapKey); + Assert.Equal ("test/AliasTarget[1]", model.Entries [1].MapKey); + Assert.Equal ("test/AliasTarget[2]", model.Entries [2].MapKey); + Assert.Equal ("test/AliasTarget", model.Entries [3].MapKey); } [Fact] @@ -875,12 +875,12 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () var attrs = ReadAllTypeMapAttributeBlobs (reader); Assert.Equal (2, attrs.Count); - var objectEntry = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); - Assert.NotNull (objectEntry.jniName); + var objectEntry = attrs.FirstOrDefault (a => a.mapKey == "java/lang/Object"); + Assert.NotNull (objectEntry.mapKey); Assert.Null (objectEntry.targetRef); - var activityEntry = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); - Assert.NotNull (activityEntry.jniName); + var activityEntry = attrs.FirstOrDefault (a => a.mapKey == "android/app/Activity"); + Assert.NotNull (activityEntry.mapKey); Assert.Equal ("Android.App.Activity, TestFixtures", activityEntry.targetRef); }); } @@ -896,9 +896,9 @@ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, Assert.True (model.Entries [0].IsUnconditional); EmitAndVerify (model, assemblyName, (pe, reader) => { - var (jniName2, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + var (mapKey2, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); - Assert.Equal (javaName, jniName2); + Assert.Equal (javaName, mapKey2); Assert.NotNull (proxyRef); Assert.Contains (expectedProxyName, proxyRef!); Assert.Null (targetRef); @@ -914,9 +914,9 @@ public void FullPipeline_McwBinding_Emits3ArgAttribute () Assert.False (model.Entries [0].IsUnconditional); EmitAndVerify (model, "Blob3ArgConditional", (pe, reader) => { - var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); + var (mapKey, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); - Assert.Equal ("android/app/Activity", jniName); + Assert.Equal ("android/app/Activity", mapKey); Assert.NotNull (proxyRef); Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); Assert.Equal ("Android.App.Activity, TestFixtures", targetRef); @@ -936,7 +936,7 @@ public void Build_SameInput_ProducesDeterministicOutput () Assert.Equal (model1.Entries.Count, model2.Entries.Count); for (int i = 0; i < model1.Entries.Count; i++) { - Assert.Equal (model1.Entries [i].JniName, model2.Entries [i].JniName); + Assert.Equal (model1.Entries [i].MapKey, model2.Entries [i].MapKey); Assert.Equal (model1.Entries [i].ProxyTypeReference, model2.Entries [i].ProxyTypeReference); Assert.Equal (model1.Entries [i].TargetTypeReference, model2.Entries [i].TargetTypeReference); } @@ -975,7 +975,8 @@ public void Build_EmitArrayEntries_HonoursMaxArrayRank () Assert.Equal (5, model5.MaxArrayRank); var rank5Entries = model5.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (5, rank5Entries.Count); - Assert.Equal ("Foo.Bar[][][][][], App", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy5, TestTypeMap", rank5Entries.Single (e => e.AnchorRank == 5).ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy5, TestTypeMap", rank5Entries.Single (e => e.AnchorRank == 5).TargetTypeReference); var model1 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 1); Assert.Equal (1, model1.MaxArrayRank); @@ -991,46 +992,78 @@ public void Build_EmitArrayEntries_EmitsRanks1Through3 () var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (3, arrayEntries.Count); Assert.Equal (new int? [] { 1, 2, 3 }, arrayEntries.Select (e => e.AnchorRank).ToArray ()); - Assert.All (arrayEntries, e => Assert.Equal ("foo/Bar", e.JniName)); + Assert.All (arrayEntries, e => Assert.Equal ("Foo.Bar, App", e.MapKey)); } [Fact] - public void Build_EmitArrayEntries_KeyIsElementJniName () + public void Build_EmitArrayEntries_KeyIsManagedElementTypeName () { - // No "[L...;" prefix at runtime — the key is the bare element JNI name and rank - // is encoded by which sentinel anchor (TGroup) the entry uses. + // No managed->JNI lookup is needed at runtime — the key is the managed element type name + // and rank is encoded by which sentinel anchor (TGroup) the entry uses. var peer = MakeMcwPeer ("java/lang/String", "System.String", "System.Runtime"); var model = BuildModelWithArrays (new [] { peer }); var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); - Assert.All (arrayEntries, e => Assert.Equal ("java/lang/String", e.JniName)); - Assert.All (arrayEntries, e => Assert.False (e.JniName.StartsWith ("[", StringComparison.Ordinal))); + Assert.All (arrayEntries, e => Assert.Equal ("System.String, System.Runtime", e.MapKey)); + Assert.All (arrayEntries, e => Assert.False (e.MapKey.StartsWith ("[", StringComparison.Ordinal))); } [Fact] - public void Build_EmitArrayEntries_TrimTargetIsClosedArrayType () + public void Build_EmitArrayEntries_MapToGeneratedArrayProxy () { - // 3rd ctor arg = the closed array type itself, so ILC's per-shape conditional - // drops the entry when the array shape is never constructed. var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModelWithArrays (new [] { peer }); var rank1 = model.Entries.Single (e => e.AnchorRank == 1); - Assert.Equal ("Foo.Bar[], App", rank1.ProxyTypeReference); - Assert.Equal ("Foo.Bar[], App", rank1.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap", rank1.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap", rank1.TargetTypeReference); var rank2 = model.Entries.Single (e => e.AnchorRank == 2); - Assert.Equal ("Foo.Bar[][], App", rank2.ProxyTypeReference); - Assert.Equal ("Foo.Bar[][], App", rank2.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap", rank2.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap", rank2.TargetTypeReference); var rank3 = model.Entries.Single (e => e.AnchorRank == 3); - Assert.Equal ("Foo.Bar[][][], App", rank3.ProxyTypeReference); - Assert.Equal ("Foo.Bar[][][], App", rank3.TargetTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, TestTypeMap", rank3.ProxyTypeReference); + Assert.Equal ("_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, TestTypeMap", rank3.TargetTypeReference); + + Assert.Equal (3, model.ArrayProxyTypes.Count); + Assert.Equal ("Foo_Bar_ArrayProxy1", model.ArrayProxyTypes [0].TypeName); + Assert.Equal ("Foo_Bar_ArrayProxy2", model.ArrayProxyTypes [1].TypeName); + Assert.Equal ("Foo_Bar_ArrayProxy3", model.ArrayProxyTypes [2].TypeName); + } + + [Fact] + public void Build_EmitArrayEntries_AssociationsMatchGetArrayTypes () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + var rank1Proxy = "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, TestTypeMap"; + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop" && + a.AliasProxyTypeReference == rank1Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[Foo.Bar, App]], Java.Interop" && + a.AliasProxyTypeReference == rank1Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Foo.Bar[], App" && + a.AliasProxyTypeReference == rank1Proxy); + + var rank2Proxy = "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, TestTypeMap"; + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaObjectArray`1[[Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop]], Java.Interop" && + a.AliasProxyTypeReference == rank2Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[Foo.Bar, App]][], Java.Interop" && + a.AliasProxyTypeReference == rank2Proxy); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Foo.Bar[][], App" && + a.AliasProxyTypeReference == rank2Proxy); } [Fact] public void Build_EmitArrayEntries_AllConditional () { // 2-arg unconditional makes no sense for arrays — the trim conditioning on the - // array shape is the whole point. + // generated array proxy is the whole point. var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); var model = BuildModelWithArrays (new [] { peer }); @@ -1070,7 +1103,7 @@ public void Build_EmitArrayEntries_ReferencedFrameworkPeer_Emitted () var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (3, arrayEntries.Count); - Assert.All (arrayEntries, e => Assert.Equal ("android/widget/Button", e.JniName)); + Assert.All (arrayEntries, e => Assert.Equal ("Android.Widget.Button, Mono.Android", e.MapKey)); } [Fact] @@ -1107,6 +1140,45 @@ public void Build_EmitArrayEntries_PrimitiveJniKeyword_Skipped (string jniKeywor Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); } + [Fact] + public void Build_EmitArrayEntries_PrimitiveEntries_SynthesizedForJavaInteropAssembly () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Java.Interop"); + var model = BuildModelWithArrays (new [] { peer }, assemblyName: "_Java.Interop.TypeMap"); + + var primitiveEntries = model.Entries + .Where (e => e.MapKey.StartsWith ("System.", StringComparison.Ordinal) && e.AnchorRank is not null) + .ToList (); + Assert.Equal (24, primitiveEntries.Count); // 8 primitive keywords × 3 ranks + + 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); + Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy1, _Java.Interop.TypeMap", sbyteRank1.TargetTypeReference); + Assert.False (sbyteRank1.IsUnconditional); + + var sbyteRank2 = primitiveEntries.Single (e => e.MapKey == "System.SByte, System.Runtime" && e.AnchorRank == 2); + Assert.Equal ("_TypeMap.ArrayProxies.Primitive_SByte_ArrayProxy2, _Java.Interop.TypeMap", sbyteRank2.TargetTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaArray`1[[System.SByte, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaPrimitiveArray`1[[System.SByte, System.Runtime]], Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + Assert.Contains (model.Associations, a => + a.SourceTypeReference == "Java.Interop.JavaSByteArray, Java.Interop" && + a.AliasProxyTypeReference == sbyteRank1.ProxyTypeReference); + } + + [Fact] + public void Build_EmitArrayEntries_PrimitiveEntries_NotDuplicatedInOtherAssemblies () + { + var peer = MakeMcwPeer ("java/lang/Object", "Java.Lang.Object", "Java.Interop"); + var model = BuildModelWithArrays (new [] { peer }, assemblyName: "_Mono.Android.TypeMap"); + + Assert.DoesNotContain (model.Entries, e => e.MapKey.StartsWith ("System.", StringComparison.Ordinal) && e.AnchorRank is not null); + Assert.DoesNotContain (model.Associations, a => a.SourceTypeReference == "System.SByte[], System.Runtime"); + } + [Fact] public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () { @@ -1119,8 +1191,8 @@ public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () var arrayEntries = model.Entries.Where (e => e.AnchorRank is not null).ToList (); Assert.Equal (6, arrayEntries.Count); // 2 peers × 3 ranks - foreach (var jni in new [] { "foo/A", "foo/B" }) { - var perPeer = arrayEntries.Where (e => e.JniName == jni).OrderBy (e => e.AnchorRank).ToList (); + foreach (var managedKey in new [] { "Foo.A, App", "Foo.B, App" }) { + var perPeer = arrayEntries.Where (e => e.MapKey == managedKey).OrderBy (e => e.AnchorRank).ToList (); Assert.Equal (3, perPeer.Count); Assert.Equal (new int? [] { 1, 2, 3 }, perPeer.Select (e => e.AnchorRank).ToArray ()); } @@ -1195,12 +1267,30 @@ public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", maxArrayRank: 3); EmitAndVerify (model, "ArrBlobs", (pe, reader) => { - var attrs = ReadAllTypeMapAttributeBlobs (reader); + var arrayAttrs = ReadAllTypeMapAttributeBlobs (reader) + .Select (a => (managedName: a.mapKey, a.proxyRef, a.targetRef)) + .ToList (); - // Three array entries should round-trip with the same JNI key + array trim targets. - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[], App"); - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][], App"); - Assert.Contains (attrs, a => a.jniName == "foo/Bar" && a.targetRef == "Foo.Bar[][][], App"); + // Three array entries should round-trip with the same managed key + generated array proxy refs. + Assert.Contains (arrayAttrs, a => a.managedName == "Foo.Bar, App" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); + Assert.Contains (arrayAttrs, a => a.managedName == "Foo.Bar, App" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy2, ArrBlobs"); + Assert.Contains (arrayAttrs, a => a.managedName == "Foo.Bar, App" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, ArrBlobs" && + a.targetRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy3, ArrBlobs"); + + var assocAttrs = ReadAllTypeMapAssociationAttributeBlobs (reader); + Assert.Contains (assocAttrs, a => + a.groupName.Contains ("__ArrayMapRank1", StringComparison.Ordinal) && + a.sourceRef == "Java.Interop.JavaArray`1[[Foo.Bar, App]], Java.Interop" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); + Assert.Contains (assocAttrs, a => + a.groupName.Contains ("__ArrayMapRank1", StringComparison.Ordinal) && + a.sourceRef == "Java.Interop.JavaObjectArray`1[[Foo.Bar, App]], Java.Interop" && + a.proxyRef == "_TypeMap.ArrayProxies.Foo_Bar_ArrayProxy1, ArrBlobs"); }); } } @@ -1216,10 +1306,10 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio } /// - /// Reads the first TypeMap assembly-level attribute blob and returns (jniName, proxyRef, targetRef). + /// Reads the first TypeMap assembly-level attribute blob and returns (mapKey, proxyRef, targetRef). /// targetRef is null for 2-arg attributes. /// - static (string? jniName, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) + static (string? mapKey, string? proxyRef, string? targetRef) ReadFirstTypeMapAttributeBlob (MetadataReader reader) { var all = ReadAllTypeMapAttributeBlobs (reader); if (all.Count == 0) { @@ -1243,7 +1333,7 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio /// If this logic breaks, the test will either fail to find TypeMap attributes or /// misidentify IgnoresAccessChecksTo as TypeMap — both cause obvious assertion failures. /// - static List<(string? jniName, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) + static List<(string? mapKey, string? proxyRef, string? targetRef)> ReadAllTypeMapAttributeBlobs (MetadataReader reader) { var result = new List<(string?, string?, string?)> (); var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); @@ -1256,12 +1346,18 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio if (ctor.Parent.Kind != HandleKind.TypeSpecification) continue; + var parent = reader.GetTypeSpecification ((TypeSpecificationHandle) ctor.Parent); + var parentName = parent.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + if (!parentName.StartsWith ("System.Runtime.InteropServices.TypeMapAttribute`1", StringComparison.Ordinal)) { + continue; + } + var blobReader = reader.GetBlobReader (attr.Value); ushort prolog = blobReader.ReadUInt16 (); if (prolog != 1) continue; - string? jniName = blobReader.ReadSerializedString (); + string? mapKey = blobReader.ReadSerializedString (); string? proxyRef = blobReader.ReadSerializedString (); // Try to read third arg (target type) — if remaining bytes are just NumNamed (2 bytes), it's 2-arg @@ -1270,11 +1366,40 @@ static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Actio targetRef = blobReader.ReadSerializedString (); } - if (string.IsNullOrEmpty (jniName) || !jniName.Contains ('/')) { + if (string.IsNullOrEmpty (mapKey)) { + continue; + } + + result.Add ((mapKey, proxyRef, targetRef)); + } + return result; + } + + static List<(string groupName, string? sourceRef, string? proxyRef)> ReadAllTypeMapAssociationAttributeBlobs (MetadataReader reader) + { + var result = new List<(string, string?, string?)> (); + var asmAttrs = reader.GetCustomAttributes (EntityHandle.AssemblyDefinition); + foreach (var attrHandle in asmAttrs) { + var attr = reader.GetCustomAttribute (attrHandle); + if (attr.Constructor.Kind != HandleKind.MemberReference) + continue; + + var ctor = reader.GetMemberReference ((MemberReferenceHandle) attr.Constructor); + if (ctor.Parent.Kind != HandleKind.TypeSpecification) + continue; + + var parent = reader.GetTypeSpecification ((TypeSpecificationHandle) ctor.Parent); + var parentName = parent.DecodeSignature (SignatureTypeProvider.Instance, genericContext: null); + if (!parentName.StartsWith ("System.Runtime.InteropServices.TypeMapAssociationAttribute`1", StringComparison.Ordinal)) { continue; } - result.Add ((jniName, proxyRef, targetRef)); + var blobReader = reader.GetBlobReader (attr.Value); + ushort prolog = blobReader.ReadUInt16 (); + if (prolog != 1) + continue; + + result.Add ((parentName, blobReader.ReadSerializedString (), blobReader.ReadSerializedString ())); } return result; } 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 3ccae0da3ad..766ae9f42f5 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 @@ -258,15 +258,38 @@ public void JavaProxyObject_ObjectMethodsUseJavaIdentitySemantics () } [Test] - public void TryGetArrayType_PrimitiveLeaf_DoesNotRequireRankMapEntry () + public void TryGetArrayProxy_ObjectLeaf_ReturnsAllRankTypes () { AssumeTrimmableTypeMapEnabled (); + AssumeGeneratedArrayProxiesEnabled (); - Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayType (typeof (byte), out var byteArrayType)); - Assert.AreEqual (typeof (byte[]), byteArrayType); + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (Java.Lang.Object), additionalRank: 1, out var objectArrayProxy)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray)); + CollectionAssert.Contains (objectArrayProxy.GetArrayTypes (), typeof (Java.Lang.Object[])); - Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayType (typeof (byte[]), out var jaggedByteArrayType)); - Assert.AreEqual (typeof (byte[][]), jaggedByteArrayType); + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (Java.Lang.Object), additionalRank: 2, out var jaggedObjectArrayProxy)); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (JavaObjectArray>)); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray[])); + CollectionAssert.Contains (jaggedObjectArrayProxy.GetArrayTypes (), typeof (Java.Lang.Object[][])); + } + + [Test] + public void TryGetArrayProxy_PrimitiveLeaf_ReturnsAllRankTypes () + { + AssumeTrimmableTypeMapEnabled (); + AssumeGeneratedArrayProxiesEnabled (); + + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (sbyte), additionalRank: 1, out var sbyteArrayProxy)); + CollectionAssert.Contains (sbyteArrayProxy.GetArrayTypes (), typeof (sbyte[])); + CollectionAssert.Contains (sbyteArrayProxy.GetArrayTypes (), typeof (Java.Interop.JavaArray)); + CollectionAssert.Contains (sbyteArrayProxy.GetArrayTypes (), typeof (JavaPrimitiveArray)); + CollectionAssert.Contains (sbyteArrayProxy.GetArrayTypes (), typeof (JavaSByteArray)); + + Assert.IsTrue (TrimmableTypeMap.Instance.TryGetArrayProxy (typeof (sbyte), additionalRank: 2, out var jaggedSbyteArrayProxy)); + CollectionAssert.Contains (jaggedSbyteArrayProxy.GetArrayTypes (), typeof (sbyte[][])); + CollectionAssert.Contains (jaggedSbyteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray>)); + CollectionAssert.Contains (jaggedSbyteArrayProxy.GetArrayTypes (), typeof (JavaObjectArray)); } static ConcurrentDictionary GetProxyCache (TrimmableTypeMap instance) @@ -292,6 +315,13 @@ static IReadOnlyList GetStaticMethodFallbackTypes (TestableTrimmableType return fallbacks ?? throw new InvalidOperationException ("Expected fallback types."); } + static void AssumeGeneratedArrayProxiesEnabled () + { + if (!RuntimeFeature.IsNativeAotRuntime && System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { + Assert.Ignore ("Generated array proxies are only emitted when dynamic code is unavailable."); + } + } + static void AssumeTrimmableTypeMapEnabled () { if (!RuntimeFeature.TrimmableTypeMap) {