diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 679423576f2..3bc2ccd9464 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -40,6 +40,12 @@ sealed class TypeMapAssemblyData /// public List AliasHolders { get; } = new (); + /// + /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} + /// sentinel TypeDefs and TypeMap entries. 0 disables. + /// + public int MaxArrayRank { get; set; } + /// /// Assembly names that need [IgnoresAccessChecksTo] for cross-assembly n_* calls. /// @@ -77,6 +83,12 @@ sealed record TypeMapAttributeData /// True for 2-arg unconditional entries (ACW types, essential runtime types). /// public bool IsUnconditional => TargetTypeReference == null; + + /// + /// 1-based array rank when this entry 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 79570a14bda..6c687784fc3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -40,7 +40,11 @@ static class ModelBuilder /// Scanned Java peer types (typically from a single input assembly). /// Output .dll path — used to derive assembly/module names if not specified. /// Explicit assembly name. If null, derived from . - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null) + /// + /// Emit per-rank array TypeMap entries + __ArrayMapRank{N} sentinels + /// for ranks 1... 0 disables array entry emission. + /// + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -48,13 +52,16 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (outputPath is null) { throw new ArgumentNullException (nameof (outputPath)); } + if (maxArrayRank < 0) { + throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); + } assemblyName ??= Path.GetFileNameWithoutExtension (outputPath); - string moduleName = Path.GetFileName (outputPath); var model = new TypeMapAssemblyData { AssemblyName = assemblyName, - ModuleName = moduleName, + ModuleName = Path.GetFileName (outputPath), + MaxArrayRank = maxArrayRank, }; // Invoker types are NOT emitted as separate proxies or TypeMap entries — @@ -89,6 +96,10 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); + + if (maxArrayRank > 0) { + EmitArrayEntries (model, jniName, peersForName, maxArrayRank); + } } // Compute IgnoresAccessChecksTo from cross-assembly references @@ -392,4 +403,54 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr static string AssemblyQualify (string typeName, string assemblyName) => $"{typeName}, {assemblyName}"; + + /// + /// Emits per-rank [L<jni>;-shaped TypeMap entries for one peer, anchored to + /// the per-assembly __ArrayMapRank{N} sentinels. Skips open generics, primitive + /// JNI keyword keys (handled by the legacy primitive-array path), 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; + } + + var peer = peersForName [0]; + if (peer.IsGenericDefinition) { + return; + } + + for (int rank = 1; rank <= maxArrayRank; rank++) { + string arrayTypeRef = AssemblyQualify (peer.ManagedTypeName + Brackets (rank), peer.AssemblyName); + model.Entries.Add (new TypeMapAttributeData { + JniName = jniName, + ProxyTypeReference = arrayTypeRef, + TargetTypeReference = arrayTypeRef, + AnchorRank = rank, + }); + } + } + + static string Brackets (int rank) => rank switch { + 1 => "[]", + 2 => "[][]", + 3 => "[][][]", + _ => BuildBrackets (rank), + }; + + static string BuildBrackets (int rank) + { + var sb = new StringBuilder (rank * 2); + for (int i = 0; i < rank; i++) { + sb.Append ("[]"); + } + return sb.ToString (); + } + + static bool IsJniPrimitiveKeyword (char c) + => c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I' + || c == 'J' || c == 'F' || c == 'D' || c == 'V'; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index d6b7bae9a22..9f3f0d60b88 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -72,7 +72,12 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// Stream to write the output PE to. /// Optional assembly name (defaults to _Microsoft.Android.TypeMaps). /// Optional module name for the PE metadata. - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null) + /// + /// Maximum array rank for which per-assembly typemaps emitted __ArrayMapRank{N} + /// sentinels. Must match the value passed to the per-assembly generators. 0 means + /// no array sentinels were emitted; the loader uses the 2-arg Initialize. + /// + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -112,8 +117,9 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha EmitAssemblyTargetAttributes (pe, anchorTypeHandle, perAssemblyTypeMapNames); // Emit [assembly: IgnoresAccessChecksTo("...")] so TypeMapLoader.Initialize() can access - // internal types (SingleUniverseTypeMap, AggregateTypeMap in Mono.Android, - // and __TypeMapAnchor in each per-assembly typemap DLL). + // internal types (TrimmableTypeMap and friends in Mono.Android, and __TypeMapAnchor + // in each per-assembly typemap DLL when in aggregate mode). Shared rank anchors + // (__ArrayMapRank{N}) live in Mono.Android already. var accessTargets = new List { "Mono.Android" }; if (!useSharedTypemapUniverse) { accessTargets.AddRange (perAssemblyTypeMapNames); @@ -121,7 +127,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank); pe.WritePE (stream); } @@ -145,7 +151,7 @@ static void EmitAssemblyTargetAttributes (PEAssemblyBuilder pe, EntityHandle anc } } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank) { var metadata = pe.Metadata; @@ -180,49 +186,32 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); + var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + if (useSharedTypemapUniverse) { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary, IReadOnlyDictionary) - var initializeRef = AddInitializeSingleRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef); + var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, + initializeRef, externalDictTypeSpec, maxArrayRank); } else { - // TrimmableTypeMap.Initialize(IReadOnlyDictionary[], IReadOnlyDictionary[]) - var initializeRef = AddInitializeAggregateRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - var externalDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef); + EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank); } } - static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, - MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, - MemberReferenceHandle initializeRef) - { - var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); - var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); - - pe.EmitBody ("Initialize", - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), - encoder => { - // TypeMapping.GetOrCreateExternalTypeMapping<__TypeMapAnchor>() - encoder.OpCode (ILOpCode.Call); - encoder.Token (getExternalSpec); - // TypeMapping.GetOrCreateProxyTypeMapping<__TypeMapAnchor>() - encoder.OpCode (ILOpCode.Call); - encoder.Token (getProxySpec); - // TrimmableTypeMap.Initialize(typeMap, proxyMap) - encoder.OpCode (ILOpCode.Call); - encoder.Token (initializeRef); - encoder.OpCode (ILOpCode.Ret); - }); - } - + /// + /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either a + /// flat arrayMapsByRank[maxArrayRank] from shared __ArrayMapRank{N} + /// anchors or null when is 0. + /// static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, IReadOnlyList perAssemblyTypeMapNames, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, + int maxArrayRank) { var count = perAssemblyTypeMapNames.Count; @@ -240,54 +229,148 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), encoder => { - // var typeMaps = new IReadOnlyDictionary[N]; - encoder.LoadConstantI4 (count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (externalDictTypeSpec); - encoder.OpCode (ILOpCode.Stloc_0); - - for (int i = 0; i < count; i++) { - encoder.OpCode (ILOpCode.Ldloc_0); - encoder.LoadConstantI4 (i); - encoder.OpCode (ILOpCode.Call); - encoder.Token (getExternalSpecs [i]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // var proxyMaps = new IReadOnlyDictionary[N]; - encoder.LoadConstantI4 (count); - encoder.OpCode (ILOpCode.Newarr); - encoder.Token (proxyDictTypeSpec); - encoder.OpCode (ILOpCode.Stloc_1); - - for (int i = 0; i < count; i++) { - encoder.OpCode (ILOpCode.Ldloc_1); - encoder.LoadConstantI4 (i); - encoder.OpCode (ILOpCode.Call); - encoder.Token (getProxySpecs [i]); - encoder.OpCode (ILOpCode.Stelem_ref); - } - - // TrimmableTypeMap.Initialize(typeMaps, proxyMaps) - encoder.OpCode (ILOpCode.Ldloc_0); - encoder.OpCode (ILOpCode.Ldloc_1); + // var typeMaps = new IReadOnlyDictionary[N]; (loc 0) + EmitNewArrayLocal (encoder, count, externalDictTypeSpec, slot: 0); + EmitFillArrayLocal (encoder, count, getExternalSpecs, slot: 0); + + // var proxyMaps = new IReadOnlyDictionary[N]; (loc 1) + EmitNewArrayLocal (encoder, count, proxyDictTypeSpec, slot: 1); + EmitFillArrayLocal (encoder, count, getProxySpecs, slot: 1); + + // TrimmableTypeMap.Initialize(typeMaps, proxyMaps, arrayMapsByRank-or-null) + encoder.LoadLocal (0); + encoder.LoadLocal (1); + EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); encoder.OpCode (ILOpCode.Call); encoder.Token (initializeRef); encoder.OpCode (ILOpCode.Ret); }, encodeLocals: localsSig => { - // LOCAL_SIG header + 2 locals localsSig.WriteByte (0x07); // LOCAL_SIG localsSig.WriteCompressedInteger (2); // count - // local 0: IReadOnlyDictionary[] - localsSig.WriteByte (0x1D); // SZARRAY + // loc 0: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // local 1: IReadOnlyDictionary[] - localsSig.WriteByte (0x1D); // SZARRAY + // loc 1: IReadOnlyDictionary[] + localsSig.WriteByte (0x1D); EncodeIReadOnlyDictType (localsSig, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); }); } + static void EmitNewArrayLocal (InstructionEncoder encoder, int count, TypeSpecificationHandle elemSpec, int slot) + { + encoder.LoadConstantI4 (count); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (elemSpec); + encoder.StoreLocal (slot); + } + + static void EmitFillArrayLocal (InstructionEncoder encoder, int count, EntityHandle[] specs, int slot) + { + for (int i = 0; i < count; i++) { + encoder.LoadLocal (slot); + encoder.LoadConstantI4 (i); + encoder.OpCode (ILOpCode.Call); + encoder.Token (specs [i]); + encoder.OpCode (ILOpCode.Stelem_ref); + } + } + + /// MemberRef for TrimmableTypeMap.Initialize(typeMaps[], proxyMaps[], arrayMapsByRank[]). + static MemberReferenceHandle AddInitializeAggregateWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (96); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (3); // parameter count + blob.WriteByte (0x01); // return type: void + // Param 1: IReadOnlyDictionary[] + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + // Param 2: IReadOnlyDictionary[] + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + // Param 3: IReadOnlyDictionary?[] + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Shared-universe IL emit. Single merged main map (anchored on Java.Lang.Object) + /// plus either a flat arrayMapsByRank[maxArrayRank] from shared + /// __ArrayMapRank{N} anchors or null when is 0. + /// + static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, + MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, + MemberReferenceHandle initializeRef, + TypeSpecificationHandle externalDictTypeSpec, + int maxArrayRank) + { + var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); + var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); + + pe.EmitBody ("Initialize", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + // TrimmableTypeMap.Initialize(GetExternal(), GetProxy(), arrayMapsByRank-or-null) + encoder.OpCode (ILOpCode.Call); + encoder.Token (getExternalSpec); + encoder.OpCode (ILOpCode.Call); + encoder.Token (getProxySpec); + EmitArrayMapsByRankOrNull (pe, encoder, getExternalMemberRef, externalDictTypeSpec, maxArrayRank); + encoder.OpCode (ILOpCode.Call); + encoder.Token (initializeRef); + encoder.OpCode (ILOpCode.Ret); + }); + } + + /// MemberRef for TrimmableTypeMap.Initialize(typeMap, proxyMap, arrayMapsByRank[]). + static MemberReferenceHandle AddInitializeSingleWithArraysRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, + TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) + { + var blob = new BlobBuilder (96); + blob.WriteByte (0x00); // DEFAULT (static) + blob.WriteCompressedInteger (3); // parameter count + blob.WriteByte (0x01); // return type: void + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + blob.WriteByte (0x1D); + EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); + return pe.Metadata.AddMemberReference (trimmableTypeMapRef, + pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); + } + + /// + /// Emits IL that pushes either a fresh IReadOnlyDictionary<string, Type>?[maxArrayRank] + /// (when > 0) or ldnull. + /// + static void EmitArrayMapsByRankOrNull (PEAssemblyBuilder pe, InstructionEncoder encoder, + MemberReferenceHandle getExternalMemberRef, TypeSpecificationHandle externalDictTypeSpec, int maxArrayRank) + { + if (maxArrayRank == 0) { + encoder.OpCode (ILOpCode.Ldnull); + return; + } + + var monoAndroidRuntimeNs = pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + encoder.LoadConstantI4 (maxArrayRank); + encoder.OpCode (ILOpCode.Newarr); + encoder.Token (externalDictTypeSpec); + for (int r = 0; r < maxArrayRank; r++) { + var rankRef = pe.Metadata.AddTypeReference (pe.MonoAndroidRef, monoAndroidRuntimeNs, + pe.Metadata.GetOrAddString ($"__ArrayMapRank{r + 1}")); + var rankSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, rankRef); + encoder.OpCode (ILOpCode.Dup); + encoder.LoadConstantI4 (r); + encoder.OpCode (ILOpCode.Call); + encoder.Token (rankSpec); + encoder.OpCode (ILOpCode.Stelem_ref); + } + } + /// /// Creates a MethodSpec for a generic method instantiation with a specific type argument. /// @@ -319,42 +402,6 @@ static MemberReferenceHandle AddTypeMappingMethodRef (PEAssemblyBuilder pe, Type pe.Metadata.GetOrAddString (methodName), pe.Metadata.GetOrAddBlob (blob)); } - /// - /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>, IReadOnlyDictionary<Type, Type>). - /// - static MemberReferenceHandle AddInitializeSingleRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (64); - blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (2); // parameter count - blob.WriteByte (0x01); // return type: void - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - return pe.Metadata.AddMemberReference (trimmableTypeMapRef, - pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); - } - - /// - /// Creates a MemberRef for TrimmableTypeMap.Initialize(IReadOnlyDictionary<string, Type>[], IReadOnlyDictionary<Type, Type>[]). - /// - static MemberReferenceHandle AddInitializeAggregateRef (PEAssemblyBuilder pe, TypeReferenceHandle trimmableTypeMapRef, - TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef) - { - var blob = new BlobBuilder (64); - blob.WriteByte (0x00); // DEFAULT (static) - blob.WriteCompressedInteger (2); // parameter count - blob.WriteByte (0x01); // return type: void - // Param 1: IReadOnlyDictionary[] - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: true); - // Param 2: IReadOnlyDictionary[] - blob.WriteByte (0x1D); // SZARRAY - EncodeIReadOnlyDictType (blob, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - return pe.Metadata.AddMemberReference (trimmableTypeMapRef, - pe.Metadata.GetOrAddString ("Initialize"), pe.Metadata.GetOrAddBlob (blob)); - } - /// /// Creates a TypeSpec for a closed IReadOnlyDictionary<K, V> generic type (for newarr). /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 6f8647dcabe..f5977b08a52 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -119,6 +119,16 @@ sealed class TypeMapAssemblyEmitter EntityHandle _anchorTypeHandle; + // Per-rank array sentinel TypeDefs, 0-indexed by (rank - 1). Empty when array entries + // aren't emitted. + EntityHandle [] _rankAnchorHandles = Array.Empty (); + + // Per-anchor TypeMap(string, Type, Type) ctor refs, lazily built. + readonly Dictionary _typeMapAttr3ArgCtorRefByAnchor = new (); + + // Cached open TypeMapAttribute`1 ref shared across closed TypeSpecs. + TypeReferenceHandle _typeMapAttrOpenRef; + /// /// Creates a new emitter. /// @@ -168,6 +178,7 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) } else { EmitAnchorType (); } + EmitRankSentinels (model); EmitMemberReferences (); // Track wrapper method names → handles for RegisterNatives @@ -265,6 +276,27 @@ void EmitAnchorType () MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); } + /// + /// Populates _rankAnchorHandles with TypeRefs to the shared + /// Microsoft.Android.Runtime.__ArrayMapRank{N} types in Mono.Android. All per-asm + /// typemap DLLs reference the same anchors so each rank's entries merge into one dict + /// at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRank{N}>(). + /// + void EmitRankSentinels (TypeMapAssemblyData model) + { + if (model.MaxArrayRank <= 0) { + return; + } + + _rankAnchorHandles = new EntityHandle [model.MaxArrayRank]; + var ns = _pe.Metadata.GetOrAddString ("Microsoft.Android.Runtime"); + for (int i = 0; i < model.MaxArrayRank; i++) { + _rankAnchorHandles [i] = _pe.Metadata.AddTypeReference ( + _pe.MonoAndroidRef, ns, + _pe.Metadata.GetOrAddString ($"__ArrayMapRank{i + 1}")); + } + } + void EmitMemberReferences () { _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", @@ -387,13 +419,15 @@ void EmitMemberReferences () void EmitTypeMapAttributeCtorRef () { var metadata = _pe.Metadata; - var typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, + _typeMapAttrOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeInteropServicesRef, metadata.GetOrAddString ("System.Runtime.InteropServices"), metadata.GetOrAddString ("TypeMapAttribute`1")); - var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (typeMapAttrOpenRef, _anchorTypeHandle); + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, _anchorTypeHandle); - // 2-arg: TypeMap(string jniName, Type proxyType) — unconditional + // 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", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), @@ -402,8 +436,27 @@ void EmitTypeMapAttributeCtorRef () p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - // 3-arg: TypeMap(string jniName, Type proxyType, Type targetType) — trimmable - _typeMapAttrCtorRef3Arg = _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", + // 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)) { + return cached; + } + var ctorRef = AddTypeMapAttr3ArgCtorRef (anchor); + _typeMapAttr3ArgCtorRefByAnchor [anchor] = ctorRef; + return ctorRef; + } + + MemberReferenceHandle AddTypeMapAttr3ArgCtorRef (EntityHandle anchor) + { + var closedAttrTypeSpec = _pe.MakeGenericTypeSpec (_typeMapAttrOpenRef, anchor); + return _pe.AddMemberRef (closedAttrTypeSpec, ".ctor", sig => sig.MethodSignature (isInstanceMethod: true).Parameters (3, rt => rt.Void (), p => { @@ -1237,7 +1290,23 @@ void AddUnmanagedCallersOnlyAttribute (MethodDefinitionHandle handle) void EmitTypeMapAttribute (TypeMapAttributeData entry) { - var ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + MemberReferenceHandle ctorRef; + 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}."); + } + int anchorIndex = rank - 1; + if ((uint)anchorIndex >= (uint)_rankAnchorHandles.Length) { + throw new InvalidOperationException ( + $"No rank-{rank} anchor available for entry '{entry.JniName}'. " + + $"Ensure TypeMapAssemblyData.MaxArrayRank was >= {rank} before emit."); + } + ctorRef = GetOrAddTypeMapAttr3ArgCtorRef (_rankAnchorHandles [anchorIndex]); + } else { + ctorRef = entry.IsUnconditional ? _typeMapAttrCtorRef2Arg : _typeMapAttrCtorRef3Arg; + } + var blob = _pe.BuildAttributeBlob (b => { b.WriteSerializedString (entry.JniName); b.WriteSerializedString (entry.ProxyTypeReference); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 939689a43a5..48ca89f45bc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -27,9 +27,10 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false) + /// Max rank for per-rank array TypeMap entries. 0 disables. + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 07d689854bb..090075d73bf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -9,6 +9,12 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; public class TrimmableTypeMapGenerator { + /// + /// Runtime-supported maximum array rank — must match the number of + /// __ArrayMapRank{N} types pre-defined in Mono.Android. + /// + public const int MaxSupportedArrayRank = 8; + readonly ITrimmableTypeMapLogger logger; static readonly HashSet RequiredFrameworkDeferredRegistrationTypes = new (StringComparer.Ordinal) { @@ -32,11 +38,20 @@ public TrimmableTypeMapResult Execute ( HashSet frameworkAssemblyNames, bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, - XDocument? manifestTemplate = null) + XDocument? manifestTemplate = null, + int maxArrayRank = 0) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); + if (maxArrayRank < 0) { + throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); + } + if (maxArrayRank > MaxSupportedArrayRank) { + throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, + $"_AndroidTrimmableTypeMapMaxArrayRank={maxArrayRank} exceeds the runtime's supported maximum ({MaxSupportedArrayRank}). " + + $"To raise the limit, add additional __ArrayMapRank{{N}} types to Mono.Android."); + } var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies); if (allPeers.Count == 0) { @@ -48,7 +63,7 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -139,7 +154,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -168,14 +183,14 @@ List GenerateTypeMapAssemblies (List allPeers, string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8104ee308a2..42d19c76dcb 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -27,12 +27,24 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { if (RuntimeFeature.TrimmableTypeMap) { - var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - if (factory is not null) - return factory.CreateArray (length, 1); + if (System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported) { + // 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); + } + + throw new NotSupportedException ( + $"No TrimmableTypeMap array entry for element type '{elementType}'. " + + $"Add an [assembly: TypeMap] entry for the closed array type or report an issue."); } - #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path + #pragma warning disable IL3050 // legacy fallback path return Array.CreateInstance (elementType, length); #pragma warning restore IL3050 } diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index 0f569b918bb..04995ea206c 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -10,16 +10,11 @@ namespace Java.Interop { /// - /// AOT-safe factory for creating typed containers (arrays, lists, collections, dictionaries) - /// without using MakeGenericType() or Array.CreateInstance(). + /// AOT-safe factory for creating typed containers (lists, collections, dictionaries). + /// Array creation lives in JNIEnv.ArrayCreateInstance. /// public abstract class JavaPeerContainerFactory { - /// - /// Creates a typed jagged array. Rank 1 = T[], rank 2 = T[][], etc. - /// - internal abstract Array CreateArray (int length, int rank); - /// /// Creates a typed JavaList<T> from a JNI handle. /// @@ -71,31 +66,6 @@ public sealed class JavaPeerContainerFactory< JavaPeerContainerFactory () { } - // TODO (https://github.com/dotnet/android/issues/10794): This might cause unnecessary code bloat for NativeAOT. Revisit - // how we use this API and instead use a differnet approach that uses AOT-safe `Array.CreateInstanceFromArrayType` - // with statically provided array types based on a statically known array type. - internal override Array CreateArray (int length, int rank) => rank switch { - 1 => new T [length], - 2 => new T [length][], - 3 => new T [length][][], - _ when rank >= 0 => CreateHigherRankArray (length, rank), - _ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Rank must be non-negative."), - }; - - static Array CreateHigherRankArray (int length, int rank) - { - if (!RuntimeFeature.IsDynamicCodeSupported) { - throw new NotSupportedException ($"Cannot create array of rank {rank} because dynamic code is not supported."); - } - - var arrayType = typeof (T); - for (int i = 0; i < rank; i++) { - arrayType = arrayType.MakeArrayType (); - } - - return Array.CreateInstanceFromArrayType (arrayType, length); - } - internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) => new Android.Runtime.JavaList (handle, transfer); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs b/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs new file mode 100644 index 00000000000..e0bf2f2568d --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/ArrayMapAnchors.cs @@ -0,0 +1,19 @@ +#nullable enable + +namespace Microsoft.Android.Runtime; + +// Shared TypeMap group anchors for per-rank array entries. Per-assembly typemap DLLs +// reference these so all rank-N entries across all assemblies merge into a single +// dictionary at runtime via TypeMapping.GetOrCreateExternalTypeMapping<__ArrayMapRankN>(). +// +// To support a higher MaxArrayRank, add additional types here and bump +// TrimmableTypeMap.MaxSupportedArrayRank. + +internal sealed class __ArrayMapRank1 { } +internal sealed class __ArrayMapRank2 { } +internal sealed class __ArrayMapRank3 { } +internal sealed class __ArrayMapRank4 { } +internal sealed class __ArrayMapRank5 { } +internal sealed class __ArrayMapRank6 { } +internal sealed class __ArrayMapRank7 { } +internal sealed class __ArrayMapRank8 { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 2ca52f03f38..9e220e2d023 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -28,32 +28,47 @@ public class TrimmableTypeMap "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); readonly ITypeMapWithAliasing _typeMap; + // Per-rank array dictionaries, 0-indexed by (rank - 1). Sourced from the shared + // __ArrayMapRank{N} TypeMap groups, so each rank is one merged dict spanning every + // per-asm typemap DLL. Empty when no array entries were emitted (CoreCLR builds). + readonly IReadOnlyDictionary?[] _arrayMapsByRank; readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); - TrimmableTypeMap (ITypeMapWithAliasing typeMap) + TrimmableTypeMap (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) { _typeMap = typeMap; + _arrayMapsByRank = arrayMapsByRank ?? Array.Empty?> (); } /// - /// Initializes the singleton with a single merged typemap universe. - /// Called from in the generated root assembly - /// (_Microsoft.Android.TypeMaps) when assembly typemaps are merged (Release builds). + /// Initializes the singleton with a single merged typemap universe and optional + /// per-rank array dictionaries (consulted by JNIEnv.ArrayCreateInstance under NativeAOT). /// - public static void Initialize (IReadOnlyDictionary typeMap, IReadOnlyDictionary proxyMap) + /// 0-indexed by (rank - 1); null when no array entries were emitted. + public static void Initialize ( + IReadOnlyDictionary typeMap, + IReadOnlyDictionary proxyMap, + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMap); ArgumentNullException.ThrowIfNull (proxyMap); - InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap)); + InitializeCore (new SingleUniverseTypeMap (typeMap, proxyMap), arrayMapsByRank); } /// - /// Initializes the singleton with multiple per-assembly typemap universes. - /// Called from in the generated root assembly - /// (_Microsoft.Android.TypeMaps) when each assembly has its own typemap universe (Debug builds). + /// Initializes the singleton with multiple per-assembly typemap universes and optional + /// merged per-rank array dictionaries. /// - public static void Initialize (IReadOnlyDictionary[] typeMaps, IReadOnlyDictionary[] proxyMaps) + /// + /// 0-indexed by (rank - 1); null when no array entries were emitted. Same shape as + /// the single-universe overload — array entries live in the shared + /// __ArrayMapRank{N} TypeMap groups regardless of merge mode. + /// + public static void Initialize ( + IReadOnlyDictionary[] typeMaps, + IReadOnlyDictionary[] proxyMaps, + IReadOnlyDictionary?[]? arrayMapsByRank) { ArgumentNullException.ThrowIfNull (typeMaps); ArgumentNullException.ThrowIfNull (proxyMaps); @@ -63,21 +78,22 @@ public static void Initialize (IReadOnlyDictionary[] typeMaps, IRe if (typeMaps.Length != proxyMaps.Length) { throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length}).", nameof (proxyMaps)); } + var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { universes [i] = new SingleUniverseTypeMap (typeMaps [i], proxyMaps [i]); } - InitializeCore (new AggregateTypeMap (universes)); + InitializeCore (new AggregateTypeMap (universes), arrayMapsByRank); } - static void InitializeCore (ITypeMapWithAliasing typeMap) + static void InitializeCore (ITypeMapWithAliasing typeMap, IReadOnlyDictionary?[]? arrayMapsByRank) { lock (s_initLock) { if (s_instance is not null) { throw new InvalidOperationException ("TrimmableTypeMap has already been initialized."); } - var instance = new TrimmableTypeMap (typeMap); + var instance = new TrimmableTypeMap (typeMap, arrayMapsByRank); instance.RegisterNatives (); s_instance = instance; } @@ -318,6 +334,53 @@ 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) + { + 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) { + return false; + } + var next = leaf.GetElementType (); + if (next is null) { + return false; + } + leaf = next; + rankIndex++; + } + + if ((uint)rankIndex >= (uint)_arrayMapsByRank.Length || _arrayMapsByRank [rankIndex] is not { } dict) { + return false; + } + + string? leafJniName = leaf.IsPrimitive + ? TryGetPrimitiveJniName (leaf, out var p) ? p : null + : TryGetJniNameForManagedType (leaf, out var jni) ? jni : null; + + return leafJniName is not null && dict.TryGetValue (leafJniName, out arrayType); + } + + /// JNI single-letter encoding for primitive element types. + static bool TryGetPrimitiveJniName (Type primitive, [NotNullWhen (true)] out string? jni) + { + 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; + } + [UnmanagedCallersOnly] static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index ec8ad004c88..10504fafd19 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -19,6 +19,11 @@ <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java + + + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 + <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 @@ -76,6 +81,7 @@ Debug="$(AndroidIncludeDebugSymbols)" NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" + MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index f80a14157f1..d3797bbddb5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -68,6 +68,13 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } + + /// + /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} + /// sentinels and TypeMap entries. 0 disables. Set via + /// $(_AndroidTrimmableTypeMapMaxArrayRank). + /// + public int MaxArrayRank { get; set; } public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -131,7 +138,8 @@ public override bool RunTask () frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, manifestConfig, - manifestTemplate); + manifestTemplate, + maxArrayRank: MaxArrayRank); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 84dd6a0a2f4..7abe1227635 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -11,11 +11,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank); stream.Position = 0; return stream; } @@ -239,4 +239,43 @@ static List GetIgnoresAccessChecksToValues (MetadataReader reader) } return result; } + + [Fact] + public void Generate_MergedMode_WithArrays_ProducesValidPEAssembly () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 3); + using var pe = new PEReader (stream); + Assert.True (pe.HasMetadata); + } + + [Fact] + public void Generate_MergedMode_WithArrays_ReferencesPerAsmRankSentinels () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 2); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + .ToList (); + Assert.Contains ("__ArrayMapRank1", typeRefNames); + Assert.Contains ("__ArrayMapRank2", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeRefNames); + } + + [Fact] + public void Generate_MergedMode_WithArrays_NoPerAsmAccessNeeded () + { + using var stream = GenerateRootAssembly (["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, maxArrayRank: 3); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var accessAttrs = GetIgnoresAccessChecksToValues (reader); + Assert.Contains ("Mono.Android", accessAttrs); + // Shared-mode root never needs per-asm internal access — rank anchors live in Mono.Android. + Assert.DoesNotContain ("_App.TypeMap", accessAttrs); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 651f1ea3c40..209f16dae5c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -16,6 +16,12 @@ static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string return ModelBuilder.Build (peers, outputPath, assemblyName); } + static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3) + { + var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); + return ModelBuilder.Build (peers, outputPath, assemblyName, maxArrayRank); + } + public class BasicStructure { [Fact] @@ -845,6 +851,233 @@ public void Build_SameInput_ProducesDeterministicOutput () } } + public class ArrayEntries + { + [Fact] + public void Build_DefaultEmitArrayEntriesFalse_NoArrayEntries () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModel (new [] { peer }); + + Assert.Equal (0, model.MaxArrayRank); + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_SetsMaxArrayRank () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + Assert.Equal (3, model.MaxArrayRank); + } + + [Fact] + public void Build_EmitArrayEntries_HonoursMaxArrayRank () + { + // Caller can ask for fewer or more ranks than the default. Verifies the + // $(_AndroidTrimmableTypeMapMaxArrayRank) MSBuild property's effect. + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + + var model5 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 5); + 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); + + var model1 = BuildModelWithArrays (new [] { peer }, maxArrayRank: 1); + Assert.Equal (1, model1.MaxArrayRank); + Assert.Single (model1.Entries.Where (e => e.AnchorRank is not null)); + } + + [Fact] + public void Build_EmitArrayEntries_EmitsRanks1Through3 () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + 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)); + } + + [Fact] + public void Build_EmitArrayEntries_KeyIsElementJniName () + { + // 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. + 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))); + } + + [Fact] + public void Build_EmitArrayEntries_TrimTargetIsClosedArrayType () + { + // 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); + var rank2 = model.Entries.Single (e => e.AnchorRank == 2); + Assert.Equal ("Foo.Bar[][], App", rank2.ProxyTypeReference); + Assert.Equal ("Foo.Bar[][], App", 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); + } + + [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. + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + foreach (var entry in model.Entries.Where (e => e.AnchorRank is not null)) { + Assert.False (entry.IsUnconditional); + Assert.NotNull (entry.TargetTypeReference); + } + } + + [Fact] + public void Build_EmitArrayEntries_OpenGenericPeer_Skipped () + { + // typeof(JavaList<>[]) is not a valid IL token. + var openGeneric = MakeMcwPeer ("java/util/ArrayList", "Android.Runtime.JavaList`1", "Mono.Android") + with { IsGenericDefinition = true }; + var model = BuildModelWithArrays (new [] { openGeneric }); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_AliasGroup_Skipped () + { + // Alias groups (multiple peers sharing one JNI name) would produce duplicate + // JNI array keys; deferred pending an alias-aware design. + var peers = new List { + MakeMcwPeer ("test/Dup", "Test.First", "App"), + MakeMcwPeer ("test/Dup", "Test.Second", "App"), + }; + var model = BuildModelWithArrays (peers); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Theory] + [InlineData ("Z")] + [InlineData ("B")] + [InlineData ("C")] + [InlineData ("S")] + [InlineData ("I")] + [InlineData ("J")] + [InlineData ("F")] + [InlineData ("D")] + public void Build_EmitArrayEntries_PrimitiveJniKeyword_Skipped (string jniKeyword) + { + // Primitive JNI keyword keys are handled by the legacy + // JniRuntime.JniTypeManager.GetPrimitiveArrayTypesForSimpleReference path. + // Emitting array entries here would shadow that built-in handling. + var peer = MakeMcwPeer (jniKeyword, "FakePrimitive.Wrapper", "App"); + var model = BuildModelWithArrays (new [] { peer }); + + Assert.DoesNotContain (model.Entries, e => e.AnchorRank is not null); + } + + [Fact] + public void Build_EmitArrayEntries_MultiplePeers_GetIndependentTrios () + { + var peers = new List { + MakeMcwPeer ("foo/A", "Foo.A", "App"), + MakeMcwPeer ("foo/B", "Foo.B", "App"), + }; + var model = BuildModelWithArrays (peers); + + 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 (); + Assert.Equal (3, perPeer.Count); + Assert.Equal (new int? [] { 1, 2, 3 }, perPeer.Select (e => e.AnchorRank).ToArray ()); + } + } + } + + public class ArrayEntriesPeBlob + { + [Fact] + public void FullPipeline_ArrayEntries_ReferencesSharedRankAnchors () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "ArrSentinels.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrSentinels", maxArrayRank: 3); + Assert.Equal (3, model.MaxArrayRank); + + EmitAndVerify (model, "ArrSentinels", (pe, reader) => { + // Per-asm DLLs no longer define their own __ArrayMapRank{N}; they reference + // the shared anchors in Mono.Android. + var typeDefNames = reader.TypeDefinitions + .Select (h => reader.GetString (reader.GetTypeDefinition (h).Name)) + .ToHashSet (StringComparer.Ordinal); + Assert.DoesNotContain ("__ArrayMapRank1", typeDefNames); + + var rankRefsToMonoAndroid = reader.TypeReferences + .Select (h => reader.GetTypeReference (h)) + .Where (t => reader.GetString (t.Name).StartsWith ("__ArrayMapRank", StringComparison.Ordinal)) + .Select (t => reader.GetString (t.Name)) + .ToHashSet (StringComparer.Ordinal); + Assert.Contains ("__ArrayMapRank1", rankRefsToMonoAndroid); + Assert.Contains ("__ArrayMapRank2", rankRefsToMonoAndroid); + Assert.Contains ("__ArrayMapRank3", rankRefsToMonoAndroid); + }); + } + + [Fact] + public void FullPipeline_NoArrayEntries_DoesNotReferenceRankAnchors () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "NoArrSentinels.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "NoArrSentinels"); + Assert.Equal (0, model.MaxArrayRank); + + EmitAndVerify (model, "NoArrSentinels", (pe, reader) => { + var typeRefNames = reader.TypeReferences + .Select (h => reader.GetString (reader.GetTypeReference (h).Name)) + .ToHashSet (StringComparer.Ordinal); + Assert.DoesNotContain ("__ArrayMapRank1", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank2", typeRefNames); + Assert.DoesNotContain ("__ArrayMapRank3", typeRefNames); + }); + } + + [Fact] + public void FullPipeline_ArrayEntries_AttributeBlobsRoundTrip () + { + var peer = MakeMcwPeer ("foo/Bar", "Foo.Bar", "App"); + var outputPath = Path.Combine (Path.GetTempPath (), "ArrBlobs.dll"); + var model = ModelBuilder.Build (new [] { peer }, outputPath, "ArrBlobs", maxArrayRank: 3); + + EmitAndVerify (model, "ArrBlobs", (pe, reader) => { + var attrs = ReadAllTypeMapAttributeBlobs (reader); + + // 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"); + }); + } + } + static void EmitAndVerify (TypeMapAssemblyData model, string assemblyName, Action verify) { var stream = new MemoryStream ();