diff --git a/src/Mono.Android/Android.Runtime/JavaCollection.cs b/src/Mono.Android/Android.Runtime/JavaCollection.cs index 9a41485cf71..48bdcd6abd2 100644 --- a/src/Mono.Android/Android.Runtime/JavaCollection.cs +++ b/src/Mono.Android/Android.Runtime/JavaCollection.cs @@ -148,11 +148,6 @@ internal Java.Lang.Object[] ToArray () // public void CopyTo (Array array, int array_index) { - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "JavaCollection constructors are preserved by the MarkJavaObjects trimmer step.")] - [return: DynamicallyAccessedMembers (Constructors)] - static Type GetElementType (Array array) => - array.GetType ().GetElementType (); - if (array == null) throw new ArgumentNullException ("array"); if (array_index < 0) @@ -163,13 +158,13 @@ static Type GetElementType (Array array) => if (id_toArray == IntPtr.Zero) id_toArray = JNIEnv.GetMethodID (collection_class, "toArray", "()[Ljava/lang/Object;"); + var converter = new JavaConvert.ArrayElementConverter (array); IntPtr lrefArray = JNIEnv.CallObjectMethod (Handle, id_toArray); for (int i = 0; i < Count; i++) array.SetValue ( - JavaConvert.FromJniHandle ( + converter.FromJniHandle ( JNIEnv.GetObjectArrayElement (lrefArray, i), - JniHandleOwnership.TransferLocalRef, - GetElementType (array)), + JniHandleOwnership.TransferLocalRef), array_index + i); JNIEnv.DeleteLocalRef (lrefArray); } diff --git a/src/Mono.Android/Android.Runtime/JavaList.cs b/src/Mono.Android/Android.Runtime/JavaList.cs index 5dd329e71cb..73323903d7e 100644 --- a/src/Mono.Android/Android.Runtime/JavaList.cs +++ b/src/Mono.Android/Android.Runtime/JavaList.cs @@ -23,26 +23,29 @@ public partial class JavaList : Java.Lang.Object, System.Collections.IList { // // https://developer.android.com/reference/java/util/List.html?hl=en#get(int) // - internal unsafe object? InternalGet ( + internal object? InternalGet ( int location, [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) + { + var obj = InternalGetReference (location); + return JavaConvert.FromJniHandle ( + obj.Handle, + JniHandleOwnership.TransferLocalRef, + targetType); + } + + internal unsafe JniObjectReference InternalGetReference (int location) { const string id = "get.(I)Ljava/lang/Object;"; - JniObjectReference obj; try { JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { new JniArgumentValue (location), }; - obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); + return list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { throw new ArgumentOutOfRangeException (ex.Message, ex); } - - return JavaConvert.FromJniHandle ( - obj.Handle, - JniHandleOwnership.TransferLocalRef, - targetType); } // @@ -267,13 +270,8 @@ public unsafe bool Contains (object? item) } } - public void CopyTo (Array array, int array_index) + public unsafe void CopyTo (Array array, int array_index) { - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "JavaList constructors are preserved by the MarkJavaObjects trimmer step.")] - [return: DynamicallyAccessedMembers (Constructors)] - static Type GetElementType (Array array) => - array.GetType ().GetElementType (); - if (array == null) throw new ArgumentNullException ("array"); if (array_index < 0) @@ -281,10 +279,12 @@ static Type GetElementType (Array array) => if (array.Length < array_index + Count) throw new ArgumentException ("array"); - var targetType = GetElementType (array); + var converter = new JavaConvert.ArrayElementConverter (array); int c = Count; - for (int i = 0; i < c; i++) - array.SetValue (InternalGet (i, targetType), array_index + i); + for (int i = 0; i < c; i++) { + var obj = InternalGetReference (i); + array.SetValue (converter.FromJniHandle (obj.Handle, JniHandleOwnership.TransferLocalRef), array_index + i); + } } public IEnumerator GetEnumerator () @@ -737,19 +737,9 @@ public JavaList (IEnumerable items) : this () // // https://developer.android.com/reference/java/util/List.html?hl=en#get(int) // - internal unsafe T? InternalGet (int location) + internal T? InternalGet (int location) { - const string id = "get.(I)Ljava/lang/Object;"; - JniObjectReference obj; - try { - JniArgumentValue* parameters = stackalloc JniArgumentValue [1] { - new JniArgumentValue (location), - }; - obj = list_members.InstanceMethods.InvokeAbstractObjectMethod (id, this, parameters); - } catch (Java.Lang.IndexOutOfBoundsException ex) when (JNIEnv.ShouldWrapJavaException (ex)) { - throw new ArgumentOutOfRangeException (ex.Message, ex); - } - + var obj = InternalGetReference (location); return JavaConvert.FromJniHandle ( obj.Handle, JniHandleOwnership.TransferLocalRef); diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ebff6c1f765..ba46b155c00 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -162,6 +162,92 @@ static Func GetJniHandleConverterForType ([D typeof (Func), m); } + internal readonly struct ArrayElementConverter + { + readonly Type? elementType; + readonly Func? converter; + readonly bool useRuntimeTypeMapping; + + public ArrayElementConverter (Array array) + { + elementType = array.GetType ().GetElementType (); + converter = elementType != null ? GetJniHandleConverter (elementType) : null; + useRuntimeTypeMapping = elementType is null || elementType == typeof (object); + } + + public object? FromJniHandle (IntPtr handle, JniHandleOwnership transfer) + { + if (handle == IntPtr.Zero) + return null; + + if (useRuntimeTypeMapping) + return FromJniHandleWithRuntimeTypeMapping (handle, transfer); + + if (elementType != null) { + var peeked = Java.Lang.Object.PeekObject (handle, elementType); + if (peeked != null) { + JNIEnv.DeleteRef (handle, transfer); + return peeked; + } + } + + if (converter != null) + return converter (handle, transfer); + + if (elementType != null && elementType.IsArray) + return JNIEnv.GetArray (handle, transfer, elementType.GetElementType ()); + + if (elementType != null && typeof (IJavaPeerable).IsAssignableFrom (elementType)) { + if (RuntimeFeature.TrimmableTypeMap) + return FromJniHandleWithTrimmableTypeMapping (handle, transfer, elementType); + return Java.Lang.Object.GetObject (handle, transfer, elementType); + } + + var value = FromJniHandleWithRuntimeTypeMapping (handle, transfer); + if (value == null || elementType == null || elementType.IsAssignableFrom (value.GetType ())) + return value; + return Convert.ChangeType (value, elementType, CultureInfo.InvariantCulture); + } + } + + static object? FromJniHandleWithRuntimeTypeMapping (IntPtr handle, JniHandleOwnership transfer) + { + var converter = GetJniHandleConverter (GetTypeMapping (handle)); + if (converter != null) + return converter (handle, transfer); + return FromJniHandle (handle, transfer); + } + + static object? FromJniHandleWithTrimmableTypeMapping (IntPtr handle, JniHandleOwnership transfer, Type elementType) + { + bool consumed = false; + try { + if (elementType.IsGenericType) { + throw new NotSupportedException ( + FormattableString.Invariant ($"Cannot convert Java collection elements to closed generic array element type '{elementType}'.")); + } + + // This path intentionally avoids the reflection fallback used by TrimmableTypeMap.CreateInstance () + // because passing array element types there would require DAM annotations. Closed generic element + // types cannot be supported without that fallback: creating a non-generic base peer would not be + // assignable to the requested closed generic array element type. If the requested element type is + // already a non-generic base type, the typemap lookup can still select that base mapping. + var peer = TrimmableTypeMap.Instance.CreateInstanceWithoutReflectionFallback (handle, elementType); + if (peer != null) { + consumed = true; + JNIEnv.DeleteRef (handle, transfer); + return peer; + } + + throw new NotSupportedException ( + FormattableString.Invariant ($"Cannot convert Java collection element to array element type '{elementType}' using the trimmable type map.")); + } finally { + if (!consumed) { + JNIEnv.DeleteRef (handle, transfer); + } + } + } + public static T? FromJniHandle< [DynamicallyAccessedMembers (Constructors)] T @@ -498,4 +584,3 @@ public static TReturn WithLocalJniHandle(object? value, Func /// Returns true when 's Java class is not assignable from /// . Throws when has no usable mapping. diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a574c00a066..915daa0e248 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -383,8 +383,74 @@ static JniMethodInfo GetClassGetInterfacesMethod () } } + internal IJavaPeerable? CreateInstance ( + IntPtr handle, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + var proxy = GetProxyForJavaObject (handle, targetType); + + IJavaPeerable? peer; + if (ShouldActivateClosedGenericTarget (proxy, targetType)) { + peer = ActivateUsingReflection (targetType, handle, JniHandleOwnership.DoNotTransfer); + } else { + peer = proxy?.CreateInstance (handle, JniHandleOwnership.DoNotTransfer); + } + if (peer is not null) { + MarkCreatedPeer (peer); + } + return peer; + } + + internal IJavaPeerable? CreateInstanceWithoutReflectionFallback (IntPtr handle, Type? targetType = null) + { + var peer = GetProxyForJavaObject (handle, targetType)?.CreateInstance (handle, JniHandleOwnership.DoNotTransfer); + if (peer is not null) { + MarkCreatedPeer (peer); + } + return peer; + } + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; + + static bool ShouldActivateClosedGenericTarget ( + [NotNullWhen (true)] JavaPeerProxy? proxy, + [NotNullWhen (true)] Type? targetType) + { + return proxy is not null && + proxy.TargetType.IsGenericTypeDefinition && + targetType is not null && + targetType.IsGenericType && + !targetType.IsGenericTypeDefinition; + } + + static IJavaPeerable? ActivateUsingReflection ( + [DynamicallyAccessedMembers (Constructors)] + Type closedType, + IntPtr handle, + JniHandleOwnership transfer) + { + var ctor = closedType.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (ctor is null) { + return null; + } + + return (IJavaPeerable) ctor.Invoke ([handle, transfer]); + } + + static void MarkCreatedPeer (IJavaPeerable peer) + { + var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; + if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { + peerState |= JniManagedPeerStates.Activatable; + } + peer.SetJniManagedPeerState (peerState); + } + /// /// Match the proxy's stored target type against a hint from the caller. /// The proxy's target type is the open generic definition for generic peers diff --git a/src/Mono.Android/metadata b/src/Mono.Android/metadata index 4b4732a5eaf..9aba17d02a9 100644 --- a/src/Mono.Android/metadata +++ b/src/Mono.Android/metadata @@ -387,7 +387,7 @@ Android.Graphics.Color ", "(Landroid/content/Context;)V"); + viewHandle = JNIEnv.NewObject (viewClass.Handle, constructor, new JValue (Android.App.Application.Context.Handle)); + AddToJavaCollection (arrayList, viewHandle); + + var values = new View [1]; + CopyToJavaCollection (arrayList, values); + Assert.IsTrue (JNIEnv.IsSameObject (viewHandle, values [0].Handle)); + + values = new View [1]; + CopyToJavaList (arrayList, values); + Assert.IsTrue (JNIEnv.IsSameObject (viewHandle, values [0].Handle)); + } finally { + JNIEnv.DeleteLocalRef (viewHandle); + JniObjectReference.Dispose (ref viewClass); + } + } + } + + [Test] + public void NonGenericCollection_CopyTo_ObjectArray_PreservesNullElement () + { + AssumeTrimmableTypeMapEnabled (); + + using (var arrayList = new Java.Util.ArrayList ()) { + arrayList.Add (42); + arrayList.Add (null); + + var values = new object [2]; + CopyToJavaCollection (arrayList, values); + Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); + + values = new object [2]; + CopyToJavaList (arrayList, values); + Assert.AreEqual (42, values [0]); + Assert.IsNull (values [1]); + } + } + + [Test] + public void NonGenericCollection_CopyTo_StringArray_ConvertsJavaString () + { + AssumeTrimmableTypeMapEnabled (); + + using (var arrayList = new Java.Util.ArrayList ()) { + arrayList.Add ("alpha"); + + var values = new string [1]; + CopyToJavaCollection (arrayList, values); + Assert.AreEqual ("alpha", values [0]); + + values = new string [1]; + CopyToJavaList (arrayList, values); + Assert.AreEqual ("alpha", values [0]); + } + } + + static void CopyToJavaCollection (Java.Util.ArrayList arrayList, Array values) + { + using (var collection = new JavaCollection (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + collection.CopyTo (values, 0); + } + } + + static void CopyToJavaList (Java.Util.ArrayList arrayList, Array values) + { + using (var list = new JavaList (arrayList.Handle, JniHandleOwnership.DoNotTransfer)) { + list.CopyTo (values, 0); + } + } + + static void AddToJavaCollection (Java.Util.ArrayList arrayList, IntPtr handle) + { + var add = JNIEnv.GetMethodID (arrayList.Class.Handle, "add", "(Ljava/lang/Object;)Z"); + JNIEnv.CallBooleanMethod (arrayList.Handle, add, new JValue (handle)); + } + static T CreateFromJava () where T : Java.Lang.Object {