From 0021abf0f8465345b4189ec85902e6273af3695f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 11:14:32 +0200 Subject: [PATCH 01/20] [TrimmableTypeMap] CoreCLR JavaMarshal value-manager split + JI bump Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Android.Runtime/JNIEnvInit.cs | 4 +- .../CoreClrJavaMarshalValueManager.cs | 224 +++++++++++++++ .../JavaMarshalValueManager.cs | 222 --------------- .../JavaMarshalValueManagerHelper.cs | 65 +++++ .../RuntimeFeature.cs | 6 + .../SimpleValueManager.cs | 259 ------------------ src/Mono.Android/Mono.Android.csproj | 4 +- 8 files changed, 300 insertions(+), 486 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs diff --git a/external/Java.Interop b/external/Java.Interop index 70493645c7d..8d544738ad2 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 70493645c7d95648010a4cef948234a28744c03f +Subproject commit 8d544738ad294b4faf13d189eeeb02f0313e00b3 diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index c2e7ea913ca..08c08e75700 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -188,11 +188,11 @@ internal static JniRuntime.JniValueManager CreateValueManager () } if (RuntimeFeature.IsCoreClrRuntime) { - return new JavaMarshalValueManager (); + return new CoreClrJavaMarshalValueManager (); } if (RuntimeFeature.IsNativeAotRuntime) { - return new JavaMarshalValueManager (); + return new CoreClrJavaMarshalValueManager (); } throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs new file mode 100644 index 00000000000..bbde9496e79 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using Android.Runtime; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +[RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] +[RequiresUnreferencedCode ("This value manager is reflection-backed and is not trimming-compatible.")] +sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManager +{ + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; + static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; + + public CoreClrJavaMarshalValueManager () + { + JavaMarshalRegisteredPeers.InitializeIfNeeded (); + } + + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + } + + public override void WaitForGCBridgeProcessing () + { + // Intentionally empty. The Mono runtime's own implementation acknowledges this + // pattern is fundamentally flawed (see FIXME in sgen-bridge.c): a thread that + // passes the check can still race with bridge processing that starts immediately + // after. The wait cannot prevent the race, only reduce its window. On CoreCLR, + // JNI wrapper threads hold their own handle copies via JniObjectReference, so + // they are not affected by the bridge swapping control_block handles. + } + + public override void CollectPeers () + { + JavaMarshalRegisteredPeers.CollectPeers (); + } + + public override void AddPeer (IJavaPeerable value) + { + JavaMarshalRegisteredPeers.AddPeer (value); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + return JavaMarshalRegisteredPeers.PeekPeer (reference); + } + + public override void RemovePeer (IJavaPeerable value) + { + JavaMarshalRegisteredPeers.RemovePeer (value); + } + + public override void FinalizePeer (IJavaPeerable value) + { + JavaMarshalRegisteredPeers.FinalizePeer (value); + } + + public override List GetSurfacedPeers () + { + return JavaMarshalRegisteredPeers.GetSurfacedPeers (); + } + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + EnsureNotDisposed (); + + if (!reference.IsValid) { + return null; + } + + targetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); + + if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { + throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); + } + + var targetSig = Runtime.TypeManager.GetTypeSignature (targetType); + if (!targetSig.IsValid || targetSig.SimpleReference == null) { + throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); + } + + if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetSig.SimpleReference, ref reference, targetType)) { + return null; + } + + var refClass = JniEnvironment.Types.GetObjectClass (reference); + try { + var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); + if (peer == null) { + throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", + JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); + } + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + return peer; + } finally { + JniObjectReference.Dispose (ref refClass); + } + } + + IJavaPeerable? CreatePeerInstance ( + ref JniObjectReference klass, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + ref JniObjectReference reference, + JniObjectReferenceOptions transfer) + { + var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); + + while (jniTypeName != null) { + JniTypeSignature sig; + if (!JniTypeSignature.TryParse (jniTypeName, out sig)) + return null; + + Type? type = GetTypeAssignableTo (sig, targetType); + if (type != null) { + var peer = TryCreatePeerInstance (ref reference, transfer, type); + + if (peer != null) { + JniObjectReference.Dispose (ref klass); + return peer; + } + } + + var super = JniEnvironment.Types.GetSuperclass (klass); + jniTypeName = super.IsValid + ? JniEnvironment.Types.GetJniTypeNameFromClass (super) + : null; + + JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); + klass = super; + } + JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); + + return TryCreatePeerInstance (ref reference, transfer, targetType); + + Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType) + { + foreach (var t in Runtime.TypeManager.GetTypes (sig)) { + if (targetType.IsAssignableFrom (t)) { + return t; + } + } + return null; + } + } + + IJavaPeerable? TryCreatePeerInstance ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type type) + { + type = Runtime.TypeManager.GetInvokerType (type) ?? type; + + var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); + self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + + var constructed = false; + try { + constructed = TryConstructPeer (self, ref reference, options, type); + } finally { + if (!constructed) { + GC.SuppressFinalize (self); + self = null; + } + } + return self; + } + + bool TryConstructPeer ( + IJavaPeerable self, + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type type) + { + var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (c != null) { + var args = new object[] { + reference.Handle, + JniHandleOwnership.DoNotTransfer, + }; + c.Invoke (self, args); + JniObjectReference.Dispose (ref reference, options); + return true; + } + + c = type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); + if (c != null) { + var args = new object[] { + reference, + options, + }; + c.Invoke (self, args); + reference = (JniObjectReference) args [0]; + return true; + } + + return false; + } + + protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) + { + var proxy = value as JavaProxyThrowable; + if (proxy != null) { + result = proxy.InnerException; + return true; + } + return base.TryUnboxPeerObject (value, out result); + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs deleted file mode 100644 index 1ae56e7ce86..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; -using Android.Runtime; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -class JavaMarshalValueManager : AndroidReflectionJniValueManager -{ - 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) }; - - bool disposed; - - public JavaMarshalValueManager () - { - JavaMarshalRegisteredPeers.InitializeIfNeeded (); - } - - protected override void Dispose (bool disposing) - { - disposed = true; - base.Dispose (disposing); - } - - void ThrowIfDisposed () - { - if (disposed) - throw new ObjectDisposedException (nameof (JavaMarshalValueManager)); - } - - public override void WaitForGCBridgeProcessing () - { - // Intentionally empty. The Mono runtime's own implementation acknowledges this - // pattern is fundamentally flawed (see FIXME in sgen-bridge.c): a thread that - // passes the check can still race with bridge processing that starts immediately - // after. The wait cannot prevent the race, only reduce its window. On CoreCLR, - // JNI wrapper threads hold their own handle copies via JniObjectReference, so - // they are not affected by the bridge swapping control_block handles. - } - - public override void CollectPeers () - { - JavaMarshalRegisteredPeers.CollectPeers (); - } - - public override void AddPeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.AddPeer (value); - } - - public override IJavaPeerable? PeekPeer (JniObjectReference reference) - { - return JavaMarshalRegisteredPeers.PeekPeer (reference); - } - - public override void RemovePeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.RemovePeer (value); - } - - public override void FinalizePeer (IJavaPeerable value) - { - JavaMarshalRegisteredPeers.FinalizePeer (value); - } - - public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) - { - if (RuntimeFeature.TrimmableTypeMap) - throw new PlatformNotSupportedException ("Activating Java peers is not supported when TrimmableTypeMap is enabled."); - - base.ActivatePeer (reference, type, cinfo, argumentValues); - } - - public override List GetSurfacedPeers () - { - return JavaMarshalRegisteredPeers.GetSurfacedPeers (); - } - - public override IJavaPeerable? CreatePeer ( - ref JniObjectReference reference, - JniObjectReferenceOptions transfer, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - ThrowIfDisposed (); - - if (!reference.IsValid) { - return null; - } - - if (RuntimeFeature.TrimmableTypeMap) { - try { - // Mirror legacy GetPeerType: callers commonly request universal - // interfaces / boxes (IJavaPeerable, object, Exception) — map these - // to a concrete peer type so the proxy lookup can succeed. - var resolvedTargetType = ResolvePeerType (targetType); - - var typeMap = TrimmableTypeMap.Instance; - var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); - if (peer is not null) { - return peer; - } - - // Disambiguate the failure — match the contract of the base - // JniRuntime.JniValueManager.CreatePeer so JavaCast / JavaAs - // surface the right exception (or null) to callers: - // - // (a) target type has no Java mapping at all → ArgumentException - // (b) Java instance is not assignable to the target's Java class - // → return null (JavaAs returns null; JavaCast wraps to - // InvalidCastException via its `??` clause) - // (c) classes are compatible but no proxy / activation failed - // → NotSupportedException (genuine generator gap) - if (resolvedTargetType is not null && - IsIncompatibleCast (typeMap, ref reference, resolvedTargetType)) { - return null; - } - - var targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; - var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); - - throw new NotSupportedException ( - $"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " + - $"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " + - $"This indicates a missing trimmable typemap proxy or association and should be fixed in the generator."); - } finally { - JniObjectReference.Dispose (ref reference, transfer); - } - } - - return base.CreatePeer (ref reference, transfer, targetType); - } - - [return: DynamicallyAccessedMembers (Constructors)] - static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) - { - if (type is null) { - return null; - } - if (type == typeof (object) || type == typeof (IJavaPeerable)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (Exception)) { - return typeof (JavaException); - } - return type; - } - - /// - /// Returns true when 's Java class is not assignable from - /// . Throws when has no usable mapping. - /// - static bool IsIncompatibleCast ( - TrimmableTypeMap typeMap, - ref JniObjectReference reference, - Type targetType) - { - if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { - throw new ArgumentException ( - $"Could not determine Java type corresponding to '{targetType.AssemblyQualifiedName}'.", - nameof (targetType)); - } - - var instanceClass = JniEnvironment.Types.GetObjectClass (reference); - JniObjectReference targetClass = default; - try { - try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); - } catch (Java.Lang.ClassNotFoundException e) { - throw new ArgumentException ( - $"Could not find Java class '{targetJniName}'.", - nameof (targetType), e); - } - - if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { - // Bad cast: callers translate null to the expected result. - return true; - } - } finally { - JniObjectReference.Dispose (ref instanceClass); - JniObjectReference.Dispose (ref targetClass); - } - - // Compatible classes mean a proxy/activation gap. - return false; - } - - protected override bool TryConstructPeer ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference.Handle, - JniHandleOwnership.DoNotTransfer, - }; - c.Invoke (self, args); - JniObjectReference.Dispose (ref reference, options); - return true; - } - return base.TryConstructPeer (self, ref reference, options, type); - } - - protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) - { - var proxy = value as JavaProxyThrowable; - if (proxy != null) { - result = proxy.InnerException; - return true; - } - return base.TryUnboxPeerObject (value, out result); - } -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs new file mode 100644 index 00000000000..26f4d3bf627 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs @@ -0,0 +1,65 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Android.Runtime; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +static class JavaMarshalValueManagerHelper +{ + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + [return: DynamicallyAccessedMembers (Constructors)] + public static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) + { + if (type is null) { + return null; + } + if (type == typeof (object) || type == typeof (IJavaPeerable)) { + return typeof (global::Java.Interop.JavaObject); + } + if (type == typeof (Exception)) { + return typeof (JavaException); + } + return type; + } + + /// + /// Returns true when 's Java class is not assignable from + /// . Throws when has no usable mapping. + /// + public static bool IsIncompatibleCast ( + string targetJniName, + ref JniObjectReference reference, + Type targetType) + { + var instanceClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference targetClass = default; + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + + if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { + // Match the legacy cast diagnostic when assembly logging is enabled. + if (Logger.LogAssembly) { + var targetSig = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (targetType); + var message = $"Handle 0x{reference.Handle:x} is of type '{JNIEnv.GetClassNameFromInstance (reference.Handle)}' which is not assignable to '{targetSig.SimpleReference}'"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + if (RuntimeFeature.IsAssignableFromCheck) { + return true; + } + } + } catch (Java.Lang.ClassNotFoundException e) { + throw new ArgumentException ( + $"Could not find Java class '{targetJniName}'.", + nameof (targetType), e); + } finally { + JniObjectReference.Dispose (ref instanceClass); + JniObjectReference.Dispose (ref targetClass); + } + + // Compatible classes mean a proxy/activation gap. + return false; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 5d20f9e5ac4..11d8bd2c835 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -18,14 +18,20 @@ static class RuntimeFeature const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}")] + [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] + [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool ManagedTypeMap { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}", out bool isEnabled) ? isEnabled : ManagedTypeMapEnabledByDefault; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}")] + [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] + [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool IsMonoRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}", out bool isEnabled) ? isEnabled : IsMonoRuntimeEnabledByDefault; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsCoreClrRuntime)}")] + [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] + [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool IsCoreClrRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsCoreClrRuntime)}", out bool isEnabled) ? isEnabled : IsCoreClrRuntimeEnabledByDefault; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs deleted file mode 100644 index 0546f43d378..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ /dev/null @@ -1,259 +0,0 @@ -// Originally from: https://github.com/dotnet/java-interop/blob/9b1d8781e8e322849d05efac32119c913b21c192/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Threading; -using Android.Runtime; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -class SimpleValueManager : AndroidReflectionJniValueManager -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - Dictionary>? RegisteredInstances = new Dictionary>(); - - internal SimpleValueManager () - { - } - - public override void WaitForGCBridgeProcessing () - { - } - - public override void CollectPeers () - { - if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (SimpleValueManager)); - - var peers = new List (); - - lock (RegisteredInstances) { - foreach (var ps in RegisteredInstances.Values) { - foreach (var p in ps) { - peers.Add (p); - } - } - RegisteredInstances.Clear (); - } - List? exceptions = null; - foreach (var peer in peers) { - try { - peer.Dispose (); - } - catch (Exception e) { - exceptions = exceptions ?? new List (); - exceptions.Add (e); - } - } - if (exceptions != null) - throw new AggregateException ("Exceptions while collecting peers.", exceptions); - } - - public override void AddPeer (IJavaPeerable value) - { - if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (SimpleValueManager)); - - var r = value.PeerReference; - if (!r.IsValid) - throw new ObjectDisposedException (value.GetType ().FullName); - - if (r.Type != JniObjectReferenceType.Global) { - value.SetPeerReference (r.NewGlobalRef ()); - JniObjectReference.Dispose (ref r, JniObjectReferenceOptions.CopyAndDispose); - } - int key = value.JniIdentityHashCode; - lock (RegisteredInstances) { - List? peers; - if (!RegisteredInstances.TryGetValue (key, out peers)) { - peers = new List () { - value, - }; - RegisteredInstances.Add (key, peers); - return; - } - - for (int i = peers.Count - 1; i >= 0; i--) { - var p = peers [i]; - if (!JniEnvironment.Types.IsSameObject (p.PeerReference, value.PeerReference)) - continue; - if (Replaceable (p)) { - peers [i] = value; - } else { - WarnNotReplacing (key, value, p); - } - return; - } - peers.Add (value); - } - } - - static bool Replaceable (IJavaPeerable peer) - { - if (peer == null) - return true; - return peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable); - } - - void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) - { - Runtime.ObjectReferenceManager.WriteGlobalReferenceLine ( - "Warning: Not registering PeerReference={0} IdentityHashCode=0x{1} Instance={2} Instance.Type={3} Java.Type={4}; " + - "keeping previously registered PeerReference={5} Instance={6} Instance.Type={7} Java.Type={8}.", - ignoreValue.PeerReference.ToString (), - key.ToString ("x", CultureInfo.InvariantCulture), - RuntimeHelpers.GetHashCode (ignoreValue).ToString ("x", CultureInfo.InvariantCulture), - ignoreValue.GetType ().FullName, - JniEnvironment.Types.GetJniTypeNameFromInstance (ignoreValue.PeerReference), - keepValue.PeerReference.ToString (), - RuntimeHelpers.GetHashCode (keepValue).ToString ("x", CultureInfo.InvariantCulture), - keepValue.GetType ().FullName, - JniEnvironment.Types.GetJniTypeNameFromInstance (keepValue.PeerReference)); - } - - public override IJavaPeerable? PeekPeer (JniObjectReference reference) - { - if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (SimpleValueManager)); - - if (!reference.IsValid) - return null; - - int key = GetJniIdentityHashCode (reference); - - lock (RegisteredInstances) { - List? peers; - if (!RegisteredInstances.TryGetValue (key, out peers)) - return null; - - for (int i = peers.Count - 1; i >= 0; i--) { - var p = peers [i]; - if (JniEnvironment.Types.IsSameObject (reference, p.PeerReference)) - return p; - } - if (peers.Count == 0) - RegisteredInstances.Remove (key); - } - return null; - } - - public override void RemovePeer (IJavaPeerable value) - { - if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (SimpleValueManager)); - - if (value == null) - throw new ArgumentNullException (nameof (value)); - - int key = value.JniIdentityHashCode; - lock (RegisteredInstances) { - List? peers; - if (!RegisteredInstances.TryGetValue (key, out peers)) - return; - - for (int i = peers.Count - 1; i >= 0; i--) { - var p = peers [i]; - if (object.ReferenceEquals (value, p)) { - peers.RemoveAt (i); - } - } - if (peers.Count == 0) - RegisteredInstances.Remove (key); - } - } - - public override void FinalizePeer (IJavaPeerable value) - { - var h = value.PeerReference; - var o = Runtime.ObjectReferenceManager; - // MUST NOT use SafeHandle.ReferenceType: local refs are tied to a JniEnvironment - // and the JniEnvironment's corresponding thread; it's a thread-local value. - // Accessing SafeHandle.ReferenceType won't kill anything (so far...), but - // instead it always returns JniReferenceType.Invalid. - if (!h.IsValid || h.Type == JniObjectReferenceType.Local) { - if (o.LogGlobalReferenceMessages) { - o.WriteGlobalReferenceLine ("Finalizing PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}", - h.ToString (), - value.JniIdentityHashCode.ToString ("x", CultureInfo.InvariantCulture), - RuntimeHelpers.GetHashCode (value).ToString ("x", CultureInfo.InvariantCulture), - value.GetType ().ToString ()); - } - RemovePeer (value); - value.SetPeerReference (new JniObjectReference ()); - value.Finalized (); - return; - } - - RemovePeer (value); - if (o.LogGlobalReferenceMessages) { - o.WriteGlobalReferenceLine ("Finalizing PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}", - h.ToString (), - value.JniIdentityHashCode.ToString ("x", CultureInfo.InvariantCulture), - RuntimeHelpers.GetHashCode (value).ToString ("x", CultureInfo.InvariantCulture), - value.GetType ().ToString ()); - } - value.SetPeerReference (new JniObjectReference ()); - JniObjectReference.Dispose (ref h); - value.Finalized (); - } - - public override List GetSurfacedPeers () - { - if (RegisteredInstances == null) - throw new ObjectDisposedException (nameof (SimpleValueManager)); - - lock (RegisteredInstances) { - var peers = new List (RegisteredInstances.Count); - foreach (var e in RegisteredInstances) { - foreach (var p in e.Value) { - peers.Add (new JniSurfacedPeerInfo (e.Key, new WeakReference (p))); - } - } - return peers; - } - } - - const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - - protected override bool TryConstructPeer ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference.Handle, - JniHandleOwnership.DoNotTransfer, - }; - c.Invoke (self, args); - JniObjectReference.Dispose (ref reference, options); - return true; - } - return base.TryConstructPeer (self, ref reference, options, type); - } - - protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)]out object? result) - { - var proxy = value as JavaProxyThrowable; - if (proxy != null) { - result = proxy.InnerException; - return true; - } - return base.TryUnboxPeerObject (value, out result); - } -} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 446513682aa..b00f744473d 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -367,9 +367,9 @@ - + - + From 1a8254d877b451a4880f28d6360de1003d0de0e3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 11:19:35 +0200 Subject: [PATCH 02/20] Remove AndroidReflectionJniValueManager (superseded by CoreCLR split + JI bump) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AndroidReflectionJniValueManager.cs | 176 ------------------ src/Mono.Android/Mono.Android.csproj | 1 - 2 files changed, 177 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs deleted file mode 100644 index 1ead336ccb6..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs +++ /dev/null @@ -1,176 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using System.Runtime.CompilerServices; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -[UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Temporary suppression for Java.Interop reflection manager base.")] -abstract class AndroidReflectionJniValueManager : JniRuntime.ReflectionJniValueManager -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - public override IJavaPeerable? CreatePeer ( - ref JniObjectReference reference, - JniObjectReferenceOptions transfer, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - EnsureNotDisposed (); - - if (!reference.IsValid) { - return null; - } - - targetType = targetType ?? typeof (global::Java.Interop.JavaObject); - targetType = GetPeerType (targetType); - - if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { - throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); - } - - var targetSig = Runtime.TypeManager.GetTypeSignature (targetType); - if (!targetSig.IsValid || targetSig.SimpleReference == null) { - throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); - } - - var refClass = JniEnvironment.Types.GetObjectClass (reference); - JniObjectReference targetClass; - try { - targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference); - } catch (Exception e) { - JniObjectReference.Dispose (ref refClass); - throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.", - nameof (targetType), - e); - } - - if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass)) { - JniObjectReference.Dispose (ref refClass); - JniObjectReference.Dispose (ref targetClass); - return null; - } - - JniObjectReference.Dispose (ref targetClass); - - var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); - if (peer == null) { - throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", - JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); - } - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - return peer; - } - - [return: DynamicallyAccessedMembers (Constructors)] - static Type GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type type) - { - if (type == typeof (object)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (IJavaPeerable)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (Exception)) { - return typeof (JavaException); - } - return type; - } - - IJavaPeerable? CreatePeerInstance ( - ref JniObjectReference klass, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - ref JniObjectReference reference, - JniObjectReferenceOptions transfer) - { - var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); - - while (jniTypeName != null) { - JniTypeSignature sig; - if (!JniTypeSignature.TryParse (jniTypeName, out sig)) { - return null; - } - - Type? type = GetTypeAssignableTo (sig, targetType); - if (type != null) { - var peer = TryCreatePeerInstance (ref reference, transfer, type); - - if (peer != null) { - JniObjectReference.Dispose (ref klass); - return peer; - } - } - - var super = JniEnvironment.Types.GetSuperclass (klass); - jniTypeName = super.IsValid - ? JniEnvironment.Types.GetJniTypeNameFromClass (super) - : null; - - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - klass = super; - } - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - - return TryCreatePeerInstance (ref reference, transfer, targetType); - - [return: DynamicallyAccessedMembers (Constructors)] - Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType) - { - foreach (var t in Runtime.TypeManager.GetReflectionConstructibleTypes (sig)) { - if (targetType.IsAssignableFrom (t.Type)) { - return t.Type; - } - } - return null; - } - } - - IJavaPeerable? TryCreatePeerInstance ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - type = Runtime.TypeManager.GetInvokerType (type) ?? type; - - var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); - self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - - var constructed = false; - try { - constructed = TryConstructPeer (self, ref reference, options, type); - } finally { - if (!constructed) { - GC.SuppressFinalize (self); - self = null; - } - } - return self; - } - - static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType (); - static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; - - protected virtual bool TryConstructPeer ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - var c = type.GetConstructor (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, null, JIConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference, - options, - }; - c.Invoke (self, args); - reference = (JniObjectReference) args [0]; - return true; - } - return false; - } -} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index b00f744473d..36f42f6cf0a 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -363,7 +363,6 @@ - From 0bad754891f6468681cb0be3c5dc71711241e953 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 11:45:18 +0200 Subject: [PATCH 03/20] Address build failures --- src/Mono.Android/Android.Graphics/Color.cs | 7 +--- .../Android.Runtime/AndroidRuntime.cs | 17 ++------- .../IJavaObjectValueMarshaler.cs | 3 -- .../Android.Runtime/JNIEnvInit.cs | 10 ++++- .../ManagedTypeManager.cs | 37 ++++--------------- .../RuntimeFeature.cs | 6 --- .../TrimmableTypeMapTypeManager.cs | 12 +----- 7 files changed, 21 insertions(+), 71 deletions(-) diff --git a/src/Mono.Android/Android.Graphics/Color.cs b/src/Mono.Android/Android.Graphics/Color.cs index 1f4bde8455c..317a3b59e73 100644 --- a/src/Mono.Android/Android.Graphics/Color.cs +++ b/src/Mono.Android/Android.Graphics/Color.cs @@ -434,18 +434,13 @@ public static void RGBToHSV (int red, int green, int blue, float[] hsv) public class ColorValueMarshaler : JniValueMarshaler { - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; public override Type MarshalType { get { return typeof (int); } } - public override Color CreateGenericValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type targetType) + public override Color CreateGenericValue (ref JniObjectReference reference, JniObjectReferenceOptions options, Type targetType) { throw new NotImplementedException (); } diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index dd0d94a73a9..7f5507cb396 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -314,11 +314,6 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value) class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { bool jniAddNativeMethodRegistrationAttributePresent; - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; - const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | Constructors; - public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) { this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; @@ -335,7 +330,6 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl } [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Temporary suppression until legacy typemap entries carry DAM annotations.")] - [return: DynamicallyAccessedMembers (MethodsConstructors)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { var type = base.GetTypeForSimpleReference (jniSimpleReference); @@ -384,10 +378,7 @@ protected override IEnumerable GetSimpleReferences (Type type) return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); } - [return: DynamicallyAccessedMembers (Constructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (Constructors)] - Type type) + protected override Type? GetInvokerTypeCore (Type type) { if (type.IsInterface || type.IsAbstract) { return JavaObjectExtensions.GetInvokerType (type) @@ -500,17 +491,17 @@ static bool CallRegisterMethodByIndex (JniNativeMethodRegistrationArguments argu [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan) instead.")] public override void RegisterNativeMembers ( JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, string? methods) => RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); [UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Type.GetType() can never statically know the string value parsed from parameter 'methods'.")] [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] + [UnconditionalSuppressMessage ("Trimming", "IL2070", Justification = "GetMethods can never statically know the string value parsed from parameter 'methods'.")] [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] public override void RegisterNativeMembers ( JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, + Type type, ReadOnlySpan methods) { try { @@ -852,7 +843,7 @@ internal void RemovePeer (IJavaPeerable value, IntPtr hash) public override void ActivatePeer ( JniObjectReference reference, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, + Type type, ConstructorInfo cinfo, object?[]? argumentValues) { diff --git a/src/Mono.Android/Android.Runtime/IJavaObjectValueMarshaler.cs b/src/Mono.Android/Android.Runtime/IJavaObjectValueMarshaler.cs index 79230b9d2eb..bd7d8942936 100644 --- a/src/Mono.Android/Android.Runtime/IJavaObjectValueMarshaler.cs +++ b/src/Mono.Android/Android.Runtime/IJavaObjectValueMarshaler.cs @@ -9,8 +9,6 @@ namespace Android.Runtime { sealed class IJavaObjectValueMarshaler : JniValueMarshaler { - - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; internal static IJavaObjectValueMarshaler Instance = new IJavaObjectValueMarshaler (); @@ -18,7 +16,6 @@ sealed class IJavaObjectValueMarshaler : JniValueMarshaler { public override IJavaObject CreateGenericValue ( ref JniObjectReference reference, JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] Type? targetType) { throw new NotImplementedException (); diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 08c08e75700..854849bab4c 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -188,14 +188,20 @@ internal static JniRuntime.JniValueManager CreateValueManager () } if (RuntimeFeature.IsCoreClrRuntime) { - return new CoreClrJavaMarshalValueManager (); + return CreateCoreClrJavaMarshalValueManager (); } if (RuntimeFeature.IsNativeAotRuntime) { - return new CoreClrJavaMarshalValueManager (); + return CreateCoreClrJavaMarshalValueManager (); } throw new NotSupportedException ("Internal error: unknown runtime not supported"); + + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] + JniRuntime.JniValueManager CreateCoreClrJavaMarshalValueManager () + { + return new CoreClrJavaMarshalValueManager (); + } } static void InitializeCommonState (JnienvInitializeArgs args) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index aaee4516788..9b0f8519c5a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -19,46 +19,25 @@ public ManagedTypeManager () { } - [return: DynamicallyAccessedMembers (Constructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (Constructors)] - Type type) + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = "'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Generic 'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] + protected override Type? GetInvokerTypeCore (Type type) { const string suffix = "Invoker"; - // https://github.com/xamarin/xamarin-android/blob/5472eec991cc075e4b0c09cd98a2331fb93aa0f3/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs#L176-L186 - const string assemblyGetTypeMessage = "'Invoker' types are preserved by the MarkJavaObjects trimmer step."; - const string makeGenericTypeMessage = "Generic 'Invoker' types are preserved by the MarkJavaObjects trimmer step."; - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = assemblyGetTypeMessage)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = assemblyGetTypeMessage)] - [return: DynamicallyAccessedMembers (Constructors)] - static Type? AssemblyGetType (Assembly assembly, string typeName) => - assembly.GetType (typeName); - - [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = makeGenericTypeMessage)] - [return: DynamicallyAccessedMembers (Constructors)] - static Type MakeGenericType ( - [DynamicallyAccessedMembers (Constructors)] - Type type, - Type [] arguments) => - // FIXME: https://github.com/dotnet/java-interop/issues/1192 - #pragma warning disable IL3050 - type.MakeGenericType (arguments); - #pragma warning restore IL3050 - Type[] arguments = type.GetGenericArguments (); if (arguments.Length == 0) - return AssemblyGetType (type.Assembly, type + suffix) ?? base.GetInvokerTypeCore (type); + return type.Assembly.GetType (type + suffix) ?? base.GetInvokerTypeCore (type); Type definition = type.GetGenericTypeDefinition (); int bt = definition.FullName!.IndexOf ("`", StringComparison.Ordinal); if (bt == -1) throw new NotSupportedException ("Generic type doesn't follow generic type naming convention! " + type.FullName); - Type? suffixDefinition = AssemblyGetType (definition.Assembly, + Type? suffixDefinition = definition.Assembly.GetType ( definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt)); if (suffixDefinition == null) return base.GetInvokerTypeCore (type); - return MakeGenericType (suffixDefinition, arguments); + return suffixDefinition.MakeGenericType (arguments); } // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` @@ -67,7 +46,6 @@ static Type MakeGenericType ( [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] public override void RegisterNativeMembers ( JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, ReadOnlySpan methods) { @@ -144,7 +122,6 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl } [UnconditionalSuppressMessage ("Trimming", "IL2068", Justification = "Temporary suppression until ManagedTypeMapping type entries carry DAM annotations.")] - [return: DynamicallyAccessedMembers (MethodsConstructors)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { var type = base.GetTypeForSimpleReference (jniSimpleReference); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 11d8bd2c835..5d20f9e5ac4 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -18,20 +18,14 @@ static class RuntimeFeature const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}")] - [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] - [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool ManagedTypeMap { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ManagedTypeMap)}", out bool isEnabled) ? isEnabled : ManagedTypeMapEnabledByDefault; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}")] - [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] - [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool IsMonoRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsMonoRuntime)}", out bool isEnabled) ? isEnabled : IsMonoRuntimeEnabledByDefault; [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsCoreClrRuntime)}")] - [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] - [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool IsCoreClrRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsCoreClrRuntime)}", out bool isEnabled) ? isEnabled : IsCoreClrRuntimeEnabledByDefault; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index c2514055757..5ff5f6d4a9d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -18,10 +18,6 @@ namespace Microsoft.Android.Runtime; class TrimmableTypeMapTypeManager : JniRuntime.ReflectionJniTypeManager { const string NoSimpleReference = "\0"; - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; - const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | Constructors; readonly ConcurrentDictionary _simpleReferenceCache = new (); protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) @@ -37,8 +33,6 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl } } - [UnconditionalSuppressMessage ("Trimming", "IL2063", Justification = "Temporary suppression until trimmable typemap type entries carry DAM annotations.")] - [return: DynamicallyAccessedMembers (MethodsConstructors)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { var type = base.GetTypeForSimpleReference (jniSimpleReference); @@ -100,10 +94,7 @@ protected override IEnumerable GetSimpleReferences (Type type) } } - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) + protected override Type? GetInvokerTypeCore (Type type) { var invokerType = TrimmableTypeMap.Instance.GetInvokerType (type); if (invokerType != null) { @@ -130,7 +121,6 @@ protected override IEnumerable GetSimpleReferences (Type type) public override void RegisterNativeMembers ( JniType nativeClass, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] Type type, ReadOnlySpan methods) { From e06209e28605faeff4f5576d4b6f9513845c43ed Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 11:45:29 +0200 Subject: [PATCH 04/20] Address warnings by enabling nullable --- src/Mono.Android/Android.App/IntentFilterAttribute.Partial.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mono.Android/Android.App/IntentFilterAttribute.Partial.cs b/src/Mono.Android/Android.App/IntentFilterAttribute.Partial.cs index 5bb6dfdcdf2..ad9cd0b90d8 100644 --- a/src/Mono.Android/Android.App/IntentFilterAttribute.Partial.cs +++ b/src/Mono.Android/Android.App/IntentFilterAttribute.Partial.cs @@ -1,4 +1,4 @@ -#nullable disable +#nullable enable using System; From 05da23c910c9819d8fbb23d5f28a75376ce406d4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 11:47:03 +0200 Subject: [PATCH 05/20] Update acceptable breaking changes --- .../acceptable-breakages-vReference-net11.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt index 0feba3a1457..c7c0f63f2a2 100644 --- a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt +++ b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt @@ -48,3 +48,4 @@ MembersMustExist : Member 'public void Android.Telecom.CallControl.RequestVideoS TypesMustExist : Type 'Android.Text.IInputType' does not exist in the implementation but it does exist in the contract. TypesMustExist : Type 'Xamarin.Android.Net.AndroidClientHandler' does not exist in the implementation but it does exist in the contract. CannotRemoveBaseTypeOrInterface : Type 'Android.Views.InputMethods.EditorInfo' does not implement interface 'Android.Text.IInputType' in the implementation but it does in the contract. +CannotRemoveAttribute : Attribute 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute' exists on parameter 'targetType' on member 'Android.Graphics.ColorValueMarshaler.CreateGenericValue(Java.Interop.JniObjectReference, Java.Interop.JniObjectReferenceOptions, System.Type)' in the contract but not the implementation. From 094788d10a5e0ee7a27a34e02401343dbbf0f27a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 13:07:05 +0200 Subject: [PATCH 06/20] Update acceptable breakages --- .../acceptable-breakages-vReference-net11.0.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt index c7c0f63f2a2..05022f14024 100644 --- a/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt +++ b/tests/api-compatibility/acceptable-breakages-vReference-net11.0.txt @@ -1,5 +1,4 @@ Compat issues with assembly Mono.Android: -CannotChangeAttribute : Attribute 'System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute' on parameter 'targetType' on member 'Android.Graphics.ColorValueMarshaler.CreateGenericValue(Java.Interop.JniObjectReference, Java.Interop.JniObjectReferenceOptions, System.Type)' changed from '[DynamicallyAccessedMembersAttribute(8199)]' in the contract to '[DynamicallyAccessedMembersAttribute(7)]' in the implementation. CannotChangeAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' on 'Android.Accounts.AccountManager.RemoveAccount(Android.Accounts.Account, Android.App.Activity, Android.Accounts.IAccountManagerCallback, Android.OS.Handler)' changed from '[RequiresPermissionAttribute("android.permission.MANAGE_ACCOUNTS")]' in the contract to '[RequiresPermissionAttribute("android.permission.REMOVE_ACCOUNTS")]' in the implementation. CannotRemoveAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' exists on 'Android.Bluetooth.BluetoothDevice.CreateInsecureL2capChannel(System.Int32)' in the contract but not the implementation. CannotRemoveAttribute : Attribute 'Android.Runtime.RequiresPermissionAttribute' exists on 'Android.Bluetooth.BluetoothDevice.CreateInsecureRfcommSocketToServiceRecord(Java.Util.UUID)' in the contract but not the implementation. From 85a605f6c6ed27fe180a19e4e8673d21442badb6 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 13:30:56 +0200 Subject: [PATCH 07/20] Suppress more warnings --- src/Mono.Android/Android.Runtime/JNIEnvInit.cs | 1 + src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 854849bab4c..d6a15bbddc9 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -198,6 +198,7 @@ internal static JniRuntime.JniValueManager CreateValueManager () throw new NotSupportedException ("Internal error: unknown runtime not supported"); [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] JniRuntime.JniValueManager CreateCoreClrJavaMarshalValueManager () { return new CoreClrJavaMarshalValueManager (); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 9b0f8519c5a..db254b65bb8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -22,6 +22,7 @@ public ManagedTypeManager () [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = "'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = "Generic 'Invoker' types are preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Generic 'Invoker' types may not be available in AOT scenarios.")] protected override Type? GetInvokerTypeCore (Type type) { const string suffix = "Invoker"; From c32c5f0c1b21a30f1909d5b8b9dd12e1f5da8aad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 15:24:29 +0200 Subject: [PATCH 08/20] [NativeAOT] Use CoreClrJavaMarshalValueManager for default value manager Fixes CS0246: 'JavaMarshalValueManager' could not be found. The default value manager now resolves to the internal CoreClrJavaMarshalValueManager (accessible via InternalsVisibleTo), matching the pattern in JNIEnvInit. Suppress the correct trimming warnings (IL2026/IL3050) instead of IL3000. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JreRuntime.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 5891149578f..9f04007ca20 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -61,7 +61,7 @@ static NativeAotRuntimeOptions CreateJreVM (NativeAotRuntimeOptions builder) builder.TypeManager ??= CreateDefaultTypeManager (); #endif // NET - builder.ValueManager ??= new JavaMarshalValueManager (); + builder.ValueManager ??= CreateDefaultValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) @@ -84,6 +84,13 @@ static JniRuntime.JniTypeManager CreateDefaultTypeManager () return new ManagedTypeManager (); } + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] + [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] + static JniRuntime.JniValueManager CreateDefaultValueManager () + { + return new CoreClrJavaMarshalValueManager (); + } + public override string? GetCurrentManagedThreadName () { return Thread.CurrentThread.Name; From 052ae81581ed6e7f3fe65bca48f306106c8d48e2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 18:04:03 +0200 Subject: [PATCH 09/20] [Mono.Android] Resolve derived interface peers in CoreClrJavaMarshalValueManager CreatePeerInstance only walked the Java superclass chain, so a Java object returned through a base-interface signature (e.g. an anonymous class that implements a derived interface) was marshalled to the base interface's invoker instead of the most-derived interface proxy. When targetType is an interface, also enumerate the Java class's interfaces (recursively into super-interfaces) and select the most-derived registered .NET type assignable to targetType, mirroring the interface-walk already used by TrimmableTypeMap.GetProxyForJavaObject. Fixes JavaInterfaceLookup_BaseInterfaceReturnType_UsesDerivedInterfaceProxy, TrustManagerFactory_GetTrustManagers_ReturnsIX509TrustManager and the dependent ServerCertificateCustomValidationCallback_* tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CoreClrJavaMarshalValueManager.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs index bbde9496e79..49de6c4ef42 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; +using System.Threading; using Android.Runtime; using Java.Interop; @@ -19,6 +20,8 @@ sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManag static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; + static JniMethodInfo? s_classGetInterfacesMethod; + public CoreClrJavaMarshalValueManager () { JavaMarshalRegisteredPeers.InitializeIfNeeded (); @@ -125,6 +128,16 @@ public override List GetSurfacedPeers () return null; Type? type = GetTypeAssignableTo (sig, targetType); + + // The superclass walk above never inspects the Java interfaces a class + // implements. When the requested targetType is itself an interface, the + // concrete Java class (e.g. an anonymous class returned through a base + // interface signature) may only advertise a more-derived interface, so we + // must enumerate the class's interfaces to find the most-derived peer. + if (type == null && targetType.IsInterface) { + type = GetInterfaceTypeAssignableTo (klass, targetType); + } + if (type != null) { var peer = TryCreatePeerInstance (ref reference, transfer, type); @@ -155,6 +168,66 @@ public override List GetSurfacedPeers () } return null; } + + // Recursively walks the Java interfaces declared on `klass` (and their + // super-interfaces) looking for a registered .NET type assignable to + // `targetType`. `Class.getInterfaces ()` only returns directly declared + // interfaces, so we must recurse to cover transitive ones. Directly + // declared interfaces are checked before their super-interfaces, so the + // most-derived match is preferred. The `klass` reference is owned by the + // caller and is not disposed here. + Type? GetInterfaceTypeAssignableTo (JniObjectReference klass, Type targetType) + { + var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (klass, GetClassGetInterfacesMethod ()); + try { + if (!interfaces.IsValid) { + return null; + } + + int count = JniEnvironment.Arrays.GetArrayLength (interfaces); + for (int i = 0; i < count; i++) { + var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); + try { + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); + if (ifaceName != null && JniTypeSignature.TryParse (ifaceName, out var ifaceSig)) { + var type = GetTypeAssignableTo (ifaceSig, targetType); + if (type != null) { + return type; + } + } + + var result = GetInterfaceTypeAssignableTo (iface, targetType); + if (result != null) { + return result; + } + } finally { + JniObjectReference.Dispose (ref iface); + } + } + } finally { + JniObjectReference.Dispose (ref interfaces); + } + + return null; + } + } + + static JniMethodInfo GetClassGetInterfacesMethod () + { + var method = s_classGetInterfacesMethod; + if (method != null) { + return method; + } + + var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); + try { + method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + } finally { + JniObjectReference.Dispose (ref classClass); + } + + var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); + return previous ?? method; } IJavaPeerable? TryCreatePeerInstance ( From f677d4535ac2b1c8650019fce3a5692d42c174f3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 18:20:46 +0200 Subject: [PATCH 10/20] [Mono.Android] Share Java interface-hierarchy walk between value/type managers Extract the Class.getInterfaces() recursive walk into a shared JavaInterfaceHierarchy.FindFirst helper and use it from both CoreClrJavaMarshalValueManager.CreatePeerInstance and TrimmableTypeMap.TryMatchInterfaces, removing the duplicated JNI plumbing (getInterfaces method id + traversal) from both. No behavior change; consolidates the interface-derived-proxy resolution added in the previous commit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CoreClrJavaMarshalValueManager.cs | 65 ++-------------- .../JavaInterfaceHierarchy.cs | 78 +++++++++++++++++++ .../TrimmableTypeMap.cs | 56 ++----------- 3 files changed, 91 insertions(+), 108 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs index 49de6c4ef42..453050ec402 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs @@ -4,7 +4,6 @@ using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; -using System.Threading; using Android.Runtime; using Java.Interop; @@ -20,8 +19,6 @@ sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManag static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; - static JniMethodInfo? s_classGetInterfacesMethod; - public CoreClrJavaMarshalValueManager () { JavaMarshalRegisteredPeers.InitializeIfNeeded (); @@ -135,7 +132,7 @@ public override List GetSurfacedPeers () // interface signature) may only advertise a more-derived interface, so we // must enumerate the class's interfaces to find the most-derived peer. if (type == null && targetType.IsInterface) { - type = GetInterfaceTypeAssignableTo (klass, targetType); + type = JavaInterfaceHierarchy.FindFirst (klass, ResolveInterfaceType); } if (type != null) { @@ -169,67 +166,17 @@ public override List GetSurfacedPeers () return null; } - // Recursively walks the Java interfaces declared on `klass` (and their - // super-interfaces) looking for a registered .NET type assignable to - // `targetType`. `Class.getInterfaces ()` only returns directly declared - // interfaces, so we must recurse to cover transitive ones. Directly - // declared interfaces are checked before their super-interfaces, so the - // most-derived match is preferred. The `klass` reference is owned by the - // caller and is not disposed here. - Type? GetInterfaceTypeAssignableTo (JniObjectReference klass, Type targetType) + // Resolves a Java interface (by its JNI type name) to the registered .NET + // type assignable to targetType, used while walking the class's interfaces. + Type? ResolveInterfaceType (string? ifaceName) { - var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (klass, GetClassGetInterfacesMethod ()); - try { - if (!interfaces.IsValid) { - return null; - } - - int count = JniEnvironment.Arrays.GetArrayLength (interfaces); - for (int i = 0; i < count; i++) { - var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); - try { - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); - if (ifaceName != null && JniTypeSignature.TryParse (ifaceName, out var ifaceSig)) { - var type = GetTypeAssignableTo (ifaceSig, targetType); - if (type != null) { - return type; - } - } - - var result = GetInterfaceTypeAssignableTo (iface, targetType); - if (result != null) { - return result; - } - } finally { - JniObjectReference.Dispose (ref iface); - } - } - } finally { - JniObjectReference.Dispose (ref interfaces); + if (ifaceName != null && JniTypeSignature.TryParse (ifaceName, out var ifaceSig)) { + return GetTypeAssignableTo (ifaceSig, targetType); } - return null; } } - static JniMethodInfo GetClassGetInterfacesMethod () - { - var method = s_classGetInterfacesMethod; - if (method != null) { - return method; - } - - var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); - try { - method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); - } finally { - JniObjectReference.Dispose (ref classClass); - } - - var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); - return previous ?? method; - } - IJavaPeerable? TryCreatePeerInstance ( ref JniObjectReference reference, JniObjectReferenceOptions options, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs new file mode 100644 index 00000000000..0bd59f87ced --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs @@ -0,0 +1,78 @@ +#nullable enable + +using System; +using System.Threading; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +/// +/// Helpers for walking the Java interface hierarchy of a class. +/// +static class JavaInterfaceHierarchy +{ + static JniMethodInfo? s_classGetInterfacesMethod; + + /// + /// Recursively visits the interfaces declared on and their + /// super-interfaces, invoking with each interface's JNI type + /// name. Returns the first non-null result. Class.getInterfaces () only returns + /// directly declared interfaces, so the walk recurses to cover transitive ones; directly + /// declared interfaces are visited before their super-interfaces, so the most-derived match + /// is preferred. The reference is owned by the caller and is not + /// disposed here. + /// + public static TResult? FindFirst (JniObjectReference klass, Func resolver) + where TResult : class + { + var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (klass, GetClassGetInterfacesMethod ()); + try { + if (!interfaces.IsValid) { + return null; + } + + int count = JniEnvironment.Arrays.GetArrayLength (interfaces); + for (int i = 0; i < count; i++) { + var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); + try { + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); + + var result = resolver (ifaceName); + if (result != null) { + return result; + } + + // Recurse into super-interfaces. + var nested = FindFirst (iface, resolver); + if (nested != null) { + return nested; + } + } finally { + JniObjectReference.Dispose (ref iface); + } + } + } finally { + JniObjectReference.Dispose (ref interfaces); + } + + return null; + } + + static JniMethodInfo GetClassGetInterfacesMethod () + { + var method = s_classGetInterfacesMethod; + if (method != null) { + return method; + } + + var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); + try { + method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + } finally { + JniObjectReference.Dispose (ref classClass); + } + + var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); + return previous ?? method; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 915daa0e248..f733d443c5d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -23,7 +23,6 @@ public class TrimmableTypeMap static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; static bool s_nativeMethodsRegistered; - static JniMethodInfo? s_classGetInterfacesMethod; internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( @@ -293,56 +292,15 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // so we recurse into super-interfaces to find the matching TypeMap entry. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { - var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (jniClass, GetClassGetInterfacesMethod ()); - try { - if (!interfaces.IsValid) { - return null; - } - - int count = JniEnvironment.Arrays.GetArrayLength (interfaces); - for (int i = 0; i < count; i++) { - var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); - try { - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); - if (ifaceName != null) { - var proxy = self.GetProxyForJniClass (ifaceName, targetType); - if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { - return proxy; - } - } - - // Recurse into super-interfaces - var result = TryMatchInterfaces (self, iface, targetType); - if (result != null) { - return result; - } - } finally { - JniObjectReference.Dispose (ref iface); + return JavaInterfaceHierarchy.FindFirst (jniClass, ifaceName => { + if (ifaceName != null) { + var proxy = self.GetProxyForJniClass (ifaceName, targetType); + if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; } } - } finally { - JniObjectReference.Dispose (ref interfaces); - } - - return null; - } - - static JniMethodInfo GetClassGetInterfacesMethod () - { - var method = s_classGetInterfacesMethod; - if (method != null) { - return method; - } - - var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); - try { - method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); - } finally { - JniObjectReference.Dispose (ref classClass); - } - - var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); - return previous ?? method; + return null; + }); } static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) From ba5a7ae33cd62a86ac6aa13a0b9684c3282af490 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 18:32:38 +0200 Subject: [PATCH 11/20] [Tests] Update NativeAOT BuildHasNoWarnings baseline 10 -> 6 The value-manager/type-manager split reduces the IL3050 AOT-analysis warnings a basic NativeAOT app produces from 10 to 6: three distinct warnings (reflection-backed ManagedTypeManager ctor, JNIEnv.MakeArrayType, JNINativeWrapper.CreateDelegate), each surfaced twice in the MSBuild summary. Count verified from build.log of the apk and aab NativeAOT runs in CI build 1485907. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Tests/Xamarin.Android.Build.Tests/BuildTest2.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 2f3c7083d59..67f24e8f1c5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -482,11 +482,14 @@ public void BuildHasNoWarnings (bool isRelease, bool multidex, string packageFor Assert.IsTrue (b.Build (proj), "Build should have succeeded."); if (runtime == AndroidRuntime.NativeAOT) { - // NativeAOT currently (Nov 2025) produces 10 `ILC : AOT analysis warning IL3050` warnings for various - // bits of code. Even though this test expects no warnings and the above likely make the app not work - // correctly at run time, it is still worth running this test under NativeAOT to test for the absence - // of other warnings. - int numberOfExpectedWarnings = 10; + // NativeAOT currently (Jun 2026) produces 6 `ILC : AOT analysis warning IL3050` + // warnings: three distinct warnings (the reflection-backed ManagedTypeManager + // ctor, JNIEnv.MakeArrayType, and JNINativeWrapper.CreateDelegate), each surfaced + // twice in the MSBuild summary (once per publish target context). Even though this + // test expects no warnings and the above likely make the app not work correctly at + // run time, it is still worth running this test under NativeAOT to test for the + // absence of other warnings. + int numberOfExpectedWarnings = 6; Assert.IsTrue ( StringAssertEx.ContainsText ( From 46311a5b5557d138378de09d2040edf6ce9f0391 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 18:36:34 +0200 Subject: [PATCH 12/20] [Tests] Update BuildReleaseArm64 SimpleDotNet apkdesc size baselines The value-manager/type-manager split shrinks the release arm64 app: - NativeAOT: libUnnamedProject.so -10.87%, package 2,474,779 -> 2,286,363 B - CoreCLR: libassembly-store.so -7.19%, package 7,497,147 -> 7,312,827 B apkdiff flags these via --descrease-is-regression, so refresh the reference descriptions. New .apkdesc files taken from the apk/aab BuildReleaseArm64 test attachments in CI build 1485907. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc | 12 ++++++------ .../BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc index 0d3d9720ca1..a00fd898add 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.CoreCLR.apkdesc @@ -8,16 +8,16 @@ "Size": 402352 }, "lib/arm64-v8a/libassembly-store.so": { - "Size": 3461344 + "Size": 3229120 }, "lib/arm64-v8a/libclrjit.so": { - "Size": 2804464 + "Size": 2835128 }, "lib/arm64-v8a/libcoreclr.so": { - "Size": 4872088 + "Size": 4891056 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1325808 + "Size": 1324320 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 72112 @@ -32,7 +32,7 @@ "Size": 168080 }, "lib/arm64-v8a/libxamarin-app.so": { - "Size": 20776 + "Size": 20984 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -59,5 +59,5 @@ "Size": 1904 } }, - "PackageSize": 7497147 + "PackageSize": 7312827 } \ No newline at end of file diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc index 79351723c31..d19175aaf76 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.NativeAOT.apkdesc @@ -8,7 +8,7 @@ "Size": 22016 }, "lib/arm64-v8a/libUnnamedProject.so": { - "Size": 5999448 + "Size": 5411456 }, "res/drawable-hdpi-v4/icon.png": { "Size": 2178 @@ -35,5 +35,5 @@ "Size": 1904 } }, - "PackageSize": 2474779 + "PackageSize": 2286363 } \ No newline at end of file From 41c1bcca6d12f3a8e51161fb7fd37f06a7558dbe Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 29 Jun 2026 19:02:18 +0200 Subject: [PATCH 13/20] [Mono.Android] Add JavaInterfaceHierarchy.cs to the compile list Mono.Android.csproj uses an explicit item list, so the new JavaInterfaceHierarchy.cs was not compiled, causing CS0103 'JavaInterfaceHierarchy does not exist' in TrimmableTypeMap.cs and CoreClrJavaMarshalValueManager.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Mono.Android.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 36f42f6cf0a..b9006f74e9f 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -362,6 +362,7 @@ + From 37f998a86325d14d330d9db5a49302eb484bf5f0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 10:24:29 +0200 Subject: [PATCH 14/20] Change name back to JavaMarshalValueManager --- .../Java.Interop/JreRuntime.cs | 2 +- .../Android.Runtime/JNIEnvInit.cs | 8 +++---- ...eManager.cs => JavaMarshalValueManager.cs} | 24 ++----------------- src/Mono.Android/Mono.Android.csproj | 2 +- 4 files changed, 8 insertions(+), 28 deletions(-) rename src/Mono.Android/Microsoft.Android.Runtime/{CoreClrJavaMarshalValueManager.cs => JavaMarshalValueManager.cs} (87%) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 9f04007ca20..9c2909e7c74 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -88,7 +88,7 @@ static JniRuntime.JniTypeManager CreateDefaultTypeManager () [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] static JniRuntime.JniValueManager CreateDefaultValueManager () { - return new CoreClrJavaMarshalValueManager (); + return new JavaMarshalValueManager (); } public override string? GetCurrentManagedThreadName () diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index d6a15bbddc9..ce424298645 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -188,20 +188,20 @@ internal static JniRuntime.JniValueManager CreateValueManager () } if (RuntimeFeature.IsCoreClrRuntime) { - return CreateCoreClrJavaMarshalValueManager (); + return CreateJavaMarshalValueManager (); } if (RuntimeFeature.IsNativeAotRuntime) { - return CreateCoreClrJavaMarshalValueManager (); + return CreateJavaMarshalValueManager (); } throw new NotSupportedException ("Internal error: unknown runtime not supported"); [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "CoreCLR value manager is preserved by the MarkJavaObjects trimmer step.")] [UnconditionalSuppressMessage ("Trimming", "IL3050", Justification = "This value manager won't be used in Native AOT builds in the future.")] - JniRuntime.JniValueManager CreateCoreClrJavaMarshalValueManager () + JniRuntime.JniValueManager CreateJavaMarshalValueManager () { - return new CoreClrJavaMarshalValueManager (); + return new JavaMarshalValueManager (); } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs similarity index 87% rename from src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs rename to src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 453050ec402..174c7d57f0c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -11,7 +11,7 @@ namespace Microsoft.Android.Runtime; [RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] [RequiresUnreferencedCode ("This value manager is reflection-backed and is not trimming-compatible.")] -sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManager +sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; @@ -19,7 +19,7 @@ sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManag static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; - public CoreClrJavaMarshalValueManager () + public JavaMarshalValueManager () { JavaMarshalRegisteredPeers.InitializeIfNeeded (); } @@ -125,16 +125,6 @@ public override List GetSurfacedPeers () return null; Type? type = GetTypeAssignableTo (sig, targetType); - - // The superclass walk above never inspects the Java interfaces a class - // implements. When the requested targetType is itself an interface, the - // concrete Java class (e.g. an anonymous class returned through a base - // interface signature) may only advertise a more-derived interface, so we - // must enumerate the class's interfaces to find the most-derived peer. - if (type == null && targetType.IsInterface) { - type = JavaInterfaceHierarchy.FindFirst (klass, ResolveInterfaceType); - } - if (type != null) { var peer = TryCreatePeerInstance (ref reference, transfer, type); @@ -165,16 +155,6 @@ public override List GetSurfacedPeers () } return null; } - - // Resolves a Java interface (by its JNI type name) to the registered .NET - // type assignable to targetType, used while walking the class's interfaces. - Type? ResolveInterfaceType (string? ifaceName) - { - if (ifaceName != null && JniTypeSignature.TryParse (ifaceName, out var ifaceSig)) { - return GetTypeAssignableTo (ifaceSig, targetType); - } - return null; - } } IJavaPeerable? TryCreatePeerInstance ( diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 7c528d44bf0..bbbbc038be8 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -370,7 +370,7 @@ - + From c684eee0008d76a69114533ee2bbaf533bafab43 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 10:42:43 +0200 Subject: [PATCH 15/20] Minimize PR: drop interface-resolution sub-feature, restore TrimmableTypeMap value-manager branch --- .../JavaInterfaceHierarchy.cs | 78 ------------------- .../JavaMarshalValueManager.cs | 46 +++++++++++ .../TrimmableTypeMap.cs | 56 +++++++++++-- src/Mono.Android/Mono.Android.csproj | 1 - 4 files changed, 95 insertions(+), 86 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs deleted file mode 100644 index 0bd59f87ced..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaInterfaceHierarchy.cs +++ /dev/null @@ -1,78 +0,0 @@ -#nullable enable - -using System; -using System.Threading; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -/// -/// Helpers for walking the Java interface hierarchy of a class. -/// -static class JavaInterfaceHierarchy -{ - static JniMethodInfo? s_classGetInterfacesMethod; - - /// - /// Recursively visits the interfaces declared on and their - /// super-interfaces, invoking with each interface's JNI type - /// name. Returns the first non-null result. Class.getInterfaces () only returns - /// directly declared interfaces, so the walk recurses to cover transitive ones; directly - /// declared interfaces are visited before their super-interfaces, so the most-derived match - /// is preferred. The reference is owned by the caller and is not - /// disposed here. - /// - public static TResult? FindFirst (JniObjectReference klass, Func resolver) - where TResult : class - { - var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (klass, GetClassGetInterfacesMethod ()); - try { - if (!interfaces.IsValid) { - return null; - } - - int count = JniEnvironment.Arrays.GetArrayLength (interfaces); - for (int i = 0; i < count; i++) { - var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); - try { - var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); - - var result = resolver (ifaceName); - if (result != null) { - return result; - } - - // Recurse into super-interfaces. - var nested = FindFirst (iface, resolver); - if (nested != null) { - return nested; - } - } finally { - JniObjectReference.Dispose (ref iface); - } - } - } finally { - JniObjectReference.Dispose (ref interfaces); - } - - return null; - } - - static JniMethodInfo GetClassGetInterfacesMethod () - { - var method = s_classGetInterfacesMethod; - if (method != null) { - return method; - } - - var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); - try { - method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); - } finally { - JniObjectReference.Dispose (ref classClass); - } - - var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); - return previous ?? method; - } -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 174c7d57f0c..cbceea01376 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -81,6 +81,52 @@ public override List GetSurfacedPeers () return null; } + if (RuntimeFeature.TrimmableTypeMap) { + try { + // Mirror legacy GetPeerType: callers commonly request universal + // interfaces / boxes (IJavaPeerable, object, Exception) — map these + // to a concrete peer type so the proxy lookup can succeed. + var resolvedTargetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType); + + var typeMap = TrimmableTypeMap.Instance; + var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); + if (peer is not null) { + return peer; + } + + // Disambiguate the failure — match the contract of the base + // JniRuntime.JniValueManager.CreatePeer so JavaCast / JavaAs + // surface the right exception (or null) to callers: + // + // (a) target type has no Java mapping at all → ArgumentException + // (b) Java instance is not assignable to the target's Java class + // → return null (JavaAs returns null; JavaCast wraps to + // InvalidCastException via its `??` clause) + // (c) classes are compatible but no proxy / activation failed + // → NotSupportedException (genuine generator gap) + if (resolvedTargetType is not null) { + if (!typeMap.TryGetJniNameForManagedType (resolvedTargetType, out var targetJniName)) { + throw new ArgumentException ( + $"Could not determine Java type corresponding to '{resolvedTargetType.AssemblyQualifiedName}'.", + nameof (targetType)); + } + if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetJniName, ref reference, resolvedTargetType)) { + return null; + } + } + + var targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; + var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + + throw new NotSupportedException ( + $"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " + + $"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " + + $"This indicates a missing trimmable typemap proxy or association and should be fixed in the generator."); + } finally { + JniObjectReference.Dispose (ref reference, transfer); + } + } + targetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 0cbd577d740..145d253050d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -24,6 +24,7 @@ public class TrimmableTypeMap static readonly JavaArrayProxy s_noArrayProxySentinel = new MissingJavaArrayProxy (); static TrimmableTypeMap? s_instance; static bool s_nativeMethodsRegistered; + static JniMethodInfo? s_classGetInterfacesMethod; internal static TrimmableTypeMap Instance => s_instance ?? throw new InvalidOperationException ( @@ -294,15 +295,56 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // so we recurse into super-interfaces to find the matching TypeMap entry. static JavaPeerProxy? TryMatchInterfaces (TrimmableTypeMap self, JniObjectReference jniClass, Type targetType) { - return JavaInterfaceHierarchy.FindFirst (jniClass, ifaceName => { - if (ifaceName != null) { - var proxy = self.GetProxyForJniClass (ifaceName, targetType); - if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { - return proxy; + var interfaces = JniEnvironment.InstanceMethods.CallObjectMethod (jniClass, GetClassGetInterfacesMethod ()); + try { + if (!interfaces.IsValid) { + return null; + } + + int count = JniEnvironment.Arrays.GetArrayLength (interfaces); + for (int i = 0; i < count; i++) { + var iface = JniEnvironment.Arrays.GetObjectArrayElement (interfaces, i); + try { + var ifaceName = JniEnvironment.Types.GetJniTypeNameFromClass (iface); + if (ifaceName != null) { + var proxy = self.GetProxyForJniClass (ifaceName, targetType); + if (proxy != null && TargetTypeMatches (targetType, proxy.TargetType)) { + return proxy; + } + } + + // Recurse into super-interfaces + var result = TryMatchInterfaces (self, iface, targetType); + if (result != null) { + return result; + } + } finally { + JniObjectReference.Dispose (ref iface); } } - return null; - }); + } finally { + JniObjectReference.Dispose (ref interfaces); + } + + return null; + } + + static JniMethodInfo GetClassGetInterfacesMethod () + { + var method = s_classGetInterfacesMethod; + if (method != null) { + return method; + } + + var classClass = JniEnvironment.Types.FindClass ("java/lang/Class"); + try { + method = JniEnvironment.InstanceMethods.GetMethodID (classClass, "getInterfaces", "()[Ljava/lang/Class;"); + } finally { + JniObjectReference.Dispose (ref classClass); + } + + var previous = Interlocked.CompareExchange (ref s_classGetInterfacesMethod, method, null); + return previous ?? method; } static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index bbbbc038be8..77ed51a5a1d 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -362,7 +362,6 @@ - From 73002abedd5c901ec0cc36365373a2ff779725ad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 11:09:58 +0200 Subject: [PATCH 16/20] Undo unnecessary changes --- .../AndroidReflectionJniValueManager.cs | 217 ++++++++++++++++++ .../JavaMarshalValueManager.cs | 120 +++++++--- .../JavaMarshalValueManagerHelper.cs | 65 ------ src/Mono.Android/Mono.Android.csproj | 2 +- 4 files changed, 302 insertions(+), 102 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs new file mode 100644 index 00000000000..1eff1f5d3e3 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs @@ -0,0 +1,217 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using System.Runtime.CompilerServices; +using Android.Runtime; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +[RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] +[RequiresUnreferencedCode ("This value manager is reflection-backed and is not trimming-compatible.")] +abstract class AndroidReflectionJniValueManager : JniRuntime.ReflectionJniValueManager +{ + protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; + static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + EnsureNotDisposed (); + + if (!reference.IsValid) { + return null; + } + + targetType = ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); + + if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { + throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); + } + + var targetSig = Runtime.TypeManager.GetTypeSignature (targetType); + if (!targetSig.IsValid || targetSig.SimpleReference == null) { + throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); + } + + if (IsIncompatibleCast (targetSig.SimpleReference, ref reference, targetType)) { + return null; + } + + var refClass = JniEnvironment.Types.GetObjectClass (reference); + try { + var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); + if (peer == null) { + throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", + JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); + } + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + return peer; + } finally { + JniObjectReference.Dispose (ref refClass); + } + } + + [return: DynamicallyAccessedMembers (Constructors)] + protected static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) + { + if (type is null) { + return null; + } + if (type == typeof (object) || type == typeof (IJavaPeerable)) { + return typeof (global::Java.Interop.JavaObject); + } + if (type == typeof (Exception)) { + return typeof (JavaException); + } + return type; + } + + /// + /// Returns true when 's Java class is not assignable from + /// . Throws when has no usable mapping. + /// + protected static bool IsIncompatibleCast ( + string targetJniName, + ref JniObjectReference reference, + Type targetType) + { + var instanceClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference targetClass = default; + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + + if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { + // Match the legacy cast diagnostic when assembly logging is enabled. + if (Logger.LogAssembly) { + var targetSig = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (targetType); + var message = $"Handle 0x{reference.Handle:x} is of type '{JNIEnv.GetClassNameFromInstance (reference.Handle)}' which is not assignable to '{targetSig.SimpleReference}'"; + Logger.Log (LogLevel.Debug, "monodroid-assembly", message); + } + + if (RuntimeFeature.IsAssignableFromCheck) { + return true; + } + } + } catch (Java.Lang.ClassNotFoundException e) { + throw new ArgumentException ( + $"Could not find Java class '{targetJniName}'.", + nameof (targetType), e); + } finally { + JniObjectReference.Dispose (ref instanceClass); + JniObjectReference.Dispose (ref targetClass); + } + + // Compatible classes mean a proxy/activation gap. + return false; + } + + IJavaPeerable? CreatePeerInstance ( + ref JniObjectReference klass, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + ref JniObjectReference reference, + JniObjectReferenceOptions transfer) + { + var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); + + while (jniTypeName != null) { + JniTypeSignature sig; + if (!JniTypeSignature.TryParse (jniTypeName, out sig)) + return null; + + Type? type = GetTypeAssignableTo (sig, targetType); + if (type != null) { + var peer = TryCreatePeerInstance (ref reference, transfer, type); + + if (peer != null) { + JniObjectReference.Dispose (ref klass); + return peer; + } + } + + var super = JniEnvironment.Types.GetSuperclass (klass); + jniTypeName = super.IsValid + ? JniEnvironment.Types.GetJniTypeNameFromClass (super) + : null; + + JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); + klass = super; + } + JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); + + return TryCreatePeerInstance (ref reference, transfer, targetType); + + Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType) + { + foreach (var t in Runtime.TypeManager.GetTypes (sig)) { + if (targetType.IsAssignableFrom (t)) { + return t; + } + } + return null; + } + } + + IJavaPeerable? TryCreatePeerInstance ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type type) + { + type = Runtime.TypeManager.GetInvokerType (type) ?? type; + + var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); + self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); + + var constructed = false; + try { + constructed = TryConstructPeer (self, ref reference, options, type); + } finally { + if (!constructed) { + GC.SuppressFinalize (self); + self = null; + } + } + return self; + } + + bool TryConstructPeer ( + IJavaPeerable self, + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type type) + { + var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); + if (c != null) { + var args = new object[] { + reference.Handle, + JniHandleOwnership.DoNotTransfer, + }; + c.Invoke (self, args); + JniObjectReference.Dispose (ref reference, options); + return true; + } + + c = type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); + if (c != null) { + var args = new object[] { + reference, + options, + }; + c.Invoke (self, args); + reference = (JniObjectReference) args [0]; + return true; + } + + return false; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index cbceea01376..82f13ed07a8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1,34 +1,27 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Reflection; -using System.Runtime.CompilerServices; using Android.Runtime; using Java.Interop; namespace Microsoft.Android.Runtime; -[RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] -[RequiresUnreferencedCode ("This value manager is reflection-backed and is not trimming-compatible.")] +[RequiresDynamicCode ("This value manager is reflection-backed and can break in AOT scenarios.")] +[RequiresUnreferencedCode ("This value manager is reflection-backed and relies on custom trimming rules.")] sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; - static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; + static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; + static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; public JavaMarshalValueManager () { JavaMarshalRegisteredPeers.InitializeIfNeeded (); } - protected override void Dispose (bool disposing) - { - base.Dispose (disposing); - } - public override void WaitForGCBridgeProcessing () { // Intentionally empty. The Mono runtime's own implementation acknowledges this @@ -83,11 +76,7 @@ public override List GetSurfacedPeers () if (RuntimeFeature.TrimmableTypeMap) { try { - // Mirror legacy GetPeerType: callers commonly request universal - // interfaces / boxes (IJavaPeerable, object, Exception) — map these - // to a concrete peer type so the proxy lookup can succeed. - var resolvedTargetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType); - + var resolvedTargetType = GetPeerType (targetType); var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); if (peer is not null) { @@ -104,15 +93,9 @@ public override List GetSurfacedPeers () // InvalidCastException via its `??` clause) // (c) classes are compatible but no proxy / activation failed // → NotSupportedException (genuine generator gap) - if (resolvedTargetType is not null) { - if (!typeMap.TryGetJniNameForManagedType (resolvedTargetType, out var targetJniName)) { - throw new ArgumentException ( - $"Could not determine Java type corresponding to '{resolvedTargetType.AssemblyQualifiedName}'.", - nameof (targetType)); - } - if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetJniName, ref reference, resolvedTargetType)) { - return null; - } + if (resolvedTargetType is not null && + IsIncompatibleCast (typeMap, ref reference, resolvedTargetType)) { + return null; } var targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; @@ -127,7 +110,8 @@ public override List GetSurfacedPeers () } } - targetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); + targetType = targetType ?? typeof (global::Java.Interop.JavaObject); + targetType = GetPeerType (targetType); if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); @@ -138,22 +122,86 @@ public override List GetSurfacedPeers () throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); } - if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetSig.SimpleReference, ref reference, targetType)) { + var refClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference targetClass; + try { + targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference); + } catch (Exception e) { + JniObjectReference.Dispose (ref refClass); + throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.", + nameof (targetType), + e); + } + + if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass)) { + JniObjectReference.Dispose (ref refClass); + JniObjectReference.Dispose (ref targetClass); return null; } - var refClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference.Dispose (ref targetClass); + + var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); + if (peer == null) { + throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", + JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); + } + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + return peer; + } + + [return: DynamicallyAccessedMembers (Constructors)] + static Type? GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) + { + if (type == typeof (object)) { + return typeof (global::Java.Interop.JavaObject); + } + if (type == typeof (IJavaPeerable)) { + return typeof (global::Java.Interop.JavaObject); + } + if (type == typeof (Exception)) { + return typeof (JavaException); + } + return type; + } + + /// + /// Returns true when 's Java class is not assignable from + /// . Throws when has no usable mapping. + /// + static bool IsIncompatibleCast ( + TrimmableTypeMap typeMap, + ref JniObjectReference reference, + Type targetType) + { + if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { + throw new ArgumentException ( + $"Could not determine Java type corresponding to '{targetType.AssemblyQualifiedName}'.", + nameof (targetType)); + } + + var instanceClass = JniEnvironment.Types.GetObjectClass (reference); + JniObjectReference targetClass = default; try { - var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); - if (peer == null) { - throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", - JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); + try { + targetClass = JniEnvironment.Types.FindClass (targetJniName); + } catch (Java.Lang.ClassNotFoundException e) { + throw new ArgumentException ( + $"Could not find Java class '{targetJniName}'.", + nameof (targetType), e); + } + + if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { + // Bad cast: callers translate null to the expected result. + return true; } - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - return peer; } finally { - JniObjectReference.Dispose (ref refClass); + JniObjectReference.Dispose (ref instanceClass); + JniObjectReference.Dispose (ref targetClass); } + + // Compatible classes mean a proxy/activation gap. + return false; } IJavaPeerable? CreatePeerInstance ( @@ -253,7 +301,7 @@ bool TryConstructPeer ( c.Invoke (self, args); reference = (JniObjectReference) args [0]; return true; - } + } return false; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs deleted file mode 100644 index 26f4d3bf627..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Android.Runtime; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -static class JavaMarshalValueManagerHelper -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - [return: DynamicallyAccessedMembers (Constructors)] - public static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) - { - if (type is null) { - return null; - } - if (type == typeof (object) || type == typeof (IJavaPeerable)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (Exception)) { - return typeof (JavaException); - } - return type; - } - - /// - /// Returns true when 's Java class is not assignable from - /// . Throws when has no usable mapping. - /// - public static bool IsIncompatibleCast ( - string targetJniName, - ref JniObjectReference reference, - Type targetType) - { - var instanceClass = JniEnvironment.Types.GetObjectClass (reference); - JniObjectReference targetClass = default; - try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); - - if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { - // Match the legacy cast diagnostic when assembly logging is enabled. - if (Logger.LogAssembly) { - var targetSig = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (targetType); - var message = $"Handle 0x{reference.Handle:x} is of type '{JNIEnv.GetClassNameFromInstance (reference.Handle)}' which is not assignable to '{targetSig.SimpleReference}'"; - Logger.Log (LogLevel.Debug, "monodroid-assembly", message); - } - - if (RuntimeFeature.IsAssignableFromCheck) { - return true; - } - } - } catch (Java.Lang.ClassNotFoundException e) { - throw new ArgumentException ( - $"Could not find Java class '{targetJniName}'.", - nameof (targetType), e); - } finally { - JniObjectReference.Dispose (ref instanceClass); - JniObjectReference.Dispose (ref targetClass); - } - - // Compatible classes mean a proxy/activation gap. - return false; - } -} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 77ed51a5a1d..bad2cda34fe 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -361,12 +361,12 @@ + - From 0d3b069ec88cfddc182c0cdef2c711c76802213a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 11:20:07 +0200 Subject: [PATCH 17/20] Renive AndroidReflectionJniValueManager (again?) --- .../AndroidReflectionJniValueManager.cs | 217 ------------------ src/Mono.Android/Mono.Android.csproj | 1 - 2 files changed, 218 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs deleted file mode 100644 index 1eff1f5d3e3..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/AndroidReflectionJniValueManager.cs +++ /dev/null @@ -1,217 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using System.Runtime.CompilerServices; -using Android.Runtime; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -[RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] -[RequiresUnreferencedCode ("This value manager is reflection-backed and is not trimming-compatible.")] -abstract class AndroidReflectionJniValueManager : JniRuntime.ReflectionJniValueManager -{ - protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; - static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; - - public override IJavaPeerable? CreatePeer ( - ref JniObjectReference reference, - JniObjectReferenceOptions transfer, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - EnsureNotDisposed (); - - if (!reference.IsValid) { - return null; - } - - targetType = ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); - - if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { - throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); - } - - var targetSig = Runtime.TypeManager.GetTypeSignature (targetType); - if (!targetSig.IsValid || targetSig.SimpleReference == null) { - throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); - } - - if (IsIncompatibleCast (targetSig.SimpleReference, ref reference, targetType)) { - return null; - } - - var refClass = JniEnvironment.Types.GetObjectClass (reference); - try { - var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); - if (peer == null) { - throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", - JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); - } - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - return peer; - } finally { - JniObjectReference.Dispose (ref refClass); - } - } - - [return: DynamicallyAccessedMembers (Constructors)] - protected static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) - { - if (type is null) { - return null; - } - if (type == typeof (object) || type == typeof (IJavaPeerable)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (Exception)) { - return typeof (JavaException); - } - return type; - } - - /// - /// Returns true when 's Java class is not assignable from - /// . Throws when has no usable mapping. - /// - protected static bool IsIncompatibleCast ( - string targetJniName, - ref JniObjectReference reference, - Type targetType) - { - var instanceClass = JniEnvironment.Types.GetObjectClass (reference); - JniObjectReference targetClass = default; - try { - targetClass = JniEnvironment.Types.FindClass (targetJniName); - - if (!JniEnvironment.Types.IsAssignableFrom (instanceClass, targetClass)) { - // Match the legacy cast diagnostic when assembly logging is enabled. - if (Logger.LogAssembly) { - var targetSig = JniRuntime.CurrentRuntime.TypeManager.GetTypeSignature (targetType); - var message = $"Handle 0x{reference.Handle:x} is of type '{JNIEnv.GetClassNameFromInstance (reference.Handle)}' which is not assignable to '{targetSig.SimpleReference}'"; - Logger.Log (LogLevel.Debug, "monodroid-assembly", message); - } - - if (RuntimeFeature.IsAssignableFromCheck) { - return true; - } - } - } catch (Java.Lang.ClassNotFoundException e) { - throw new ArgumentException ( - $"Could not find Java class '{targetJniName}'.", - nameof (targetType), e); - } finally { - JniObjectReference.Dispose (ref instanceClass); - JniObjectReference.Dispose (ref targetClass); - } - - // Compatible classes mean a proxy/activation gap. - return false; - } - - IJavaPeerable? CreatePeerInstance ( - ref JniObjectReference klass, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - ref JniObjectReference reference, - JniObjectReferenceOptions transfer) - { - var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); - - while (jniTypeName != null) { - JniTypeSignature sig; - if (!JniTypeSignature.TryParse (jniTypeName, out sig)) - return null; - - Type? type = GetTypeAssignableTo (sig, targetType); - if (type != null) { - var peer = TryCreatePeerInstance (ref reference, transfer, type); - - if (peer != null) { - JniObjectReference.Dispose (ref klass); - return peer; - } - } - - var super = JniEnvironment.Types.GetSuperclass (klass); - jniTypeName = super.IsValid - ? JniEnvironment.Types.GetJniTypeNameFromClass (super) - : null; - - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - klass = super; - } - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - - return TryCreatePeerInstance (ref reference, transfer, targetType); - - Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType) - { - foreach (var t in Runtime.TypeManager.GetTypes (sig)) { - if (targetType.IsAssignableFrom (t)) { - return t; - } - } - return null; - } - } - - IJavaPeerable? TryCreatePeerInstance ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - type = Runtime.TypeManager.GetInvokerType (type) ?? type; - - var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); - self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - - var constructed = false; - try { - constructed = TryConstructPeer (self, ref reference, options, type); - } finally { - if (!constructed) { - GC.SuppressFinalize (self); - self = null; - } - } - return self; - } - - bool TryConstructPeer ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference.Handle, - JniHandleOwnership.DoNotTransfer, - }; - c.Invoke (self, args); - JniObjectReference.Dispose (ref reference, options); - return true; - } - - c = type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference, - options, - }; - c.Invoke (self, args); - reference = (JniObjectReference) args [0]; - return true; - } - - return false; - } -} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index bad2cda34fe..771ad4e48d6 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -361,7 +361,6 @@ - From ea2f9df65d97de20a18357de79a2cba72b51acd9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 11:29:03 +0200 Subject: [PATCH 18/20] Remove unnecessary changes from JavaMarshalValueManager --- .../JavaMarshalValueManager.cs | 139 ++---------------- 1 file changed, 12 insertions(+), 127 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 82f13ed07a8..cfb1ad410d8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -14,7 +14,6 @@ sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; public JavaMarshalValueManager () @@ -76,7 +75,11 @@ public override List GetSurfacedPeers () if (RuntimeFeature.TrimmableTypeMap) { try { - var resolvedTargetType = GetPeerType (targetType); + // Mirror legacy GetPeerType: callers commonly request universal + // interfaces / boxes (IJavaPeerable, object, Exception) — map these + // to a concrete peer type so the proxy lookup can succeed. + var resolvedTargetType = ResolvePeerType (targetType); + var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); if (peer is not null) { @@ -110,53 +113,16 @@ public override List GetSurfacedPeers () } } - targetType = targetType ?? typeof (global::Java.Interop.JavaObject); - targetType = GetPeerType (targetType); - - if (!typeof (IJavaPeerable).IsAssignableFrom (targetType)) { - throw new ArgumentException ($"targetType `{targetType.AssemblyQualifiedName}` must implement IJavaPeerable!", nameof (targetType)); - } - - var targetSig = Runtime.TypeManager.GetTypeSignature (targetType); - if (!targetSig.IsValid || targetSig.SimpleReference == null) { - throw new ArgumentException ($"Could not determine Java type corresponding to `{targetType.AssemblyQualifiedName}`.", nameof (targetType)); - } - - var refClass = JniEnvironment.Types.GetObjectClass (reference); - JniObjectReference targetClass; - try { - targetClass = JniEnvironment.Types.FindClass (targetSig.SimpleReference); - } catch (Exception e) { - JniObjectReference.Dispose (ref refClass); - throw new ArgumentException ($"Could not find Java class `{targetSig.SimpleReference}`.", - nameof (targetType), - e); - } - - if (!JniEnvironment.Types.IsAssignableFrom (refClass, targetClass)) { - JniObjectReference.Dispose (ref refClass); - JniObjectReference.Dispose (ref targetClass); - return null; - } - - JniObjectReference.Dispose (ref targetClass); - - var peer = CreatePeerInstance (ref refClass, targetType, ref reference, transfer); - if (peer == null) { - throw new NotSupportedException (string.Format (CultureInfo.InvariantCulture, "Could not find an appropriate constructable wrapper type for Java type '{0}', targetType='{1}'.", - JniEnvironment.Types.GetJniTypeNameFromInstance (reference), targetType)); - } - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - return peer; + return base.CreatePeer (ref reference, transfer, targetType); } [return: DynamicallyAccessedMembers (Constructors)] - static Type? GetPeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) + static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) { - if (type == typeof (object)) { - return typeof (global::Java.Interop.JavaObject); + if (type is null) { + return null; } - if (type == typeof (IJavaPeerable)) { + if (type == typeof (object) || type == typeof (IJavaPeerable)) { return typeof (global::Java.Interop.JavaObject); } if (type == typeof (Exception)) { @@ -204,77 +170,7 @@ static bool IsIncompatibleCast ( return false; } - IJavaPeerable? CreatePeerInstance ( - ref JniObjectReference klass, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - ref JniObjectReference reference, - JniObjectReferenceOptions transfer) - { - var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); - - while (jniTypeName != null) { - JniTypeSignature sig; - if (!JniTypeSignature.TryParse (jniTypeName, out sig)) - return null; - - Type? type = GetTypeAssignableTo (sig, targetType); - if (type != null) { - var peer = TryCreatePeerInstance (ref reference, transfer, type); - - if (peer != null) { - JniObjectReference.Dispose (ref klass); - return peer; - } - } - - var super = JniEnvironment.Types.GetSuperclass (klass); - jniTypeName = super.IsValid - ? JniEnvironment.Types.GetJniTypeNameFromClass (super) - : null; - - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - klass = super; - } - JniObjectReference.Dispose (ref klass, JniObjectReferenceOptions.CopyAndDispose); - - return TryCreatePeerInstance (ref reference, transfer, targetType); - - Type? GetTypeAssignableTo (JniTypeSignature sig, Type targetType) - { - foreach (var t in Runtime.TypeManager.GetTypes (sig)) { - if (targetType.IsAssignableFrom (t)) { - return t; - } - } - return null; - } - } - - IJavaPeerable? TryCreatePeerInstance ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) - { - type = Runtime.TypeManager.GetInvokerType (type) ?? type; - - var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); - self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - - var constructed = false; - try { - constructed = TryConstructPeer (self, ref reference, options, type); - } finally { - if (!constructed) { - GC.SuppressFinalize (self); - self = null; - } - } - return self; - } - - bool TryConstructPeer ( + protected override bool TryConstructPeer ( IJavaPeerable self, ref JniObjectReference reference, JniObjectReferenceOptions options, @@ -292,18 +188,7 @@ bool TryConstructPeer ( return true; } - c = type.GetConstructor (ActivationConstructorBindingFlags, null, JIConstructorSignature, null); - if (c != null) { - var args = new object[] { - reference, - options, - }; - c.Invoke (self, args); - reference = (JniObjectReference) args [0]; - return true; - } - - return false; + return base.TryConstructPeer (self, ref reference, options, type); } protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) From 8873f1c01fdb0a08338e3d4225c698dcef25cf87 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 11:31:27 +0200 Subject: [PATCH 19/20] Undo a few more changes --- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 3 +-- src/Mono.Android/Mono.Android.csproj | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index cfb1ad410d8..bfd8c633428 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -14,7 +14,7 @@ sealed class JavaMarshalValueManager : JniRuntime.ReflectionJniValueManager const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; + static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; public JavaMarshalValueManager () { @@ -187,7 +187,6 @@ protected override bool TryConstructPeer ( JniObjectReference.Dispose (ref reference, options); return true; } - return base.TryConstructPeer (self, ref reference, options, type); } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 771ad4e48d6..9b90b93ce48 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -366,9 +366,9 @@ + - From 6cb72aed59812f2d7770fd7be6536db7f234272a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 30 Jun 2026 11:35:11 +0200 Subject: [PATCH 20/20] Bring back ActivatePeer method --- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index bfd8c633428..a584a991e0d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -56,6 +56,14 @@ public override void FinalizePeer (IJavaPeerable value) JavaMarshalRegisteredPeers.FinalizePeer (value); } + public override void ActivatePeer (JniObjectReference reference, Type type, ConstructorInfo cinfo, object?[]? argumentValues) + { + if (RuntimeFeature.TrimmableTypeMap) + throw new PlatformNotSupportedException ("Activating Java peers is not supported when TrimmableTypeMap is enabled."); + + base.ActivatePeer (reference, type, cinfo, argumentValues); + } + public override List GetSurfacedPeers () { return JavaMarshalRegisteredPeers.GetSurfacedPeers ();