Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
163 changes: 128 additions & 35 deletions src/Mono.Android/Android.Runtime/JNIEnv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,48 +28,14 @@ public static partial class JNIEnv {
static Array ArrayCreateInstance (Type elementType, int length)
{
if (RuntimeFeature.TrimmableTypeMap) {
if (RuntimeFeature.IsCoreClrRuntime) {
// CoreCLR runtime type loader can construct any T[] dynamically.
// IsDynamicCodeSupported is a [FeatureGuard] so this branch is
// dead-coded under PublishAot.
return Array.CreateInstance (elementType, length);
}

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);
}

#pragma warning disable IL3050 // legacy fallback path
return Array.CreateInstance (elementType, length);
#pragma warning restore IL3050
}

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 @@ -605,11 +571,29 @@ 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 (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 @@ -684,6 +668,14 @@ 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 (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 @@ -709,6 +701,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 @@ -913,6 +911,14 @@ 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 (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 @@ -942,6 +948,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 @@ -1045,6 +1068,14 @@ public static void CopyArray<T> (T[] src, IntPtr dest)
CopyArray (source, r);
return r;
} },
{ typeof (bool?), (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 @@ -1069,6 +1100,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 ("elementType");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 💡 Formatting — Use nameof (elementType) instead of the string literal "elementType" so the argument name stays correct under future renames.

throw new ArgumentNullException (nameof (elementType));

(Rule: Prefer nameof for argument names — repo-conventions)


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 @@ -1321,12 +1362,48 @@ 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 (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 @@ -1436,6 +1513,14 @@ 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 (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 @@ -1455,6 +1540,14 @@ static IntPtr NewArray (Array value, Type elementType, IntPtr elementClass)
};
}

static void SetObjectArrayElementFromManagedValue (IntPtr dest, int index, object? value)
{
JavaConvert.WithLocalJniHandle (value, lref => {
SetObjectArrayElement (dest, index, lref);
return IntPtr.Zero;
});
}

static unsafe void _SetBooleanArrayRegion (IntPtr array, int start, int length, bool[] buffer)
{
fixed (bool* p = buffer)
Expand Down
96 changes: 13 additions & 83 deletions src/Mono.Android/Java.Interop/JavaConvert.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,19 +57,6 @@ static class JavaConvert {
} },
};

static readonly Dictionary<Type, JavaPeerContainerFactory> ScalarContainerFactories = new Dictionary<Type, JavaPeerContainerFactory> {
{ typeof (bool), JavaPeerContainerFactory<bool>.Instance },
{ typeof (byte), JavaPeerContainerFactory<byte>.Instance },
{ typeof (sbyte), JavaPeerContainerFactory<sbyte>.Instance },
{ typeof (char), JavaPeerContainerFactory<char>.Instance },
{ typeof (short), JavaPeerContainerFactory<short>.Instance },
{ typeof (int), JavaPeerContainerFactory<int>.Instance },
{ typeof (long), JavaPeerContainerFactory<long>.Instance },
{ typeof (float), JavaPeerContainerFactory<float>.Instance },
{ typeof (double), JavaPeerContainerFactory<double>.Instance },
{ typeof (string), JavaPeerContainerFactory<string>.Instance },
};

static Func<IntPtr, JniHandleOwnership, object?>? GetJniHandleConverter (Type? target)
{
if (target == null)
Expand All @@ -89,9 +76,14 @@ static class JavaConvert {

if (target.IsGenericType && !target.IsGenericTypeDefinition) {
if (RuntimeFeature.TrimmableTypeMap) {
var factoryConverter = TryGetFactoryBasedConverter (target);
if (factoryConverter != null)
return factoryConverter;
if (SafeJavaCollectionFactory.TryGetCollectionType (target, out _)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Performance — The resolved collection type from TryGetCollectionType is discarded here (out _) and used only as a boolean gate. TryCreateFromJniHandle then calls TryGetCollectionType again internally, so for reference-type generic collections (e.g. IList<string>, IDictionary<string,string>) MakeGenericType() runs twice per marshal — and GetJniHandleConverter isn't cached, so this repeats on every FromJniHandle call. Consider having the converter reuse the already-resolved Type (capture it in the closure) instead of recomputing it, to avoid the redundant reflection/allocation on a hot marshaling path.

For value/value shapes this probe is also slightly at odds with the AOT-safety design: TryGetCollectionTypeGetClosedCollectionType calls MakeGenericType(JavaDictionary<,>, [int,long]) as an availability check even though creation goes through the rooted ValueTypeFactory path — the probe still relies on the exact instantiation being rooted.

(Rule: Avoid redundant reflection on hot paths — repo-conventions Performance)

return (h, t) => {
if (SafeJavaCollectionFactory.TryCreateFromJniHandle (target, h, t, out var collection)) {
return collection;
}
return null;
};
}
} else {
var factoryConverter = TryMakeGenericCollectionTypeFactory (target);
if (factoryConverter != null)
Expand All @@ -110,6 +102,11 @@ static class JavaConvert {

[UnconditionalSuppressMessage ("ReflectionAnalysis", "IL2055:RequiresUnreferencedCode",
Justification = "The target generic type is expected to be preserved by the trimmer as the target type in marshaling.")]
[UnconditionalSuppressMessage ("AOT", "IL3050:RequiresDynamicCode",
Justification = "This legacy MakeGenericType() path is only reached from the '!RuntimeFeature.TrimmableTypeMap' branch above, " +
"i.e. on the reflection-based Mono/CoreCLR runtimes where dynamic code is available. NativeAOT always enables the trimmable " +
"type map, so under AOT the branch above (SafeJavaCollectionFactory) is used instead and this helper is never executed. " +
"The AOT-safe generic collection construction lives in SafeJavaCollectionFactory.")]
static Func<IntPtr, JniHandleOwnership, object?>? TryMakeGenericCollectionTypeFactory (Type target)
{
if (target.GetGenericTypeDefinition() == typeof (IDictionary<,>)) {
Expand All @@ -129,73 +126,6 @@ static class JavaConvert {
}
}

/// <summary>
/// AOT-safe converter using <see cref="JavaPeerContainerFactory"/> from the generated proxy.
/// Avoids <c>MakeGenericType()</c> by using the pre-typed factory from the proxy attribute.
/// </summary>
static Func<IntPtr, JniHandleOwnership, object?>? TryGetFactoryBasedConverter (Type target)
{
if (TryGetSingleGenericArgument (target, typeof (IList<>), typeof (JavaList<>), out var listElementType)) {
var factory = TryGetContainerFactory (listElementType);
if (factory != null)
return (h, t) => factory.CreateList (h, t);
}

if (TryGetSingleGenericArgument (target, typeof (ICollection<>), typeof (JavaCollection<>), out var collectionElementType)) {
var factory = TryGetContainerFactory (collectionElementType);
if (factory != null)
return (h, t) => factory.CreateCollection (h, t);
}

if (TryGetDictionaryArguments (target, out var typeArgs)) {
var keyFactory = TryGetContainerFactory (typeArgs [0]);
var valueFactory = TryGetContainerFactory (typeArgs [1]);
if (keyFactory != null && valueFactory != null)
return (h, t) => valueFactory.CreateDictionary (keyFactory, h, t);
}

return null;

static bool TryGetSingleGenericArgument (Type target, Type interfaceType, Type wrapperType, [NotNullWhen (true)] out Type? argument)
{
if (target.IsGenericType && !target.IsGenericTypeDefinition) {
var genericDef = target.GetGenericTypeDefinition ();
if (genericDef == interfaceType || genericDef == wrapperType) {
argument = target.GetGenericArguments () [0];
return true;
}
}

argument = null;
return false;
}

static bool TryGetDictionaryArguments (Type target, [NotNullWhen (true)] out Type []? arguments)
{
if (target.IsGenericType && !target.IsGenericTypeDefinition) {
var genericDef = target.GetGenericTypeDefinition ();
if (genericDef == typeof (IDictionary<,>) || genericDef == typeof (JavaDictionary<,>)) {
arguments = target.GetGenericArguments ();
return true;
}
}

arguments = null;
return false;
}

static JavaPeerContainerFactory? TryGetContainerFactory (Type elementType)
{
if (ScalarContainerFactories.TryGetValue (elementType, out var scalarFactory))
return scalarFactory;

if (typeof (IJavaPeerable).IsAssignableFrom (elementType))
return TrimmableTypeMap.Instance?.GetContainerFactory (elementType);

return null;
}
}

static Func<IntPtr, JniHandleOwnership, object> GetJniHandleConverterForType ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t)
{
MethodInfo m = t.GetMethod ("FromJniHandle", BindingFlags.Static | BindingFlags.Public)!;
Expand Down
Loading