Skip to content

Commit 4b0b089

Browse files
[TrimmableTypeMap] Replace per-T array factory with TypeMap entries
Generator: for every per-peer TypeMap entry, also emit speculative `[L<jni>;` / `[[L<jni>;` / `[[[L<jni>;` entries pointing at the corresponding closed managed array type (`T[]`, `T[][]`, `T[][][]`). Emitted as 3-arg conditional attributes so the trimmer drops entries whose array target type is not live in the shipped app. Skips open generics, primitive JNI keyword keys (`Z`, `B`, …), and alias groups (deferred until a real-world need). Runtime: new `ITypeMapWithAliasing.TryGetType` raw lookup that bypasses `JavaPeerProxy` filtering — array entries point directly at the closed `Type` and have no proxy. New `TrimmableTypeMap.TryGetArrayType(Type elementType, out Type? arrayType)` walks down `elementType.IsArray` / `GetElementType()` to find the leaf type and array depth, resolves the leaf JNI encoding (primitive static dict OR `TryGetJniNameForManagedType` wrapped as `L<jni>;`), prepends `[` × (depth+1), and looks up the closed array `Type` via the raw typemap. Caller then uses AOT-safe `Array.CreateInstanceFromArrayType`. `JNIEnv.ArrayCreateInstance` swaps to the new flow on hit; throws `NotSupportedException` on miss under trimmable. Legacy non-trimmable fallback unchanged. Cleanup: deletes `JavaPeerContainerFactory<T>.CreateArray` and the private `CreateHigherRankArray` helper. The base abstract method goes too. Other (collection) factory methods stay for the follow-up PR. Tests: 13 new `ArrayEntries` unit tests in `TypeMapModelBuilderTests` covering ranks 1-3, conditional emission, open-generic skip, alias-group skip, and primitive-JNI-keyword skip. Existing count-asserting tests updated via a `NonArrayEntries` helper. Tracking: #11234 (sub-task — arrays only; collection factory removal follows in a separate PR). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c5062a6 commit 4b0b089

8 files changed

Lines changed: 353 additions & 65 deletions

File tree

src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ static class ModelBuilder
4040
/// <param name="peers">Scanned Java peer types (typically from a single input assembly).</param>
4141
/// <param name="outputPath">Output .dll path — used to derive assembly/module names if not specified.</param>
4242
/// <param name="assemblyName">Explicit assembly name. If null, derived from <paramref name="outputPath"/>.</param>
43+
/// <remarks>
44+
/// In addition to the per-peer <c>TypeMap</c> entry, this method also emits speculative
45+
/// <c>[L&lt;jni&gt;;</c>, <c>[[L&lt;jni&gt;;</c>, and <c>[[[L&lt;jni&gt;;</c> entries pointing at
46+
/// the corresponding closed managed array types (<c>T[]</c>, <c>T[][]</c>, <c>T[][][]</c>).
47+
/// They are 3-arg (conditional) attributes, so the trimmer drops entries whose array
48+
/// target type is not live in the shipped app. Required for the AOT-safe array creation
49+
/// path in <c>JNIEnv.ArrayCreateInstance</c>.
50+
/// </remarks>
4351
public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, string outputPath, string? assemblyName = null)
4452
{
4553
if (peers is null) {
@@ -89,6 +97,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList<JavaPeerInfo> peers, stri
8997
}
9098

9199
EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames);
100+
EmitArrayEntries (model, jniName, peersForName);
92101
}
93102

94103
// Compute IgnoresAccessChecksTo from cross-assembly references
@@ -198,6 +207,76 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
198207
});
199208
}
200209

210+
/// <summary>
211+
/// Maximum jagged-array rank emitted per peer. Matches the legacy
212+
/// <c>JavaPeerContainerFactory&lt;T&gt;.CreateArray</c> switch (ranks 1–3) — higher ranks
213+
/// already throw <see cref="NotSupportedException"/> under NativeAOT today.
214+
/// </summary>
215+
const int MaxArrayRank = 3;
216+
217+
/// <summary>
218+
/// Emits speculative <c>[L&lt;jni&gt;;</c> / <c>[[L&lt;jni&gt;;</c> / <c>[[[L&lt;jni&gt;;</c> TypeMap entries
219+
/// for a single peer. Each entry maps a JNI array name to the corresponding closed
220+
/// managed array type (e.g. <c>typeof(SomePeer[])</c>) and is emitted as a 3-arg
221+
/// (conditional) attribute so the trimmer can drop entries whose array target type is
222+
/// not live in the shipped app.
223+
/// </summary>
224+
/// <remarks>
225+
/// Skips:
226+
/// <list type="bullet">
227+
/// <item>Open-generic peers (<c>JavaPeerInfo.IsGenericDefinition</c>) — <c>typeof(T&lt;&gt;[])</c> is invalid.</item>
228+
/// <item>JNI keyword keys (<c>Z</c>, <c>B</c>, …) — primitives are handled by
229+
/// <c>JniRuntime.JniTypeManager.GetPrimitiveArrayTypesForSimpleReference</c>; emitting
230+
/// array entries here would collide with that built-in path.</item>
231+
/// <item>Alias groups — multiple peers sharing a JNI name would produce duplicate
232+
/// array-key entries. Array-typemap support for aliases would need its own
233+
/// indexed-alias scheme; deferred until a real-world need is identified.</item>
234+
/// </list>
235+
/// </remarks>
236+
static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List<JavaPeerInfo> peersForName)
237+
{
238+
// Primitive single-letter JNI keywords are handled by the legacy primitive path.
239+
// Skip them so we don't shadow the built-in [Z, [B, etc. array entries.
240+
if (jniName.Length == 1 && IsJniPrimitiveKeyword (jniName [0])) {
241+
return;
242+
}
243+
244+
// Alias groups would produce duplicate JNI array keys (one per peer). Defer
245+
// alias-aware array emission until we have a concrete use case.
246+
if (peersForName.Count != 1) {
247+
return;
248+
}
249+
250+
var peer = peersForName [0];
251+
if (peer.IsGenericDefinition) {
252+
return;
253+
}
254+
255+
for (int rank = 1; rank <= MaxArrayRank; rank++) {
256+
string arrayJniName = string.Concat (new string ('[', rank), "L", jniName, ";");
257+
string arrayTargetRef = AssemblyQualify (peer.ManagedTypeName + Brackets (rank), peer.AssemblyName);
258+
model.Entries.Add (new TypeMapAttributeData {
259+
JniName = arrayJniName,
260+
ProxyTypeReference = arrayTargetRef,
261+
TargetTypeReference = arrayTargetRef,
262+
});
263+
}
264+
}
265+
266+
static string Brackets (int rank)
267+
{
268+
switch (rank) {
269+
case 1: return "[]";
270+
case 2: return "[][]";
271+
case 3: return "[][][]";
272+
default: return new string ('[', rank).Replace ("[", "[]");
273+
}
274+
}
275+
276+
static bool IsJniPrimitiveKeyword (char c)
277+
=> c == 'Z' || c == 'B' || c == 'C' || c == 'S' || c == 'I'
278+
|| c == 'J' || c == 'F' || c == 'D' || c == 'V';
279+
201280
/// <summary>
202281
/// Determines whether a type should use the unconditional (2-arg) TypeMap attribute.
203282
/// Unconditional types are always preserved by the trimmer.

src/Mono.Android/Android.Runtime/JNIEnv.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,12 @@ public static partial class JNIEnv {
2727
static Array ArrayCreateInstance (Type elementType, int length)
2828
{
2929
if (RuntimeFeature.TrimmableTypeMap) {
30-
var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType);
31-
if (factory is not null)
32-
return factory.CreateArray (length, 1);
30+
if (TrimmableTypeMap.Instance.TryGetArrayType (elementType, out var arrayType)) {
31+
return Array.CreateInstanceFromArrayType (arrayType, length);
32+
}
33+
throw new NotSupportedException (
34+
$"No TrimmableTypeMap array entry for element type '{elementType}'. " +
35+
$"Add an [assembly: TypeMap] entry for the closed array type or report an issue.");
3336
}
3437

3538
#pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path

src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs

Lines changed: 8 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,17 @@
1010
namespace Java.Interop
1111
{
1212
/// <summary>
13-
/// AOT-safe factory for creating typed containers (arrays, lists, collections, dictionaries)
14-
/// without using <c>MakeGenericType()</c> or <c>Array.CreateInstance()</c>.
13+
/// AOT-safe factory for creating typed containers (lists, collections, dictionaries)
14+
/// without using <c>MakeGenericType()</c>.
1515
/// </summary>
16+
/// <remarks>
17+
/// Array creation has been moved out of this factory: <c>JNIEnv.ArrayCreateInstance</c>
18+
/// now resolves closed array types via the trimmable typemap and
19+
/// <see cref="Array.CreateInstanceFromArrayType"/> instead of going through a per-T
20+
/// factory instantiation.
21+
/// </remarks>
1622
public abstract class JavaPeerContainerFactory
1723
{
18-
/// <summary>
19-
/// Creates a typed jagged array. Rank 1 = T[], rank 2 = T[][], etc.
20-
/// </summary>
21-
internal abstract Array CreateArray (int length, int rank);
22-
2324
/// <summary>
2425
/// Creates a typed <c>JavaList&lt;T&gt;</c> from a JNI handle.
2526
/// </summary>
@@ -71,31 +72,6 @@ public sealed class JavaPeerContainerFactory<
7172

7273
JavaPeerContainerFactory () { }
7374

74-
// TODO (https://github.com/dotnet/android/issues/10794): This might cause unnecessary code bloat for NativeAOT. Revisit
75-
// how we use this API and instead use a differnet approach that uses AOT-safe `Array.CreateInstanceFromArrayType`
76-
// with statically provided array types based on a statically known array type.
77-
internal override Array CreateArray (int length, int rank) => rank switch {
78-
1 => new T [length],
79-
2 => new T [length][],
80-
3 => new T [length][][],
81-
_ when rank >= 0 => CreateHigherRankArray (length, rank),
82-
_ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Rank must be non-negative."),
83-
};
84-
85-
static Array CreateHigherRankArray (int length, int rank)
86-
{
87-
if (!RuntimeFeature.IsDynamicCodeSupported) {
88-
throw new NotSupportedException ($"Cannot create array of rank {rank} because dynamic code is not supported.");
89-
}
90-
91-
var arrayType = typeof (T);
92-
for (int i = 0; i < rank; i++) {
93-
arrayType = arrayType.MakeArrayType ();
94-
}
95-
96-
return Array.CreateInstanceFromArrayType (arrayType, length);
97-
}
98-
9975
internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer)
10076
=> new Android.Runtime.JavaList<T> (handle, transfer);
10177

src/Mono.Android/Microsoft.Android.Runtime/AggregateTypeMap.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ public IEnumerable<Type> GetTypes (string jniName)
3030
}
3131
}
3232

33+
public bool TryGetType (string jniName, [NotNullWhen (true)] out Type? type)
34+
{
35+
// First-wins: each JNI name appears in at most one universe (or is duplicated
36+
// across universes with the same target — order-independent).
37+
foreach (var universe in _universes) {
38+
if (universe.TryGetType (jniName, out type)) {
39+
return true;
40+
}
41+
}
42+
type = null;
43+
return false;
44+
}
45+
3346
public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType)
3447
{
3548
// First-wins: each managed type exists in exactly one assembly

src/Mono.Android/Microsoft.Android.Runtime/ITypeMapWithAliasing.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,14 @@ interface ITypeMapWithAliasing
2626
/// carries the <see cref="JavaPeerProxy"/> attribute).
2727
/// </summary>
2828
bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType);
29+
30+
/// <summary>
31+
/// Raw lookup of a managed type by JNI name, without the
32+
/// <see cref="JavaPeerProxy"/> attribute filtering applied by
33+
/// <see cref="GetTypes"/>. Used by the AOT-safe array creation path,
34+
/// where the typemap entry points directly at a closed array
35+
/// <see cref="Type"/> (e.g. <c>typeof(byte[][])</c>) that has no
36+
/// proxy attribute.
37+
/// </summary>
38+
bool TryGetType (string jniName, [NotNullWhen (true)] out Type? type);
2939
}

src/Mono.Android/Microsoft.Android.Runtime/SingleUniverseTypeMap.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ public IEnumerable<Type> GetTypes (string jniName)
5353
}
5454
}
5555

56+
public bool TryGetType (string jniName, [NotNullWhen (true)] out Type? type)
57+
{
58+
return _typeMap.TryGetValue (jniName, out type);
59+
}
60+
5661
public bool TryGetProxyType (Type managedType, [NotNullWhen (true)] out Type? proxyType)
5762
{
5863
if (!_proxyTypeMap.TryGetValue (managedType, out var mappedProxyType)) {

src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,88 @@ internal static bool TargetTypeMatches (Type targetType, Type proxyTargetType)
318318
return GetProxyForManagedType (type)?.GetContainerFactory ();
319319
}
320320

321+
/// <summary>
322+
/// AOT-safe lookup of the closed array <see cref="Type"/> for the supplied
323+
/// <paramref name="elementType"/> via the trimmable typemap. Implements the
324+
/// flow:
325+
/// <list type="number">
326+
/// <item>Walk down <see cref="Type.IsArray"/> / <see cref="Type.GetElementType"/>
327+
/// to find the leaf element type and the depth of array nesting.</item>
328+
/// <item>Resolve the leaf JNI element encoding — primitive (<c>byte</c> →
329+
/// <c>"B"</c>, etc.) or reference (<see cref="TryGetJniNameForManagedType"/>
330+
/// wrapped as <c>"L&lt;jni&gt;;"</c>).</item>
331+
/// <item>Build the full JNI array name by prepending <c>'['</c> for each
332+
/// array level (the outer rank that is being created plus the depth of
333+
/// the element type itself).</item>
334+
/// <item>Look the array Type up via the raw typemap (<see cref="ITypeMapWithAliasing.TryGetType"/>),
335+
/// which bypasses proxy attribute filtering — array entries point directly
336+
/// at the closed managed array <see cref="Type"/> and have no
337+
/// <see cref="JavaPeerProxy"/>.</item>
338+
/// </list>
339+
/// Returns false if any step fails (unknown leaf type, no typemap entry).
340+
/// </summary>
341+
internal bool TryGetArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType)
342+
{
343+
if (elementType is null) {
344+
arrayType = null;
345+
return false;
346+
}
347+
348+
// 1. Walk down to the leaf and count the element's own array depth.
349+
var leaf = elementType;
350+
int elementDepth = 0;
351+
while (leaf.IsArray) {
352+
var next = leaf.GetElementType ();
353+
if (next is null) {
354+
arrayType = null;
355+
return false;
356+
}
357+
leaf = next;
358+
elementDepth++;
359+
}
360+
361+
// 2. Resolve the leaf JNI element encoding.
362+
string leafEncoding;
363+
if (leaf.IsPrimitive) {
364+
if (!TryGetPrimitiveJniEncoding (leaf, out var primitive)) {
365+
arrayType = null;
366+
return false;
367+
}
368+
leafEncoding = primitive;
369+
} else {
370+
if (!TryGetJniNameForManagedType (leaf, out var leafJni)) {
371+
arrayType = null;
372+
return false;
373+
}
374+
leafEncoding = "L" + leafJni + ";";
375+
}
376+
377+
// 3. Build the full JNI array name. The "+1" accounts for the outer rank
378+
// being created by the caller (ArrayCreateInstance(elementType, length)
379+
// creates new elementType[length]).
380+
int totalArrayDepth = elementDepth + 1;
381+
string arrayJniName = string.Concat (new string ('[', totalArrayDepth), leafEncoding);
382+
383+
// 4. Raw typemap lookup — no proxy filtering.
384+
return _typeMap.TryGetType (arrayJniName, out arrayType);
385+
}
386+
387+
static bool TryGetPrimitiveJniEncoding (Type primitive, [NotNullWhen (true)] out string? encoding)
388+
{
389+
// JNI single-letter encodings for primitive types.
390+
// Reference: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/types.html
391+
if (primitive == typeof (bool)) { encoding = "Z"; return true; }
392+
if (primitive == typeof (byte)) { encoding = "B"; return true; }
393+
if (primitive == typeof (char)) { encoding = "C"; return true; }
394+
if (primitive == typeof (short)) { encoding = "S"; return true; }
395+
if (primitive == typeof (int)) { encoding = "I"; return true; }
396+
if (primitive == typeof (long)) { encoding = "J"; return true; }
397+
if (primitive == typeof (float)) { encoding = "F"; return true; }
398+
if (primitive == typeof (double)) { encoding = "D"; return true; }
399+
encoding = null;
400+
return false;
401+
}
402+
321403
[UnmanagedCallersOnly]
322404
static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle)
323405
{

0 commit comments

Comments
 (0)