From 646f0dcf89bc2bb88b6a568d7cbd0bdbfc7f8ba6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 15 Jul 2026 11:45:18 +0200 Subject: [PATCH] Fix legacy generic dictionary conversion Route requested generic dictionary targets through JavaConvert while preserving assignable cached peers and non-generic inference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0e043be9-9070-43bc-aea4-a48438690bdf --- .../Android.Runtime/AndroidRuntime.cs | 32 ++++++++ src/Mono.Android/Java.Interop/JavaConvert.cs | 13 +++- .../JavaMarshalValueManager.cs | 32 ++++++++ .../Java.Interop/JavaConvertTest.cs | 78 ++++++++++++++++++- 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 34bafabbe58..a89a1a68daf 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -609,6 +609,38 @@ class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); + protected override object? GetValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type? targetType = null) + { + if (JavaConvert.IsGenericDictionary (targetType)) + return JavaConvert.FromObjectReference (ref reference, options, targetType); + + return base.GetValueCore (ref reference, options, targetType); + } + + [return: MaybeNull] + protected override T GetValueCore<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type? targetType = null) + { + if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) + return base.GetValueCore (ref reference, options, targetType); + + var requestedType = targetType ?? typeof (T); + if (JavaConvert.IsGenericDictionary (requestedType)) { +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + return (T) JavaConvert.FromObjectReference (ref reference, options, requestedType); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + + return base.GetValueCore (ref reference, options, targetType); + } + public override void WaitForGCBridgeProcessing () { if (!AndroidRuntimeInternal.BridgeProcessing) diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index e8ee8741acb..c441e31ec61 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -124,7 +124,8 @@ static class JavaConvert { [RequiresDynamicCode ("This API uses reflection to create generic types at runtime, which is not supported in AOT scenarios.")] static Func? TryMakeGenericCollectionTypeFactory (Type target) { - if (target.GetGenericTypeDefinition() == typeof (IDictionary<,>)) { + var genericTypeDefinition = target.GetGenericTypeDefinition (); + if (genericTypeDefinition == typeof (IDictionary<,>) || genericTypeDefinition == typeof (JavaDictionary<,>)) { Type t = typeof (JavaDictionary<,>).MakeGenericType (target.GetGenericArguments ()); return GetJniHandleConverterForType (t); } @@ -141,6 +142,16 @@ static class JavaConvert { } } + internal static bool IsGenericDictionary (Type? type) + { + if (type == null || !type.IsGenericType || type.IsGenericTypeDefinition) + return false; + + var genericTypeDefinition = type.GetGenericTypeDefinition (); + return genericTypeDefinition == typeof (IDictionary<,>) || + genericTypeDefinition == typeof (JavaDictionary<,>); + } + /// /// AOT-safe converter using from the generated proxy. /// Avoids MakeGenericType() by using the pre-typed factory from the proxy attribute. diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e83d20fd046..d4706429004 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -21,6 +21,38 @@ public JavaMarshalValueManager () JavaMarshalRegisteredPeers.InitializeIfNeeded (); } + protected override object? GetValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + if (JavaConvert.IsGenericDictionary (targetType)) + return JavaConvert.FromObjectReference (ref reference, options, targetType); + + return base.GetValueCore (ref reference, options, targetType); + } + + [return: MaybeNull] + protected override T GetValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) + return base.GetValueCore (ref reference, options, targetType); + + var requestedType = targetType ?? typeof (T); + if (JavaConvert.IsGenericDictionary (requestedType)) { +#pragma warning disable CS8600 // Converting null literal or possible null value to non-nullable type. + return (T) JavaConvert.FromObjectReference (ref reference, options, requestedType); +#pragma warning restore CS8600 // Converting null literal or possible null value to non-nullable type. + } + + return base.GetValueCore (ref reference, options, targetType); + } + public override void WaitForGCBridgeProcessing () { // Intentionally empty. The Mono runtime's own implementation acknowledges this diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs index 92552cf6033..eb3c14c601c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaConvertTest.cs @@ -1,10 +1,12 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using Android.App; using Android.Content; using Android.Runtime; +using Java.Interop; using NUnit.Framework; @@ -113,6 +115,81 @@ public void NullStringMarshalsAsIntPtrZero () Assert.AreEqual (null, list [0]); } + [Test] + public void ValueManagerGenericDictionaryConversions () + { + AssertValueManagerDictionaryByType (42, "forty-two", typeof (IDictionary)); + AssertValueManagerDictionaryByType (7, 3.5, typeof (JavaDictionary)); + + using (var value = new MyIntent ()) + AssertValueManagerDictionary ("intent", value); + + using (var key = new MyIntent ()) + AssertValueManagerJavaDictionary (key, 64); + } + + [Test] + public void ValueManagerNonGenericDictionaryConversionPreservesCachedPeer () + { + using (var source = new JavaDictionary ()) { + source.Add ("answer", 42); + var reference = new JniObjectReference (source.Handle); + var actual = JniEnvironment.Runtime.ValueManager.GetValue ( + ref reference, JniObjectReferenceOptions.Copy, typeof (IDictionary)); + + Assert.AreSame (source, actual); + Assert.AreEqual (42, ((IDictionary) actual) ["answer"]); + } + } + + [Test] + public void ValueManagerGenericDictionaryConversionPreservesAssignableCachedPeer () + { + using (var source = new JavaDictionary { { 42, "forty-two" } }) { + var actual = JniEnvironment.Runtime.ValueManager.GetValue> (source.Handle); + + Assert.AreSame (source, actual); + } + } + + static void AssertValueManagerDictionaryByType (TKey key, TValue value, Type targetType) + { + using (var source = new JavaDictionary ()) { + source.Add (key, value); + var reference = new JniObjectReference (source.Handle); + var actual = JniEnvironment.Runtime.ValueManager.GetValue ( + ref reference, JniObjectReferenceOptions.Copy, targetType); + + Assert.AreEqual (typeof (JavaDictionary), actual.GetType ()); + using (var dictionary = (JavaDictionary) actual) + Assert.AreEqual (value, dictionary [key]); + } + } + + static void AssertValueManagerDictionary (TKey key, TValue value) + { + using (var source = new JavaDictionary ()) { + source.Add (key, value); + var actual = JniEnvironment.Runtime.ValueManager.GetValue> (source.Handle); + + Assert.AreEqual (typeof (JavaDictionary), actual.GetType ()); + using (var dictionary = (JavaDictionary) actual) + Assert.AreEqual (value, dictionary [key]); + } + } + + static void AssertValueManagerJavaDictionary (TKey key, TValue value) + { + using (var source = new JavaDictionary ()) { + source.Add (key, value); + var actual = JniEnvironment.Runtime.ValueManager.GetValue> (source.Handle); + + Assert.AreEqual (typeof (JavaDictionary), actual.GetType ()); + using (actual) + Assert.AreEqual (value, actual [key]); + } + } + [Test] public void MarshalInt23Array () { @@ -140,4 +217,3 @@ static Java.Util.ArrayList CreateList (params int[][] items) } } } -