Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ca78fa2
[TrimmableTypeMap] Add AOT-safe array and collection factories
simonrozsival Jul 9, 2026
db04690
[TrimmableTypeMap] Address safe collection factory review
simonrozsival Jul 9, 2026
9e0ce62
[TrimmableTypeMap] Document NativeAOT factory assumptions
simonrozsival Jul 9, 2026
0c62664
[TrimmableTypeMap] Share primitive value type factories
simonrozsival Jul 9, 2026
53d50fb
[TrimmableTypeMap] Clarify array factory AOT justification
simonrozsival Jul 9, 2026
b959f4a
[TrimmableTypeMap] Fix NativeAOT warning leak and update apk size bas…
simonrozsival Jul 9, 2026
1143044
[TrimmableTypeMap] Address review: file-scoped namespaces, drop unuse…
simonrozsival Jul 9, 2026
d41d005
[TrimmableTypeMap] Fix nullable warnings in SafeJavaCollectionFactory
simonrozsival Jul 9, 2026
187d9fa
[TrimmableTypeMap] Address review: value/value dict test, drop unused…
simonrozsival Jul 9, 2026
c0ca782
Merge origin/main into android-trimmable-revisit-arrays-generic-colle…
simonrozsival Jul 13, 2026
f663705
Merge remote-tracking branch 'origin/main' into dev/simonrozsival/and…
simonrozsival Jul 13, 2026
cb99cd2
[TrimmableTypeMap] Address review: nameof and redundant MakeGenericType
simonrozsival Jul 13, 2026
e01bc8c
[TrimmableTypeMap] Fix NativeAOT array-types fallback to return full set
simonrozsival Jul 13, 2026
6eabdc9
[TrimmableTypeMap] Restore byte collection support on trimmable path
simonrozsival Jul 13, 2026
2a9cba7
[TrimmableTypeMap] Address collection factory review
simonrozsival Jul 14, 2026
0ce5dd1
[TrimmableTypeMap] Strengthen factory regression coverage
simonrozsival Jul 14, 2026
da5f681
Merge origin/main into trimmable type map work
simonrozsival Jul 14, 2026
949eef2
[TrimmableTypeMap] Complete runtime array proxy replacement
simonrozsival Jul 14, 2026
8d6578f
[TrimmableTypeMap] Update CoreCLR APK size baseline
simonrozsival Jul 14, 2026
409b05b
[TrimmableTypeMap] Fix byte collection test build
simonrozsival Jul 14, 2026
1fc0450
[tests] Scope trimmable factory coverage
simonrozsival Jul 15, 2026
f87ade5
[tests] Restore JavaConvert factory coverage
simonrozsival Jul 15, 2026
cf8cd39
[Java.Interop] Map nullable byte to java.lang.Byte
simonrozsival Jul 15, 2026
8fc311d
[tests] Cover nullable byte vectors
simonrozsival Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,15 @@ stages:
artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab
artifactFolder: $(DotNetTargetFramework)-NativeAOT

- template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml
parameters:
configuration: $(XA.Build.Configuration)
testName: Mono.Android.NET_Tests-NativeAOTTrimmable
project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj
extraBuildArgs: -p:TestsFlavor=NativeAOTTrimmable -p:PublishAot=true -p:_AndroidTypeMapImplementation=trimmable
artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab
artifactFolder: $(DotNetTargetFramework)-NativeAOTTrimmable

- template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml
parameters:
configuration: $(XA.Build.Configuration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ static bool GetBuiltInTypeSignature (Type type, ref JniTypeSignature signature)
signature = GetCachedTypeSignature (ref __BooleanNullableTypeSignature, "java/lang/Boolean");
return true;
}
if (type == typeof (SByte?)) {
if (type == typeof (Byte?) || type == typeof (SByte?)) {
signature = GetCachedTypeSignature (ref __SByteNullableTypeSignature, "java/lang/Byte");
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ public void GetTypeSignature_Type ()
AssertGetJniTypeInfoForType (typeof (bool), "Z", true, 0);
AssertGetJniTypeInfoForType (typeof (void), "V", true, 0);

AssertGetJniTypeInfoForType (typeof (float?), "java/lang/Float", true, 0);
AssertGetJniTypeInfoForType (typeof (float?), "java/lang/Float", true, 0);
AssertGetJniTypeInfoForType (typeof (byte?), "java/lang/Byte", true, 0);
AssertGetJniTypeInfoForType (typeof (byte?[]), "[Ljava/lang/Byte;", true, 1);
AssertGetJniTypeInfoForType (typeof (byte?[][]), "[[Ljava/lang/Byte;", true, 2);
AssertGetJniTypeInfoForType (typeof (byte?[][][]), "[[[Ljava/lang/Byte;", true, 3);

AssertGetJniTypeInfoForType (typeof (JavaObject), "java/lang/Object", false, 0);

Expand Down Expand Up @@ -136,6 +140,7 @@ static void AssertGetJniTypeInfoForType (Type type, string jniType, bool isKeywo
Assert.AreEqual (typeof (float), GetType ("F"));
Assert.AreEqual (typeof (double), GetType ("D"));
Assert.AreEqual (typeof (string), GetType ("java/lang/String"));
Assert.AreEqual (typeof (sbyte?), GetType ("java/lang/Byte"));
Assert.AreEqual (typeof (float?), GetType ("java/lang/Float"));
Assert.AreEqual (null, GetType ("com/example/does/not/exist"));
Assert.AreEqual (null, GetType ("Lcom/example/does/not/exist;"));
Expand Down
167 changes: 139 additions & 28 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,7 @@ static Array ArrayCreateInstance (Type elementType, int length)
}

if (RuntimeFeature.TrimmableTypeMap) {
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);
}
}

int arrayRank = GetArrayRank (elementType, out var leafElementType);
throw new NotSupportedException (
$"No TrimmableTypeMap array proxy entry for element type '{elementType}' " +
$"(leaf element type '{leafElementType}', rank {arrayRank}). " +
$"Array lookups use the leaf element type within the per-rank __ArrayMapRank{arrayRank} typemap group; " +
$"ensure the mapping is emitted for that rank (for example by increasing _AndroidTrimmableTypeMapMaxArrayRank) or report an issue.");
return SafeArrayFactory.CreateInstance (elementType, rank: 1, length);
}

if (RuntimeFeature.ManagedTypeMap) {
Expand All @@ -61,21 +49,6 @@ Array ArrayCreateInstanceWithSuppression (Type elementType, int length)
throw new NotSupportedException ($"It is not possible to create an array with element type '{elementType}'.");
}

static int GetArrayRank (Type elementType, out Type leafElementType)
{
int rank = 1;
while (elementType.IsSZArray) {
rank++;
var nestedElementType = elementType.GetElementType ();
if (nestedElementType is null) {
break;
}
elementType = nestedElementType;
}
leafElementType = elementType;
return rank;
}

internal static IntPtr IdentityHash (IntPtr v)
{
return JniEnvironment.References.GetIdentityHashCode (new JniObjectReference (v));
Expand Down Expand Up @@ -613,11 +586,30 @@ static void AssertCompatibleArrayTypes (IntPtr sourceArray, Type destElementType

static IntPtr FindArrayClassByElementType (Type elementType)
{
var boxedPrimitiveJniClassName = GetBoxedPrimitiveJniClassName (elementType);
if (boxedPrimitiveJniClassName != null) {
return FindClass ("[L" + boxedPrimitiveJniClassName + ";");
}

int rank = JavaNativeTypeManager.GetArrayInfo (elementType, out elementType) + 1;
var typeSignature = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (elementType).AddArrayRank (rank);
return FindClass (typeSignature.Name);
}

static string? GetBoxedPrimitiveJniClassName (Type type)
{
if (type == typeof (bool?)) return "java/lang/Boolean";
if (type == typeof (byte?)) return "java/lang/Byte";
if (type == typeof (sbyte?)) return "java/lang/Byte";
if (type == typeof (char?)) return "java/lang/Character";
if (type == typeof (short?)) return "java/lang/Short";
if (type == typeof (int?)) return "java/lang/Integer";
if (type == typeof (long?)) return "java/lang/Long";
if (type == typeof (float?)) return "java/lang/Float";
if (type == typeof (double?)) return "java/lang/Double";
return null;
}

public static void CopyArray (IntPtr src, bool[] dest)
{
if (dest == null)
Expand Down Expand Up @@ -692,6 +684,15 @@ public static void CopyArray (IntPtr src, string[] dest)
_GetDoubleArrayRegion (source, index, 1, r);
return r [0];
} },
{ typeof (bool?), (type, source, index) => GetNullableArrayElement (source, index, typeof (bool?)) },
{ typeof (byte?), (type, source, index) => GetNullableArrayElement (source, index, typeof (byte?)) },
{ typeof (sbyte?), (type, source, index) => GetNullableArrayElement (source, index, typeof (sbyte?)) },
{ typeof (char?), (type, source, index) => GetNullableArrayElement (source, index, typeof (char?)) },
{ typeof (short?), (type, source, index) => GetNullableArrayElement (source, index, typeof (short?)) },
{ typeof (int?), (type, source, index) => GetNullableArrayElement (source, index, typeof (int?)) },
{ typeof (long?), (type, source, index) => GetNullableArrayElement (source, index, typeof (long?)) },
{ typeof (float?), (type, source, index) => GetNullableArrayElement (source, index, typeof (float?)) },
{ typeof (double?), (type, source, index) => GetNullableArrayElement (source, index, typeof (double?)) },
{ typeof (string), (type, source, index) => {
IntPtr elem = GetObjectArrayElement (source, index);
if (type == typeof (Java.Lang.String))
Expand All @@ -717,6 +718,12 @@ public static void CopyArray (IntPtr src, string[] dest)
};
}

static object? GetNullableArrayElement (IntPtr source, int index, [DynamicallyAccessedMembers (Constructors)] Type targetType)
{
IntPtr elem = GetObjectArrayElement (source, index);
return JavaConvert.FromJniHandle (elem, JniHandleOwnership.TransferLocalRef, targetType);
}

static TValue GetConverter<TValue>(Dictionary<Type, TValue> dict, Type? elementType, IntPtr array)
{
TValue? converter;
Expand Down Expand Up @@ -921,6 +928,15 @@ static Dictionary<Type, Action<Array, IntPtr>> CreateCopyManagedToNativeArray ()
{ typeof (long), (source, dest) => CopyArray ((long[]) source, dest) },
{ typeof (float), (source, dest) => CopyArray ((float[]) source, dest) },
{ typeof (double), (source, dest) => CopyArray ((double[]) source, dest) },
{ typeof (bool?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (byte?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (sbyte?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (char?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (short?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (int?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (long?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (float?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (double?), (source, dest) => CopyManagedObjectArray (source, dest) },
{ typeof (string), (source, dest) => {
var s = source as string[];
if (s != null) {
Expand Down Expand Up @@ -950,6 +966,23 @@ static Dictionary<Type, Action<Array, IntPtr>> CreateCopyManagedToNativeArray ()
};
}

static void CopyManagedObjectArray (Array source, IntPtr dest)
{
// Inlined equivalent of JavaConvert.WithLocalJniHandle to avoid allocating a capturing
// closure per element (this runs once per array element for both NewObjectArray and the
// nullable-primitive CopyManagedToNativeArray entries).
for (int i = 0; i < source.Length; i++) {
object? value = source.GetValue (i);
IntPtr lref = JavaConvert.ToLocalJniHandle (value);
try {
SetObjectArrayElement (dest, i, lref);
} finally {
DeleteLocalRef (lref);
GC.KeepAlive (value);
}
}
}

public static void CopyArray (Array source, Type elementType, IntPtr dest)
{
if (source == null)
Expand Down Expand Up @@ -1053,6 +1086,15 @@ public static void CopyArray<T> (T[] src, IntPtr dest)
CopyArray (source, r);
return r;
} },
{ typeof (bool?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (byte?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (sbyte?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (char?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (short?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (int?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (long?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (float?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (double?), (type, source, len) => CreateManagedArrayFromObjectArray (type, source, len) },
{ typeof (string), (type, source, len) => {
if (type != null && typeof (Java.Lang.Object).IsAssignableFrom (type)) {
var r = new Java.Lang.String [len];
Expand All @@ -1077,6 +1119,16 @@ public static void CopyArray<T> (T[] src, IntPtr dest)
};
}

static Array CreateManagedArrayFromObjectArray (Type? elementType, IntPtr source, int len)
{
if (elementType == null)
throw new ArgumentNullException (nameof (elementType));

var r = ArrayCreateInstance (elementType, len);
CopyArray (source, r, elementType);
return r;
}

static Array? _GetArray (IntPtr array_ptr, Type? element_type)
{
if (array_ptr == IntPtr.Zero)
Expand Down Expand Up @@ -1329,12 +1381,49 @@ static Dictionary<Type, Func<Array, IntPtr>> CreateCreateManagedToNativeArray ()
{ typeof (long), (source) => NewArray ((long[]) source) },
{ typeof (float), (source) => NewArray ((float[]) source) },
{ typeof (double), (source) => NewArray ((double[]) source) },
{ typeof (bool?), (source) => NewObjectArray (source, typeof (bool?)) },
{ typeof (byte?), (source) => NewObjectArray (source, typeof (byte?)) },
{ typeof (sbyte?), (source) => NewObjectArray (source, typeof (sbyte?)) },
{ typeof (char?), (source) => NewObjectArray (source, typeof (char?)) },
{ typeof (short?), (source) => NewObjectArray (source, typeof (short?)) },
{ typeof (int?), (source) => NewObjectArray (source, typeof (int?)) },
{ typeof (long?), (source) => NewObjectArray (source, typeof (long?)) },
{ typeof (float?), (source) => NewObjectArray (source, typeof (float?)) },
{ typeof (double?), (source) => NewObjectArray (source, typeof (double?)) },
{ typeof (string), (source) => NewArray ((string[]) source) },
{ typeof (IJavaObject), (source) => NewArray ((IJavaObject[]) source) },
{ typeof (Array), (source) => NewArray (source) },
};
}

static IntPtr NewObjectArray (Array value, Type elementType)
{
IntPtr grefArrayElementClass = FindObjectArrayElementClass (elementType);
try {
IntPtr array = IntPtr.Zero;
try {
array = NewObjectArray (value.Length, grefArrayElementClass, IntPtr.Zero);
CopyManagedObjectArray (value, array);
return array;
} catch {
DeleteLocalRef (array);
throw;
}
} finally {
DeleteGlobalRef (grefArrayElementClass);
}
}

static IntPtr FindObjectArrayElementClass (Type elementType)
{
var boxedPrimitiveJniClassName = GetBoxedPrimitiveJniClassName (elementType);
if (boxedPrimitiveJniClassName != null) {
return FindClass (boxedPrimitiveJniClassName);
}

return FindClass (elementType);
}

public static IntPtr NewArray (Array value, Type? elementType = null)
{
if (value == null)
Expand Down Expand Up @@ -1444,6 +1533,15 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass)
var _value = new[]{(double) value!};
_SetDoubleArrayRegion (dest, index, _value.Length, _value);
} },
{ typeof (bool?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (byte?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (sbyte?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (char?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (short?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (int?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (long?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (float?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (double?), (dest, index, value) => SetObjectArrayElementFromManagedValue (dest, index, value) },
{ typeof (string), (dest, index, value) => {
IntPtr s = NewString (value!.ToString ());
try {
Expand All @@ -1463,6 +1561,19 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass)
};
}

static void SetObjectArrayElementFromManagedValue (IntPtr dest, int index, object? value)
{
// Inlined equivalent of JavaConvert.WithLocalJniHandle to avoid allocating a capturing
// closure on every element set (this runs once per element on the SetArrayItem path).
IntPtr lref = JavaConvert.ToLocalJniHandle (value);
try {
SetObjectArrayElement (dest, index, lref);
} finally {
DeleteLocalRef (lref);
GC.KeepAlive (value);
}
}

static unsafe void _SetBooleanArrayRegion (IntPtr array, int start, int length, bool[] buffer)
{
fixed (bool* p = buffer)
Expand Down
Loading