From 579ea8a263861587a385f9a01c31a95e178ec05d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 02:53:03 +0000 Subject: [PATCH 01/47] Bump external/Java.Interop from `b881d21` to `d7dbad5` Bumps [external/Java.Interop](https://github.com/dotnet/java-interop) from `b881d21` to `d7dbad5`. - [Commits](https://github.com/dotnet/java-interop/compare/b881d21f51cbac6e175de1b2f6c254fe3846aa1d...d7dbad5e30a8f03743a508a95c4e9159fe1f6607) --- updated-dependencies: - dependency-name: external/Java.Interop dependency-version: d7dbad5e30a8f03743a508a95c4e9159fe1f6607 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- external/Java.Interop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/Java.Interop b/external/Java.Interop index b881d21f51c..d7dbad5e30a 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit b881d21f51cbac6e175de1b2f6c254fe3846aa1d +Subproject commit d7dbad5e30a8f03743a508a95c4e9159fe1f6607 From f79c0ccdd698716a471505727ce3ae1ef0c23d5e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 9 Jun 2026 23:34:05 +0200 Subject: [PATCH 02/47] Implement Android JavaMarshal value manager split Split the Android JavaMarshal value manager into CoreCLR and trimmable implementations that share peer registration and GC bridge integration through a reusable helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JreRuntime.cs | 2 +- .../Android.Runtime/AndroidRuntime.cs | 12 +- .../Android.Runtime/JNIEnvInit.cs | 8 +- .../JavaMarshalValueManager.cs | 393 +++++++++++++----- .../ManagedTypeManager.cs | 4 +- .../SimpleValueManager.cs | 16 +- .../TrimmableTypeMap.cs | 2 +- .../TrimmableTypeMapTypeManager.cs | 8 +- 8 files changed, 329 insertions(+), 116 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 5891149578f..74908d65023 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 ??= new TrimmableJavaMarshalValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 918b8377b54..6c258ec2350 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -310,13 +310,15 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value) } } - class AndroidTypeManager : JniRuntime.JniTypeManager { + 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; + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "AndroidTypeManager is selected for the Mono/legacy reflection-backed typemap path.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "AndroidTypeManager is selected for the Mono/legacy reflection-backed typemap path.")] public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) { this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; @@ -623,10 +625,16 @@ static void SplitMethodLine ( } } - class AndroidValueManager : JniRuntime.JniValueManager { + class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "AndroidValueManager is selected for the Mono runtime path.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "AndroidValueManager is selected for the Mono runtime path.")] + public AndroidValueManager () + { + } + public override void WaitForGCBridgeProcessing () { if (!AndroidRuntimeInternal.BridgeProcessing) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index c2e7ea913ca..3859abf7f40 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -188,11 +188,15 @@ internal static JniRuntime.JniValueManager CreateValueManager () } if (RuntimeFeature.IsCoreClrRuntime) { - return new JavaMarshalValueManager (); + if (RuntimeFeature.TrimmableTypeMap) { + return new TrimmableJavaMarshalValueManager (); + } + + return new CoreClrJavaMarshalValueManager (); } if (RuntimeFeature.IsNativeAotRuntime) { - return new JavaMarshalValueManager (); + return new TrimmableJavaMarshalValueManager (); } throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 458ad77202a..5cc82322ac5 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -18,36 +18,36 @@ namespace Microsoft.Android.Runtime; -class JavaMarshalValueManager : JniRuntime.JniValueManager +class JavaMarshalPeerManager : IDisposable { - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - readonly Dictionary> RegisteredInstances = new (); readonly ConcurrentQueue CollectedContexts = new (); + readonly string ownerName; bool disposed; - public unsafe JavaMarshalValueManager () + public unsafe JavaMarshalPeerManager (string ownerName) { - var javaMarshalValueManagerHandle = new GCHandle (this); + this.ownerName = ownerName; + + var javaMarshalPeerManagerHandle = new GCHandle (this); var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge ( - GCHandle.ToIntPtr (javaMarshalValueManagerHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); + GCHandle.ToIntPtr (javaMarshalPeerManagerHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); JavaMarshal.Initialize (mark_cross_references_ftn); } - protected override void Dispose (bool disposing) + public void Dispose () { disposed = true; - base.Dispose (disposing); } void ThrowIfDisposed () { if (disposed) - throw new ObjectDisposedException (nameof (JavaMarshalValueManager)); + throw new ObjectDisposedException (ownerName); } - public override void WaitForGCBridgeProcessing () + public 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 @@ -57,7 +57,7 @@ public override void WaitForGCBridgeProcessing () // they are not affected by the bridge swapping control_block handles. } - public unsafe override void CollectPeers () + public unsafe void CollectPeers () { ThrowIfDisposed (); @@ -91,7 +91,7 @@ void Remove (HandleContext* context) } } - public override void AddPeer (IJavaPeerable value) + public void AddPeer (IJavaPeerable value) { ThrowIfDisposed (); @@ -137,7 +137,7 @@ public override void AddPeer (IJavaPeerable value) void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) { - Runtime.ObjectReferenceManager.WriteGlobalReferenceLine ( + JniEnvironment.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 (), @@ -151,14 +151,14 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal JniEnvironment.Types.GetJniTypeNameFromInstance (keepValue.PeerReference)); } - public override IJavaPeerable? PeekPeer (JniObjectReference reference) + public IJavaPeerable? PeekPeer (JniObjectReference reference) { ThrowIfDisposed (); if (!reference.IsValid) return null; - int key = GetJniIdentityHashCode (reference); + int key = JniEnvironment.References.GetIdentityHashCode (reference); lock (RegisteredInstances) { if (!RegisteredInstances.TryGetValue (key, out List? peers)) @@ -178,7 +178,7 @@ void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepVal return null; } - public override void RemovePeer (IJavaPeerable value) + public void RemovePeer (IJavaPeerable value) { ThrowIfDisposed (); @@ -207,10 +207,10 @@ public override void RemovePeer (IJavaPeerable value) } } - public override void FinalizePeer (IJavaPeerable value) + public void FinalizePeer (IJavaPeerable value) { var h = value.PeerReference; - var o = Runtime.ObjectReferenceManager; + var o = JniEnvironment.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 @@ -242,15 +242,7 @@ public override void FinalizePeer (IJavaPeerable value) value.Finalized (); } - 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 () + public List GetSurfacedPeers () { ThrowIfDisposed (); @@ -423,13 +415,13 @@ static unsafe void BridgeProcessingStarted (MarkCrossReferencesArgs* mcr) } [UnmanagedCallersOnly] - static unsafe void BridgeProcessingFinished (IntPtr javaMarshalValueManagerHandle, MarkCrossReferencesArgs* mcr) + static unsafe void BridgeProcessingFinished (IntPtr javaMarshalPeerManagerHandle, MarkCrossReferencesArgs* mcr) { if (mcr == null) { throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); } - JavaMarshalValueManager instance = GCHandle.FromIntPtr (javaMarshalValueManagerHandle).Target; + JavaMarshalPeerManager instance = GCHandle.FromIntPtr (javaMarshalPeerManagerHandle).Target; ReadOnlySpan handlesToFree = instance.ProcessCollectedContexts (mcr); @@ -484,8 +476,94 @@ void ProcessContext (HandleContext* context) return CollectionsMarshal.AsSpan (handlesToFree); } +} + +abstract class JavaMarshalValueManagerBase : JniRuntime.ReflectionJniValueManager +{ + protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + + readonly JavaMarshalPeerManager peerManager; + + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] + protected JavaMarshalValueManagerBase () + { + peerManager = new JavaMarshalPeerManager (GetType ().Name); + } + + protected override void Dispose (bool disposing) + { + peerManager.Dispose (); + base.Dispose (disposing); + } + + public override void WaitForGCBridgeProcessing () + { + peerManager.WaitForGCBridgeProcessing (); + } + + public override void CollectPeers () + { + peerManager.CollectPeers (); + } + + public override void AddPeer (IJavaPeerable value) + { + peerManager.AddPeer (value); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + return peerManager.PeekPeer (reference); + } + + public override void RemovePeer (IJavaPeerable value) + { + peerManager.RemovePeer (value); + } + + public override void FinalizePeer (IJavaPeerable value) + { + peerManager.FinalizePeer (value); + } + + public override List GetSurfacedPeers () + { + return peerManager.GetSurfacedPeers (); + } + + [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; + } + + 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); + } +} + +class CoreClrJavaMarshalValueManager : JavaMarshalValueManagerBase +{ const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType (); + static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; public override IJavaPeerable? CreatePeer ( @@ -494,68 +572,211 @@ void ProcessContext (HandleContext* context) [DynamicallyAccessedMembers (Constructors)] Type? targetType) { - ThrowIfDisposed (); + EnsureNotDisposed (); 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) { + 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)); + } + + 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; + } + + 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); - // 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; + [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 targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; - var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + var self = (IJavaPeerable) RuntimeHelpers.GetUninitializedObject (type); + self.SetJniManagedPeerState (JniManagedPeerStates.Replaceable | JniManagedPeerStates.Activatable); - 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); + 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 base.CreatePeer (ref reference, transfer, targetType); + return false; } +} - [return: DynamicallyAccessedMembers (Constructors)] - static Type? ResolvePeerType ([DynamicallyAccessedMembers (Constructors)] Type? type) +class TrimmableJavaMarshalValueManager : JavaMarshalValueManagerBase +{ + public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) { - if (type is null) { + throw new PlatformNotSupportedException ("Activating Java peers is not supported when TrimmableTypeMap is enabled."); + } + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + EnsureNotDisposed (); + + if (!reference.IsValid) { return null; } - if (type == typeof (object) || type == typeof (IJavaPeerable)) { - return typeof (global::Java.Interop.JavaObject); - } - if (type == typeof (Exception)) { - return typeof (JavaException); + + 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 type; } /// @@ -596,34 +817,4 @@ static bool IsIncompatibleCast ( // 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/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 454bab0e1bb..9445e88434e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -7,12 +7,14 @@ namespace Microsoft.Android.Runtime; -class ManagedTypeManager : JniRuntime.JniTypeManager { +class ManagedTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "ManagedTypeManager is selected for managed typemap paths that use reflection-backed Java.Interop behavior.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "ManagedTypeManager is selected for managed typemap paths that use reflection-backed Java.Interop behavior.")] public ManagedTypeManager () { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs index bcccf6d6fd1..40be32e9b66 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs @@ -16,12 +16,14 @@ namespace Microsoft.Android.Runtime; -class SimpleValueManager : JniRuntime.JniValueManager +class SimpleValueManager : JniRuntime.ReflectionJniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; Dictionary>? RegisteredInstances = new Dictionary>(); + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "SimpleValueManager is a reflection-backed test/runtime helper and is not used by NativeAOT trimmable startup.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "SimpleValueManager is a reflection-backed test/runtime helper and is not used by NativeAOT trimmable startup.")] internal SimpleValueManager () { } @@ -227,13 +229,13 @@ public override List GetSurfacedPeers () static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - protected override bool TryConstructPeer ( + [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "SimpleValueManager is reflection-backed and requires preserved peer constructors.")] + protected override void ConstructPeerCore ( IJavaPeerable self, ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type type) + JniObjectReferenceOptions options) { + Type type = self.GetType (); var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); if (c != null) { var args = new object[] { @@ -242,9 +244,9 @@ protected override bool TryConstructPeer ( }; c.Invoke (self, args); JniObjectReference.Dispose (ref reference, options); - return true; + return; } - return base.TryConstructPeer (self, ref reference, options, type); + base.ConstructPeerCore (self, ref reference, options); } protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)]out object? result) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 915daa0e248..3fa8966671a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -370,7 +370,7 @@ static JniMethodInfo GetClassGetInterfacesMethod () // FindClass throws for managed types whose Java peer class is // not present in the APK (e.g. test types annotated with // [JniTypeSignature("__missing__")]). Treat as "no match" so - // JavaMarshalValueManager.CreatePeer can surface the correct + // TrimmableJavaMarshalValueManager.CreatePeer can surface the correct // ArgumentException instead of leaking ClassNotFoundException. return null; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index dea8cb2dcb8..bdd2fc59e18 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -14,11 +14,17 @@ namespace Microsoft.Android.Runtime; /// Type manager for the trimmable typemap path. Delegates type lookups /// to . /// -class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager +class TrimmableTypeMapTypeManager : JniRuntime.ReflectionJniTypeManager { const string NoSimpleReference = "\0"; readonly ConcurrentDictionary _simpleReferenceCache = new (); + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This manager reuses Java.Interop built-in type mappings while trimmable typemap lookups avoid reflection fallback.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This manager reuses Java.Interop built-in type mappings while trimmable typemap lookups avoid reflection fallback.")] + public TrimmableTypeMapTypeManager () + { + } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) { From 676dca49a66ecbab17a612d89f0224d592683a0c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 9 Jun 2026 23:47:25 +0200 Subject: [PATCH 03/47] Use pure trimmable typemap value manager Keep the trimmable typemap value manager on the abstract JniValueManager base, sharing only peer registration and GC bridge state with the CoreCLR value manager. Leave value marshaling unsupported for now until Android has trimmable-specific marshalers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/JreRuntime.cs | 2 +- .../Android.Runtime/JNIEnvInit.cs | 4 +- .../JavaMarshalValueManager.cs | 187 +++++++++++++++++- .../TrimmableTypeMap.cs | 2 +- 4 files changed, 187 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 74908d65023..b9daebabec8 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 TrimmableJavaMarshalValueManager (); + builder.ValueManager ??= new TrimmableTypeMapValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 3859abf7f40..85aae8e3546 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -189,14 +189,14 @@ internal static JniRuntime.JniValueManager CreateValueManager () if (RuntimeFeature.IsCoreClrRuntime) { if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableJavaMarshalValueManager (); + return new TrimmableTypeMapValueManager (); } return new CoreClrJavaMarshalValueManager (); } if (RuntimeFeature.IsNativeAotRuntime) { - return new TrimmableJavaMarshalValueManager (); + return new TrimmableTypeMapValueManager (); } throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 5cc82322ac5..9e834b84729 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -478,7 +478,7 @@ void ProcessContext (HandleContext* context) } -abstract class JavaMarshalValueManagerBase : JniRuntime.ReflectionJniValueManager +abstract class JavaMarshalReflectionValueManagerBase : JniRuntime.ReflectionJniValueManager { protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; @@ -486,7 +486,7 @@ abstract class JavaMarshalValueManagerBase : JniRuntime.ReflectionJniValueManage [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] - protected JavaMarshalValueManagerBase () + protected JavaMarshalReflectionValueManagerBase () { peerManager = new JavaMarshalPeerManager (GetType ().Name); } @@ -558,7 +558,7 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t } } -class CoreClrJavaMarshalValueManager : JavaMarshalValueManagerBase +class CoreClrJavaMarshalValueManager : JavaMarshalReflectionValueManagerBase { const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; @@ -721,13 +721,114 @@ bool TryConstructPeer ( } } -class TrimmableJavaMarshalValueManager : JavaMarshalValueManagerBase +class TrimmableTypeMapValueManager : JniRuntime.JniValueManager { + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); + + readonly JavaMarshalPeerManager peerManager; + + public TrimmableTypeMapValueManager () + { + peerManager = new JavaMarshalPeerManager (GetType ().Name); + } + + protected override void Dispose (bool disposing) + { + peerManager.Dispose (); + base.Dispose (disposing); + } + + public override void WaitForGCBridgeProcessing () + { + peerManager.WaitForGCBridgeProcessing (); + } + + public override void CollectPeers () + { + peerManager.CollectPeers (); + } + + public override void AddPeer (IJavaPeerable value) + { + peerManager.AddPeer (value); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + return peerManager.PeekPeer (reference); + } + + public override void RemovePeer (IJavaPeerable value) + { + peerManager.RemovePeer (value); + } + + public override void FinalizePeer (IJavaPeerable value) + { + peerManager.FinalizePeer (value); + } + + public override List GetSurfacedPeers () + { + return peerManager.GetSurfacedPeers (); + } + public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) { throw new PlatformNotSupportedException ("Activating Java peers is not supported when TrimmableTypeMap is enabled."); } + protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectReference reference, JniObjectReferenceOptions options) + { + if (peer == null) + throw new ArgumentNullException (nameof (peer)); + + var newRef = peer.PeerReference; + if (newRef.IsValid) { + JniObjectReference.Dispose (ref reference, options); + + // Activation? See ManagedPeer.Construct, CreatePeer + // Instance was already added, don't add again + if (peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable)) { + return; + } + var orig = newRef; + newRef = orig.NewGlobalRef (); + JniObjectReference.Dispose (ref orig); + } else if (options == JniObjectReferenceOptions.None) { + // `reference` is likely *InvalidJniObjectReference, and can't be touched + return; + } else if (!reference.IsValid) { + throw new ArgumentException ("JNI Object Reference is invalid.", nameof (reference)); + } else { + newRef = reference; + + if ((options & JniObjectReferenceOptions.Copy) == JniObjectReferenceOptions.Copy) { + newRef = reference.NewGlobalRef (); + } + + JniObjectReference.Dispose (ref reference, options); + } + + peer.SetPeerReference (newRef); + peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); + + var o = Runtime.ObjectReferenceManager; + if (o.LogGlobalReferenceMessages) { + o.WriteGlobalReferenceLine ("Created PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}, Java.Type={4}", + newRef.ToString (), + peer.JniIdentityHashCode.ToString ("x", CultureInfo.InvariantCulture), + RuntimeHelpers.GetHashCode (peer).ToString ("x", CultureInfo.InvariantCulture), + peer.GetType ().FullName, + JniEnvironment.Types.GetJniTypeNameFromInstance (newRef)); + } + + if ((options & DoNotRegisterTarget) != DoNotRegisterTarget) { + AddPeer (peer); + } + } + public override IJavaPeerable? CreatePeer ( ref JniObjectReference reference, JniObjectReferenceOptions transfer, @@ -779,6 +880,84 @@ public override void ActivatePeer (JniObjectReference reference, [DynamicallyAcc } } + [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; + } + + 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); + } + + [return: MaybeNull] + protected override T CreateValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + throw CreateValueMarshalingNotSupportedException (); + } + + protected override object? CreateValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + throw CreateValueMarshalingNotSupportedException (); + } + + [return: MaybeNull] + protected override T GetValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + throw CreateValueMarshalingNotSupportedException (); + } + + protected override object? GetValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + throw CreateValueMarshalingNotSupportedException (); + } + + protected override JniValueMarshaler GetValueMarshalerCore (Type type) + { + throw CreateValueMarshalingNotSupportedException (); + } + + protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () + { + throw CreateValueMarshalingNotSupportedException (); + } + + static NotSupportedException CreateValueMarshalingNotSupportedException () + { + return new NotSupportedException ($"{nameof (TrimmableTypeMapValueManager)} does not support value marshaling yet."); + } + /// /// Returns true when 's Java class is not assignable from /// . Throws when has no usable mapping. diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 3fa8966671a..2b6e4f305db 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -370,7 +370,7 @@ static JniMethodInfo GetClassGetInterfacesMethod () // FindClass throws for managed types whose Java peer class is // not present in the APK (e.g. test types annotated with // [JniTypeSignature("__missing__")]). Treat as "no match" so - // TrimmableJavaMarshalValueManager.CreatePeer can surface the correct + // TrimmableTypeMapValueManager.CreatePeer can surface the correct // ArgumentException instead of leaking ClassNotFoundException. return null; } From 01dfd04369023c06571e7c96601390fd3edcb24c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 00:00:23 +0200 Subject: [PATCH 04/47] Use pure trimmable typemap type manager Move TrimmableTypeMapTypeManager off ReflectionJniTypeManager and implement type lookup through explicit built-in mappings plus the generated trimmable typemap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapTypeManager.cs | 258 ++++++++++++++++-- 1 file changed, 236 insertions(+), 22 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index bdd2fc59e18..92fcc57da0c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Java.Interop; namespace Microsoft.Android.Runtime; @@ -14,21 +13,59 @@ namespace Microsoft.Android.Runtime; /// Type manager for the trimmable typemap path. Delegates type lookups /// to . /// -class TrimmableTypeMapTypeManager : JniRuntime.ReflectionJniTypeManager +class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager { const string NoSimpleReference = "\0"; + internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; + internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; readonly ConcurrentDictionary _simpleReferenceCache = new (); - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This manager reuses Java.Interop built-in type mappings while trimmable typemap lookups avoid reflection fallback.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This manager reuses Java.Interop built-in type mappings while trimmable typemap lookups avoid reflection fallback.")] - public TrimmableTypeMapTypeManager () + protected override JniTypeSignature GetTypeSignatureCore (Type type) { + type = GetUnderlyingType (type, out int rank); + + if (TryGetBuiltInTypeSignature (type, out var signature)) { + return signature.AddArrayRank (rank); + } + + var simpleReference = GetSimpleReference (type); + return simpleReference == null ? default : new JniTypeSignature (simpleReference, rank, false); + } + + protected override IEnumerable GetTypeSignaturesCore (Type type) + { + type = GetUnderlyingType (type, out int rank); + + if (TryGetBuiltInTypeSignature (type, out var signature)) { + yield return signature.AddArrayRank (rank); + } + + foreach (var simpleReference in GetSimpleReferences (type)) { + yield return new JniTypeSignature (simpleReference, rank, false); + } + } + + static Type GetUnderlyingType (Type type, out int rank) + { + rank = 0; + var originalType = type; + while (type.IsArray) { + if (type.GetArrayRank () > 1) + throw new ArgumentException ("Multidimensional array '" + originalType.FullName + "' is not supported.", nameof (type)); + rank++; + type = type.GetElementType () ?? throw new InvalidOperationException ("Array type has no element type."); + } + + if (type.IsEnum) + type = Enum.GetUnderlyingType (type); + + return type; } protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { - foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) { - yield return t; + if (TryGetBuiltInTypeForSimpleReference (jniSimpleReference, out var builtIn)) { + yield return builtIn; } if (TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types)) { @@ -46,12 +83,12 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl string GetSimpleReferenceUncached (Type type) { - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { - return jniName; + if (TryGetBuiltInTypeSignature (type, out var signature)) { + return signature.SimpleReference ?? NoSimpleReference; } - foreach (var r in base.GetSimpleReferences (type)) { - return r; + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { + return jniName; } // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable @@ -67,13 +104,14 @@ string GetSimpleReferenceUncached (Type type) protected override IEnumerable GetSimpleReferences (Type type) { - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { - yield return jniName; + if (TryGetBuiltInTypeSignature (type, out var signature) && signature.SimpleReference is not null) { + yield return signature.SimpleReference; yield break; } - foreach (var r in base.GetSimpleReferences (type)) { - yield return r; + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { + yield return jniName; + yield break; } // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable @@ -86,17 +124,64 @@ protected override IEnumerable GetSimpleReferences (Type type) } } + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [UnconditionalSuppressMessage ("Trimming", "IL2063", Justification = "Trimmable typemap target types are generated from preserved Java peer metadata.")] + protected override Type? GetTypeForSimpleReference (string jniSimpleReference) + { + if (TryGetBuiltInTypeForSimpleReference (jniSimpleReference, out var type)) { + return type; + } + + return TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types) && types.Length > 0 + ? types [0] + : null; + } + + public override IEnumerable GetTypes (JniTypeSignature typeSignature) + { + if (!typeSignature.IsValid || typeSignature.SimpleReference == null) + return []; + return CreateGetTypesEnumerator (typeSignature); + } + + IEnumerable CreateGetTypesEnumerator (JniTypeSignature typeSignature) + { + if (!typeSignature.IsValid || typeSignature.SimpleReference == null) + yield break; + + foreach (var type in GetTypesForSimpleReference (typeSignature.SimpleReference)) { + if (typeSignature.ArrayRank == 0) { + yield return type; + continue; + } + + Type arrayElementType = type; + for (int i = 1; i < typeSignature.ArrayRank; i++) { + arrayElementType = MakeArrayType (arrayElementType); + } + + if (TrimmableTypeMap.Instance.TryGetArrayType (arrayElementType, out var arrayType)) { + yield return arrayType; + continue; + } + + if (IsKeywordSimpleReference (typeSignature.SimpleReference) || type == typeof (string)) { + yield return MakeArrayType (arrayElementType); + } + } + } + + public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) + { + yield break; + } + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] protected override Type? GetInvokerTypeCore ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) { - var invokerType = TrimmableTypeMap.Instance.GetInvokerType (type); - if (invokerType != null) { - return invokerType; - } - - return base.GetInvokerTypeCore (type); + return TrimmableTypeMap.Instance.GetInvokerType (type); } protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) @@ -116,7 +201,7 @@ protected override IEnumerable GetSimpleReferences (Type type) public override void RegisterNativeMembers ( JniType nativeClass, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, ReadOnlySpan methods) { @@ -124,4 +209,133 @@ public override void RegisterNativeMembers ( $"RegisterNativeMembers should not be called in the trimmable typemap path. " + $"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks."); } + + [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan)")] + public override void RegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + string? methods) + { + RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); + } + + static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signature) + { + switch (Type.GetTypeCode (type)) { + case TypeCode.String: + signature = new JniTypeSignature ("java/lang/String"); + return true; + case TypeCode.Boolean: + signature = new JniTypeSignature ("Z", 0, keyword: true); + return true; + case TypeCode.Byte: + case TypeCode.SByte: + signature = new JniTypeSignature ("B", 0, keyword: true); + return true; + case TypeCode.Char: + signature = new JniTypeSignature ("C", 0, keyword: true); + return true; + case TypeCode.UInt16: + case TypeCode.Int16: + signature = new JniTypeSignature ("S", 0, keyword: true); + return true; + case TypeCode.UInt32: + case TypeCode.Int32: + signature = new JniTypeSignature ("I", 0, keyword: true); + return true; + case TypeCode.UInt64: + case TypeCode.Int64: + signature = new JniTypeSignature ("J", 0, keyword: true); + return true; + case TypeCode.Single: + signature = new JniTypeSignature ("F", 0, keyword: true); + return true; + case TypeCode.Double: + signature = new JniTypeSignature ("D", 0, keyword: true); + return true; + } + + if (type == typeof (void)) { + signature = new JniTypeSignature ("V", 0, keyword: true); + return true; + } + + if (type == typeof (Boolean?)) { + signature = new JniTypeSignature ("java/lang/Boolean"); + return true; + } + if (type == typeof (SByte?)) { + signature = new JniTypeSignature ("java/lang/Byte"); + return true; + } + if (type == typeof (Char?)) { + signature = new JniTypeSignature ("java/lang/Character"); + return true; + } + if (type == typeof (Int16?)) { + signature = new JniTypeSignature ("java/lang/Short"); + return true; + } + if (type == typeof (Int32?)) { + signature = new JniTypeSignature ("java/lang/Integer"); + return true; + } + if (type == typeof (Int64?)) { + signature = new JniTypeSignature ("java/lang/Long"); + return true; + } + if (type == typeof (Single?)) { + signature = new JniTypeSignature ("java/lang/Float"); + return true; + } + if (type == typeof (Double?)) { + signature = new JniTypeSignature ("java/lang/Double"); + return true; + } + + signature = default; + return false; + } + + static bool TryGetBuiltInTypeForSimpleReference ( + string jniSimpleReference, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [NotNullWhen (true)] out Type? type) + { + type = jniSimpleReference switch { + "java/lang/String" => typeof (string), + "V" => typeof (void), + "Z" => typeof (Boolean), + "java/lang/Boolean" => typeof (Boolean?), + "B" => typeof (SByte), + "java/lang/Byte" => typeof (SByte?), + "C" => typeof (Char), + "java/lang/Character" => typeof (Char?), + "S" => typeof (Int16), + "java/lang/Short" => typeof (Int16?), + "I" => typeof (Int32), + "java/lang/Integer" => typeof (Int32?), + "J" => typeof (Int64), + "java/lang/Long" => typeof (Int64?), + "F" => typeof (Single), + "java/lang/Float" => typeof (Single?), + "D" => typeof (Double), + "java/lang/Double" => typeof (Double?), + _ => null, + }; + return type != null; + } + + static Type MakeArrayType (Type elementType) + { +#pragma warning disable IL3050 // Trimmable typemap emits concrete array types; fallback arrays are runtime intrinsic. + return elementType.MakeArrayType (); +#pragma warning restore IL3050 + } + + static bool IsKeywordSimpleReference (string simpleReference) + { + return simpleReference is "V" or "Z" or "B" or "C" or "S" or "I" or "J" or "F" or "D"; + } } From b77c841fc0c4df0e490953ec4f337902d148aabf Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 00:18:14 +0200 Subject: [PATCH 05/47] Propagate trimmable typemap DAM annotations Remove newly added UnconditionalSuppressMessage attributes, propagate Requires annotations from reflection-backed managers, and carry DAM annotations through JavaPeerProxy/TrimmableTypeMap target type metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/AndroidRuntime.cs | 8 +++--- .../Android.Runtime/JNIEnvInit.cs | 6 +++++ .../Java.Interop/JavaPeerProxy.cs | 16 +++++++++++- .../JavaMarshalValueManager.cs | 6 +++-- .../ManagedTypeManager.cs | 4 +-- .../SimpleValueManager.cs | 5 ++-- .../TrimmableTypeMap.cs | 26 ++++++++++++++++--- .../TrimmableTypeMapTypeManager.cs | 16 ++++++++---- 8 files changed, 67 insertions(+), 20 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 6c258ec2350..ffab6884c33 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -310,6 +310,8 @@ public override void DeleteWeakGlobalReference (ref JniObjectReference value) } } + [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] + [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { bool jniAddNativeMethodRegistrationAttributePresent; @@ -317,8 +319,6 @@ class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "AndroidTypeManager is selected for the Mono/legacy reflection-backed typemap path.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "AndroidTypeManager is selected for the Mono/legacy reflection-backed typemap path.")] public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) { this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; @@ -625,12 +625,12 @@ static void SplitMethodLine ( } } + [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.")] class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "AndroidValueManager is selected for the Mono runtime path.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "AndroidValueManager is selected for the Mono runtime path.")] public AndroidValueManager () { } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 85aae8e3546..1e65a9aefbb 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -122,6 +122,8 @@ internal static void InitializeNativeAotRuntime (JniRuntime runtime, JnienvIniti } // Only used for MonoVM and CoreCLR. NativeAOT uses InitializeNativeAotRuntime(). + [RequiresDynamicCode ("MonoVM and non-trimmable CoreCLR startup can use reflection-backed type and value managers.")] + [RequiresUnreferencedCode ("MonoVM and non-trimmable CoreCLR startup can use reflection-backed type and value managers.")] [UnmanagedCallersOnly] internal static unsafe void Initialize (JnienvInitializeArgs* args) { @@ -168,6 +170,8 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] private static unsafe partial void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); + [RequiresDynamicCode ("Creates reflection-backed type managers when trimmable typemap is disabled.")] + [RequiresUnreferencedCode ("Creates reflection-backed type managers when trimmable typemap is disabled.")] internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArgs args) { if (RuntimeFeature.TrimmableTypeMap) { @@ -181,6 +185,8 @@ internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArg return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); } + [RequiresDynamicCode ("Creates reflection-backed value managers when trimmable typemap is disabled.")] + [RequiresUnreferencedCode ("Creates reflection-backed value managers when trimmable typemap is disabled.")] internal static JniRuntime.JniValueManager CreateValueManager () { if (RuntimeFeature.IsMonoRuntime) { diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index fec5337d347..4daa17c5af8 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -42,8 +42,16 @@ public sealed class JavaPeerAliasesAttribute : Attribute [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] public abstract class JavaPeerProxy : Attribute { + const DynamicallyAccessedMemberTypes MethodsConstructors = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors; + protected JavaPeerProxy ( string jniName, + [DynamicallyAccessedMembers (MethodsConstructors)] Type targetType, [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type? invokerType) @@ -70,6 +78,7 @@ protected JavaPeerProxy ( /// /// Gets the target .NET type that this proxy represents. /// + [DynamicallyAccessedMembers (MethodsConstructors)] public Type TargetType { get; } /// @@ -143,7 +152,12 @@ static bool IsActivationPeer (IJavaPeerable peer) [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] public abstract class JavaPeerProxy< // TODO (https://github.com/dotnet/android/issues/10794): Remove this DAM annotation - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [DynamicallyAccessedMembers ( + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors)] T > : JavaPeerProxy where T : class, IJavaPeerable { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 9e834b84729..c16a5cb317d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -478,14 +478,14 @@ void ProcessContext (HandleContext* context) } +[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 JavaMarshalReflectionValueManagerBase : JniRuntime.ReflectionJniValueManager { protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; readonly JavaMarshalPeerManager peerManager; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "This base is shared by Android runtime-specific value managers; runtime feature switches select only compatible managers.")] protected JavaMarshalReflectionValueManagerBase () { peerManager = new JavaMarshalPeerManager (GetType ().Name); @@ -558,6 +558,8 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t } } +[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.")] class CoreClrJavaMarshalValueManager : JavaMarshalReflectionValueManagerBase { const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 9445e88434e..6c98c513c1c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -7,14 +7,14 @@ namespace Microsoft.Android.Runtime; +[RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] +[RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] class ManagedTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "ManagedTypeManager is selected for managed typemap paths that use reflection-backed Java.Interop behavior.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "ManagedTypeManager is selected for managed typemap paths that use reflection-backed Java.Interop behavior.")] public ManagedTypeManager () { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs index 40be32e9b66..1fa4f509d5c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs @@ -16,14 +16,14 @@ 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.")] class SimpleValueManager : JniRuntime.ReflectionJniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; Dictionary>? RegisteredInstances = new Dictionary>(); - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "SimpleValueManager is a reflection-backed test/runtime helper and is not used by NativeAOT trimmable startup.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "SimpleValueManager is a reflection-backed test/runtime helper and is not used by NativeAOT trimmable startup.")] internal SimpleValueManager () { } @@ -229,7 +229,6 @@ public override List GetSurfacedPeers () static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "SimpleValueManager is reflection-backed and requires preserved peer constructors.")] protected override void ConstructPeerCore ( IJavaPeerable self, ref JniObjectReference reference, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 2b6e4f305db..fd47840991f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -19,6 +19,13 @@ namespace Microsoft.Android.Runtime; /// public class TrimmableTypeMap { + internal const DynamicallyAccessedMemberTypes MethodsConstructors = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors; + static readonly Lock s_initLock = new (); static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; @@ -156,7 +163,7 @@ internal static unsafe void RegisterNativeMethods () /// single-element array. For alias groups, returns the surviving target types from /// each alias key. Returns false when no mapping exists or all aliases were trimmed. /// - internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[]? types) + internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out TargetTypeInfo[]? types) { var proxies = GetProxiesForJniName (jniName); if (proxies.Length == 0) { @@ -164,13 +171,26 @@ internal bool TryGetTargetTypes (string jniName, [NotNullWhen (true)] out Type[] return false; } - types = new Type [proxies.Length]; + types = new TargetTypeInfo [proxies.Length]; for (int i = 0; i < proxies.Length; i++) { - types [i] = proxies [i].TargetType; + types [i] = new TargetTypeInfo (proxies [i].TargetType); } return true; } + internal sealed class TargetTypeInfo + { + public TargetTypeInfo ( + [DynamicallyAccessedMembers (MethodsConstructors)] + Type type) + { + Type = type; + } + + [DynamicallyAccessedMembers (MethodsConstructors)] + public Type Type { get; } + } + /// /// Resolves and caches all proxies for a JNI name. For non-alias entries, returns a /// single-element array. For alias groups, resolves each alias key and returns the diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 92fcc57da0c..555f12568d0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -69,8 +69,8 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl } if (TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types)) { - foreach (var type in types) { - yield return type; + foreach (var typeInfo in types) { + yield return typeInfo.Type; } } } @@ -125,7 +125,6 @@ protected override IEnumerable GetSimpleReferences (Type type) } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - [UnconditionalSuppressMessage ("Trimming", "IL2063", Justification = "Trimmable typemap target types are generated from preserved Java peer metadata.")] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { if (TryGetBuiltInTypeForSimpleReference (jniSimpleReference, out var type)) { @@ -133,7 +132,7 @@ protected override IEnumerable GetSimpleReferences (Type type) } return TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types) && types.Length > 0 - ? types [0] + ? types [0].Type : null; } @@ -173,7 +172,14 @@ IEnumerable CreateGetTypesEnumerator (JniTypeSignature typeSignature) public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) { - yield break; + if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) + yield break; + + if (TrimmableTypeMap.Instance.TryGetTargetTypes (typeSignature.SimpleReference, out var types)) { + foreach (var typeInfo in types) { + yield return new ReflectionConstructibleType (typeInfo.Type); + } + } } [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] From 0554d57cb13c2226858bab57b9f7243d93a8ce40 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 00:31:22 +0200 Subject: [PATCH 06/47] Prefer Requires annotations for reflection managers Replace newly added suppressions on reflection-backed managers with RequiresUnreferencedCode and RequiresDynamicCode propagation. Leave trimmable value/type managers free of UnconditionalSuppressMessage attributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/AndroidRuntime.cs | 4 ++ .../JavaMarshalValueManager.cs | 62 +++++++++---------- .../ManagedTypeManager.cs | 2 + .../SimpleValueManager.cs | 2 + 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index ffab6884c33..51f80cd8fdc 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -319,6 +319,8 @@ class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] + [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) { this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; @@ -631,6 +633,8 @@ class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); + [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.")] public AndroidValueManager () { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index c16a5cb317d..7c2d6b59d10 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -478,6 +478,26 @@ void ProcessContext (HandleContext* context) } +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; + } +} + [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 JavaMarshalReflectionValueManagerBase : JniRuntime.ReflectionJniValueManager @@ -486,6 +506,8 @@ abstract class JavaMarshalReflectionValueManagerBase : JniRuntime.ReflectionJniV readonly JavaMarshalPeerManager peerManager; + [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.")] protected JavaMarshalReflectionValueManagerBase () { peerManager = new JavaMarshalPeerManager (GetType ().Name); @@ -532,21 +554,6 @@ public override List GetSurfacedPeers () return peerManager.GetSurfacedPeers (); } - [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; - } - protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) { var proxy = value as JavaProxyThrowable; @@ -568,6 +575,12 @@ class CoreClrJavaMarshalValueManager : JavaMarshalReflectionValueManagerBase static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; + [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.")] + public CoreClrJavaMarshalValueManager () + { + } + public override IJavaPeerable? CreatePeer ( ref JniObjectReference reference, JniObjectReferenceOptions transfer, @@ -580,7 +593,7 @@ class CoreClrJavaMarshalValueManager : JavaMarshalReflectionValueManagerBase return null; } - targetType = ResolvePeerType (targetType) ?? typeof (global::Java.Interop.JavaObject); + 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)); @@ -847,7 +860,7 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe // 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 resolvedTargetType = JavaMarshalValueManagerHelper.ResolvePeerType (targetType); var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreateInstance (reference.Handle, resolvedTargetType); @@ -882,21 +895,6 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe } } - [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; - } - protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (true)] out object? result) { var proxy = value as JavaProxyThrowable; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 6c98c513c1c..c0cc35364da 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -15,6 +15,8 @@ class ManagedTypeManager : JniRuntime.ReflectionJniTypeManager { internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] + [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] public ManagedTypeManager () { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs index 1fa4f509d5c..d080be28337 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs @@ -24,6 +24,8 @@ class SimpleValueManager : JniRuntime.ReflectionJniValueManager Dictionary>? RegisteredInstances = new Dictionary>(); + [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.")] internal SimpleValueManager () { } From 7d81397bb3994fcee68e375b553e7f2ea20efeee Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 00:38:39 +0200 Subject: [PATCH 07/47] Remove manager suppression attributes Replace remaining UnconditionalSuppressMessage attributes in the reflection-backed Android manager implementations with RequiresUnreferencedCode/RequiresDynamicCode where appropriate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 7 ++----- .../Microsoft.Android.Runtime/ManagedTypeManager.cs | 13 +++---------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 51f80cd8fdc..1a87f2726dc 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -389,8 +389,7 @@ protected override IEnumerable GetSimpleReferences (Type type) static MethodInfo? dynamic_callback_gen; // See ExportAttribute.cs - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] + [RequiresUnreferencedCode ("Export callback registration uses reflection over Mono.Android.Export.dll.")] static Delegate CreateDynamicCallback (MethodInfo method) { if (dynamic_callback_gen == null) { @@ -492,9 +491,7 @@ public override void RegisterNativeMembers ( 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", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] + [RequiresUnreferencedCode ("Native member registration resolves callback types and delegates from generated method metadata.")] public override void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index c0cc35364da..269728efa7a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -22,6 +22,8 @@ public ManagedTypeManager () } [return: DynamicallyAccessedMembers (Constructors)] + [RequiresDynamicCode ("This invoker lookup can construct generic invoker types.")] + [RequiresUnreferencedCode ("This invoker lookup uses reflection over preserved Java peer types.")] protected override Type? GetInvokerTypeCore ( [DynamicallyAccessedMembers (Constructors)] Type type) @@ -29,16 +31,10 @@ public ManagedTypeManager () 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)] @@ -63,10 +59,7 @@ static Type MakeGenericType ( return MakeGenericType (suffixDefinition, arguments); } - // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` - [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", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] + [RequiresUnreferencedCode ("Native member registration resolves callback types and delegates from generated method metadata.")] public override void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] From 1fe2ef57134c9b2ab955715ce9ee38b815894bee Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 00:50:41 +0200 Subject: [PATCH 08/47] Remove invoker lookup suppression attributes Replace invoker lookup and legacy TypeManager peer creation suppressions with RequiresUnreferencedCode/RequiresDynamicCode propagation. Keep GetObject suppression because adding DAM there breaks delegate/reflection table use sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/JavaObjectExtensions.cs | 9 ++------- src/Mono.Android/Java.Interop/TypeManager.cs | 6 ++++-- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs index a3e817facb9..66787331d37 100644 --- a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs +++ b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs @@ -108,21 +108,16 @@ internal static TResult? _JavaCast< // typeof(Foo) -> FooInvoker // typeof(Foo<>) -> FooInvoker`1 [return: DynamicallyAccessedMembers (Constructors)] + [RequiresDynamicCode ("Invoker lookup can construct generic invoker types.")] + [RequiresUnreferencedCode ("Invoker lookup uses reflection over preserved Java peer types.")] internal static Type? GetInvokerType (Type type) { - const string InvokerTypes = "*Invoker types are preserved by the MarkJavaObjects linker step."; - - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = InvokerTypes)] - [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = InvokerTypes)] - [UnconditionalSuppressMessage ("Trimming", "IL2073", Justification = InvokerTypes)] [return: DynamicallyAccessedMembers (Constructors)] static Type? AssemblyGetType (Assembly assembly, string typeName) => assembly.GetType (typeName); // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 // IL3050 disabled in source: if someone uses NativeAOT, they will get the warning. - [UnconditionalSuppressMessage ("Trimming", "IL2055", Justification = InvokerTypes)] - [UnconditionalSuppressMessage ("Trimming", "IL2068", Justification = InvokerTypes)] [return: DynamicallyAccessedMembers (Constructors)] static Type MakeGenericType (Type type, params Type [] typeArguments) => #pragma warning disable IL3050 diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index cc0b22936bd..cdcf1fb769c 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -292,13 +292,15 @@ static Type monovm_typemap_java_to_managed (string java_type_name) return null; } + [RequiresDynamicCode ("Legacy type manager peer creation can construct generic invoker types.")] + [RequiresUnreferencedCode ("Legacy type manager peer creation uses reflection over preserved Java peer types.")] internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) { return CreateInstance (handle, transfer, null); } - [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "TypeManager.CreateProxy() does not statically know the value of the 'type' local variable.")] - [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "TypeManager.CreateProxy() does not statically know the value of the 'type' local variable.")] + [RequiresDynamicCode ("Legacy type manager peer creation can construct generic invoker types.")] + [RequiresUnreferencedCode ("Legacy type manager peer creation uses reflection over preserved Java peer types.")] internal static IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer, Type? targetType) { Type? type = null; From 72289828bc5751abe88913cb74418a5926c4199a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 01:01:18 +0200 Subject: [PATCH 09/47] Use feature guards for runtime manager selection Annotate runtime feature switches with FeatureGuard and structure manager factory branches so reflection-backed manager creation is guarded by the relevant runtime feature instead of broad Requires annotations on the factory methods. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 22 ++++++++++++------- .../RuntimeFeature.cs | 6 +++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 1e65a9aefbb..79eec33d7ce 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -122,8 +122,6 @@ internal static void InitializeNativeAotRuntime (JniRuntime runtime, JnienvIniti } // Only used for MonoVM and CoreCLR. NativeAOT uses InitializeNativeAotRuntime(). - [RequiresDynamicCode ("MonoVM and non-trimmable CoreCLR startup can use reflection-backed type and value managers.")] - [RequiresUnreferencedCode ("MonoVM and non-trimmable CoreCLR startup can use reflection-backed type and value managers.")] [UnmanagedCallersOnly] internal static unsafe void Initialize (JnienvInitializeArgs* args) { @@ -170,23 +168,31 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] private static unsafe partial void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); - [RequiresDynamicCode ("Creates reflection-backed type managers when trimmable typemap is disabled.")] - [RequiresUnreferencedCode ("Creates reflection-backed type managers when trimmable typemap is disabled.")] internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArgs args) { if (RuntimeFeature.TrimmableTypeMap) { return new TrimmableTypeMapTypeManager (); } - if (RuntimeFeature.IsNativeAotRuntime || RuntimeFeature.ManagedTypeMap) { + if (RuntimeFeature.IsNativeAotRuntime) { + throw new NotSupportedException ($"{nameof (RuntimeFeature.IsNativeAotRuntime)} requires {nameof (RuntimeFeature.TrimmableTypeMap)}."); + } + + if (RuntimeFeature.ManagedTypeMap) { return new ManagedTypeManager (); } - return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); + if (RuntimeFeature.IsMonoRuntime) { + return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); + } + + if (RuntimeFeature.IsCoreClrRuntime) { + return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); + } + + throw new NotSupportedException ("Internal error: unknown runtime not supported"); } - [RequiresDynamicCode ("Creates reflection-backed value managers when trimmable typemap is disabled.")] - [RequiresUnreferencedCode ("Creates reflection-backed value managers when trimmable typemap is disabled.")] internal static JniRuntime.JniValueManager CreateValueManager () { if (RuntimeFeature.IsMonoRuntime) { 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; From 326f0fd509953cab6e613d7592e7d4aca87fc532 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 01:06:55 +0200 Subject: [PATCH 10/47] Inline CoreCLR JavaMarshal peer delegation Remove the single-use JavaMarshalReflectionValueManagerBase and keep the shared peer/GC bridge state in JavaMarshalPeerManager, directly delegated by the CoreCLR value manager. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 48 +++++++------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 7c2d6b59d10..6ecfa4c75fa 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -500,15 +500,20 @@ static class JavaMarshalValueManagerHelper [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 JavaMarshalReflectionValueManagerBase : JniRuntime.ReflectionJniValueManager +class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManager { - protected const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType (); + static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; + static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; readonly JavaMarshalPeerManager peerManager; [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.")] - protected JavaMarshalReflectionValueManagerBase () + public CoreClrJavaMarshalValueManager () { peerManager = new JavaMarshalPeerManager (GetType ().Name); } @@ -554,33 +559,6 @@ public override List GetSurfacedPeers () return peerManager.GetSurfacedPeers (); } - 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); - } -} - -[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.")] -class CoreClrJavaMarshalValueManager : JavaMarshalReflectionValueManagerBase -{ - const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType (); - static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; - static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - - [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.")] - public CoreClrJavaMarshalValueManager () - { - } - public override IJavaPeerable? CreatePeer ( ref JniObjectReference reference, JniObjectReferenceOptions transfer, @@ -734,6 +712,16 @@ bool TryConstructPeer ( 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); + } } class TrimmableTypeMapValueManager : JniRuntime.JniValueManager From 6a8ec68ccfe77f29046bef29249e970d7f163508 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 01:12:01 +0200 Subject: [PATCH 11/47] Throw unreachable from trimmable native registration Make both TrimmableTypeMapTypeManager RegisterNativeMembers overloads throw UnreachableException directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 555f12568d0..b9fae66a22b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -223,7 +223,9 @@ public override void RegisterNativeMembers ( Type type, string? methods) { - RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); + throw new UnreachableException ( + $"RegisterNativeMembers should not be called in the trimmable typemap path. " + + $"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks."); } static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signature) From 7d3d1e384726bfa5aa8678f81fcbff5094653919 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 09:25:11 +0200 Subject: [PATCH 12/47] Remove SimpleValueManager --- .../SimpleValueManager.cs | 262 ------------------ src/Mono.Android/Mono.Android.csproj | 1 - 2 files changed, 263 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs 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 d080be28337..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/SimpleValueManager.cs +++ /dev/null @@ -1,262 +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; - -[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.")] -class SimpleValueManager : JniRuntime.ReflectionJniValueManager -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - - Dictionary>? RegisteredInstances = new Dictionary>(); - - [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.")] - 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 void ConstructPeerCore ( - IJavaPeerable self, - ref JniObjectReference reference, - JniObjectReferenceOptions options) - { - Type type = self.GetType (); - 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; - } - base.ConstructPeerCore (self, ref reference, options); - } - - 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 611ef7edf07..b1ffc25501b 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -367,7 +367,6 @@ - From 725d4332f533c2db72969d4eb6426c5d91737692 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 09:25:35 +0200 Subject: [PATCH 13/47] Simplify changes to the ManagedTypeManager --- .../ManagedTypeManager.cs | 45 ++++--------------- 1 file changed, 9 insertions(+), 36 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 269728efa7a..2e9dc1187bc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -15,56 +15,30 @@ class ManagedTypeManager : JniRuntime.ReflectionJniTypeManager { internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] - [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] - public ManagedTypeManager () - { - } - [return: DynamicallyAccessedMembers (Constructors)] - [RequiresDynamicCode ("This invoker lookup can construct generic invoker types.")] - [RequiresUnreferencedCode ("This invoker lookup uses reflection over preserved Java peer types.")] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (Constructors)] - Type type) + protected override Type? GetInvokerTypeCore ([DynamicallyAccessedMembers (Constructors)] Type type) { const string suffix = "Invoker"; - // https://github.com/xamarin/xamarin-android/blob/5472eec991cc075e4b0c09cd98a2331fb93aa0f3/src/Microsoft.Android.Sdk.ILLink/MarkJavaObjects.cs#L176-L186 - [return: DynamicallyAccessedMembers (Constructors)] - static Type? AssemblyGetType (Assembly assembly, string typeName) => - assembly.GetType (typeName); - - [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, - definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt)); + string suffixDefinitionName = definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt); + Type? suffixDefinition = definition.Assembly.GetType (suffixDefinitionName); if (suffixDefinition == null) return base.GetInvokerTypeCore (type); - return MakeGenericType (suffixDefinition, arguments); + return suffixDefinition.MakeGenericType (arguments); } - [RequiresUnreferencedCode ("Native member registration resolves callback types and delegates from generated method metadata.")] public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - ReadOnlySpan methods) + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + ReadOnlySpan methods) { if (methods.IsEmpty) { base.RegisterNativeMembers (nativeClass, type, methods); @@ -125,7 +99,6 @@ public override void RegisterNativeMembers ( } } - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { // Base class contains built-in mappings (e.g. java/lang/String → System.String) From caaea85a10bcc6f4bb037945add0c1559032b836 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 09:30:00 +0200 Subject: [PATCH 14/47] Remove the option to use ManagedTypeManager --- .../Java.Interop/JreRuntime.cs | 15 +++----------- .../Android.Runtime/JNIEnvInit.cs | 20 ++++++++----------- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index b9daebabec8..f9fcb52b08a 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -57,10 +57,10 @@ static NativeAotRuntimeOptions CreateJreVM (NativeAotRuntimeOptions builder) string.IsNullOrEmpty (builder.JvmLibraryPath)) throw new InvalidOperationException ($"Member `{nameof (NativeAotRuntimeOptions)}.{nameof (NativeAotRuntimeOptions.JvmLibraryPath)}` must be set."); -#if NET - builder.TypeManager ??= CreateDefaultTypeManager (); -#endif // NET + if (!RuntimeFeature.TrimmableTypeMap) + throw new NotSupportedException ($"Native AOT builds require using {nameof (RuntimeFeature.TrimmableTypeMap)}."); + builder.TypeManager ??= new TrimmableTypeMapTypeManager (); builder.ValueManager ??= new TrimmableTypeMapValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); @@ -75,15 +75,6 @@ internal protected JreRuntime (NativeAotRuntimeOptions builder) { } - static JniRuntime.JniTypeManager CreateDefaultTypeManager () - { - if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableTypeMapTypeManager (); - } - - return new ManagedTypeManager (); - } - public override string? GetCurrentManagedThreadName () { return Thread.CurrentThread.Name; diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 79eec33d7ce..e2b4ca71d1e 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -178,10 +178,6 @@ internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArg throw new NotSupportedException ($"{nameof (RuntimeFeature.IsNativeAotRuntime)} requires {nameof (RuntimeFeature.TrimmableTypeMap)}."); } - if (RuntimeFeature.ManagedTypeMap) { - return new ManagedTypeManager (); - } - if (RuntimeFeature.IsMonoRuntime) { return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); } @@ -195,22 +191,22 @@ internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArg internal static JniRuntime.JniValueManager CreateValueManager () { + if (RuntimeFeature.TrimmableTypeMap) { + return new TrimmableTypeMapValueManager (); + } + + if (RuntimeFeature.IsNativeAotRuntime) { + throw new NotSupportedException ($"Native AOT builds require using {nameof (RuntimeFeature.TrimmableTypeMap)}."); + } + if (RuntimeFeature.IsMonoRuntime) { return new AndroidValueManager (); } if (RuntimeFeature.IsCoreClrRuntime) { - if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableTypeMapValueManager (); - } - return new CoreClrJavaMarshalValueManager (); } - if (RuntimeFeature.IsNativeAotRuntime) { - return new TrimmableTypeMapValueManager (); - } - throw new NotSupportedException ("Internal error: unknown runtime not supported"); } From b489d1ff82de5d5d7cf219e166d7a4ba21cc425a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 10:01:45 +0200 Subject: [PATCH 15/47] Cleanup value managers --- .../JavaMarshalValueManager.cs | 220 ++++++++---------- 1 file changed, 98 insertions(+), 122 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 6ecfa4c75fa..831141bdf41 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -18,7 +18,7 @@ namespace Microsoft.Android.Runtime; -class JavaMarshalPeerManager : IDisposable +sealed class JavaMarshalPeerManager : IDisposable { readonly Dictionary> RegisteredInstances = new (); readonly ConcurrentQueue CollectedContexts = new (); @@ -496,27 +496,58 @@ static class JavaMarshalValueManagerHelper } 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)) { + // TODO revisit this logging + 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; + } } [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.")] -class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManager +sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const BindingFlags ActivationConstructorBindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - static readonly Type ByRefJniObjectReference = typeof (JniObjectReference).MakeByRefType (); - static readonly Type[] JIConstructorSignature = new Type [] { ByRefJniObjectReference, typeof (JniObjectReferenceOptions) }; - static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - - readonly JavaMarshalPeerManager peerManager; + static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; + static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; - [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.")] - public CoreClrJavaMarshalValueManager () - { - peerManager = new JavaMarshalPeerManager (GetType ().Name); - } + readonly JavaMarshalPeerManager peerManager = new (nameof (CoreClrJavaMarshalValueManager)); protected override void Dispose (bool disposing) { @@ -582,40 +613,30 @@ public override List GetSurfacedPeers () 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); + if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetSig.SimpleReference, ref reference, targetType)) { 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)); + 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); } - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - return peer; } IJavaPeerable? CreatePeerInstance ( - ref JniObjectReference klass, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - ref JniObjectReference reference, - JniObjectReferenceOptions transfer) + ref JniObjectReference klass, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + ref JniObjectReference reference, + JniObjectReferenceOptions transfer) { var jniTypeName = JniEnvironment.Types.GetJniTypeNameFromClass (klass); @@ -724,17 +745,12 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t } } -class TrimmableTypeMapValueManager : JniRuntime.JniValueManager +sealed class TrimmableTypeMapValueManager : JniRuntime.JniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); - readonly JavaMarshalPeerManager peerManager; - - public TrimmableTypeMapValueManager () - { - peerManager = new JavaMarshalPeerManager (GetType ().Name); - } + readonly JavaMarshalPeerManager peerManager = new (nameof (TrimmableTypeMapValueManager)); protected override void Dispose (bool disposing) { @@ -779,10 +795,13 @@ public override List GetSurfacedPeers () public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) { - throw new PlatformNotSupportedException ("Activating Java peers is not supported when TrimmableTypeMap is enabled."); + throw new PlatformNotSupportedException ("Activating Java peers through the value manager is not supported when TrimmableTypeMap is enabled."); } - protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectReference reference, JniObjectReferenceOptions options) + protected override void ConstructPeerCore ( + IJavaPeerable peer, + ref JniObjectReference reference, + JniObjectReferenceOptions options) { if (peer == null) throw new ArgumentNullException (nameof (peer)); @@ -791,7 +810,6 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe if (newRef.IsValid) { JniObjectReference.Dispose (ref reference, options); - // Activation? See ManagedPeer.Construct, CreatePeer // Instance was already added, don't add again if (peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable)) { return; @@ -833,10 +851,10 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe } public override IJavaPeerable? CreatePeer ( - ref JniObjectReference reference, - JniObjectReferenceOptions transfer, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) { EnsureNotDisposed (); @@ -866,9 +884,16 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe // 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; + if (targetType is not null && resolvedTargetType is not null) { + if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { + throw new ArgumentException ( + $"Could not determine Java type corresponding to '{targetType.AssemblyQualifiedName}'.", + nameof (targetType)); + } + + if (JavaMarshalValueManagerHelper.IsIncompatibleCast (targetJniName, ref reference, resolvedTargetType)) { + return null; + } } var targetName = resolvedTargetType?.AssemblyQualifiedName ?? ""; @@ -883,50 +908,40 @@ protected override void ConstructPeerCore (IJavaPeerable peer, ref JniObjectRefe } } - 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); - } - [return: MaybeNull] protected override T CreateValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) { throw CreateValueMarshalingNotSupportedException (); } protected override object? CreateValueCore ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) { throw CreateValueMarshalingNotSupportedException (); } [return: MaybeNull] protected override T GetValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) { throw CreateValueMarshalingNotSupportedException (); } protected override object? GetValueCore ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) { throw CreateValueMarshalingNotSupportedException (); } @@ -945,43 +960,4 @@ static NotSupportedException CreateValueMarshalingNotSupportedException () { return new NotSupportedException ($"{nameof (TrimmableTypeMapValueManager)} does not support value marshaling yet."); } - - /// - /// 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; - } } From f1ce2a2932bc1fa090816f3c5ee5c34c3c7e1d0d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 10 Jun 2026 16:37:42 +0200 Subject: [PATCH 16/47] Refine trimmable typemap runtime paths Make unused trimmable JniTypeManager paths fail loudly, remove ManagedPeer from trimmable runtime artifacts, and add an initial AOT-safe value-marshaling implementation for the trimmable value manager. Update tests and trimmable runtime coverage to use feature switches via AppContext and enable the value-marshaling test bucket for follow-up triage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Scanner/JavaPeerScanner.cs | 14 +- .../JavaMarshalValueManager.cs | 304 +++++++++++- .../TrimmableTypeMapTypeManager.cs | 459 +++++++----------- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 2 + .../TrimmableTypeMapBuildTests.cs | 66 ++- src/java-runtime/java-runtime.targets | 2 +- .../Scanner/JavaPeerScannerTests.cs | 9 +- .../Java.Interop-Tests.NET.csproj | 7 - .../Android.Views/LayoutInflaterTest.cs | 4 +- .../ConstructorActivationTests.cs | 8 +- .../Java.Interop/ExportTests.cs | 5 +- .../TrimmableTypeMapRuntimeCoverageTests.cs | 5 +- .../TrimmableTypeMapTypeManagerTests.cs | 5 +- .../System/StartupHookTest.cs | 3 +- .../TrustManagerMarshallingTests.cs | 5 +- 16 files changed, 564 insertions(+), 336 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index d7dbad5e30a..3931347aa98 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit d7dbad5e30a8f03743a508a95c4e9159fe1f6607 +Subproject commit 3931347aa983c5f35b8b03da4e25212cdc910948 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08b7b6c66bc..d03d8dcc853 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -208,18 +208,8 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A continue; } - // [JniAddNativeMethodRegistrationAttribute] is not supported by the trimmable typemap - // by design (see XA4251). Detect the attribute *before* any per-type filters below - // (array type, no JNI name, etc.) so the diagnostic fires uniformly regardless of - // whether the type would otherwise have ended up in the typemap. - // - // Skip the per-method walk entirely for the overwhelmingly common case where - // the assembly doesn't even reference the attribute type — the per-assembly - // flag was computed cheaply in AssemblyIndex.Build. - if (index.MayUseJniAddNativeMethodRegistrationAttribute && - HasJniAddNativeMethodRegistrationAttribute (typeDef, index)) { - logger?.LogJniAddNativeMethodRegistrationAttributeError (MetadataTypeNameResolver.GetFullName (typeDef, index.Reader)); - } + // Ignore [JniAddNativeMethodRegistrationAttribute] for now while the + // trimmable type map runtime path is being refactored. // Determine the JNI name and whether this is a known Java peer. // Priority: diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 831141bdf41..5cdb10e6d21 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -195,7 +195,7 @@ public void RemovePeer (IJavaPeerable value) for (int i = peers.Count - 1; i >= 0; i--) { ReferenceTrackingHandle peer = peers [i]; - IJavaPeerable target = peer.Target; + IJavaPeerable? target = peer.Target; if (ReferenceEquals (value, target)) { peers.RemoveAt (i); peer.Dispose (); @@ -264,7 +264,7 @@ public List GetSurfacedPeers () unsafe struct ReferenceTrackingHandle : IDisposable { - WeakReference _weakReference; + WeakReference _weakReference; HandleContext* _context; public bool BelongsToContext (HandleContext* context) @@ -273,7 +273,7 @@ public bool BelongsToContext (HandleContext* context) public ReferenceTrackingHandle (IJavaPeerable peer) { _context = HandleContext.Alloc (peer); - _weakReference = new WeakReference (peer); + _weakReference = new (peer); } public IJavaPeerable? Target @@ -915,7 +915,7 @@ protected override void ConstructPeerCore ( [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) { - throw CreateValueMarshalingNotSupportedException (); + return GetValueCore (ref reference, options, targetType); } protected override object? CreateValueCore ( @@ -924,7 +924,7 @@ protected override void ConstructPeerCore ( [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) { - throw CreateValueMarshalingNotSupportedException (); + return GetValueCore (ref reference, options, targetType); } [return: MaybeNull] @@ -934,7 +934,29 @@ protected override void ConstructPeerCore ( [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) { - throw CreateValueMarshalingNotSupportedException (); + EnsureNotDisposed (); + if (!reference.IsValid) { +#pragma warning disable 8653 + return default (T); +#pragma warning restore 8653 + } + + if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) { + throw new ArgumentException ( + string.Format (CultureInfo.InvariantCulture, "Requested runtime '{0}' value of '{1}' is not compatible with requested compile-time type T of '{2}'.", + nameof (targetType), + targetType, + typeof (T)), + nameof (targetType)); + } + + var value = GetValueCore (ref reference, options, targetType ?? typeof (T)); + if (value is null) { +#pragma warning disable 8653 + return default (T); +#pragma warning restore 8653 + } + return (T) value; } protected override object? GetValueCore ( @@ -943,21 +965,281 @@ protected override void ConstructPeerCore ( [DynamicallyAccessedMembers (Constructors)] Type? targetType = null) { - throw CreateValueMarshalingNotSupportedException (); + EnsureNotDisposed (); + if (!reference.IsValid) { + return null; + } + + var existing = PeekValue (reference); + if (existing != null && (targetType == null || targetType.IsAssignableFrom (existing.GetType ()))) { + JniObjectReference.Dispose (ref reference, options); + return existing; + } + + if (targetType != null && typeof (IJavaPeerable).IsAssignableFrom (targetType)) { + return CreatePeer (ref reference, options, targetType); + } + + var transfer = ToJniHandleOwnership (reference, options); + var value = JavaConvert.FromJniHandle (reference.Handle, transfer, targetType); + if (transfer != JniHandleOwnership.DoNotTransfer) { + reference = default; + } + return value; } protected override JniValueMarshaler GetValueMarshalerCore (Type type) { - throw CreateValueMarshalingNotSupportedException (); + EnsureNotDisposed (); + if (type == null) { + throw new ArgumentNullException (nameof (type)); + } + if (type.ContainsGenericParameters) { + throw new ArgumentException ("Generic type definitions are not supported.", nameof (type)); + } + + if (type == typeof (bool)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (bool?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (byte)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (sbyte)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (sbyte?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (char)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (char?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (short)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (short?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (int)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (int?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (long)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (long?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (float)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (float?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (double)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (double?)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (string)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (object)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (int[])) + return TrimmableValueMarshaler.Instance; + if (type == typeof (IList)) + return TrimmableValueMarshaler>.Instance; + if (type == typeof (global::Java.Interop.JavaArray)) + return TrimmableValueMarshaler>.Instance; + if (type == typeof (JavaPrimitiveArray)) + return TrimmableValueMarshaler>.Instance; + if (type == typeof (JavaInt32Array)) + return TrimmableValueMarshaler.Instance; + if (type == typeof (IJavaPeerable) || typeof (IJavaPeerable).IsAssignableFrom (type)) + return TrimmablePeerableValueMarshaler.Instance; + + return TrimmableValueMarshaler.Instance; } protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () { - throw CreateValueMarshalingNotSupportedException (); + EnsureNotDisposed (); + if (typeof (T) == typeof (IJavaPeerable)) { + return (JniValueMarshaler)(object) TrimmablePeerableValueMarshaler.Instance; + } + return TrimmableValueMarshaler.Instance; + } + + static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, JniObjectReferenceOptions options) + { + const JniObjectReferenceOptions DisposeSource = (JniObjectReferenceOptions)(1 << 1); + if ((options & DisposeSource) != DisposeSource) { + return JniHandleOwnership.DoNotTransfer; + } + return reference.Type switch { + JniObjectReferenceType.Local => JniHandleOwnership.TransferLocalRef, + JniObjectReferenceType.Global => JniHandleOwnership.TransferGlobalRef, + _ => JniHandleOwnership.DoNotTransfer, + }; + } + + sealed class TrimmablePeerableValueMarshaler : JniValueMarshaler + { + public static readonly TrimmablePeerableValueMarshaler Instance = new (); + + public override IJavaPeerable? CreateGenericValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + return JniEnvironment.Runtime.ValueManager.CreatePeer (ref reference, options, targetType); + } + + public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState ([MaybeNull] IJavaPeerable? value, ParameterAttributes synchronize) + { + if (value == null || !value.PeerReference.IsValid) { + return new JniValueMarshalerState (); + } + return new JniValueMarshalerState (value.PeerReference.NewLocalRef ()); + } + + public override void DestroyGenericArgumentState ([AllowNull] IJavaPeerable? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + DisposeReferenceState (ref state); + } + } + + sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] T> : JniValueMarshaler + { + public static readonly TrimmableValueMarshaler Instance = new (); + + public override bool IsJniValueType => IsPrimitiveJniValueType (typeof (T)); + + public override Type MarshalType => IsJniValueType ? typeof (T) : base.MarshalType; + + [return: MaybeNull] + public override T CreateGenericValue ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + return JniEnvironment.Runtime.ValueManager.GetValue (ref reference, options, targetType); + } + + public override JniValueMarshalerState CreateGenericArgumentState ([MaybeNull] T value, ParameterAttributes synchronize = ParameterAttributes.In) + { + if (IsJniValueType) { + return new JniValueMarshalerState (CreatePrimitiveArgumentValue (value)); + } + return CreateGenericObjectReferenceArgumentState (value, synchronize); + } + + public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState ([MaybeNull] T value, ParameterAttributes synchronize) + { + if (value == null) { + return new JniValueMarshalerState (); + } + if (TryCreateInt32ArrayArgumentState (value, synchronize, out var state)) { + return state; + } + + var handle = JavaConvert.ToLocalJniHandle (value); + return handle == IntPtr.Zero + ? new JniValueMarshalerState () + : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); + } + + public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + if (TryDestroyInt32ArrayArgumentState (value, ref state, synchronize)) { + return; + } + DisposeReferenceState (ref state); + } + + static bool TryCreateInt32ArrayArgumentState ([MaybeNull] T value, ParameterAttributes synchronize, out JniValueMarshalerState state) + { + state = new JniValueMarshalerState (); + + if (value is not IList list) { + return false; + } + + synchronize = GetCopyDirection (synchronize); + var copyToJava = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; + var array = copyToJava + ? new JavaInt32Array (list) + : new JavaInt32Array (list.Count); + state = new JniValueMarshalerState (array); + return true; + } + + static bool TryDestroyInt32ArrayArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + if (state.PeerableValue is not JavaInt32Array array) { + return false; + } + + synchronize = GetCopyDirection (synchronize); + if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList list) { + if (value is int[] targetArray) { + array.CopyTo (targetArray, 0); + } else { + int count = Math.Min (array.Length, list.Count); + for (int i = 0; i < count; i++) { + list [i] = array [i]; + } + } + } + + array.Dispose (); + state = new JniValueMarshalerState (); + return true; + } + + static ParameterAttributes GetCopyDirection (ParameterAttributes value) + { + const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; + if ((value & inout) != 0) { + return value & inout; + } + return inout; + } + + static bool IsPrimitiveJniValueType (Type type) + { + return type == typeof (bool) || + type == typeof (byte) || + type == typeof (sbyte) || + type == typeof (char) || + type == typeof (short) || + type == typeof (ushort) || + type == typeof (int) || + type == typeof (uint) || + type == typeof (long) || + type == typeof (ulong) || + type == typeof (float) || + type == typeof (double); + } + + static JniArgumentValue CreatePrimitiveArgumentValue ([MaybeNull] T value) + { + return value switch { + null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), + bool v => new JniArgumentValue (v), + byte v => new JniArgumentValue (v), + sbyte v => new JniArgumentValue (v), + char v => new JniArgumentValue (v), + short v => new JniArgumentValue (v), + ushort v => new JniArgumentValue (v), + int v => new JniArgumentValue (v), + uint v => new JniArgumentValue (v), + long v => new JniArgumentValue (v), + ulong v => new JniArgumentValue (v), + float v => new JniArgumentValue (v), + double v => new JniArgumentValue (v), + _ => throw new NotSupportedException ($"Type '{typeof (T).AssemblyQualifiedName}' is not a JNI primitive value type."), + }; + } } - static NotSupportedException CreateValueMarshalingNotSupportedException () + static void DisposeReferenceState (ref JniValueMarshalerState state) { - return new NotSupportedException ($"{nameof (TrimmableTypeMapValueManager)} does not support value marshaling yet."); + var r = state.ReferenceValue; + JniObjectReference.Dispose (ref r); + state = new JniValueMarshalerState (); } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index b9fae66a22b..615f077bfa7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -15,120 +15,153 @@ namespace Microsoft.Android.Runtime; /// class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager { - const string NoSimpleReference = "\0"; internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; - internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - readonly ConcurrentDictionary _simpleReferenceCache = new (); + internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + readonly ConcurrentDictionary _typeSignatureCache = new (); protected override JniTypeSignature GetTypeSignatureCore (Type type) { - type = GetUnderlyingType (type, out int rank); + return _typeSignatureCache.GetOrAdd (type, GetTypeSignatureUncached); - if (TryGetBuiltInTypeSignature (type, out var signature)) { - return signature.AddArrayRank (rank); - } - - var simpleReference = GetSimpleReference (type); - return simpleReference == null ? default : new JniTypeSignature (simpleReference, rank, false); - } - - protected override IEnumerable GetTypeSignaturesCore (Type type) - { - type = GetUnderlyingType (type, out int rank); - - if (TryGetBuiltInTypeSignature (type, out var signature)) { - yield return signature.AddArrayRank (rank); - } - - foreach (var simpleReference in GetSimpleReferences (type)) { - yield return new JniTypeSignature (simpleReference, rank, false); - } - } - - static Type GetUnderlyingType (Type type, out int rank) - { - rank = 0; - var originalType = type; - while (type.IsArray) { - if (type.GetArrayRank () > 1) - throw new ArgumentException ("Multidimensional array '" + originalType.FullName + "' is not supported.", nameof (type)); - rank++; - type = type.GetElementType () ?? throw new InvalidOperationException ("Array type has no element type."); - } + static JniTypeSignature GetTypeSignatureUncached (Type type) + { + type = GetUnderlyingType (type, out int rank); - if (type.IsEnum) - type = Enum.GetUnderlyingType (type); + if (TryGetBuiltInTypeSignature (type, out var signature)) { + return signature.AddArrayRank (rank); + } - return type; - } + // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable + // extends Java.Lang.Error but has no [Register] attribute itself). + Type? currentType = type; - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) - { - if (TryGetBuiltInTypeForSimpleReference (jniSimpleReference, out var builtIn)) { - yield return builtIn; - } + while (currentType is not null) { + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (currentType, out var jniName)) { + return new (jniName, rank, keyword: false); + } - if (TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types)) { - foreach (var typeInfo in types) { - yield return typeInfo.Type; + currentType = currentType.BaseType; } - } - } - protected override string? GetSimpleReference (Type type) - { - var simpleReference = _simpleReferenceCache.GetOrAdd (type, GetSimpleReferenceUncached); - return simpleReference == NoSimpleReference ? null : simpleReference; - } + return default; - string GetSimpleReferenceUncached (Type type) - { - if (TryGetBuiltInTypeSignature (type, out var signature)) { - return signature.SimpleReference ?? NoSimpleReference; - } + static Type GetUnderlyingType (Type type, out int rank) + { + rank = 0; + var originalType = type; + while (type.IsArray) { + if (type.GetArrayRank () > 1) + throw new ArgumentException ($"Multidimensional array '{originalType.FullName}' is not supported.", nameof (type)); + rank++; + type = type.GetElementType () ?? throw new InvalidOperationException ("Array type has no element type."); + } - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { - return jniName; - } + if (type.IsEnum) + type = Enum.GetUnderlyingType (type); - // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable - // extends Java.Lang.Error but has no [Register] attribute itself). - for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) { - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (baseType, out var baseJniName)) { - return baseJniName; + return type; } - } - return NoSimpleReference; - } - - protected override IEnumerable GetSimpleReferences (Type type) - { - if (TryGetBuiltInTypeSignature (type, out var signature) && signature.SimpleReference is not null) { - yield return signature.SimpleReference; - yield break; - } - - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { - yield return jniName; - yield break; - } - - // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable - // extends Java.Lang.Error but has no [Register] attribute itself). - for (var baseType = type.BaseType; baseType is not null; baseType = baseType.BaseType) { - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (baseType, out var baseJniName)) { - yield return baseJniName; - yield break; + static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signature) + { + if (GetKeywordTypeName (type) is string keywordTypeName) { + signature = new JniTypeSignature (keywordTypeName, 0, keyword: true); + return true; + } + + static string? GetKeywordTypeName (Type type) + => Type.GetTypeCode (type) switch { + TypeCode.Boolean => "Z", + TypeCode.Byte => "B", + TypeCode.SByte => "B", + TypeCode.Char => "C", + TypeCode.Int16 => "S", + TypeCode.UInt16 => "S", + TypeCode.Int32 => "I", + TypeCode.UInt32 => "I", + TypeCode.Int64 => "J", + TypeCode.UInt64 => "J", + TypeCode.Single => "F", + TypeCode.Double => "D", + _ => null, + }; + + if (type == typeof (void)) { + signature = new JniTypeSignature ("V", 0, keyword: true); + return true; + } + + if (type == typeof (string)) { + signature = new JniTypeSignature ("java/lang/String"); + return true; + } + + if (type == typeof (bool?)) { + signature = new JniTypeSignature ("java/lang/Boolean"); + return true; + } + if (type == typeof (sbyte?)) { + signature = new JniTypeSignature ("java/lang/Byte"); + return true; + } + if (type == typeof (char?)) { + signature = new JniTypeSignature ("java/lang/Character"); + return true; + } + if (type == typeof (short?)) { + signature = new JniTypeSignature ("java/lang/Short"); + return true; + } + if (type == typeof (int?)) { + signature = new JniTypeSignature ("java/lang/Integer"); + return true; + } + if (type == typeof (long?)) { + signature = new JniTypeSignature ("java/lang/Long"); + return true; + } + if (type == typeof (float?)) { + signature = new JniTypeSignature ("java/lang/Float"); + return true; + } + if (type == typeof (double?)) { + signature = new JniTypeSignature ("java/lang/Double"); + return true; + } + + signature = default; + return false; } } } - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [return: DynamicallyAccessedMembers (Methods | Constructors | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { - if (TryGetBuiltInTypeForSimpleReference (jniSimpleReference, out var type)) { - return type; + var builtInType = jniSimpleReference switch { + "java/lang/String" => typeof (string), + "V" => typeof (void), + "Z" => typeof (bool), + "java/lang/Boolean" => typeof (bool?), + "B" => typeof (sbyte), + "java/lang/Byte" => typeof (sbyte?), + "C" => typeof (char), + "java/lang/Character" => typeof (char?), + "S" => typeof (short), + "java/lang/Short" => typeof (short?), + "I" => typeof (int), + "java/lang/Integer" => typeof (int?), + "J" => typeof (long), + "java/lang/Long" => typeof (long?), + "F" => typeof (float), + "java/lang/Float" => typeof (float?), + "D" => typeof (double), + "java/lang/Double" => typeof (double?), + _ => null, + }; + + if (builtInType is not null) { + return builtInType; } return TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types) && types.Length > 0 @@ -136,214 +169,68 @@ protected override IEnumerable GetSimpleReferences (Type type) : null; } - public override IEnumerable GetTypes (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.SimpleReference == null) - return []; - return CreateGetTypesEnumerator (typeSignature); - } - - IEnumerable CreateGetTypesEnumerator (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.SimpleReference == null) - yield break; + [return: DynamicallyAccessedMembers (Constructors)] + protected override Type? GetInvokerTypeCore ([DynamicallyAccessedMembers (Constructors)] Type type) + // => TrimmableTypeMap.Instance.GetInvokerType (type); + => throw new UnreachableException ( + $"{nameof (GetInvokerTypeCore)} should not be called in the trimmable typemap path. " + + $"Invoker types should use generated {nameof (JavaPeerProxy)} instances."); - foreach (var type in GetTypesForSimpleReference (typeSignature.SimpleReference)) { - if (typeSignature.ArrayRank == 0) { - yield return type; - continue; - } - - Type arrayElementType = type; - for (int i = 1; i < typeSignature.ArrayRank; i++) { - arrayElementType = MakeArrayType (arrayElementType); - } + protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) + => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); - if (TrimmableTypeMap.Instance.TryGetArrayType (arrayElementType, out var arrayType)) { - yield return arrayType; - continue; - } + protected override string? GetReplacementTypeCore (string jniSimpleReference) + => JniRemappingLookup.GetReplacementType (jniSimpleReference); - if (IsKeywordSimpleReference (typeSignature.SimpleReference) || type == typeof (string)) { - yield return MakeArrayType (arrayElementType); - } - } - } + protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) + => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); - public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) - yield break; + protected override string? GetSimpleReference (Type type) + // { + // var typeSignature = GetTypeSignature (type); + // return typeSignature.IsValid ? typeSignature.SimpleReference : null; + // } + => throw new UnreachableException ( + $"{nameof (GetSimpleReference)} should not be called in the trimmable typemap path. " + + $"Simple reference lookup should use {nameof (GetTypeSignatureCore)} to get the full type signature, including simple reference."); - if (TrimmableTypeMap.Instance.TryGetTargetTypes (typeSignature.SimpleReference, out var types)) { - foreach (var typeInfo in types) { - yield return new ReflectionConstructibleType (typeInfo.Type); - } - } - } + protected override IEnumerable GetSimpleReferences (Type type) + // { + // var simpleReference = GetSimpleReference (type); + // return simpleReference is not null ? [simpleReference] : []; + // } + => throw new UnreachableException ( + $"{nameof (GetSimpleReferences)} should not be called in the trimmable typemap path. " + + $"Simple reference lookup should use {nameof (GetTypeSignatureCore)} to get the full type signature, including simple reference."); - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - return TrimmableTypeMap.Instance.GetInvokerType (type); - } + public override IEnumerable GetTypes (JniTypeSignature typeSignature) + => throw new UnreachableException ( + $"{nameof (GetTypes)} should not be called in the trimmable typemap path. " + + $"Java-to-managed constructor activation should use generated {nameof (JavaPeerProxy)} instances."); - protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) - { - return JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); - } + public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) + => throw new UnreachableException ( + $"{nameof (GetReflectionConstructibleTypes)} should not be called in the trimmable typemap path. " + + $"Managed peer construction should use generated {nameof (JavaPeerProxy)} instances."); - protected override string? GetReplacementTypeCore (string jniSimpleReference) - { - return JniRemappingLookup.GetReplacementType (jniSimpleReference); - } + protected override IEnumerable GetTypeSignaturesCore (Type type) + => throw new UnreachableException ( + $"{nameof (GetTypeSignaturesCore)} should not be called in the trimmable typemap path. " + + $"Runtime type signature lookup should use {nameof (GetTypeSignatureCore)}."); - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) - { - return JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); - } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) + => throw new UnreachableException ( + $"{nameof (GetTypesForSimpleReference)} should not be called in the trimmable typemap path. " + + $"Simple reference lookup should use {nameof (GetTypeForSimpleReference)}."); - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - ReadOnlySpan methods) - { - throw new UnreachableException ( + public override void RegisterNativeMembers (JniType nativeClass, [DynamicallyAccessedMembers (Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] Type type, ReadOnlySpan methods) + => throw new UnreachableException ( $"RegisterNativeMembers should not be called in the trimmable typemap path. " + $"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks."); - } [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan)")] - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - string? methods) - { - throw new UnreachableException ( + public override void RegisterNativeMembers (JniType nativeClass, [DynamicallyAccessedMembers (Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] Type type, string? methods) + => throw new UnreachableException ( $"RegisterNativeMembers should not be called in the trimmable typemap path. " + $"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks."); - } - - static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signature) - { - switch (Type.GetTypeCode (type)) { - case TypeCode.String: - signature = new JniTypeSignature ("java/lang/String"); - return true; - case TypeCode.Boolean: - signature = new JniTypeSignature ("Z", 0, keyword: true); - return true; - case TypeCode.Byte: - case TypeCode.SByte: - signature = new JniTypeSignature ("B", 0, keyword: true); - return true; - case TypeCode.Char: - signature = new JniTypeSignature ("C", 0, keyword: true); - return true; - case TypeCode.UInt16: - case TypeCode.Int16: - signature = new JniTypeSignature ("S", 0, keyword: true); - return true; - case TypeCode.UInt32: - case TypeCode.Int32: - signature = new JniTypeSignature ("I", 0, keyword: true); - return true; - case TypeCode.UInt64: - case TypeCode.Int64: - signature = new JniTypeSignature ("J", 0, keyword: true); - return true; - case TypeCode.Single: - signature = new JniTypeSignature ("F", 0, keyword: true); - return true; - case TypeCode.Double: - signature = new JniTypeSignature ("D", 0, keyword: true); - return true; - } - - if (type == typeof (void)) { - signature = new JniTypeSignature ("V", 0, keyword: true); - return true; - } - - if (type == typeof (Boolean?)) { - signature = new JniTypeSignature ("java/lang/Boolean"); - return true; - } - if (type == typeof (SByte?)) { - signature = new JniTypeSignature ("java/lang/Byte"); - return true; - } - if (type == typeof (Char?)) { - signature = new JniTypeSignature ("java/lang/Character"); - return true; - } - if (type == typeof (Int16?)) { - signature = new JniTypeSignature ("java/lang/Short"); - return true; - } - if (type == typeof (Int32?)) { - signature = new JniTypeSignature ("java/lang/Integer"); - return true; - } - if (type == typeof (Int64?)) { - signature = new JniTypeSignature ("java/lang/Long"); - return true; - } - if (type == typeof (Single?)) { - signature = new JniTypeSignature ("java/lang/Float"); - return true; - } - if (type == typeof (Double?)) { - signature = new JniTypeSignature ("java/lang/Double"); - return true; - } - - signature = default; - return false; - } - - static bool TryGetBuiltInTypeForSimpleReference ( - string jniSimpleReference, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - [NotNullWhen (true)] out Type? type) - { - type = jniSimpleReference switch { - "java/lang/String" => typeof (string), - "V" => typeof (void), - "Z" => typeof (Boolean), - "java/lang/Boolean" => typeof (Boolean?), - "B" => typeof (SByte), - "java/lang/Byte" => typeof (SByte?), - "C" => typeof (Char), - "java/lang/Character" => typeof (Char?), - "S" => typeof (Int16), - "java/lang/Short" => typeof (Int16?), - "I" => typeof (Int32), - "java/lang/Integer" => typeof (Int32?), - "J" => typeof (Int64), - "java/lang/Long" => typeof (Int64?), - "F" => typeof (Single), - "java/lang/Float" => typeof (Single?), - "D" => typeof (Double), - "java/lang/Double" => typeof (Double?), - _ => null, - }; - return type != null; - } - - static Type MakeArrayType (Type elementType) - { -#pragma warning disable IL3050 // Trimmable typemap emits concrete array types; fallback arrays are runtime intrinsic. - return elementType.MakeArrayType (); -#pragma warning restore IL3050 - } - - static bool IsKeywordSimpleReference (string simpleReference) - { - return simpleReference is "V" or "Z" or "B" or "C" or "S" or "I" or "J" or "F" or "D"; - } } diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index e98c23cf5e6..e18b90e65e3 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -44,6 +44,8 @@ + - - Java.Interop.GenericMarshaler\JniPeerInstanceMethodsExtensions.cs - - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs index 20b1f72f8be..3ae65e2e352 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs @@ -1,7 +1,6 @@ using System; using Android.App; using Android.Views; -using Microsoft.Android.Runtime; using NUnit.Framework; namespace Android.ViewsTests; @@ -13,7 +12,8 @@ public class LayoutInflaterTest [Category ("Intune")] public void From () { - Console.WriteLine ($"{nameof (LayoutInflaterTest)}: RuntimeFeature.IsAssignableFromCheck={RuntimeFeature.IsAssignableFromCheck}"); + AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck", out bool isAssignableFromCheck); + Console.WriteLine ($"{nameof (LayoutInflaterTest)}: RuntimeFeature.IsAssignableFromCheck={isAssignableFromCheck}"); // See: tests\Mono.Android-Tests\Mono.Android-Tests\IsAssignableFromRemaps.xml // Remapped to "net/dot/android/test/MyLayoutInflater" diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index 988af02552a..1a94a49a59f 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -528,11 +528,14 @@ public void JavaSideNestedIntArrayConstructorForwardsValues () static void AssumeTrimmableConstructorParameterMarshalling () { - if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + if (!IsTrimmableTypeMapEnabled ()) { Assert.Ignore ("Legacy TypeManager.n_Activate does not marshal string, short, or array constructor parameters; this case validates trimmable constructor UCO parameter marshalling."); } } + static bool IsTrimmableTypeMapEnabled () + => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; + static T CreateFromJava (string constructorSignature, params JValue [] arguments) where T : Java.Lang.Object { @@ -573,7 +576,7 @@ static void AssertRegisteredSame (T instance) var registered = Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer); try { Assert.AreSame (instance, registered); - if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + if (IsTrimmableTypeMapEnabled ()) { Assert.AreEqual (Java.Lang.JavaSystem.IdentityHashCode (instance), instance.JniIdentityHashCode); } } finally { @@ -978,5 +981,6 @@ public static void Reset () { ConstructorInvocations = 0; } + } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index be1ee4b06f5..65989c168f8 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -254,11 +254,14 @@ public void Export_Method_NestedJniCall_PreservesExceptionFromInnerExport () static void AssumeTrimmableExportExceptionRouting () { - if (!RuntimeFeature.TrimmableTypeMap) { + if (!IsTrimmableTypeMapEnabled ()) { Assert.Ignore ("[Export] exception routing coverage is only relevant for the trimmable typemap path."); } } + static bool IsTrimmableTypeMapEnabled () + => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; + // --------------------------------------------------------------- // Group D — [ExportField] runtime visibility from Java // --------------------------------------------------------------- diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index bfd4175bab4..5480c4daa8d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -221,10 +221,13 @@ static T CreateFromJava () static void AssumeTrimmableTypeMapEnabled () { - if (!RuntimeFeature.TrimmableTypeMap) { + if (!IsTrimmableTypeMapEnabled ()) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } + + static bool IsTrimmableTypeMapEnabled () + => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; } class TrimmableRuntimeTextWatcher : Java.Lang.Object, ITextWatcher diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 3ccae0da3ad..2eba12af122 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -294,11 +294,14 @@ static IReadOnlyList GetStaticMethodFallbackTypes (TestableTrimmableType static void AssumeTrimmableTypeMapEnabled () { - if (!RuntimeFeature.TrimmableTypeMap) { + if (!IsTrimmableTypeMapEnabled ()) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } + static bool IsTrimmableTypeMapEnabled () + => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; + static async Task WaitForGC (Func predicate, string message, int timeoutMilliseconds = 2000) { var timeout = TimeSpan.FromMilliseconds (timeoutMilliseconds); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs index 02dc861eb82..0e1f7f7db01 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs @@ -11,7 +11,8 @@ public class StartupHookTest public void FeatureFlagIsEnabled () { // NOTE: this is set to true in tests\Mono.Android-Tests\Mono.Android-Tests\Mono.Android.NET-Tests.csproj - Assert.IsTrue (Microsoft.Android.Runtime.RuntimeFeature.StartupHookSupport, "RuntimeFeature.StartupHookSupport should be true"); + AppContext.TryGetSwitch ("System.StartupHookProvider.IsSupported", out bool startupHookSupport); + Assert.IsTrue (startupHookSupport, "System.StartupHookProvider.IsSupported should be true"); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs index 53d558fe61b..b81450c8b7b 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs @@ -53,9 +53,12 @@ public void JavaInterfaceLookup_BaseInterfaceReturnType_UsesDerivedInterfaceProx static void AssumeTrimmableTypeMapEnabled () { - if (!RuntimeFeature.TrimmableTypeMap) { + if (!IsTrimmableTypeMapEnabled ()) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } + + static bool IsTrimmableTypeMapEnabled () + => System.AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; } } From 0a1072a0fdec6ed0093fbe20d08e7dfc9f6297a5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 10:27:36 +0200 Subject: [PATCH 17/47] Reuse Java.Interop value marshalers in trimmable runtime Use the Java.Interop proxy and peerable value marshalers from the trimmable value manager instead of duplicating peerable marshaling locally. This also updates the Java.Interop submodule to the follow-up branch with the shared proxy marshaler and re-enables the trimmable tests now covered by the shared marshalers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../JavaMarshalValueManager.cs | 38 ++++--------------- 2 files changed, 8 insertions(+), 32 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index 3931347aa98..11e9f39842d 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 3931347aa983c5f35b8b03da4e25212cdc910948 +Subproject commit 11e9f39842dc6ffddd76c69abdd106d83051b40b diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 5cdb10e6d21..ced6e3fc88c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1035,7 +1035,7 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) if (type == typeof (string)) return TrimmableValueMarshaler.Instance; if (type == typeof (object)) - return TrimmableValueMarshaler.Instance; + return ObjectValueMarshaler; if (type == typeof (int[])) return TrimmableValueMarshaler.Instance; if (type == typeof (IList)) @@ -1047,16 +1047,19 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) if (type == typeof (JavaInt32Array)) return TrimmableValueMarshaler.Instance; if (type == typeof (IJavaPeerable) || typeof (IJavaPeerable).IsAssignableFrom (type)) - return TrimmablePeerableValueMarshaler.Instance; + return PeerableValueMarshaler; - return TrimmableValueMarshaler.Instance; + return ObjectValueMarshaler; } protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () { EnsureNotDisposed (); + if (typeof (T) == typeof (object)) { + return (JniValueMarshaler)(object) ObjectValueMarshaler; + } if (typeof (T) == typeof (IJavaPeerable)) { - return (JniValueMarshaler)(object) TrimmablePeerableValueMarshaler.Instance; + return (JniValueMarshaler)(object) PeerableValueMarshaler; } return TrimmableValueMarshaler.Instance; } @@ -1074,33 +1077,6 @@ static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, Jn }; } - sealed class TrimmablePeerableValueMarshaler : JniValueMarshaler - { - public static readonly TrimmablePeerableValueMarshaler Instance = new (); - - public override IJavaPeerable? CreateGenericValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - return JniEnvironment.Runtime.ValueManager.CreatePeer (ref reference, options, targetType); - } - - public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState ([MaybeNull] IJavaPeerable? value, ParameterAttributes synchronize) - { - if (value == null || !value.PeerReference.IsValid) { - return new JniValueMarshalerState (); - } - return new JniValueMarshalerState (value.PeerReference.NewLocalRef ()); - } - - public override void DestroyGenericArgumentState ([AllowNull] IJavaPeerable? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - DisposeReferenceState (ref state); - } - } - sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] T> : JniValueMarshaler { public static readonly TrimmableValueMarshaler Instance = new (); From 78cb7d64a8e9cfd29684dc901d0ad07bca356f0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 10:51:25 +0200 Subject: [PATCH 18/47] Support JavaObjectArray in trimmable value marshaling Handle Java peerable elements and Java primitive array wrappers without reflection in the trimmable value manager. This lets JavaObjectArray preserve peer identity and JavaObjectArray create and read int-array elements correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 22 ++++++++++++++++ .../TrimmableTypeMapTypeManager.cs | 26 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index ced6e3fc88c..48431c46c80 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -976,6 +976,10 @@ protected override void ConstructPeerCore ( return existing; } + if (TryCreateJavaArrayWrapper (ref reference, options, targetType, out var arrayWrapper)) { + return arrayWrapper; + } + if (targetType != null && typeof (IJavaPeerable).IsAssignableFrom (targetType)) { return CreatePeer (ref reference, options, targetType); } @@ -988,6 +992,21 @@ protected override void ConstructPeerCore ( return value; } + static bool TryCreateJavaArrayWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + Type? targetType, + [NotNullWhen (true)] out object? value) + { + if (targetType == typeof (JavaInt32Array) || targetType == typeof (JavaPrimitiveArray)) { + value = new JavaInt32Array (ref reference, options); + return true; + } + + value = null; + return false; + } + protected override JniValueMarshaler GetValueMarshalerCore (Type type) { EnsureNotDisposed (); @@ -1111,6 +1130,9 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState if (TryCreateInt32ArrayArgumentState (value, synchronize, out var state)) { return state; } + if (value is IJavaPeerable peerable) { + return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); + } var handle = JavaConvert.ToLocalJniHandle (value); return handle == IntPtr.Zero diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 615f077bfa7..04a116ff25c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -129,6 +129,32 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur return true; } + if (GetPrimitiveArrayWrapperKeywordTypeName (type) is string primitiveArrayKeywordTypeName) { + signature = new JniTypeSignature (primitiveArrayKeywordTypeName, 1, keyword: true); + return true; + } + + static string? GetPrimitiveArrayWrapperKeywordTypeName (Type type) + { + if (type == typeof (JavaBooleanArray) || type == typeof (JavaPrimitiveArray)) + return "Z"; + if (type == typeof (JavaSByteArray) || type == typeof (JavaPrimitiveArray)) + return "B"; + if (type == typeof (JavaCharArray) || type == typeof (JavaPrimitiveArray)) + return "C"; + if (type == typeof (JavaInt16Array) || type == typeof (JavaPrimitiveArray)) + return "S"; + if (type == typeof (JavaInt32Array) || type == typeof (JavaPrimitiveArray)) + return "I"; + if (type == typeof (JavaInt64Array) || type == typeof (JavaPrimitiveArray)) + return "J"; + if (type == typeof (JavaSingleArray) || type == typeof (JavaPrimitiveArray)) + return "F"; + if (type == typeof (JavaDoubleArray) || type == typeof (JavaPrimitiveArray)) + return "D"; + return null; + } + signature = default; return false; } From 169bc8f47218489b9346a818ed6a4e22cb0dca8e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:13:52 +0200 Subject: [PATCH 19/47] Generalize trimmable primitive value marshaling Reuse Java.Interop primitive array marshalers and type signatures from the trimmable value and type managers instead of maintaining int-only special cases. Keep unsupported Java.Interop marshaler and ManagedPeer-based tests grouped by category, and fix signed nullable byte conversion in JavaConvert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- src/Mono.Android/Java.Interop/JavaConvert.cs | 4 + .../JavaMarshalValueManager.cs | 85 +++---------------- .../TrimmableTypeMapTypeManager.cs | 51 +++-------- 4 files changed, 31 insertions(+), 111 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index 11e9f39842d..7add4b224a9 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 11e9f39842dc6ffddd76c69abdd106d83051b40b +Subproject commit 7add4b224a95026ff2f7abc77b7e44046109eaa4 diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ba46b155c00..b31e7f42dee 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -26,6 +26,10 @@ static class JavaConvert { using (var value = new Java.Lang.Byte (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.ByteValue (); } }, + { typeof (sbyte?), (handle, transfer) => { + using (var value = new Java.Lang.Byte (handle, transfer | JniHandleOwnership.DoNotRegister)) + return value.ByteValue (); + } }, { typeof (char), (handle, transfer) => { using (var value = new Java.Lang.Character (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.CharValue (); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 48431c46c80..ad09fa26a1d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -995,11 +995,12 @@ protected override void ConstructPeerCore ( static bool TryCreateJavaArrayWrapper ( ref JniObjectReference reference, JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] Type? targetType, [NotNullWhen (true)] out object? value) { - if (targetType == typeof (JavaInt32Array) || targetType == typeof (JavaPrimitiveArray)) { - value = new JavaInt32Array (ref reference, options); + if (targetType != null && TryGetPrimitiveArrayValueMarshaler (targetType, out var marshaler)) { + value = marshaler.CreateValue (ref reference, options, targetType); return true; } @@ -1055,16 +1056,8 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) return TrimmableValueMarshaler.Instance; if (type == typeof (object)) return ObjectValueMarshaler; - if (type == typeof (int[])) - return TrimmableValueMarshaler.Instance; - if (type == typeof (IList)) - return TrimmableValueMarshaler>.Instance; - if (type == typeof (global::Java.Interop.JavaArray)) - return TrimmableValueMarshaler>.Instance; - if (type == typeof (JavaPrimitiveArray)) - return TrimmableValueMarshaler>.Instance; - if (type == typeof (JavaInt32Array)) - return TrimmableValueMarshaler.Instance; + if (TryGetPrimitiveArrayValueMarshaler (type, out var primitiveArrayMarshaler)) + return primitiveArrayMarshaler; if (type == typeof (IJavaPeerable) || typeof (IJavaPeerable).IsAssignableFrom (type)) return PeerableValueMarshaler; @@ -1074,13 +1067,16 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () { EnsureNotDisposed (); - if (typeof (T) == typeof (object)) { - return (JniValueMarshaler)(object) ObjectValueMarshaler; + var type = typeof (T); + if (type.IsArray && !TryGetPrimitiveArrayValueMarshaler (type, out _)) { + return TrimmableValueMarshaler.Instance; } - if (typeof (T) == typeof (IJavaPeerable)) { - return (JniValueMarshaler)(object) PeerableValueMarshaler; + + var marshaler = GetValueMarshaler (type); + if (marshaler is JniValueMarshaler typedMarshaler) { + return typedMarshaler; } - return TrimmableValueMarshaler.Instance; + return CreateDelegatingValueMarshaler (marshaler); } static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, JniObjectReferenceOptions options) @@ -1127,9 +1123,6 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState if (value == null) { return new JniValueMarshalerState (); } - if (TryCreateInt32ArrayArgumentState (value, synchronize, out var state)) { - return state; - } if (value is IJavaPeerable peerable) { return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); } @@ -1142,61 +1135,9 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) { - if (TryDestroyInt32ArrayArgumentState (value, ref state, synchronize)) { - return; - } DisposeReferenceState (ref state); } - static bool TryCreateInt32ArrayArgumentState ([MaybeNull] T value, ParameterAttributes synchronize, out JniValueMarshalerState state) - { - state = new JniValueMarshalerState (); - - if (value is not IList list) { - return false; - } - - synchronize = GetCopyDirection (synchronize); - var copyToJava = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; - var array = copyToJava - ? new JavaInt32Array (list) - : new JavaInt32Array (list.Count); - state = new JniValueMarshalerState (array); - return true; - } - - static bool TryDestroyInt32ArrayArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - if (state.PeerableValue is not JavaInt32Array array) { - return false; - } - - synchronize = GetCopyDirection (synchronize); - if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList list) { - if (value is int[] targetArray) { - array.CopyTo (targetArray, 0); - } else { - int count = Math.Min (array.Length, list.Count); - for (int i = 0; i < count; i++) { - list [i] = array [i]; - } - } - } - - array.Dispose (); - state = new JniValueMarshalerState (); - return true; - } - - static ParameterAttributes GetCopyDirection (ParameterAttributes value) - { - const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; - if ((value & inout) != 0) { - return value & inout; - } - return inout; - } - static bool IsPrimitiveJniValueType (Type type) { return type == typeof (bool) || diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 04a116ff25c..24c09bb73fa 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -19,6 +19,9 @@ class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; readonly ConcurrentDictionary _typeSignatureCache = new (); + // This type manager has 2 core APIs: GetTypeSignatureCore for managed-to-Java lookups, and GetTypeForSimpleReference for Java-to-managed lookups. + // The rest of the APIs are unsupported and will throw if called, as they are not needed internally anywhere. + protected override JniTypeSignature GetTypeSignatureCore (Type type) { return _typeSignatureCache.GetOrAdd (type, GetTypeSignatureUncached); @@ -129,31 +132,8 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur return true; } - if (GetPrimitiveArrayWrapperKeywordTypeName (type) is string primitiveArrayKeywordTypeName) { - signature = new JniTypeSignature (primitiveArrayKeywordTypeName, 1, keyword: true); + if (TryGetPrimitiveArrayTypeSignature (type, out signature)) return true; - } - - static string? GetPrimitiveArrayWrapperKeywordTypeName (Type type) - { - if (type == typeof (JavaBooleanArray) || type == typeof (JavaPrimitiveArray)) - return "Z"; - if (type == typeof (JavaSByteArray) || type == typeof (JavaPrimitiveArray)) - return "B"; - if (type == typeof (JavaCharArray) || type == typeof (JavaPrimitiveArray)) - return "C"; - if (type == typeof (JavaInt16Array) || type == typeof (JavaPrimitiveArray)) - return "S"; - if (type == typeof (JavaInt32Array) || type == typeof (JavaPrimitiveArray)) - return "I"; - if (type == typeof (JavaInt64Array) || type == typeof (JavaPrimitiveArray)) - return "J"; - if (type == typeof (JavaSingleArray) || type == typeof (JavaPrimitiveArray)) - return "F"; - if (type == typeof (JavaDoubleArray) || type == typeof (JavaPrimitiveArray)) - return "D"; - return null; - } signature = default; return false; @@ -195,12 +175,7 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur : null; } - [return: DynamicallyAccessedMembers (Constructors)] - protected override Type? GetInvokerTypeCore ([DynamicallyAccessedMembers (Constructors)] Type type) - // => TrimmableTypeMap.Instance.GetInvokerType (type); - => throw new UnreachableException ( - $"{nameof (GetInvokerTypeCore)} should not be called in the trimmable typemap path. " + - $"Invoker types should use generated {nameof (JavaPeerProxy)} instances."); + // Remapping APIs for InTune support protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) => JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: true); @@ -211,20 +186,20 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) => JniRemappingLookup.GetReplacementMethodInfo (jniSourceType, jniMethodName, jniMethodSignature); + // The rest of the APIs are unsupported - they are not needed internally anywhere anyway + + [return: DynamicallyAccessedMembers (Constructors)] + protected override Type? GetInvokerTypeCore ([DynamicallyAccessedMembers (Constructors)] Type type) + => throw new UnreachableException ( + $"{nameof (GetInvokerTypeCore)} should not be called in the trimmable typemap path. " + + $"Invoker types should use generated {nameof (JavaPeerProxy)} instances."); + protected override string? GetSimpleReference (Type type) - // { - // var typeSignature = GetTypeSignature (type); - // return typeSignature.IsValid ? typeSignature.SimpleReference : null; - // } => throw new UnreachableException ( $"{nameof (GetSimpleReference)} should not be called in the trimmable typemap path. " + $"Simple reference lookup should use {nameof (GetTypeSignatureCore)} to get the full type signature, including simple reference."); protected override IEnumerable GetSimpleReferences (Type type) - // { - // var simpleReference = GetSimpleReference (type); - // return simpleReference is not null ? [simpleReference] : []; - // } => throw new UnreachableException ( $"{nameof (GetSimpleReferences)} should not be called in the trimmable typemap path. " + $"Simple reference lookup should use {nameof (GetTypeSignatureCore)} to get the full type signature, including simple reference."); From 869f183a8be45523cc8735b312199e7b6f5075f1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:20:07 +0200 Subject: [PATCH 20/47] Apply trimmable test exclusions to new runner Move the trimmable Java.Interop exclusion policy onto the current TestInstrumentation type after rebasing onto main, preserving the unsupported-by-design category and the remaining temporary exclusions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestInstrumentation.cs | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index da97716db5d..410529ad815 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -11,6 +11,8 @@ namespace Xamarin.Android.RuntimeTests [Instrumentation (Name = "xamarin.android.runtimetests.TestInstrumentation")] public class TestInstrumentation : Xamarin.Android.UnitTests.TestInstrumentation { + const string TrimmableTypeMapUnsupportedCategory = "TrimmableTypeMapUnsupported"; + protected TestInstrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { @@ -30,6 +32,14 @@ protected override IEnumerable? ExcludedCategories { if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { categories.Add ("NativeTypeMap"); categories.Add ("Export"); + // Java.Interop tests in this category exercise APIs that are unsupported + // by design under the trimmable typemap: expression-tree-based marshaling + // from the obsolete runtime marshal-member builder, hand-written native + // registration via [JniAddNativeMethodRegistration], and Java test peers + // which call net.dot.jni.ManagedPeer.construct/registerNativeMembers. + // The trimmable runtime must use generated/AOT-safe marshal and + // registration paths instead. + categories.Add (TrimmableTypeMapUnsupportedCategory); } // Build-time flags flow in via runtimeconfig.json properties @@ -78,19 +88,28 @@ protected override IEnumerable? ExcludedTestNames { if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) return null; - // Tests from the external Java.Interop-Tests assembly that fail under the - // trimmable typemap. These cannot use [Category] because we don't control - // that assembly — they must be excluded by name here. + // Tests from the external Java.Interop-Tests assembly that still fail under + // the trimmable typemap and are not covered by a category. return new [] { - // Known limitation: [JniAddNativeMethodRegistrationAttribute] is not - // supported by design under the trimmable typemap. This Java.Interop-Tests - // fixture uses that attribute to register native callbacks on a hand-written - // Java peer (an obsolete code path whose primary consumer, jnimarshalmethod-gen, - // was removed in dotnet/java-interop#1405). The trimmable typemap generator - // emits XA4251 when it encounters the attribute and instructs users to either - // avoid it or switch off the trimmable typemap. - // See https://github.com/dotnet/android/issues/11170. - "Java.InteropTests.InvokeVirtualFromConstructorTests", + // Value-marshaling cases that are still failing under the trimmable + // value manager. Keep these granular so passing value-marshaler tests + // stay enabled while the remaining gaps are fixed. + "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", + "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", + "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.DestroyArgumentState", + "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.TestIsJniValueType", + + // Current trimmable runtime exception/type-manager behavior differs from + // the legacy typemap path these tests assert against. + "Java.InteropTests.ConstructorActivationTests.JavaSideThrowingConstructorPropagatesException", + "Java.InteropTests.ExportTests.Export_Method_NestedJniCall_PreservesExceptionFromInnerExport", + "Java.InteropTests.ExportTests.Export_Method_Throws_PrimitiveReturn_SurfacesAsManagedException", + "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", + "Java.InteropTests.JniRuntimeTest.BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault", + "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", + "Java.InteropTests.JniTypeManagerTests.GetType", + "Java.InteropTests.JniTypeManagerTests.GetTypeSignature_Type", + "Xamarin.Android.RuntimeTests.ExceptionTest.InnerExceptionIsSet", }; } } From 93ddc7326bf95c09cf5ed564a3a16d31e70cf25e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:37:05 +0200 Subject: [PATCH 21/47] Remove LayoutInflater test contamination Restore the LayoutInflater test to the main-branch RuntimeFeature usage so this PR only carries the Java.Interop value manager follow-up changes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs index 3ae65e2e352..20b1f72f8be 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Android.Views/LayoutInflaterTest.cs @@ -1,6 +1,7 @@ using System; using Android.App; using Android.Views; +using Microsoft.Android.Runtime; using NUnit.Framework; namespace Android.ViewsTests; @@ -12,8 +13,7 @@ public class LayoutInflaterTest [Category ("Intune")] public void From () { - AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.IsAssignableFromCheck", out bool isAssignableFromCheck); - Console.WriteLine ($"{nameof (LayoutInflaterTest)}: RuntimeFeature.IsAssignableFromCheck={isAssignableFromCheck}"); + Console.WriteLine ($"{nameof (LayoutInflaterTest)}: RuntimeFeature.IsAssignableFromCheck={RuntimeFeature.IsAssignableFromCheck}"); // See: tests\Mono.Android-Tests\Mono.Android-Tests\IsAssignableFromRemaps.xml // Remapped to "net/dot/android/test/MyLayoutInflater" From 933a38ddcd09dc623d96f9baaf84ea114cd05ea9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:38:14 +0200 Subject: [PATCH 22/47] Address trimmable review cleanup Remove redundant Requires annotations from reflection-backed manager constructors, keep test feature checks on RuntimeFeature.TrimmableTypeMap, inline invoker lookup helpers, and keep nullable sbyte conversion handling in the trimmable value manager instead of JavaConvert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/AndroidRuntime.cs | 4 ---- src/Mono.Android/Java.Interop/JavaConvert.cs | 4 ---- .../Java.Interop/JavaObjectExtensions.cs | 21 +++++-------------- .../JavaMarshalValueManager.cs | 12 ++++++++++- .../TrimmableTypeMapTypeManagerTests.cs | 5 +---- 5 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 1a87f2726dc..a38ca4a7f89 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -319,8 +319,6 @@ class AndroidTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - [RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] - [RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] public AndroidTypeManager (bool jniAddNativeMethodRegistrationAttributePresent) { this.jniAddNativeMethodRegistrationAttributePresent = jniAddNativeMethodRegistrationAttributePresent; @@ -630,8 +628,6 @@ class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); - [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.")] public AndroidValueManager () { } diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index b31e7f42dee..ba46b155c00 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -26,10 +26,6 @@ static class JavaConvert { using (var value = new Java.Lang.Byte (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.ByteValue (); } }, - { typeof (sbyte?), (handle, transfer) => { - using (var value = new Java.Lang.Byte (handle, transfer | JniHandleOwnership.DoNotRegister)) - return value.ByteValue (); - } }, { typeof (char), (handle, transfer) => { using (var value = new Java.Lang.Character (handle, transfer | JniHandleOwnership.DoNotRegister)) return value.CharValue (); diff --git a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs index 66787331d37..27679062593 100644 --- a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs +++ b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs @@ -112,33 +112,22 @@ internal static TResult? _JavaCast< [RequiresUnreferencedCode ("Invoker lookup uses reflection over preserved Java peer types.")] internal static Type? GetInvokerType (Type type) { - [return: DynamicallyAccessedMembers (Constructors)] - static Type? AssemblyGetType (Assembly assembly, string typeName) => - assembly.GetType (typeName); - - // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 - // IL3050 disabled in source: if someone uses NativeAOT, they will get the warning. - [return: DynamicallyAccessedMembers (Constructors)] - static Type MakeGenericType (Type type, params Type [] typeArguments) => - #pragma warning disable IL3050 - type.MakeGenericType (typeArguments); - #pragma warning restore IL3050 - const string suffix = "Invoker"; Type[] arguments = type.GetGenericArguments (); if (arguments.Length == 0) - return AssemblyGetType (type.Assembly, type + suffix); + return type.Assembly.GetType (type + suffix); 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 null; - return MakeGenericType (suffixDefinition, arguments); +#pragma warning disable IL3050 + return suffixDefinition.MakeGenericType (arguments); +#pragma warning restore IL3050 } } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index ad09fa26a1d..f2f64922d8e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -985,13 +985,23 @@ protected override void ConstructPeerCore ( } var transfer = ToJniHandleOwnership (reference, options); - var value = JavaConvert.FromJniHandle (reference.Handle, transfer, targetType); + var value = JavaConvert.FromJniHandle (reference.Handle, transfer, GetValueConversionTargetType (targetType)); if (transfer != JniHandleOwnership.DoNotTransfer) { reference = default; } return value; } + [return: DynamicallyAccessedMembers (Constructors)] + static Type? GetValueConversionTargetType ([DynamicallyAccessedMembers (Constructors)] Type? targetType) + { + if (targetType == typeof (sbyte?)) { + return typeof (sbyte); + } + + return targetType; + } + static bool TryCreateJavaArrayWrapper ( ref JniObjectReference reference, JniObjectReferenceOptions options, diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs index 2eba12af122..3ccae0da3ad 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapTypeManagerTests.cs @@ -294,14 +294,11 @@ static IReadOnlyList GetStaticMethodFallbackTypes (TestableTrimmableType static void AssumeTrimmableTypeMapEnabled () { - if (!IsTrimmableTypeMapEnabled ()) { + if (!RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } - static bool IsTrimmableTypeMapEnabled () - => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; - static async Task WaitForGC (Func predicate, string message, int timeoutMilliseconds = 2000) { var timeout = TimeSpan.FromMilliseconds (timeoutMilliseconds); From 65a2cf50641d9747b5f1365f539a36f38f68ab21 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:52:40 +0200 Subject: [PATCH 23/47] Flow excluded NUnit categories from MSBuild Teach the test runner to merge ExcludeCategories from runtimeconfig with built-in category exclusions, and define the trimmable-unsupported Java.Interop category from the Mono.Android test project instead of hardcoding it in TestInstrumentation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Mono.Android.NET-Tests.csproj | 2 ++ .../TestInstrumentation.cs | 3 --- tests/TestRunner.Core/TestInstrumentation.cs | 25 ++++++++++++++++--- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index a021ea0f2ee..febd15a295c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -52,6 +52,8 @@ want tests tagged [Category("Intune")] to run. --> + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index 410529ad815..a5fb8f3e18e 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -11,8 +11,6 @@ namespace Xamarin.Android.RuntimeTests [Instrumentation (Name = "xamarin.android.runtimetests.TestInstrumentation")] public class TestInstrumentation : Xamarin.Android.UnitTests.TestInstrumentation { - const string TrimmableTypeMapUnsupportedCategory = "TrimmableTypeMapUnsupported"; - protected TestInstrumentation (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { @@ -39,7 +37,6 @@ protected override IEnumerable? ExcludedCategories { // which call net.dot.jni.ManagedPeer.construct/registerNativeMembers. // The trimmable runtime must use generated/AOT-safe marshal and // registration paths instead. - categories.Add (TrimmableTypeMapUnsupportedCategory); } // Build-time flags flow in via runtimeconfig.json properties diff --git a/tests/TestRunner.Core/TestInstrumentation.cs b/tests/TestRunner.Core/TestInstrumentation.cs index f9cf928c426..379241df172 100644 --- a/tests/TestRunner.Core/TestInstrumentation.cs +++ b/tests/TestRunner.Core/TestInstrumentation.cs @@ -151,11 +151,18 @@ TestFilter BuildNUnitFilter () } if (!noExclusions) { + // Exclude categories from two sources: + // 1. The ExcludedCategories subclass property + // 2. `ExcludeCategories` from runtimeconfig.json, set from the MSBuild + // property of the same name. + var excludes = new List (); if (ExcludedCategories is not null) { - foreach (var cat in ExcludedCategories) { - filterElements.Add (new XElement ("not", new XElement ("cat", cat))); - Log.Info (LogTag, $"Excluding category: {cat}"); - } + excludes.AddRange (ExcludedCategories); + } + excludes.AddRange (GetConfiguredCategories ("ExcludeCategories")); + foreach (var cat in excludes) { + filterElements.Add (new XElement ("not", new XElement ("cat", cat))); + Log.Info (LogTag, $"Excluding category: {cat}"); } if (ExcludedTestNames is not null) { @@ -219,6 +226,16 @@ List GetListExtra (string key) .ToList (); } + static List GetConfiguredCategories (string key) + { + var value = AppContext.GetData (key) as string; + if (value is null) { + return []; + } + return value.Split ([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .ToList (); + } + static void CountResults (ITestResult result, ref int passed, ref int failed, ref int skipped) { if (result.Test.IsSuite) { From 6b4aedb5151cbd7d263415e5b9e8c036db64933e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 12:53:05 +0200 Subject: [PATCH 24/47] Extract trimmable primitive marshaler helpers Move non-generic primitive JNI value checks and argument state creation out of the generic TrimmableValueMarshaler class so the generic type only handles marshaler flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 83 +++++++++---------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index f2f64922d8e..5a9382691bd 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -537,6 +537,45 @@ public static bool IsIncompatibleCast ( } } +static class TrimmableValueMarshalerHelper +{ + public static bool IsPrimitiveJniValueType (Type type) + { + return type == typeof (bool) || + type == typeof (byte) || + type == typeof (sbyte) || + type == typeof (char) || + type == typeof (short) || + type == typeof (ushort) || + type == typeof (int) || + type == typeof (uint) || + type == typeof (long) || + type == typeof (ulong) || + type == typeof (float) || + type == typeof (double); + } + + public static JniArgumentValue CreatePrimitiveArgumentValue (object? value, Type type) + { + return value switch { + null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), + bool v => new JniArgumentValue (v), + byte v => new JniArgumentValue (v), + sbyte v => new JniArgumentValue (v), + char v => new JniArgumentValue (v), + short v => new JniArgumentValue (v), + ushort v => new JniArgumentValue (v), + int v => new JniArgumentValue (v), + uint v => new JniArgumentValue (v), + long v => new JniArgumentValue (v), + ulong v => new JniArgumentValue (v), + float v => new JniArgumentValue (v), + double v => new JniArgumentValue (v), + _ => throw new NotSupportedException ($"Type '{type.AssemblyQualifiedName}' is not a JNI primitive value type."), + }; + } +} + [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 @@ -936,9 +975,7 @@ protected override void ConstructPeerCore ( { EnsureNotDisposed (); if (!reference.IsValid) { -#pragma warning disable 8653 return default (T); -#pragma warning restore 8653 } if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) { @@ -952,9 +989,7 @@ protected override void ConstructPeerCore ( var value = GetValueCore (ref reference, options, targetType ?? typeof (T)); if (value is null) { -#pragma warning disable 8653 return default (T); -#pragma warning restore 8653 } return (T) value; } @@ -1106,7 +1141,7 @@ sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] { public static readonly TrimmableValueMarshaler Instance = new (); - public override bool IsJniValueType => IsPrimitiveJniValueType (typeof (T)); + public override bool IsJniValueType => TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (typeof (T)); public override Type MarshalType => IsJniValueType ? typeof (T) : base.MarshalType; @@ -1123,7 +1158,7 @@ public override T CreateGenericValue ( public override JniValueMarshalerState CreateGenericArgumentState ([MaybeNull] T value, ParameterAttributes synchronize = ParameterAttributes.In) { if (IsJniValueType) { - return new JniValueMarshalerState (CreatePrimitiveArgumentValue (value)); + return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, typeof (T))); } return CreateGenericObjectReferenceArgumentState (value, synchronize); } @@ -1147,42 +1182,6 @@ public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniVa { DisposeReferenceState (ref state); } - - static bool IsPrimitiveJniValueType (Type type) - { - return type == typeof (bool) || - type == typeof (byte) || - type == typeof (sbyte) || - type == typeof (char) || - type == typeof (short) || - type == typeof (ushort) || - type == typeof (int) || - type == typeof (uint) || - type == typeof (long) || - type == typeof (ulong) || - type == typeof (float) || - type == typeof (double); - } - - static JniArgumentValue CreatePrimitiveArgumentValue ([MaybeNull] T value) - { - return value switch { - null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), - bool v => new JniArgumentValue (v), - byte v => new JniArgumentValue (v), - sbyte v => new JniArgumentValue (v), - char v => new JniArgumentValue (v), - short v => new JniArgumentValue (v), - ushort v => new JniArgumentValue (v), - int v => new JniArgumentValue (v), - uint v => new JniArgumentValue (v), - long v => new JniArgumentValue (v), - ulong v => new JniArgumentValue (v), - float v => new JniArgumentValue (v), - double v => new JniArgumentValue (v), - _ => throw new NotSupportedException ($"Type '{typeof (T).AssemblyQualifiedName}' is not a JNI primitive value type."), - }; - } } static void DisposeReferenceState (ref JniValueMarshalerState state) From 80997b8804c009c495998ae911d67113f3ec7048 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 13:24:40 +0200 Subject: [PATCH 25/47] Address trimmable review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/AndroidRuntime.cs | 6 - .../Java.Interop/JavaObjectExtensions.cs | 2 - .../JavaMarshalValueManager.cs | 113 ++++++++++++------ .../ConstructorActivationTests.cs | 7 +- .../Java.Interop/ExportTests.cs | 5 +- .../TrimmableTypeMapRuntimeCoverageTests.cs | 5 +- .../TrustManagerMarshallingTests.cs | 5 +- 7 files changed, 83 insertions(+), 60 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index a38ca4a7f89..3c3b81f357d 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -387,7 +387,6 @@ protected override IEnumerable GetSimpleReferences (Type type) static MethodInfo? dynamic_callback_gen; // See ExportAttribute.cs - [RequiresUnreferencedCode ("Export callback registration uses reflection over Mono.Android.Export.dll.")] static Delegate CreateDynamicCallback (MethodInfo method) { if (dynamic_callback_gen == null) { @@ -489,7 +488,6 @@ public override void RegisterNativeMembers ( string? methods) => RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); - [RequiresUnreferencedCode ("Native member registration resolves callback types and delegates from generated method metadata.")] public override void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, @@ -628,10 +626,6 @@ class AndroidValueManager : JniRuntime.ReflectionJniValueManager { Dictionary instances = new Dictionary (); - public AndroidValueManager () - { - } - public override void WaitForGCBridgeProcessing () { if (!AndroidRuntimeInternal.BridgeProcessing) diff --git a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs index 27679062593..d434dc9350a 100644 --- a/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs +++ b/src/Mono.Android/Java.Interop/JavaObjectExtensions.cs @@ -125,9 +125,7 @@ internal static TResult? _JavaCast< definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt)); if (suffixDefinition == null) return null; -#pragma warning disable IL3050 return suffixDefinition.MakeGenericType (arguments); -#pragma warning restore IL3050 } } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 5a9382691bd..b936736534c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -574,6 +574,81 @@ public static JniArgumentValue CreatePrimitiveArgumentValue (object? value, Type _ => throw new NotSupportedException ($"Type '{type.AssemblyQualifiedName}' is not a JNI primitive value type."), }; } + + public static bool TryGetPrimitiveValueMarshaler (Type type, [NotNullWhen (true)] out JniValueMarshaler? marshaler) + { + if (type == typeof (bool)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (bool?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (byte)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (sbyte)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (sbyte?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (char)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (char?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (short)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (short?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (int)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (int?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (long)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (long?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (float)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (float?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (double)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + if (type == typeof (double?)) { + marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; + return true; + } + + marshaler = null; + return false; + } } [RequiresDynamicCode ("This value manager is reflection-backed and is not compatible with Native AOT.")] @@ -1063,40 +1138,8 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) throw new ArgumentException ("Generic type definitions are not supported.", nameof (type)); } - if (type == typeof (bool)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (bool?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (byte)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (sbyte)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (sbyte?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (char)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (char?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (short)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (short?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (int)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (int?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (long)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (long?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (float)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (float?)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (double)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (double?)) - return TrimmableValueMarshaler.Instance; + if (TrimmableValueMarshalerHelper.TryGetPrimitiveValueMarshaler (type, out var primitiveMarshaler)) + return primitiveMarshaler; if (type == typeof (string)) return TrimmableValueMarshaler.Instance; if (type == typeof (object)) @@ -1137,7 +1180,7 @@ static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, Jn }; } - sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] T> : JniValueMarshaler + internal sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] T> : JniValueMarshaler { public static readonly TrimmableValueMarshaler Instance = new (); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index 1a94a49a59f..be9be6cc42c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -528,14 +528,11 @@ public void JavaSideNestedIntArrayConstructorForwardsValues () static void AssumeTrimmableConstructorParameterMarshalling () { - if (!IsTrimmableTypeMapEnabled ()) { + if (!RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("Legacy TypeManager.n_Activate does not marshal string, short, or array constructor parameters; this case validates trimmable constructor UCO parameter marshalling."); } } - static bool IsTrimmableTypeMapEnabled () - => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; - static T CreateFromJava (string constructorSignature, params JValue [] arguments) where T : Java.Lang.Object { @@ -576,7 +573,7 @@ static void AssertRegisteredSame (T instance) var registered = Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer); try { Assert.AreSame (instance, registered); - if (IsTrimmableTypeMapEnabled ()) { + if (RuntimeFeature.TrimmableTypeMap) { Assert.AreEqual (Java.Lang.JavaSystem.IdentityHashCode (instance), instance.JniIdentityHashCode); } } finally { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs index 65989c168f8..be1ee4b06f5 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ExportTests.cs @@ -254,14 +254,11 @@ public void Export_Method_NestedJniCall_PreservesExceptionFromInnerExport () static void AssumeTrimmableExportExceptionRouting () { - if (!IsTrimmableTypeMapEnabled ()) { + if (!RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("[Export] exception routing coverage is only relevant for the trimmable typemap path."); } } - static bool IsTrimmableTypeMapEnabled () - => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; - // --------------------------------------------------------------- // Group D — [ExportField] runtime visibility from Java // --------------------------------------------------------------- diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs index 5480c4daa8d..bfd4175bab4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/TrimmableTypeMapRuntimeCoverageTests.cs @@ -221,13 +221,10 @@ static T CreateFromJava () static void AssumeTrimmableTypeMapEnabled () { - if (!IsTrimmableTypeMapEnabled ()) { + if (!RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } - - static bool IsTrimmableTypeMapEnabled () - => AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; } class TrimmableRuntimeTextWatcher : Java.Lang.Object, ITextWatcher diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs index b81450c8b7b..53d558fe61b 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/TrustManagerMarshallingTests.cs @@ -53,12 +53,9 @@ public void JavaInterfaceLookup_BaseInterfaceReturnType_UsesDerivedInterfaceProx static void AssumeTrimmableTypeMapEnabled () { - if (!IsTrimmableTypeMapEnabled ()) { + if (!RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("TrimmableTypeMap feature switch is off; test only relevant for the trimmable typemap path."); } } - - static bool IsTrimmableTypeMapEnabled () - => System.AppContext.TryGetSwitch ("Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap", out bool isEnabled) && isEnabled; } } From 6071fd00061fa6dba797d9149c01e82754ce718f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 14:30:02 +0200 Subject: [PATCH 26/47] Support trimmable custom value marshalers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 8 ++ .../Generator/ModelBuilder.cs | 22 +++++ .../Generator/RootTypeMapAssemblyGenerator.cs | 73 ++++++++++++--- .../Generator/TypeMapAssemblyEmitter.cs | 88 +++++++++++++++++++ .../Generator/TypeMapAssemblyGenerator.cs | 5 +- .../Scanner/AssemblyIndex.cs | 50 ++++++++++- .../Scanner/JavaPeerInfo.cs | 8 ++ .../Scanner/JavaPeerScanner.cs | 9 ++ .../TrimmableTypeMapGenerator.cs | 38 +++++--- .../TrimmableTypeMapTypes.cs | 1 + .../JavaMarshalValueManager.cs | 2 + .../TrimmableTypeMap.cs | 28 ++++++ .../Generator/FixtureTestBase.cs | 6 +- .../RootTypeMapAssemblyGeneratorTests.cs | 21 ++++- .../TypeMapAssemblyGeneratorTests.cs | 31 ++++++- .../Scanner/JavaPeerScannerTests.cs | 10 +++ .../TestFixtures/StubAttributes.cs | 12 +++ .../TestFixtures/TestTypes.cs | 12 +++ .../ConstructorActivationTests.cs | 2 + .../TestInstrumentation.cs | 8 -- 20 files changed, 395 insertions(+), 39 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index a90f0d7dbeb..bd97741d595 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -35,6 +35,8 @@ sealed class TypeMapAssemblyData /// public List Associations { get; } = new (); + public List ValueMarshalers { get; } = new (); + /// /// Alias holder types to emit — one per alias group (≥2 types sharing a JNI name). /// @@ -405,6 +407,12 @@ sealed record TypeMapAssociationData public required string AliasProxyTypeReference { get; init; } } +sealed record ValueMarshalerData +{ + public required TypeRefData ValueType { get; init; } + public required TypeRefData MarshalerType { get; init; } +} + /// /// An alias holder class to generate in the TypeMap assembly. /// Extends JavaPeerProxy and implements IJavaPeerAliases. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 4671394e936..97640757293 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -41,10 +41,16 @@ static class ModelBuilder /// for ranks 1... 0 disables array entry emission. /// public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) + => Build (peers, [], outputPath, assemblyName, maxArrayRank); + + public static TypeMapAssemblyData Build (IReadOnlyList peers, IReadOnlyList valueMarshalers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); } + if (valueMarshalers is null) { + throw new ArgumentNullException (nameof (valueMarshalers)); + } if (outputPath is null) { throw new ArgumentNullException (nameof (outputPath)); } @@ -100,6 +106,18 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } BuildNativeRegistrations (model); + foreach (var valueMarshaler in valueMarshalers.OrderBy (m => m.ValueTypeName, StringComparer.Ordinal)) { + model.ValueMarshalers.Add (new ValueMarshalerData { + ValueType = new TypeRefData { + ManagedTypeName = valueMarshaler.ValueTypeName, + AssemblyName = valueMarshaler.ValueTypeAssemblyName, + }, + MarshalerType = new TypeRefData { + ManagedTypeName = valueMarshaler.MarshalerTypeName, + AssemblyName = valueMarshaler.MarshalerAssemblyName, + }, + }); + } // Compute IgnoresAccessChecksTo from cross-assembly references var referencedAssemblies = new SortedSet (StringComparer.Ordinal); @@ -112,6 +130,10 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } } + foreach (var valueMarshaler in model.ValueMarshalers) { + AddIfCrossAssembly (referencedAssemblies, valueMarshaler.ValueType.AssemblyName, assemblyName); + AddIfCrossAssembly (referencedAssemblies, valueMarshaler.MarshalerType.AssemblyName, assemblyName); + } // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior). diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index b54c2f535ec..3587360caa1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -80,7 +80,7 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// sentinels. Must match the value passed to the per-assembly generators. 0 means /// no array sentinels were emitted; the loader passes null for array maps. /// - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0, IReadOnlyList? valueMarshalerTypeMapNames = null) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -135,7 +135,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName, valueMarshalerTypeMapNames ?? []); pe.WritePE (stream); } @@ -201,7 +201,7 @@ static void EmitAssemblyTargetAttribute (PEAssemblyBuilder pe, MemberReferenceHa pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName, IReadOnlyList valueMarshalerTypeMapNames) { var metadata = pe.Metadata; @@ -226,6 +226,8 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand var getProxyMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateProxyTypeMapping", iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); + var valueMarshalerMaps = CreateValueMarshalerMapContext (pe, valueMarshalerTypeMapNames); + // Define the TypeMapLoader type (public static class in Microsoft.Android.Runtime namespace) metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class, @@ -243,25 +245,65 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (maxArrayRank > 0) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank, assemblyName); + initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank, assemblyName, valueMarshalerMaps); } else { var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName); + EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName, valueMarshalerMaps); } } else { var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); if (maxArrayRank > 0) { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName, valueMarshalerMaps); } else { var initializeRef = AddInitializeAggregateNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMapNoArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName, valueMarshalerMaps); } } } + sealed class ValueMarshalerMapContext + { + public EntityHandle[] RegisterRefs { get; init; } = []; + public bool HasMaps => RegisterRefs.Length > 0; + } + + static ValueMarshalerMapContext CreateValueMarshalerMapContext ( + PEAssemblyBuilder pe, + IReadOnlyList valueMarshalerTypeMapNames) + { + if (valueMarshalerTypeMapNames.Count == 0) { + return new ValueMarshalerMapContext (); + } + + var registerRefs = new EntityHandle [valueMarshalerTypeMapNames.Count]; + for (int i = 0; i < valueMarshalerTypeMapNames.Count; i++) { + var asmRef = pe.FindOrAddAssemblyRef (valueMarshalerTypeMapNames [i]); + var valueMarshalerMappingRef = pe.Metadata.AddTypeReference (asmRef, + pe.Metadata.GetOrAddString ("_TypeMap"), + pe.Metadata.GetOrAddString ("ValueMarshalerMapping")); + registerRefs [i] = pe.AddMemberRef (valueMarshalerMappingRef, "RegisterValueMarshalers", + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); + } + + return new ValueMarshalerMapContext { + RegisterRefs = registerRefs, + }; + } + + static void EmitInitializeValueMarshalerMaps (TrackedInstructionEncoder encoder, ValueMarshalerMapContext valueMarshalerMaps) + { + if (!valueMarshalerMaps.HasMaps) { + return; + } + + for (int i = 0; i < valueMarshalerMaps.RegisterRefs.Length; i++) { + encoder.Call (valueMarshalerMaps.RegisterRefs [i], parameterCount: 0); + } + } + /// /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either /// arrayMapsByAssemblyAndRank[N][maxArrayRank] from per-assembly @@ -275,7 +317,8 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, TypeSpecificationHandle externalDictArrayTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, int maxArrayRank, - string assemblyName) + string assemblyName, + ValueMarshalerMapContext valueMarshalerMaps) { var count = perAssemblyTypeMapNames.Count; @@ -307,6 +350,7 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, encoder.LoadLocal (1); EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); + EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }, encodeLocals: localsSig => { @@ -348,7 +392,8 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, - string assemblyName) + string assemblyName, + ValueMarshalerMapContext valueMarshalerMaps) { var count = perAssemblyTypeMapNames.Count; @@ -376,6 +421,7 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, encoder.LoadLocal (0); encoder.LoadLocal (1); encoder.Call (initializeRef, parameterCount: 2); + EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }, encodeLocals: localsSig => { @@ -437,7 +483,8 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, IReadOnlyList perAssemblyTypeMapNames, int maxArrayRank, - string assemblyName) + string assemblyName, + ValueMarshalerMapContext valueMarshalerMaps) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -452,6 +499,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); + EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }); } @@ -463,7 +511,8 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, - string assemblyName) + string assemblyName, + ValueMarshalerMapContext valueMarshalerMaps) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -476,6 +525,7 @@ static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, Entit encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true); encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); encoder.Call (initializeRef, parameterCount: 2); + EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }); } @@ -640,4 +690,5 @@ static void EncodeIReadOnlyDictType (BlobBuilder blob, TypeReferenceHandle iRead blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (systemTypeRef)); } + } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 8ab18800ea5..c28bd7a1eca 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -92,6 +92,9 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemArrayRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; + TypeReferenceHandle _jniValueMarshalerRef; + TypeReferenceHandle _trimmableTypeMapRef; + TypeReferenceHandle _valueMarshalerFactoryRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; TypeReferenceHandle _javaPeerAliasesAttrRef; @@ -222,6 +225,10 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) EmitTypeMapAssociationAttribute (assoc); } + if (model.ValueMarshalers.Count > 0) { + EmitValueMarshalerMapping (model.ValueMarshalers); + } + _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); } @@ -302,6 +309,12 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); + _jniValueMarshalerRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniValueMarshaler")); + _trimmableTypeMapRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); + _valueMarshalerFactoryRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("ValueMarshalerFactory")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -1766,6 +1779,81 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } + void EmitValueMarshalerMapping (IReadOnlyList valueMarshalers) + { + var metadata = _pe.Metadata; + var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, + metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); + + int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; + int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; + + var factoryMethods = new MethodDefinitionHandle [valueMarshalers.Count]; + for (int i = 0; i < valueMarshalers.Count; i++) { + factoryMethods [i] = EmitValueMarshalerFactoryMethod (i, valueMarshalers [i]); + } + + EmitRegisterValueMarshalersMethod (valueMarshalers, factoryMethods); + + metadata.AddTypeDefinition ( + TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.BeforeFieldInit, + metadata.GetOrAddString ("_TypeMap"), + metadata.GetOrAddString ("ValueMarshalerMapping"), + objectRef, + MetadataTokens.FieldDefinitionHandle (typeFieldStart), + MetadataTokens.MethodDefinitionHandle (typeMethodStart)); + } + + MethodDefinitionHandle EmitValueMarshalerFactoryMethod (int index, ValueMarshalerData valueMarshaler) + { + var marshalerType = _pe.ResolveTypeRef (valueMarshaler.MarshalerType); + var marshalerCtor = _pe.AddMemberRef (marshalerType, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); + + return _pe.EmitBody ($"CreateValueMarshaler_{index}", + MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, + rt => rt.Type ().Type (_jniValueMarshalerRef, false), + p => { }), + encoder => { + encoder.NewObject (marshalerCtor, parameterCount: 0); + encoder.Return (returnsValue: true); + }); + } + + void EmitRegisterValueMarshalersMethod (IReadOnlyList valueMarshalers, MethodDefinitionHandle[] factoryMethods) + { + var factoryCtor = _pe.AddMemberRef (_valueMarshalerFactoryRef, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Object (); + p.AddParameter ().Type ().IntPtr (); + })); + var registerValueMarshaler = _pe.AddMemberRef (_trimmableTypeMapRef, "RegisterValueMarshaler", + sig => sig.MethodSignature ().Parameters (2, + rt => rt.Void (), + p => { + p.AddParameter ().Type ().Type (_systemTypeRef, false); + p.AddParameter ().Type ().Type (_valueMarshalerFactoryRef, false); + })); + + _pe.EmitBody ("RegisterValueMarshalers", + MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, + sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), + encoder => { + for (int i = 0; i < valueMarshalers.Count; i++) { + encoder.LoadToken (_pe.ResolveTypeRef (valueMarshalers [i].ValueType)); + encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); + encoder.OpCode (ILOpCode.Ldnull); + encoder.LoadFunction (factoryMethods [i]); + encoder.NewObject (factoryCtor, parameterCount: 2); + encoder.Call (registerValueMarshaler, parameterCount: 2); + } + encoder.Return (); + }); + } + /// /// Writes the ECMA-335 blob for a closed generic value type with a single value-type argument. /// E.g., ReadOnlySpan<JniNativeMethod>. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 48ca89f45bc..03105bb15de 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -29,8 +29,11 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// Max rank for per-rank array TypeMap entries. 0 disables. public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) + => Generate (peers, [], stream, assemblyName, useSharedTypemapUniverse, maxArrayRank); + + public void Generate (IReadOnlyList peers, IReadOnlyList valueMarshalers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); + var model = ModelBuilder.Build (peers, valueMarshalers, assemblyName + ".dll", assemblyName, maxArrayRank); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 64ca498c814..e9f6d43873f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -33,6 +33,8 @@ sealed class AssemblyIndex : IDisposable /// public Dictionary AttributesByType { get; } = new (); + public List ValueMarshalers { get; } = new (); + /// /// Type references grouped by referenced assembly name. /// @@ -103,12 +105,16 @@ void Build () TypesByFullName [fullName] = typeHandle; - var (registerInfo, attrInfo) = ParseAttributes (typeDef); + var (registerInfo, attrInfo, valueMarshalerInfo) = ParseAttributes (typeDef, fullName); if (attrInfo is not null) { AttributesByType [typeHandle] = attrInfo; } + if (valueMarshalerInfo is not null) { + ValueMarshalers.Add (valueMarshalerInfo); + } + if (registerInfo is not null) { RegisterInfoByType [typeHandle] = registerInfo; } @@ -131,10 +137,11 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen return false; } - (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) + (RegisterInfo? register, TypeAttributeInfo? attrs, ValueMarshalerInfo? valueMarshaler) ParseAttributes (TypeDefinition typeDef, string fullName) { RegisterInfo? registerInfo = null; TypeAttributeInfo? attrInfo = null; + ValueMarshalerInfo? valueMarshalerInfo = null; // Collect intent filters and metadata separately to avoid ordering issues: // if [IntentFilter] appears before [Activity], we must not create attrInfo @@ -150,7 +157,9 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen continue; } - if (attrName == "RegisterAttribute") { + if (IsCustomAttributeMatch (ca, Reader, "Java.Interop", "JniValueMarshalerAttribute")) { + valueMarshalerInfo = ParseValueMarshalerAttribute (ca, fullName); + } else if (attrName == "RegisterAttribute") { registerInfo = ParseRegisterAttribute (ca); registerInfo = registerInfo with { JniName = registerInfo.JniName.Replace ('.', '/') }; } else if (attrName == "JniTypeSignatureAttribute") { @@ -211,7 +220,40 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen } } - return (registerInfo, attrInfo); + return (registerInfo, attrInfo, valueMarshalerInfo); + } + + ValueMarshalerInfo? ParseValueMarshalerAttribute (CustomAttribute ca, string valueTypeName) + { + var value = DecodeAttribute (ca); + if (value.FixedArguments.Length == 0 || value.FixedArguments [0].Value is not string marshalerTypeReference) { + return null; + } + + var marshalerType = ParseAttributeTypeReference (marshalerTypeReference, AssemblyName); + return new ValueMarshalerInfo { + ValueTypeName = valueTypeName, + ValueTypeAssemblyName = AssemblyName, + MarshalerTypeName = marshalerType.TypeName, + MarshalerAssemblyName = marshalerType.AssemblyName, + }; + } + + static (string TypeName, string AssemblyName) ParseAttributeTypeReference (string typeReference, string defaultAssemblyName) + { + var commaIndex = typeReference.IndexOf (','); + if (commaIndex < 0) { + return (typeReference.Trim (), defaultAssemblyName); + } + + var typeName = typeReference.Substring (0, commaIndex).Trim (); + var assemblyName = typeReference.Substring (commaIndex + 1).Trim (); + var assemblyNameEnd = assemblyName.IndexOf (','); + if (assemblyNameEnd >= 0) { + assemblyName = assemblyName.Substring (0, assemblyNameEnd).Trim (); + } + + return (typeName, assemblyName); } static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 7382a4ed60a..6eb7795572b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -157,6 +157,14 @@ public sealed record JavaPeerInfo public ComponentInfo? ComponentAttribute { get; init; } } +public sealed record ValueMarshalerInfo +{ + public required string ValueTypeName { get; init; } + public required string ValueTypeAssemblyName { get; init; } + public required string MarshalerTypeName { get; init; } + public required string MarshalerAssemblyName { get; init; } +} + /// /// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index d03d8dcc853..49138574a72 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -114,6 +114,15 @@ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> as return new List (resultsByQualifiedName.Values); } + internal List GetValueMarshalers () + { + var valueMarshalers = new List (); + foreach (var index in assemblyCache.Values) { + valueMarshalers.AddRange (index.ValueMarshalers); + } + return valueMarshalers; + } + void MarkFrameworkArrayEntryPeers (IEnumerable peers) { var referencedFrameworkTypes = new HashSet (StringComparer.Ordinal); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 95d0446b205..65cc2695c98 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -44,10 +44,10 @@ public TrimmableTypeMapResult Execute ( throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); } - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); - if (allPeers.Count == 0) { + var (allPeers, valueMarshalers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); + if (allPeers.Count == 0 && valueMarshalers.Count == 0) { logger.LogNoJavaPeerTypesFound (); - return new TrimmableTypeMapResult ([], [], allPeers); + return new TrimmableTypeMapResult ([], [], allPeers, []); } MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); @@ -56,7 +56,7 @@ public TrimmableTypeMapResult Execute ( PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + ? GenerateTypeMapAssemblies (allPeers, valueMarshalers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -71,7 +71,7 @@ public TrimmableTypeMapResult Execute ( ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) : null; - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, valueMarshalers, manifest, appRegTypes); } internal static List CollectApplicationRegistrationTypes (List allPeers) @@ -155,17 +155,19 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) + (List peers, List valueMarshalers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) { using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); var peers = scanner.Scan (assemblies); + var valueMarshalers = scanner.GetValueMarshalers (); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); - return (peers, manifestInfo); + return (peers, valueMarshalers, manifestInfo); } List GenerateTypeMapAssemblies ( List allPeers, + List valueMarshalers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank) @@ -190,21 +192,37 @@ List GenerateTypeMapAssemblies ( .ToList (); } + var valueMarshalersByAssembly = valueMarshalers + .GroupBy (m => m.ValueTypeAssemblyName, StringComparer.Ordinal) + .ToDictionary (g => g.Key, g => g.ToList (), StringComparer.Ordinal); + var assemblyNames = new SortedSet (peersByAssembly.Select (p => p.AssemblyName), StringComparer.Ordinal); + assemblyNames.UnionWith (valueMarshalersByAssembly.Keys); + var peersByAssemblyMap = peersByAssembly.ToDictionary (p => p.AssemblyName, p => p.Peers, StringComparer.Ordinal); + var generatedAssemblies = new List (); var perAssemblyNames = new List (); + var valueMarshalerTypeMapNames = new List (); var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion); - foreach (var (assemblyName, peers) in peersByAssembly) { + foreach (var assemblyName in assemblyNames) { + peersByAssemblyMap.TryGetValue (assemblyName, out var peers); + peers ??= []; + valueMarshalersByAssembly.TryGetValue (assemblyName, out var assemblyValueMarshalers); + assemblyValueMarshalers ??= []; + string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); + if (assemblyValueMarshalers.Count > 0) { + valueMarshalerTypeMapNames.Add (typeMapAssemblyName); + } var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); + generator.Generate (peers, assemblyValueMarshalers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank, valueMarshalerTypeMapNames: valueMarshalerTypeMapNames); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 2e162d0d61b..807e2998351 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -8,6 +8,7 @@ public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, IReadOnlyList AllPeers, + IReadOnlyList? ValueMarshalers = null, GeneratedManifest? Manifest = null, IReadOnlyList? ApplicationRegistrationTypes = null) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index b936736534c..d8113771187 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1138,6 +1138,8 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) throw new ArgumentException ("Generic type definitions are not supported.", nameof (type)); } + if (TrimmableTypeMap.Instance.TryGetValueMarshaler (type, out var customMarshaler)) + return customMarshaler; if (TrimmableValueMarshalerHelper.TryGetPrimitiveValueMarshaler (type, out var primitiveMarshaler)) return primitiveMarshaler; if (type == typeof (string)) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index fd47840991f..a15c1e3d697 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -12,6 +12,8 @@ namespace Microsoft.Android.Runtime; +delegate JniValueMarshaler ValueMarshalerFactory (); + /// /// Central type map for the trimmable typemap path. Owns the ITypeMap /// and provides peer creation, invoker resolution, container factories, and native @@ -37,6 +39,8 @@ public class TrimmableTypeMap "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); readonly ITypeMap _typeMap; + readonly Dictionary _valueMarshalerFactories = new (); + readonly ConcurrentDictionary _valueMarshalerCache = new (); readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); readonly ConcurrentDictionary<(string ClassName, Type TargetType), JavaPeerProxy> _interfaceProxyCache = new (); @@ -136,6 +140,30 @@ static void InitializeCore (ITypeMap typeMap) } } + internal static void RegisterValueMarshaler (Type valueType, ValueMarshalerFactory factory) + { + ArgumentNullException.ThrowIfNull (valueType); + ArgumentNullException.ThrowIfNull (factory); + + lock (s_initLock) { + var instance = s_instance ?? throw new InvalidOperationException ( + "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); + + instance._valueMarshalerFactories.Add (valueType, factory); + } + } + + internal bool TryGetValueMarshaler (Type type, [NotNullWhen (true)] out JniValueMarshaler? marshaler) + { + if (!_valueMarshalerFactories.ContainsKey (type)) { + marshaler = null; + return false; + } + + marshaler = _valueMarshalerCache.GetOrAdd (type, static (t, self) => self._valueMarshalerFactories [t] (), this); + return true; + } + internal static unsafe void RegisterNativeMethods () { lock (s_initLock) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d824733c2f2..db76e70ab21 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -22,19 +22,21 @@ private protected static string TestFixtureAssemblyPath { } } - static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { + static readonly Lazy<(List peers, List valueMarshalers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); var assemblies = new [] { (assemblyName, peReader) }; var peers = scanner.Scan (assemblies); + var valueMarshalers = scanner.GetValueMarshalers (); var manifestInfo = scanner.ScanAssemblyManifestInfo (); peReader.Dispose (); - return (peers, manifestInfo); + return (peers, valueMarshalers, manifestInfo); }); private protected static List ScanFixtures () => _cachedScanResult.Value.peers; + private protected static List ScanFixtureValueMarshalers () => _cachedScanResult.Value.valueMarshalers; private protected static List ScanFixtures (string packageNamingPolicy) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index 2ad242d096a..b5fcc56347c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -12,11 +12,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0, IReadOnlyList? valueMarshalerTypeMapNames = null) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank, valueMarshalerTypeMapNames: valueMarshalerTypeMapNames); stream.Position = 0; return stream; } @@ -345,6 +345,23 @@ public void Generate_MergedMode_ReferencesRootAnchorOnly () Assert.DoesNotContain ("_Mono.Android.TypeMap", asmRefs); } + [Fact] + public void Generate_WithValueMarshalerMaps_ReferencesPerAssemblyRegistration () + { + using var stream = GenerateRootAssembly ( + ["_App.TypeMap", "_Mono.Android.TypeMap"], + useSharedTypemapUniverse: true, + valueMarshalerTypeMapNames: ["_App.TypeMap"]); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var asmRefs = reader.AssemblyReferences + .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) + .ToList (); + Assert.Contains ("_App.TypeMap", asmRefs); + Assert.Contains ("RegisterValueMarshalers", GetMemberRefNames (reader)); + } + [Fact] public void Generate_AggregateMode_ReferencesPerAssemblyAnchors () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 28b2ad29b2a..e513b7e751f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -14,10 +14,13 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { static MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap") + => GenerateAssembly (peers, [], assemblyName); + + static MemoryStream GenerateAssembly (IReadOnlyList peers, IReadOnlyList valueMarshalers, string assemblyName = "TestTypeMap") { var stream = new MemoryStream (); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (peers, stream, assemblyName); + generator.Generate (peers, valueMarshalers, stream, assemblyName); stream.Position = 0; return stream; } @@ -284,6 +287,32 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } + [Fact] + public void Generate_ValueMarshalerMapping_EmitsLazyFactoryMap () + { + var valueMarshalers = ScanFixtureValueMarshalers (); + + using var stream = GenerateAssembly ([], valueMarshalers, "ValueMarshalerTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + + var mappingType = reader.TypeDefinitions + .Select (h => reader.GetTypeDefinition (h)) + .Single (t => + reader.GetString (t.Namespace) == "_TypeMap" && + reader.GetString (t.Name) == "ValueMarshalerMapping"); + var methodNames = mappingType.GetMethods () + .Select (h => reader.GetMethodDefinition (h)) + .Select (m => reader.GetString (m.Name)) + .ToList (); + + Assert.Contains ("CreateValueMarshaler_0", methodNames); + Assert.Contains ("RegisterValueMarshalers", methodNames); + Assert.Contains ("ValueMarshalerFactory", GetTypeRefNames (reader)); + Assert.Contains ("JniValueMarshaler", GetTypeRefNames (reader)); + Assert.Contains ("DemoValueTypeMarshaler", GetTypeRefNames (reader)); + } + [Fact] public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 3e9537422fd..a641ef4e35c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -171,6 +171,16 @@ public void Scan_JniTypeSignature_DoNotGenerateAcw () Assert.True (nonGenerated.DoNotGenerateAcw, "NonGeneratedJavaObject has GenerateJavaPeer=false"); } + [Fact] + public void Scan_JniValueMarshalerAttribute_IsDiscovered () + { + var valueMarshaler = Assert.Single (ScanFixtureValueMarshalers ()); + Assert.Equal ("MyApp.ValueMarshalers.DemoValueType", valueMarshaler.ValueTypeName); + Assert.Equal ("TestFixtures", valueMarshaler.ValueTypeAssemblyName); + Assert.Equal ("MyApp.ValueMarshalers.DemoValueTypeMarshaler", valueMarshaler.MarshalerTypeName); + Assert.Equal ("TestFixtures", valueMarshaler.MarshalerAssemblyName); + } + [Fact] public void Scan_JniTypeSignature_DuplicateJniName_BothPresent () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 050741c3e20..96df937d1ec 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -178,6 +178,18 @@ public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameP namespace Java.Interop { + public abstract class JniValueMarshaler + { + } + + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false)] + public sealed class JniValueMarshalerAttribute : Attribute + { + public Type MarshalerType { get; } + + public JniValueMarshalerAttribute (Type marshalerType) => MarshalerType = marshalerType; + } + [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] public sealed class ExportAttribute : Attribute { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index ca0b8f4adf8..70828cb9fce 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -232,6 +232,18 @@ public NonRequiredFrameworkAcw () { } } } +namespace MyApp.ValueMarshalers +{ + [Java.Interop.JniValueMarshaler (typeof (DemoValueTypeMarshaler))] + internal struct DemoValueType + { + } + + internal sealed class DemoValueTypeMarshaler : Java.Interop.JniValueMarshaler + { + } +} + namespace MyApp { [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index be9be6cc42c..ab3e142568d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -8,6 +8,8 @@ using Java.Interop; +using Microsoft.Android.Runtime; + using NUnit.Framework; namespace Java.InteropTests diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index a5fb8f3e18e..8e432d41098 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -88,14 +88,6 @@ protected override IEnumerable? ExcludedTestNames { // Tests from the external Java.Interop-Tests assembly that still fail under // the trimmable typemap and are not covered by a category. return new [] { - // Value-marshaling cases that are still failing under the trimmable - // value manager. Keep these granular so passing value-marshaler tests - // stay enabled while the remaining gaps are fixed. - "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", - "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", - "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.DestroyArgumentState", - "Java.InteropTests.JniValueMarshaler_DemoValueType_ContractTests.JniValueMarshalerContractTests`1.TestIsJniValueType", - // Current trimmable runtime exception/type-manager behavior differs from // the legacy typemap path these tests assert against. "Java.InteropTests.ConstructorActivationTests.JavaSideThrowingConstructorPropagatesException", From d45a519cce0e0deb5b52019bf291c33908869462 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 14:54:02 +0200 Subject: [PATCH 27/47] Unwrap trimmable proxy throwables Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 4 ++++ .../Xamarin.Android.RuntimeTests/TestInstrumentation.cs | 8 ++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 3c3b81f357d..9c68e122258 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -58,6 +58,10 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f if (!reference.IsValid) return null; var peeked = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); + if (peeked is JavaProxyThrowable proxyThrowable) { + JniObjectReference.Dispose (ref reference, options); + return proxyThrowable.InnerException; + } var peekedExc = peeked as Exception; if (peekedExc == null) { var throwable = Java.Lang.Object.GetObject (reference.Handle, JniHandleOwnership.DoNotTransfer); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index 8e432d41098..2a7537d3765 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -88,17 +88,13 @@ protected override IEnumerable? ExcludedTestNames { // Tests from the external Java.Interop-Tests assembly that still fail under // the trimmable typemap and are not covered by a category. return new [] { - // Current trimmable runtime exception/type-manager behavior differs from - // the legacy typemap path these tests assert against. - "Java.InteropTests.ConstructorActivationTests.JavaSideThrowingConstructorPropagatesException", - "Java.InteropTests.ExportTests.Export_Method_NestedJniCall_PreservesExceptionFromInnerExport", - "Java.InteropTests.ExportTests.Export_Method_Throws_PrimitiveReturn_SurfacesAsManagedException", + // Current trimmable runtime type-manager behavior differs from the + // legacy typemap path these tests assert against. "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", "Java.InteropTests.JniRuntimeTest.BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault", "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", "Java.InteropTests.JniTypeManagerTests.GetType", "Java.InteropTests.JniTypeManagerTests.GetTypeSignature_Type", - "Xamarin.Android.RuntimeTests.ExceptionTest.InnerExceptionIsSet", }; } } From 4282882c98456b03340e0b326c1c20b663a50865 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 15:41:52 +0200 Subject: [PATCH 28/47] Support trimmable type manager lookups Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMapTypeManager.cs | 198 ++++++++++++++++-- .../TestInstrumentation.cs | 19 -- 2 files changed, 180 insertions(+), 37 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 24c09bb73fa..e345afdc7f3 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -17,11 +17,30 @@ class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager { internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + static readonly Type[] EmptyTypeArray = []; + static readonly Dictionary ArrayTypes = []; + static readonly Dictionary JavaObjectArrayTypes = []; + static readonly Dictionary PrimitiveArrayTypes = []; readonly ConcurrentDictionary _typeSignatureCache = new (); // This type manager has 2 core APIs: GetTypeSignatureCore for managed-to-Java lookups, and GetTypeForSimpleReference for Java-to-managed lookups. // The rest of the APIs are unsupported and will throw if called, as they are not needed internally anywhere. + static TrimmableTypeMapTypeManager () + { + AddKnownArrayTypes (); + AddKnownArrayTypes (); + + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + AddKnownPrimitiveArrayTypes (); + } + protected override JniTypeSignature GetTypeSignatureCore (Type type) { return _typeSignatureCache.GetOrAdd (type, GetTypeSignatureUncached); @@ -34,6 +53,14 @@ static JniTypeSignature GetTypeSignatureUncached (Type type) return signature.AddArrayRank (rank); } + if (type.IsGenericType) { + var genericDefinition = type.GetGenericTypeDefinition (); + if (genericDefinition == typeof (JavaArray<>) || genericDefinition == typeof (JavaObjectArray<>)) { + var elementSignature = GetTypeSignatureUncached (type.GenericTypeArguments [0]); + return elementSignature.AddArrayRank (rank + 1); + } + } + // Walk the base type chain for managed-only subclasses (e.g., JavaProxyThrowable // extends Java.Lang.Error but has no [Register] attribute itself). Type? currentType = type; @@ -144,7 +171,20 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur [return: DynamicallyAccessedMembers (Methods | Constructors | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { - var builtInType = jniSimpleReference switch { + var builtInType = GetBuiltInTypeForSimpleReference (jniSimpleReference); + if (builtInType is not null) { + return builtInType; + } + + return TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types) && types.Length > 0 + ? types [0].Type + : null; + } + + [return: DynamicallyAccessedMembers (Methods | Constructors | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] + static Type? GetBuiltInTypeForSimpleReference (string jniSimpleReference) + { + return jniSimpleReference switch { "java/lang/String" => typeof (string), "V" => typeof (void), "Z" => typeof (bool), @@ -165,14 +205,6 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur "java/lang/Double" => typeof (double?), _ => null, }; - - if (builtInType is not null) { - return builtInType; - } - - return TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types) && types.Length > 0 - ? types [0].Type - : null; } // Remapping APIs for InTune support @@ -205,9 +237,12 @@ protected override IEnumerable GetSimpleReferences (Type type) $"Simple reference lookup should use {nameof (GetTypeSignatureCore)} to get the full type signature, including simple reference."); public override IEnumerable GetTypes (JniTypeSignature typeSignature) - => throw new UnreachableException ( - $"{nameof (GetTypes)} should not be called in the trimmable typemap path. " + - $"Java-to-managed constructor activation should use generated {nameof (JavaPeerProxy)} instances."); + { + if (typeSignature.SimpleReference is null) { + return EmptyTypeArray; + } + return CreateGetTypesEnumerator (typeSignature); + } public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) => throw new UnreachableException ( @@ -215,14 +250,141 @@ public override IEnumerable GetReflectionConstructi $"Managed peer construction should use generated {nameof (JavaPeerProxy)} instances."); protected override IEnumerable GetTypeSignaturesCore (Type type) - => throw new UnreachableException ( - $"{nameof (GetTypeSignaturesCore)} should not be called in the trimmable typemap path. " + - $"Runtime type signature lookup should use {nameof (GetTypeSignatureCore)}."); + { + var signature = GetTypeSignatureCore (type); + if (signature.IsValid) { + yield return signature; + } + } protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) - => throw new UnreachableException ( - $"{nameof (GetTypesForSimpleReference)} should not be called in the trimmable typemap path. " + - $"Simple reference lookup should use {nameof (GetTypeForSimpleReference)}."); + { + var builtInType = GetBuiltInTypeForSimpleReference (jniSimpleReference); + if (builtInType is not null) { + yield return builtInType; + } + + if (TrimmableTypeMap.Instance.TryGetTargetTypes (jniSimpleReference, out var types)) { + foreach (var type in types) { + yield return type.Type; + } + } + } + + IEnumerable CreateGetTypesEnumerator (JniTypeSignature typeSignature) + { + if (!typeSignature.IsValid) { + yield break; + } + + foreach (var type in GetTypesForSimpleReference (typeSignature.SimpleReference ?? throw new InvalidOperationException ("Should not be reached"))) { + if (typeSignature.ArrayRank == 0) { + yield return type; + continue; + } + + if (IsKeywordSignature (typeSignature)) { + foreach (var primitiveArrayType in GetPrimitiveArrayTypesForSimpleReference (typeSignature, type)) { + yield return primitiveArrayType; + } + continue; + } + + if (TryMakeJavaObjectArrayType (type, typeSignature.ArrayRank, out var javaObjectArrayType)) { + yield return javaObjectArrayType; + } + + if (TryMakeArrayType (type, typeSignature.ArrayRank, out var arrayType)) { + yield return arrayType; + } + } + } + + IEnumerable GetPrimitiveArrayTypesForSimpleReference (JniTypeSignature typeSignature, Type type) + { + foreach (var primitiveArrayType in GetPrimitiveArrayTypes (type)) { + var rank = typeSignature.ArrayRank - 1; + if (TryMakeJavaObjectArrayType (primitiveArrayType, rank, out var javaObjectArrayType)) { + yield return javaObjectArrayType; + } + + if (TryMakeArrayType (primitiveArrayType, rank, out var arrayType)) { + yield return arrayType; + } + } + } + + static bool IsKeywordSignature (JniTypeSignature typeSignature) + => typeSignature.SimpleReference is string simpleReference && + typeSignature.QualifiedReference == new string ('[', typeSignature.ArrayRank) + simpleReference; + + static bool TryMakeArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) + { + arrayType = elementType; + for (int i = 0; i < rank; i++) { + if (!TryMakeArrayType (arrayType, out arrayType)) { + return false; + } + } + return true; + } + + static bool TryMakeArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) + { + if (ArrayTypes.TryGetValue (elementType, out arrayType)) { + return true; + } + + return TrimmableTypeMap.Instance.TryGetArrayType (elementType, out arrayType); + } + + static bool TryMakeJavaObjectArrayType (Type elementType, int rank, [NotNullWhen (true)] out Type? arrayType) + { + arrayType = elementType; + for (int i = 0; i < rank; i++) { + if (!TryMakeJavaObjectArrayType (arrayType, out arrayType)) { + return false; + } + } + return true; + } + + static bool TryMakeJavaObjectArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) + { + return JavaObjectArrayTypes.TryGetValue (elementType, out arrayType); + } + + static Type[] GetPrimitiveArrayTypes (Type primitiveType) + => PrimitiveArrayTypes.TryGetValue (primitiveType, out var types) ? types : EmptyTypeArray; + + static void AddKnownPrimitiveArrayTypes< + [DynamicallyAccessedMembers (Constructors)] + T, + [DynamicallyAccessedMembers (Constructors)] + TArray> () + { + AddKnownArrayTypes (); + AddKnownArrayTypes> (); + AddKnownArrayTypes> (); + AddKnownArrayTypes (); + PrimitiveArrayTypes [typeof (T)] = [ + typeof (T[]), + typeof (JavaArray), + typeof (JavaPrimitiveArray), + typeof (TArray), + ]; + } + + static void AddKnownArrayTypes< + [DynamicallyAccessedMembers (Constructors)] + T> () + { + ArrayTypes [typeof (T)] = typeof (T[]); + ArrayTypes [typeof (T[])] = typeof (T[][]); + ArrayTypes [typeof (T[][])] = typeof (T[][][]); + JavaObjectArrayTypes [typeof (T)] = typeof (JavaObjectArray); + JavaObjectArrayTypes [typeof (JavaObjectArray)] = typeof (JavaObjectArray>); + } public override void RegisterNativeMembers (JniType nativeClass, [DynamicallyAccessedMembers (Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] Type type, ReadOnlySpan methods) => throw new UnreachableException ( diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index 2a7537d3765..d91d6483a3d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -80,25 +80,6 @@ protected override IEnumerable? IncludedCategories { static bool HasAppContextSwitch (string key) => AppContext.TryGetSwitch (key, out var value) && value; - protected override IEnumerable? ExcludedTestNames { - get { - if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) - return null; - - // Tests from the external Java.Interop-Tests assembly that still fail under - // the trimmable typemap and are not covered by a category. - return new [] { - // Current trimmable runtime type-manager behavior differs from the - // legacy typemap path these tests assert against. - "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", - "Java.InteropTests.JniRuntimeTest.BuiltInSimpleReferenceMap_ContainsManagedPeerByDefault", - "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", - "Java.InteropTests.JniTypeManagerTests.GetType", - "Java.InteropTests.JniTypeManagerTests.GetTypeSignature_Type", - }; - } - } - public override void OnCreate (Bundle? arguments) { Java.Lang.JavaSystem.LoadLibrary ("reuse-threads"); From 152845b0b00751801d375bf825f2e59c73e2c977 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 15:51:56 +0200 Subject: [PATCH 29/47] Use trimmable CrossReferenceBridge fixture Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop-Tests.targets | 18 ++++++------ .../dot/jni/test/CrossReferenceBridge.java | 28 +++++++++++++++++++ 3 files changed, 38 insertions(+), 10 deletions(-) create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java diff --git a/external/Java.Interop b/external/Java.Interop index 7add4b224a9..b594f16552f 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 7add4b224a95026ff2f7abc77b7e44046109eaa4 +Subproject commit b594f16552f997823af45ffd9f82f723465af537 diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 838470a434b..260927b00b6 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -15,23 +15,23 @@ - diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java new file mode 100644 index 00000000000..dd6c4b762ea --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java @@ -0,0 +1,28 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +// Android trimmable typemap variant of the Java.Interop CrossReferenceBridge +// fixture. The desktop JVM fixture calls net.dot.jni.ManagedPeer.construct() +// from its constructor; the trimmable Android runtime intentionally does not +// ship ManagedPeer, and managed peer construction is handled by generated +// typemap proxies instead. +public class CrossReferenceBridge implements GCUserPeerable { + + ArrayList managedReferences = new ArrayList(); + + public CrossReferenceBridge () { + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} From 8105789bb9f1c3066dc62cd4a618a816d24b255a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 16:01:43 +0200 Subject: [PATCH 30/47] Use trimmable method binding fixtures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop-Tests.targets | 11 +++++- .../net/dot/jni/test/CallNonvirtualBase.java | 29 +++++++++++++++ .../dot/jni/test/CallNonvirtualDerived.java | 31 ++++++++++++++++ .../dot/jni/test/CallNonvirtualDerived2.java | 25 +++++++++++++ .../net/dot/jni/test/RenameClassBase1.java | 30 ++++++++++++++++ .../net/dot/jni/test/RenameClassBase2.java | 36 +++++++++++++++++++ .../net/dot/jni/test/RenameClassDerived.java | 31 ++++++++++++++++ 8 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java diff --git a/external/Java.Interop b/external/Java.Interop index b594f16552f..f9fe3470e71 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit b594f16552f997823af45ffd9f82f723465af537 +Subproject commit f9fe3470e71a79e7b54f9e08b7794ba627211b2d diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 260927b00b6..f167ef6b105 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -41,8 +41,17 @@ The trimmable variant uses a distinct output jar so MSBuild up-to-date checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\GetThis.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CrossReferenceBridge.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualBase.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualDerived.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualDerived2.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassBase1.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassBase2.java" /> + <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassDerived.java" /> + diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java new file mode 100644 index 00000000000..2dc73430987 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java @@ -0,0 +1,29 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualBase implements GCUserPeerable { + + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualBase () { + } + + boolean methodInvoked; + public void method () { + System.out.println ("CallNonvirtualBase.method() invoked!"); + methodInvoked = true; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java new file mode 100644 index 00000000000..9f7f11e831b --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java @@ -0,0 +1,31 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualDerived + extends CallNonvirtualBase + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualDerived () { + } + + boolean methodInvoked; + public void method () { + System.out.println ("CallNonvirtualDerived.method() invoked!"); + methodInvoked = true; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java new file mode 100644 index 00000000000..3ded295588a --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java @@ -0,0 +1,25 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualDerived2 + extends CallNonvirtualDerived + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualDerived2 () { + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java new file mode 100644 index 00000000000..5715e2651b3 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java @@ -0,0 +1,30 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassBase1 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassBase1 () { + System.out.println("RenameClassBase.()"); + } + + public int hashCode () { + System.out.println("RenameClassBase1.hashCode()"); + return 16; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java new file mode 100644 index 00000000000..cda0752a806 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java @@ -0,0 +1,36 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassBase2 + extends RenameClassBase1 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassBase2 () { + System.out.println("RenameClassBase.()"); + } + + public int hashCode () { + System.out.println("RenameClassBase2.hashCode()"); + return 32; + } + + public int myNewHashCode() { + System.out.println("RenameClassBase2.myNewHashCode()"); + return 33; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java new file mode 100644 index 00000000000..e6b53d82c53 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java @@ -0,0 +1,31 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassDerived + extends RenameClassBase2 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassDerived () { + System.out.println("RenameClassDerived.()"); + } + + public int hashCode () { + System.out.println("RenameClassDerived.hashCode()"); + return 64; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} From 7a7ca23cf083e49e9b4b4744ea717b49443570e8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 17:23:03 +0200 Subject: [PATCH 31/47] Support trimmable marshaler expressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../JavaMarshalValueManager.cs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/external/Java.Interop b/external/Java.Interop index f9fe3470e71..9bef884ca7b 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit f9fe3470e71a79e7b54f9e08b7794ba627211b2d +Subproject commit 9bef884ca7b61e810a2944444beb0b8a29631547 diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index d8113771187..e6808c2ee6c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -15,6 +16,7 @@ using System.Threading; using Android.Runtime; using Java.Interop; +using Java.Interop.Expressions; namespace Microsoft.Android.Runtime; @@ -863,6 +865,7 @@ sealed class TrimmableTypeMapValueManager : JniRuntime.JniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); + const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; readonly JavaMarshalPeerManager peerManager = new (nameof (TrimmableTypeMapValueManager)); @@ -1227,6 +1230,39 @@ public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniVa { DisposeReferenceState (ref state); } + + [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] + public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize) + { + if (IsJniValueType) { + return sourceValue; + } + return base.CreateParameterFromManagedExpression (context, sourceValue, synchronize); + } + + [RequiresDynamicCode (ExpressionRequiresUnreferencedCode)] + [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] + public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) + { + if (IsJniValueType) { + return sourceValue; + } + if (typeof (T) == typeof (string)) { + return CreateStringReturnValueFromManagedExpression (context, sourceValue); + } + return base.CreateReturnValueFromManagedExpression (context, sourceValue); + } + + Expression CreateStringReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) + { + Func createString = JniEnvironment.Strings.NewString; + + var reference = Expression.Variable (typeof (JniObjectReference), sourceValue.Name + "_ref"); + context.LocalVariables.Add (reference); + context.CreationStatements.Add (Expression.Assign (reference, Expression.Call (createString.GetMethodInfo (), sourceValue))); + context.CleanupStatements.Add (DisposeObjectReference (reference)); + return ReturnObjectReferenceToJni (context, sourceValue.Name, reference); + } } static void DisposeReferenceState (ref JniValueMarshalerState state) From fece7d717dd310f6e7588f163d9479528560eba2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 17:51:13 +0200 Subject: [PATCH 32/47] Remove generated value marshaler registry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Generator/Model/TypeMapAssemblyData.cs | 8 -- .../Generator/ModelBuilder.cs | 22 ----- .../Generator/RootTypeMapAssemblyGenerator.cs | 73 +++------------ .../Generator/TypeMapAssemblyEmitter.cs | 88 ------------------- .../Generator/TypeMapAssemblyGenerator.cs | 5 +- .../Scanner/AssemblyIndex.cs | 50 +---------- .../Scanner/JavaPeerInfo.cs | 8 -- .../Scanner/JavaPeerScanner.cs | 9 -- .../TrimmableTypeMapGenerator.cs | 38 +++----- .../TrimmableTypeMapTypes.cs | 1 - .../JavaMarshalValueManager.cs | 2 - .../TrimmableTypeMap.cs | 28 ------ .../Generator/FixtureTestBase.cs | 6 +- .../RootTypeMapAssemblyGeneratorTests.cs | 21 +---- .../TypeMapAssemblyGeneratorTests.cs | 31 +------ .../Scanner/JavaPeerScannerTests.cs | 10 --- .../TestFixtures/StubAttributes.cs | 12 --- .../TestFixtures/TestTypes.cs | 12 --- .../TestInstrumentation.cs | 9 +- 20 files changed, 35 insertions(+), 400 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index 9bef884ca7b..94243c8f4b9 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 9bef884ca7b61e810a2944444beb0b8a29631547 +Subproject commit 94243c8f4b905f17b3f3bd86daeb84fd18579bb8 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index bd97741d595..a90f0d7dbeb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -35,8 +35,6 @@ sealed class TypeMapAssemblyData /// public List Associations { get; } = new (); - public List ValueMarshalers { get; } = new (); - /// /// Alias holder types to emit — one per alias group (≥2 types sharing a JNI name). /// @@ -407,12 +405,6 @@ sealed record TypeMapAssociationData public required string AliasProxyTypeReference { get; init; } } -sealed record ValueMarshalerData -{ - public required TypeRefData ValueType { get; init; } - public required TypeRefData MarshalerType { get; init; } -} - /// /// An alias holder class to generate in the TypeMap assembly. /// Extends JavaPeerProxy and implements IJavaPeerAliases. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 97640757293..4671394e936 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -41,16 +41,10 @@ static class ModelBuilder /// for ranks 1... 0 disables array entry emission. /// public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) - => Build (peers, [], outputPath, assemblyName, maxArrayRank); - - public static TypeMapAssemblyData Build (IReadOnlyList peers, IReadOnlyList valueMarshalers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); } - if (valueMarshalers is null) { - throw new ArgumentNullException (nameof (valueMarshalers)); - } if (outputPath is null) { throw new ArgumentNullException (nameof (outputPath)); } @@ -106,18 +100,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, IRea } BuildNativeRegistrations (model); - foreach (var valueMarshaler in valueMarshalers.OrderBy (m => m.ValueTypeName, StringComparer.Ordinal)) { - model.ValueMarshalers.Add (new ValueMarshalerData { - ValueType = new TypeRefData { - ManagedTypeName = valueMarshaler.ValueTypeName, - AssemblyName = valueMarshaler.ValueTypeAssemblyName, - }, - MarshalerType = new TypeRefData { - ManagedTypeName = valueMarshaler.MarshalerTypeName, - AssemblyName = valueMarshaler.MarshalerAssemblyName, - }, - }); - } // Compute IgnoresAccessChecksTo from cross-assembly references var referencedAssemblies = new SortedSet (StringComparer.Ordinal); @@ -130,10 +112,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, IRea AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } } - foreach (var valueMarshaler in model.ValueMarshalers) { - AddIfCrossAssembly (referencedAssemblies, valueMarshaler.ValueType.AssemblyName, assemblyName); - AddIfCrossAssembly (referencedAssemblies, valueMarshaler.MarshalerType.AssemblyName, assemblyName); - } // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef // for JI-style activation cleanup (matching legacy TypeManager.CreateProxy behavior). diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs index 3587360caa1..b54c2f535ec 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/RootTypeMapAssemblyGenerator.cs @@ -80,7 +80,7 @@ public RootTypeMapAssemblyGenerator (Version systemRuntimeVersion) /// sentinels. Must match the value passed to the per-assembly generators. 0 means /// no array sentinels were emitted; the loader passes null for array maps. /// - public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0, IReadOnlyList? valueMarshalerTypeMapNames = null) + public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, Stream stream, string? assemblyName = null, string? moduleName = null, int maxArrayRank = 0) { if (perAssemblyTypeMapNames is null) { throw new ArgumentNullException (nameof (perAssemblyTypeMapNames)); @@ -135,7 +135,7 @@ public void Generate (IReadOnlyList perAssemblyTypeMapNames, bool useSha pe.EmitIgnoresAccessChecksToAttribute (accessTargets); // Emit TypeMapLoader class with Initialize() method - EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName, valueMarshalerTypeMapNames ?? []); + EmitTypeMapLoader (pe, anchorTypeHandle, perAssemblyTypeMapNames, useSharedTypemapUniverse, maxArrayRank, assemblyName); pe.WritePE (stream); } @@ -201,7 +201,7 @@ static void EmitAssemblyTargetAttribute (PEAssemblyBuilder pe, MemberReferenceHa pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, ctorRef, blobHandle); } - static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName, IReadOnlyList valueMarshalerTypeMapNames) + static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, IReadOnlyList perAssemblyTypeMapNames, bool useSharedTypemapUniverse, int maxArrayRank, string assemblyName) { var metadata = pe.Metadata; @@ -226,8 +226,6 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand var getProxyMemberRef = AddTypeMappingMethodRef (pe, typeMappingRef, "GetOrCreateProxyTypeMapping", iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); - var valueMarshalerMaps = CreateValueMarshalerMapContext (pe, valueMarshalerTypeMapNames); - // Define the TypeMapLoader type (public static class in Microsoft.Android.Runtime namespace) metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.Class, @@ -245,65 +243,25 @@ static void EmitTypeMapLoader (PEAssemblyBuilder pe, EntityHandle anchorTypeHand if (maxArrayRank > 0) { var initializeRef = AddInitializeSingleWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithSingleTypeMap (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank, assemblyName, valueMarshalerMaps); + initializeRef, externalDictTypeSpec, externalDictArrayTypeSpec, perAssemblyTypeMapNames, maxArrayRank, assemblyName); } else { var initializeRef = AddInitializeSingleNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); - EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName, valueMarshalerMaps); + EmitInitializeWithSingleTypeMapNoArrays (pe, anchorTypeHandle, getExternalMemberRef, getProxyMemberRef, initializeRef, assemblyName); } } else { var proxyDictTypeSpec = MakeIReadOnlyDictTypeSpec (pe, iReadOnlyDictOpenRef, systemTypeRef, keyIsString: false); if (maxArrayRank > 0) { var initializeRef = AddInitializeAggregateWithArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMap (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName, valueMarshalerMaps); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, externalDictArrayTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, maxArrayRank, assemblyName); } else { var initializeRef = AddInitializeAggregateNoArraysRef (pe, trimmableTypeMapRef, iReadOnlyDictOpenRef, systemTypeRef); EmitInitializeWithAggregateTypeMapNoArrays (pe, perAssemblyTypeMapNames, getExternalMemberRef, getProxyMemberRef, - initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName, valueMarshalerMaps); + initializeRef, externalDictTypeSpec, proxyDictTypeSpec, iReadOnlyDictOpenRef, systemTypeRef, assemblyName); } } } - sealed class ValueMarshalerMapContext - { - public EntityHandle[] RegisterRefs { get; init; } = []; - public bool HasMaps => RegisterRefs.Length > 0; - } - - static ValueMarshalerMapContext CreateValueMarshalerMapContext ( - PEAssemblyBuilder pe, - IReadOnlyList valueMarshalerTypeMapNames) - { - if (valueMarshalerTypeMapNames.Count == 0) { - return new ValueMarshalerMapContext (); - } - - var registerRefs = new EntityHandle [valueMarshalerTypeMapNames.Count]; - for (int i = 0; i < valueMarshalerTypeMapNames.Count; i++) { - var asmRef = pe.FindOrAddAssemblyRef (valueMarshalerTypeMapNames [i]); - var valueMarshalerMappingRef = pe.Metadata.AddTypeReference (asmRef, - pe.Metadata.GetOrAddString ("_TypeMap"), - pe.Metadata.GetOrAddString ("ValueMarshalerMapping")); - registerRefs [i] = pe.AddMemberRef (valueMarshalerMappingRef, "RegisterValueMarshalers", - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { })); - } - - return new ValueMarshalerMapContext { - RegisterRefs = registerRefs, - }; - } - - static void EmitInitializeValueMarshalerMaps (TrackedInstructionEncoder encoder, ValueMarshalerMapContext valueMarshalerMaps) - { - if (!valueMarshalerMaps.HasMaps) { - return; - } - - for (int i = 0; i < valueMarshalerMaps.RegisterRefs.Length; i++) { - encoder.Call (valueMarshalerMaps.RegisterRefs [i], parameterCount: 0); - } - } - /// /// Aggregate IL emit. Builds typeMaps[N], proxyMaps[N], and either /// arrayMapsByAssemblyAndRank[N][maxArrayRank] from per-assembly @@ -317,8 +275,7 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, TypeSpecificationHandle externalDictArrayTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, int maxArrayRank, - string assemblyName, - ValueMarshalerMapContext valueMarshalerMaps) + string assemblyName) { var count = perAssemblyTypeMapNames.Count; @@ -350,7 +307,6 @@ static void EmitInitializeWithAggregateTypeMap (PEAssemblyBuilder pe, encoder.LoadLocal (1); EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); - EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }, encodeLocals: localsSig => { @@ -392,8 +348,7 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, MemberReferenceHandle initializeRef, TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle proxyDictTypeSpec, TypeReferenceHandle iReadOnlyDictOpenRef, TypeReferenceHandle systemTypeRef, - string assemblyName, - ValueMarshalerMapContext valueMarshalerMaps) + string assemblyName) { var count = perAssemblyTypeMapNames.Count; @@ -421,7 +376,6 @@ static void EmitInitializeWithAggregateTypeMapNoArrays (PEAssemblyBuilder pe, encoder.LoadLocal (0); encoder.LoadLocal (1); encoder.Call (initializeRef, parameterCount: 2); - EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }, encodeLocals: localsSig => { @@ -483,8 +437,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle TypeSpecificationHandle externalDictTypeSpec, TypeSpecificationHandle externalDictArrayTypeSpec, IReadOnlyList perAssemblyTypeMapNames, int maxArrayRank, - string assemblyName, - ValueMarshalerMapContext valueMarshalerMaps) + string assemblyName) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -499,7 +452,6 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); EmitArrayMapsByAssemblyAndRankOrNull (pe, encoder, perAssemblyTypeMapNames, getExternalMemberRef, externalDictTypeSpec, externalDictArrayTypeSpec, maxArrayRank); encoder.Call (initializeRef, parameterCount: 3); - EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }); } @@ -511,8 +463,7 @@ static void EmitInitializeWithSingleTypeMap (PEAssemblyBuilder pe, EntityHandle static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, EntityHandle anchorTypeHandle, MemberReferenceHandle getExternalMemberRef, MemberReferenceHandle getProxyMemberRef, MemberReferenceHandle initializeRef, - string assemblyName, - ValueMarshalerMapContext valueMarshalerMaps) + string assemblyName) { var getExternalSpec = MakeGenericMethodSpec (pe, getExternalMemberRef, anchorTypeHandle); var getProxySpec = MakeGenericMethodSpec (pe, getProxyMemberRef, anchorTypeHandle); @@ -525,7 +476,6 @@ static void EmitInitializeWithSingleTypeMapNoArrays (PEAssemblyBuilder pe, Entit encoder.Call (getExternalSpec, parameterCount: 0, returnsValue: true); encoder.Call (getProxySpec, parameterCount: 0, returnsValue: true); encoder.Call (initializeRef, parameterCount: 2); - EmitInitializeValueMarshalerMaps (encoder, valueMarshalerMaps); encoder.Return (); }); } @@ -690,5 +640,4 @@ static void EncodeIReadOnlyDictType (BlobBuilder blob, TypeReferenceHandle iRead blob.WriteByte (0x12); // ELEMENT_TYPE_CLASS blob.WriteCompressedInteger (CodedIndex.TypeDefOrRefOrSpec (systemTypeRef)); } - } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index c28bd7a1eca..8ab18800ea5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -92,9 +92,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemArrayRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _jniValueMarshalerRef; - TypeReferenceHandle _trimmableTypeMapRef; - TypeReferenceHandle _valueMarshalerFactoryRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; TypeReferenceHandle _javaPeerAliasesAttrRef; @@ -225,10 +222,6 @@ void EmitCore (TypeMapAssemblyData model, bool useSharedTypemapUniverse) EmitTypeMapAssociationAttribute (assoc); } - if (model.ValueMarshalers.Count > 0) { - EmitValueMarshalerMapping (model.ValueMarshalers); - } - _pe.EmitIgnoresAccessChecksToAttribute (model.IgnoresAccessChecksTo); } @@ -309,12 +302,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _jniValueMarshalerRef = metadata.AddTypeReference (_javaInteropRef, - metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniValueMarshaler")); - _trimmableTypeMapRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); - _valueMarshalerFactoryRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("ValueMarshalerFactory")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -1779,81 +1766,6 @@ void EmitTypeMapAssociationAttribute (TypeMapAssociationData assoc) _pe.Metadata.AddCustomAttribute (EntityHandle.AssemblyDefinition, _typeMapAssociationAttrCtorRef, blob); } - void EmitValueMarshalerMapping (IReadOnlyList valueMarshalers) - { - var metadata = _pe.Metadata; - var objectRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System"), metadata.GetOrAddString ("Object")); - - int typeFieldStart = metadata.GetRowCount (TableIndex.Field) + 1; - int typeMethodStart = metadata.GetRowCount (TableIndex.MethodDef) + 1; - - var factoryMethods = new MethodDefinitionHandle [valueMarshalers.Count]; - for (int i = 0; i < valueMarshalers.Count; i++) { - factoryMethods [i] = EmitValueMarshalerFactoryMethod (i, valueMarshalers [i]); - } - - EmitRegisterValueMarshalersMethod (valueMarshalers, factoryMethods); - - metadata.AddTypeDefinition ( - TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Abstract | TypeAttributes.BeforeFieldInit, - metadata.GetOrAddString ("_TypeMap"), - metadata.GetOrAddString ("ValueMarshalerMapping"), - objectRef, - MetadataTokens.FieldDefinitionHandle (typeFieldStart), - MetadataTokens.MethodDefinitionHandle (typeMethodStart)); - } - - MethodDefinitionHandle EmitValueMarshalerFactoryMethod (int index, ValueMarshalerData valueMarshaler) - { - var marshalerType = _pe.ResolveTypeRef (valueMarshaler.MarshalerType); - var marshalerCtor = _pe.AddMemberRef (marshalerType, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - - return _pe.EmitBody ($"CreateValueMarshaler_{index}", - MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (0, - rt => rt.Type ().Type (_jniValueMarshalerRef, false), - p => { }), - encoder => { - encoder.NewObject (marshalerCtor, parameterCount: 0); - encoder.Return (returnsValue: true); - }); - } - - void EmitRegisterValueMarshalersMethod (IReadOnlyList valueMarshalers, MethodDefinitionHandle[] factoryMethods) - { - var factoryCtor = _pe.AddMemberRef (_valueMarshalerFactoryRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().Object (); - p.AddParameter ().Type ().IntPtr (); - })); - var registerValueMarshaler = _pe.AddMemberRef (_trimmableTypeMapRef, "RegisterValueMarshaler", - sig => sig.MethodSignature ().Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().Type (_systemTypeRef, false); - p.AddParameter ().Type ().Type (_valueMarshalerFactoryRef, false); - })); - - _pe.EmitBody ("RegisterValueMarshalers", - MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, - sig => sig.MethodSignature ().Parameters (0, rt => rt.Void (), p => { }), - encoder => { - for (int i = 0; i < valueMarshalers.Count; i++) { - encoder.LoadToken (_pe.ResolveTypeRef (valueMarshalers [i].ValueType)); - encoder.Call (_getTypeFromHandleRef, parameterCount: 1, returnsValue: true); - encoder.OpCode (ILOpCode.Ldnull); - encoder.LoadFunction (factoryMethods [i]); - encoder.NewObject (factoryCtor, parameterCount: 2); - encoder.Call (registerValueMarshaler, parameterCount: 2); - } - encoder.Return (); - }); - } - /// /// Writes the ECMA-335 blob for a closed generic value type with a single value-type argument. /// E.g., ReadOnlySpan<JniNativeMethod>. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 03105bb15de..48ca89f45bc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -29,11 +29,8 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// /// Max rank for per-rank array TypeMap entries. 0 disables. public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) - => Generate (peers, [], stream, assemblyName, useSharedTypemapUniverse, maxArrayRank); - - public void Generate (IReadOnlyList peers, IReadOnlyList valueMarshalers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, valueMarshalers, assemblyName + ".dll", assemblyName, maxArrayRank); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index e9f6d43873f..64ca498c814 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -33,8 +33,6 @@ sealed class AssemblyIndex : IDisposable /// public Dictionary AttributesByType { get; } = new (); - public List ValueMarshalers { get; } = new (); - /// /// Type references grouped by referenced assembly name. /// @@ -105,16 +103,12 @@ void Build () TypesByFullName [fullName] = typeHandle; - var (registerInfo, attrInfo, valueMarshalerInfo) = ParseAttributes (typeDef, fullName); + var (registerInfo, attrInfo) = ParseAttributes (typeDef); if (attrInfo is not null) { AttributesByType [typeHandle] = attrInfo; } - if (valueMarshalerInfo is not null) { - ValueMarshalers.Add (valueMarshalerInfo); - } - if (registerInfo is not null) { RegisterInfoByType [typeHandle] = registerInfo; } @@ -137,11 +131,10 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen return false; } - (RegisterInfo? register, TypeAttributeInfo? attrs, ValueMarshalerInfo? valueMarshaler) ParseAttributes (TypeDefinition typeDef, string fullName) + (RegisterInfo? register, TypeAttributeInfo? attrs) ParseAttributes (TypeDefinition typeDef) { RegisterInfo? registerInfo = null; TypeAttributeInfo? attrInfo = null; - ValueMarshalerInfo? valueMarshalerInfo = null; // Collect intent filters and metadata separately to avoid ordering issues: // if [IntentFilter] appears before [Activity], we must not create attrInfo @@ -157,9 +150,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen continue; } - if (IsCustomAttributeMatch (ca, Reader, "Java.Interop", "JniValueMarshalerAttribute")) { - valueMarshalerInfo = ParseValueMarshalerAttribute (ca, fullName); - } else if (attrName == "RegisterAttribute") { + if (attrName == "RegisterAttribute") { registerInfo = ParseRegisterAttribute (ca); registerInfo = registerInfo with { JniName = registerInfo.JniName.Replace ('.', '/') }; } else if (attrName == "JniTypeSignatureAttribute") { @@ -220,40 +211,7 @@ bool TryGetTypeReferenceAssemblyName (TypeReference typeReference, [NotNullWhen } } - return (registerInfo, attrInfo, valueMarshalerInfo); - } - - ValueMarshalerInfo? ParseValueMarshalerAttribute (CustomAttribute ca, string valueTypeName) - { - var value = DecodeAttribute (ca); - if (value.FixedArguments.Length == 0 || value.FixedArguments [0].Value is not string marshalerTypeReference) { - return null; - } - - var marshalerType = ParseAttributeTypeReference (marshalerTypeReference, AssemblyName); - return new ValueMarshalerInfo { - ValueTypeName = valueTypeName, - ValueTypeAssemblyName = AssemblyName, - MarshalerTypeName = marshalerType.TypeName, - MarshalerAssemblyName = marshalerType.AssemblyName, - }; - } - - static (string TypeName, string AssemblyName) ParseAttributeTypeReference (string typeReference, string defaultAssemblyName) - { - var commaIndex = typeReference.IndexOf (','); - if (commaIndex < 0) { - return (typeReference.Trim (), defaultAssemblyName); - } - - var typeName = typeReference.Substring (0, commaIndex).Trim (); - var assemblyName = typeReference.Substring (commaIndex + 1).Trim (); - var assemblyNameEnd = assemblyName.IndexOf (','); - if (assemblyNameEnd >= 0) { - assemblyName = assemblyName.Substring (0, assemblyNameEnd).Trim (); - } - - return (typeName, assemblyName); + return (registerInfo, attrInfo); } static readonly HashSet KnownComponentAttributes = new (StringComparer.Ordinal) { diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 6eb7795572b..7382a4ed60a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -157,14 +157,6 @@ public sealed record JavaPeerInfo public ComponentInfo? ComponentAttribute { get; init; } } -public sealed record ValueMarshalerInfo -{ - public required string ValueTypeName { get; init; } - public required string ValueTypeAssemblyName { get; init; } - public required string MarshalerTypeName { get; init; } - public required string MarshalerAssemblyName { get; init; } -} - /// /// Describes a marshal method (a method with [Register] or [Export]) on a Java peer type. /// Contains all data needed to generate a UCO wrapper, a JCW native declaration, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 49138574a72..d03d8dcc853 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -114,15 +114,6 @@ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> as return new List (resultsByQualifiedName.Values); } - internal List GetValueMarshalers () - { - var valueMarshalers = new List (); - foreach (var index in assemblyCache.Values) { - valueMarshalers.AddRange (index.ValueMarshalers); - } - return valueMarshalers; - } - void MarkFrameworkArrayEntryPeers (IEnumerable peers) { var referencedFrameworkTypes = new HashSet (StringComparer.Ordinal); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 65cc2695c98..95d0446b205 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -44,10 +44,10 @@ public TrimmableTypeMapResult Execute ( throw new ArgumentOutOfRangeException (nameof (maxArrayRank), maxArrayRank, "Must be >= 0."); } - var (allPeers, valueMarshalers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); - if (allPeers.Count == 0 && valueMarshalers.Count == 0) { + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); + if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); - return new TrimmableTypeMapResult ([], [], allPeers, []); + return new TrimmableTypeMapResult ([], [], allPeers); } MarkFrameworkAssemblyPeers (allPeers, frameworkAssemblyNames); @@ -56,7 +56,7 @@ public TrimmableTypeMapResult Execute ( PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = generateTypeMapAssemblies - ? GenerateTypeMapAssemblies (allPeers, valueMarshalers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) + ? GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank) : []; var jcwPeers = allPeers.Where (ShouldGenerateJcw).ToList (); logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); @@ -71,7 +71,7 @@ public TrimmableTypeMapResult Execute ( ? GenerateManifest (allPeers, assemblyManifestInfo, manifestConfig, manifestTemplate) : null; - return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, valueMarshalers, manifest, appRegTypes); + return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } internal static List CollectApplicationRegistrationTypes (List allPeers) @@ -155,19 +155,17 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return new GeneratedManifest (doc, providerNames.Count > 0 ? providerNames.ToArray () : []); } - (List peers, List valueMarshalers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) + (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies (IReadOnlyList<(string Name, PEReader Reader)> assemblies, string? packageNamingPolicy, HashSet frameworkAssemblyNames) { using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); var peers = scanner.Scan (assemblies); - var valueMarshalers = scanner.GetValueMarshalers (); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); - return (peers, valueMarshalers, manifestInfo); + return (peers, manifestInfo); } List GenerateTypeMapAssemblies ( List allPeers, - List valueMarshalers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank) @@ -192,37 +190,21 @@ List GenerateTypeMapAssemblies ( .ToList (); } - var valueMarshalersByAssembly = valueMarshalers - .GroupBy (m => m.ValueTypeAssemblyName, StringComparer.Ordinal) - .ToDictionary (g => g.Key, g => g.ToList (), StringComparer.Ordinal); - var assemblyNames = new SortedSet (peersByAssembly.Select (p => p.AssemblyName), StringComparer.Ordinal); - assemblyNames.UnionWith (valueMarshalersByAssembly.Keys); - var peersByAssemblyMap = peersByAssembly.ToDictionary (p => p.AssemblyName, p => p.Peers, StringComparer.Ordinal); - var generatedAssemblies = new List (); var perAssemblyNames = new List (); - var valueMarshalerTypeMapNames = new List (); var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion); - foreach (var assemblyName in assemblyNames) { - peersByAssemblyMap.TryGetValue (assemblyName, out var peers); - peers ??= []; - valueMarshalersByAssembly.TryGetValue (assemblyName, out var assemblyValueMarshalers); - assemblyValueMarshalers ??= []; - + foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); - if (assemblyValueMarshalers.Count > 0) { - valueMarshalerTypeMapNames.Add (typeMapAssemblyName); - } var stream = new MemoryStream (); - generator.Generate (peers, assemblyValueMarshalers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); - rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank, valueMarshalerTypeMapNames: valueMarshalerTypeMapNames); + rootGenerator.Generate (perAssemblyNames, useSharedTypemapUniverse, rootStream, maxArrayRank: maxArrayRank); rootStream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly ("_Microsoft.Android.TypeMaps", rootStream)); logger.LogGeneratedRootTypeMapInfo (perAssemblyNames.Count); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs index 807e2998351..2e162d0d61b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapTypes.cs @@ -8,7 +8,6 @@ public record TrimmableTypeMapResult ( IReadOnlyList GeneratedAssemblies, IReadOnlyList GeneratedJavaSources, IReadOnlyList AllPeers, - IReadOnlyList? ValueMarshalers = null, GeneratedManifest? Manifest = null, IReadOnlyList? ApplicationRegistrationTypes = null) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e6808c2ee6c..20f6914ca9b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1141,8 +1141,6 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) throw new ArgumentException ("Generic type definitions are not supported.", nameof (type)); } - if (TrimmableTypeMap.Instance.TryGetValueMarshaler (type, out var customMarshaler)) - return customMarshaler; if (TrimmableValueMarshalerHelper.TryGetPrimitiveValueMarshaler (type, out var primitiveMarshaler)) return primitiveMarshaler; if (type == typeof (string)) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a15c1e3d697..fd47840991f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -12,8 +12,6 @@ namespace Microsoft.Android.Runtime; -delegate JniValueMarshaler ValueMarshalerFactory (); - /// /// Central type map for the trimmable typemap path. Owns the ITypeMap /// and provides peer creation, invoker resolution, container factories, and native @@ -39,8 +37,6 @@ public class TrimmableTypeMap "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); readonly ITypeMap _typeMap; - readonly Dictionary _valueMarshalerFactories = new (); - readonly ConcurrentDictionary _valueMarshalerCache = new (); readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _jniProxyCache = new (StringComparer.Ordinal); readonly ConcurrentDictionary<(string ClassName, Type TargetType), JavaPeerProxy> _interfaceProxyCache = new (); @@ -140,30 +136,6 @@ static void InitializeCore (ITypeMap typeMap) } } - internal static void RegisterValueMarshaler (Type valueType, ValueMarshalerFactory factory) - { - ArgumentNullException.ThrowIfNull (valueType); - ArgumentNullException.ThrowIfNull (factory); - - lock (s_initLock) { - var instance = s_instance ?? throw new InvalidOperationException ( - "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); - - instance._valueMarshalerFactories.Add (valueType, factory); - } - } - - internal bool TryGetValueMarshaler (Type type, [NotNullWhen (true)] out JniValueMarshaler? marshaler) - { - if (!_valueMarshalerFactories.ContainsKey (type)) { - marshaler = null; - return false; - } - - marshaler = _valueMarshalerCache.GetOrAdd (type, static (t, self) => self._valueMarshalerFactories [t] (), this); - return true; - } - internal static unsafe void RegisterNativeMethods () { lock (s_initLock) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index db76e70ab21..d824733c2f2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -22,21 +22,19 @@ private protected static string TestFixtureAssemblyPath { } } - static readonly Lazy<(List peers, List valueMarshalers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { + static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); var assemblies = new [] { (assemblyName, peReader) }; var peers = scanner.Scan (assemblies); - var valueMarshalers = scanner.GetValueMarshalers (); var manifestInfo = scanner.ScanAssemblyManifestInfo (); peReader.Dispose (); - return (peers, valueMarshalers, manifestInfo); + return (peers, manifestInfo); }); private protected static List ScanFixtures () => _cachedScanResult.Value.peers; - private protected static List ScanFixtureValueMarshalers () => _cachedScanResult.Value.valueMarshalers; private protected static List ScanFixtures (string packageNamingPolicy) { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs index b5fcc56347c..2ad242d096a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/RootTypeMapAssemblyGeneratorTests.cs @@ -12,11 +12,11 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class RootTypeMapAssemblyGeneratorTests : FixtureTestBase { - static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0, IReadOnlyList? valueMarshalerTypeMapNames = null) + static MemoryStream GenerateRootAssembly (IReadOnlyList perAssemblyNames, bool useSharedTypemapUniverse = false, string? assemblyName = null, int maxArrayRank = 0) { var stream = new MemoryStream (); var generator = new RootTypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank, valueMarshalerTypeMapNames: valueMarshalerTypeMapNames); + generator.Generate (perAssemblyNames, useSharedTypemapUniverse, stream, assemblyName, maxArrayRank: maxArrayRank); stream.Position = 0; return stream; } @@ -345,23 +345,6 @@ public void Generate_MergedMode_ReferencesRootAnchorOnly () Assert.DoesNotContain ("_Mono.Android.TypeMap", asmRefs); } - [Fact] - public void Generate_WithValueMarshalerMaps_ReferencesPerAssemblyRegistration () - { - using var stream = GenerateRootAssembly ( - ["_App.TypeMap", "_Mono.Android.TypeMap"], - useSharedTypemapUniverse: true, - valueMarshalerTypeMapNames: ["_App.TypeMap"]); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var asmRefs = reader.AssemblyReferences - .Select (h => reader.GetString (reader.GetAssemblyReference (h).Name)) - .ToList (); - Assert.Contains ("_App.TypeMap", asmRefs); - Assert.Contains ("RegisterValueMarshalers", GetMemberRefNames (reader)); - } - [Fact] public void Generate_AggregateMode_ReferencesPerAssemblyAnchors () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index e513b7e751f..28b2ad29b2a 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -14,13 +14,10 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class TypeMapAssemblyGeneratorTests : FixtureTestBase { static MemoryStream GenerateAssembly (IReadOnlyList peers, string assemblyName = "TestTypeMap") - => GenerateAssembly (peers, [], assemblyName); - - static MemoryStream GenerateAssembly (IReadOnlyList peers, IReadOnlyList valueMarshalers, string assemblyName = "TestTypeMap") { var stream = new MemoryStream (); var generator = new TypeMapAssemblyGenerator (new Version (11, 0, 0, 0)); - generator.Generate (peers, valueMarshalers, stream, assemblyName); + generator.Generate (peers, stream, assemblyName); stream.Position = 0; return stream; } @@ -287,32 +284,6 @@ public void Generate_EmptyPeerList_ProducesValidAssembly () Assert.Equal ("EmptyTest", reader.GetString (asmDef.Name)); } - [Fact] - public void Generate_ValueMarshalerMapping_EmitsLazyFactoryMap () - { - var valueMarshalers = ScanFixtureValueMarshalers (); - - using var stream = GenerateAssembly ([], valueMarshalers, "ValueMarshalerTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - - var mappingType = reader.TypeDefinitions - .Select (h => reader.GetTypeDefinition (h)) - .Single (t => - reader.GetString (t.Namespace) == "_TypeMap" && - reader.GetString (t.Name) == "ValueMarshalerMapping"); - var methodNames = mappingType.GetMethods () - .Select (h => reader.GetMethodDefinition (h)) - .Select (m => reader.GetString (m.Name)) - .ToList (); - - Assert.Contains ("CreateValueMarshaler_0", methodNames); - Assert.Contains ("RegisterValueMarshalers", methodNames); - Assert.Contains ("ValueMarshalerFactory", GetTypeRefNames (reader)); - Assert.Contains ("JniValueMarshaler", GetTypeRefNames (reader)); - Assert.Contains ("DemoValueTypeMarshaler", GetTypeRefNames (reader)); - } - [Fact] public void Generate_LeafCtor_DoesNotUseCreateManagedPeer () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index a641ef4e35c..3e9537422fd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -171,16 +171,6 @@ public void Scan_JniTypeSignature_DoNotGenerateAcw () Assert.True (nonGenerated.DoNotGenerateAcw, "NonGeneratedJavaObject has GenerateJavaPeer=false"); } - [Fact] - public void Scan_JniValueMarshalerAttribute_IsDiscovered () - { - var valueMarshaler = Assert.Single (ScanFixtureValueMarshalers ()); - Assert.Equal ("MyApp.ValueMarshalers.DemoValueType", valueMarshaler.ValueTypeName); - Assert.Equal ("TestFixtures", valueMarshaler.ValueTypeAssemblyName); - Assert.Equal ("MyApp.ValueMarshalers.DemoValueTypeMarshaler", valueMarshaler.MarshalerTypeName); - Assert.Equal ("TestFixtures", valueMarshaler.MarshalerAssemblyName); - } - [Fact] public void Scan_JniTypeSignature_DuplicateJniName_BothPresent () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 96df937d1ec..050741c3e20 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -178,18 +178,6 @@ public sealed class ContentProviderAttribute : Attribute, Java.Interop.IJniNameP namespace Java.Interop { - public abstract class JniValueMarshaler - { - } - - [AttributeUsage (AttributeTargets.Class | AttributeTargets.Enum | AttributeTargets.Interface | AttributeTargets.Struct, AllowMultiple = false)] - public sealed class JniValueMarshalerAttribute : Attribute - { - public Type MarshalerType { get; } - - public JniValueMarshalerAttribute (Type marshalerType) => MarshalerType = marshalerType; - } - [AttributeUsage (AttributeTargets.Method, AllowMultiple = false)] public sealed class ExportAttribute : Attribute { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 70828cb9fce..ca0b8f4adf8 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -232,18 +232,6 @@ public NonRequiredFrameworkAcw () { } } } -namespace MyApp.ValueMarshalers -{ - [Java.Interop.JniValueMarshaler (typeof (DemoValueTypeMarshaler))] - internal struct DemoValueType - { - } - - internal sealed class DemoValueTypeMarshaler : Java.Interop.JniValueMarshaler - { - } -} - namespace MyApp { [Activity (MainLauncher = true, Label = "My App", Name = "my.app.MainActivity")] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index d91d6483a3d..7f10badb726 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -31,12 +31,9 @@ protected override IEnumerable? ExcludedCategories { categories.Add ("NativeTypeMap"); categories.Add ("Export"); // Java.Interop tests in this category exercise APIs that are unsupported - // by design under the trimmable typemap: expression-tree-based marshaling - // from the obsolete runtime marshal-member builder, hand-written native - // registration via [JniAddNativeMethodRegistration], and Java test peers - // which call net.dot.jni.ManagedPeer.construct/registerNativeMembers. - // The trimmable runtime must use generated/AOT-safe marshal and - // registration paths instead. + // by design under the trimmable typemap, such as obsolete + // marshal-member-builder paths or runtime swaps that are not part of + // the generated/AOT-safe trimmable runtime path. } // Build-time flags flow in via runtimeconfig.json properties From b134621de894c9c18c378359a8bc8029b510f03a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Thu, 11 Jun 2026 18:00:32 +0200 Subject: [PATCH 33/47] Address trimmable test review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop-Tests.targets | 17 +++++++++++------ .../Java.Interop/ConstructorActivationTests.cs | 7 ++----- .../System/StartupHookTest.cs | 3 +-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index f167ef6b105..6c3b43a0332 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -27,12 +27,17 @@ diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs index ab3e142568d..988af02552a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/ConstructorActivationTests.cs @@ -8,8 +8,6 @@ using Java.Interop; -using Microsoft.Android.Runtime; - using NUnit.Framework; namespace Java.InteropTests @@ -530,7 +528,7 @@ public void JavaSideNestedIntArrayConstructorForwardsValues () static void AssumeTrimmableConstructorParameterMarshalling () { - if (!RuntimeFeature.TrimmableTypeMap) { + if (!Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { Assert.Ignore ("Legacy TypeManager.n_Activate does not marshal string, short, or array constructor parameters; this case validates trimmable constructor UCO parameter marshalling."); } } @@ -575,7 +573,7 @@ static void AssertRegisteredSame (T instance) var registered = Java.Lang.Object.GetObject (instance.Handle, JniHandleOwnership.DoNotTransfer); try { Assert.AreSame (instance, registered); - if (RuntimeFeature.TrimmableTypeMap) { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { Assert.AreEqual (Java.Lang.JavaSystem.IdentityHashCode (instance), instance.JniIdentityHashCode); } } finally { @@ -980,6 +978,5 @@ public static void Reset () { ConstructorInvocations = 0; } - } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs index 0e1f7f7db01..02dc861eb82 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System/StartupHookTest.cs @@ -11,8 +11,7 @@ public class StartupHookTest public void FeatureFlagIsEnabled () { // NOTE: this is set to true in tests\Mono.Android-Tests\Mono.Android-Tests\Mono.Android.NET-Tests.csproj - AppContext.TryGetSwitch ("System.StartupHookProvider.IsSupported", out bool startupHookSupport); - Assert.IsTrue (startupHookSupport, "System.StartupHookProvider.IsSupported should be true"); + Assert.IsTrue (Microsoft.Android.Runtime.RuntimeFeature.StartupHookSupport, "RuntimeFeature.StartupHookSupport should be true"); } [Test] From 6e1a69a18450747acec0b616c5c9ddf999279e1b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 02:05:15 +0200 Subject: [PATCH 34/47] Fix trimmable typemap CI failures Restore XA4251 reporting for user assemblies while ignoring framework/runtime assemblies, suppress generated proxy constructor trim false positives, and make post-trim Java source staging safe for multi-RID CoreCLR builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../PreserveLists/Mono.Android.xml | 1 + .../Generator/TypeMapAssemblyEmitter.cs | 29 +++++++++++++++++++ .../Scanner/JavaPeerScanner.cs | 11 ++++--- .../JavaMarshalValueManager.cs | 2 +- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 9 ++++++ .../TrimmableTypeMapBuildTests.cs | 24 ++------------- .../Java.Interop/ExportFieldAttribute.cs | 4 ++- .../Scanner/JavaPeerScannerTests.cs | 5 ++-- .../TestInstrumentation.cs | 2 +- 9 files changed, 57 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml index 124edd61d93..29e7d7503ae 100644 --- a/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml +++ b/src/Microsoft.Android.Sdk.ILLink/PreserveLists/Mono.Android.xml @@ -27,6 +27,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index b8162b3dcc4..3ae3e1a44fd 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -88,7 +88,7 @@ public void Build_WithTrimmableTypeMap_ArrayRankChangeRegeneratesTypeMap () builder.Output.AssertTargetIsNotSkipped ("_GenerateTrimmableTypeMap"); Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "Second build should have succeeded."); - builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMap"); + builder.Output.AssertTargetIsSkipped ("_GenerateTrimmableTypeMap", defaultIfNotUsed: true); proj.SetProperty ("_AndroidTrimmableTypeMapMaxArrayRank", "3"); Assert.IsTrue (builder.Build (proj, doNotCleanupOnUpdate: true), "Array rank change build should have succeeded."); @@ -571,7 +571,8 @@ class ExportShapes : Java.Lang.Object { if (!ilWarningRegex.IsMatch (line)) { continue; } - if (line.Contains ("ExportAttribute", StringComparison.Ordinal) + if ((line.Contains ("ExportAttribute", StringComparison.Ordinal) || + line.Contains ("ExportFieldAttribute", StringComparison.Ordinal)) && line.Contains ("RequiresUnreferencedCode", StringComparison.Ordinal)) { continue; } @@ -679,31 +680,12 @@ ISet ReadPackagedManagedAssemblyNames (string apkPath, AndroidTargetArch void AssertPostTrimR8InputsExcludeDeadFrameworkImplementor (string dexFile, string javaSourceDirectory, string acwMapPath, string proguardPrimaryPath) { - const string deadManagedType = "Android.Animation.Animator+IAnimatorListenerImplementor"; - const string deadJavaName = "Lmono/android/animation/Animator_AnimatorListenerImplementor;"; - const string deadJavaDotName = "mono.android.animation.Animator_AnimatorListenerImplementor"; - Assert.IsTrue ( Directory.EnumerateFiles (javaSourceDirectory, "MainActivity.java", SearchOption.AllDirectories).Any (), "Post-trim Java source generation should keep the app activity JCW."); - FileAssert.DoesNotExist ( - Path.Combine (javaSourceDirectory, "mono", "android", "animation", "Animator_AnimatorListenerImplementor.java"), - "Post-trim Java source generation should not copy framework listener implementors removed by ILLink."); FileAssert.Exists (acwMapPath, "Post-trim scan should rewrite acw-map.txt for R8."); - var acwMap = File.ReadAllText (acwMapPath); - Assert.IsFalse (acwMap.Contains (deadManagedType, StringComparison.Ordinal), $"{acwMapPath} should be based on linked assemblies."); - Assert.IsFalse (acwMap.Contains (deadJavaDotName, StringComparison.Ordinal), $"{acwMapPath} should not keep removed framework listener implementors."); - FileAssert.Exists (proguardPrimaryPath, "R8 should generate a primary proguard configuration from the post-trim acw-map."); - Assert.IsFalse ( - File.ReadAllText (proguardPrimaryPath).Contains (deadJavaDotName, StringComparison.Ordinal), - $"{proguardPrimaryPath} should not keep removed framework listener implementors."); - - FileAssert.Exists (dexFile, "R8 should produce classes.dex."); - Assert.IsFalse ( - DexUtils.ContainsClass (deadJavaName, dexFile, AndroidSdkPath), - $"{dexFile} should not contain the removed framework listener implementor."); } static void AssertApkDexDoesNotContain (string apk, string value) diff --git a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs index 888636f60f0..cd596409b30 100644 --- a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs +++ b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs @@ -7,6 +7,9 @@ namespace Java.Interop { [AttributeUsage (AttributeTargets.Method, AllowMultiple=false, Inherited=false)] +#if !NETSTANDARD2_0 + [RequiresUnreferencedCode ("[ExportFieldAttribute] uses dynamic features.")] +#endif #if !JCW_ONLY_TYPE_NAMES public #endif // !JCW_ONLY_TYPE_NAMES @@ -22,4 +25,3 @@ public ExportFieldAttribute (string name) } } - diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 3e9537422fd..c2a087a68c9 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -61,7 +61,7 @@ public void Scan_MarksFrameworkAssemblyPeers () } [Fact] - public void Scan_JniAddNativeMethodRegistrationAttribute_IgnoresAttribute () + public void Scan_JniAddNativeMethodRegistrationAttribute_ReportsXA4251 () { var errors = new List (); var logger = new RecordingLogger (errors); @@ -72,7 +72,8 @@ public void Scan_JniAddNativeMethodRegistrationAttribute_IgnoresAttribute () var assemblyName = reader.GetString (reader.GetAssemblyDefinition ().Name); _ = scanner.Scan (new List<(string, PEReader)> { (assemblyName, peReader) }); - Assert.Empty (errors); + Assert.Contains (errors, e => e.Contains ("HandWrittenNativeRegistrationPeer")); + Assert.Contains (errors, e => e.Contains ("NonPeerNativeRegistration")); } sealed class RecordingLogger (List errors) : ITrimmableTypeMapLogger diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index 7f10badb726..a0d6864172c 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -70,7 +70,7 @@ protected override IEnumerable? IncludedCategories { var value = AppContext.GetData ("IncludeCategories") as string; if (string.IsNullOrEmpty (value)) return null; - return value!.Split (new [] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + return value.Split (new [] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); } } From 8447f63da8bb0a4665078969c362521da9a476a5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 03:03:11 +0200 Subject: [PATCH 35/47] Simplify generated proxy trim suppression Move the required trim suppressions to JavaPeerProxy constructors and remove generated UnconditionalSuppressMessage attribute emission from the typemap PE emitter. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 29 ------------------- .../Java.Interop/JavaPeerProxy.cs | 14 +++++++-- 2 files changed, 11 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 14ce575d47c..8ab18800ea5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -126,7 +126,6 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _exceptionRef; TypeReferenceHandle _androidRuntimeInternalRef; TypeReferenceHandle _androidEnvironmentInternalRef; - TypeReferenceHandle _unconditionalSuppressMessageAttributeRef; MemberReferenceHandle _beginMarshalMethodRef; MemberReferenceHandle _endMarshalMethodRef; @@ -137,9 +136,6 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _jniTypePeerReferenceRef; MemberReferenceHandle _jniEnvTypesRegisterNativesRef; MemberReferenceHandle _readOnlySpanOfJniNativeMethodCtorRef; - MemberReferenceHandle _unconditionalSuppressMessageAttributeCtorRef; - BlobHandle _suppressIl2026BlobHandle; - BlobHandle _suppressIl2111BlobHandle; EntityHandle _anchorTypeHandle; @@ -330,8 +326,6 @@ void EmitTypeReferences () metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidRuntimeInternal")); _androidEnvironmentInternalRef = metadata.AddTypeReference (monoAndroidRuntimeRef, metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("AndroidEnvironmentInternal")); - _unconditionalSuppressMessageAttributeRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, - metadata.GetOrAddString ("System.Diagnostics.CodeAnalysis"), metadata.GetOrAddString ("UnconditionalSuppressMessageAttribute")); // ReadOnlySpan — TypeSpec for generic instantiation _readOnlySpanOpenRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -525,16 +519,6 @@ void EmitMemberReferences () // Legacy marshal-method UCO wrappers use the default unmanaged calling convention. _ucoAttrBlobHandle = _pe.BuildAttributeBlob (b => { }); - _unconditionalSuppressMessageAttributeCtorRef = _pe.AddMemberRef (_unconditionalSuppressMessageAttributeRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, - rt => rt.Void (), - p => { - p.AddParameter ().Type ().String (); - p.AddParameter ().Type ().String (); - })); - _suppressIl2026BlobHandle = BuildUnconditionalSuppressMessageBlob ("IL2026"); - _suppressIl2111BlobHandle = BuildUnconditionalSuppressMessageBlob ("IL2111"); - // JniEnvironment.BeginMarshalMethod(nint jnienv, out JniTransition, out JniRuntime?) -> bool _beginMarshalMethodRef = _pe.AddMemberRef (_jniEnvironmentRef, "BeginMarshalMethod", sig => sig.MethodSignature ().Parameters (3, @@ -746,7 +730,6 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary { }); metadata.AddCustomAttribute (typeDefHandle, selfAttrCtorDef, selfAttrBlob); - AddGeneratedProxyConstructorSuppressions (selfAttrCtorDef); // CreateInstance EmitCreateInstance (proxy); @@ -770,18 +753,6 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary _pe.BuildAttributeBlob (b => { - b.WriteSerializedString ("Trimming"); - b.WriteSerializedString (checkId); - }); - - void AddGeneratedProxyConstructorSuppressions (MethodDefinitionHandle method) - { - _pe.Metadata.AddCustomAttribute (method, _unconditionalSuppressMessageAttributeCtorRef, _suppressIl2026BlobHandle); - _pe.Metadata.AddCustomAttribute (method, _unconditionalSuppressMessageAttributeCtorRef, _suppressIl2111BlobHandle); - } - void EmitAliasHolderType (AliasHolderData holder) { var metadata = _pe.Metadata; diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index 4daa17c5af8..24ceb052f2e 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -49,11 +49,17 @@ public abstract class JavaPeerProxy : Attribute DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + internal const DynamicallyAccessedMemberTypes Constructors = + DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors; + + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Generated proxy constructors pass compile-time-known type tokens to the proxy base constructor.")] + [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Generated proxy constructors pass compile-time-known type tokens to the proxy base constructor.")] protected JavaPeerProxy ( string jniName, [DynamicallyAccessedMembers (MethodsConstructors)] Type targetType, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [DynamicallyAccessedMembers (Constructors)] Type? invokerType) { JniName = jniName ?? throw new ArgumentNullException (nameof (jniName)); @@ -85,7 +91,7 @@ protected JavaPeerProxy ( /// Gets the invoker type for interfaces and abstract classes. /// Returns null for concrete types that can be directly instantiated. /// - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [DynamicallyAccessedMembers (Constructors)] public Type? InvokerType { get; } /// @@ -161,9 +167,11 @@ public abstract class JavaPeerProxy< T > : JavaPeerProxy where T : class, IJavaPeerable { + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Generated proxy constructors pass compile-time-known type tokens to the proxy base constructor.")] + [UnconditionalSuppressMessage ("Trimming", "IL2111", Justification = "Generated proxy constructors pass compile-time-known type tokens to the proxy base constructor.")] protected JavaPeerProxy ( string jniName, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + [DynamicallyAccessedMembers (Constructors)] Type? invokerType) : base (jniName, typeof (T), invokerType) { } From 18f39bccd586e265ff50be7c11372602be4349fb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 10:50:17 +0200 Subject: [PATCH 36/47] Address trimmable runtime review feedback Simplify peer tracking, remove ManagedPeer fixture replacements, keep trimmable unsupported test exclusion in the instrumentation layer, and make expression-based trimmable marshalers unreachable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Scanner/JavaPeerScanner.cs | 5 + .../Android.Runtime/AndroidRuntime.cs | 2 +- .../Android.Runtime/JavaProxyThrowable.cs | 1 + .../JavaMarshalValueManager.cs | 159 ++++++++---------- .../TrimmableTypeMapTypeManager.cs | 22 +-- .../Java.Interop-Tests.targets | 38 ++--- .../net/dot/jni/test/CallNonvirtualBase.java | 29 ---- .../dot/jni/test/CallNonvirtualDerived.java | 31 ---- .../dot/jni/test/CallNonvirtualDerived2.java | 25 --- .../dot/jni/test/CrossReferenceBridge.java | 28 --- .../net/dot/jni/test/RenameClassBase1.java | 30 ---- .../net/dot/jni/test/RenameClassBase2.java | 36 ---- .../net/dot/jni/test/RenameClassDerived.java | 31 ---- .../Mono.Android.NET-Tests.csproj | 2 - .../TestInstrumentation.cs | 5 +- tests/TestRunner.Core/TestInstrumentation.cs | 25 +-- 17 files changed, 102 insertions(+), 369 deletions(-) delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java diff --git a/external/Java.Interop b/external/Java.Interop index 94243c8f4b9..df2aa0a78bd 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 94243c8f4b905f17b3f3bd86daeb84fd18579bb8 +Subproject commit df2aa0a78bd02588797b5b66b6aebf10e1c8de54 diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 5d8dfcb228a..fa92522e530 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -210,6 +210,11 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + // Framework/runtime assemblies contain internal [JniAddNativeMethodRegistration] + // users such as Java.Interop.JavaProxyObject and Java.Interop.ManagedPeer. + // The diagnostic is for user assemblies because the trimmable runtime either + // has generated replacements for framework registration or intentionally + // disables unsupported runtime paths. if (!frameworkAssemblyNames.Contains (index.AssemblyName) && index.MayUseJniAddNativeMethodRegistrationAttribute && HasJniAddNativeMethodRegistrationAttribute (typeDef, index)) { diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 9c68e122258..42523569e86 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -60,7 +60,7 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f var peeked = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); if (peeked is JavaProxyThrowable proxyThrowable) { JniObjectReference.Dispose (ref reference, options); - return proxyThrowable.InnerException; + return proxyThrowable.Exception; } var peekedExc = peeked as Exception; if (peekedExc == null) { diff --git a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs index a8b48686157..f46ad52f191 100644 --- a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs +++ b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs @@ -12,6 +12,7 @@ namespace Android.Runtime { sealed class JavaProxyThrowable : Java.Lang.Error { public readonly Exception InnerException; + public Exception Exception => InnerException; JavaProxyThrowable (string message, Exception innerException) : base (message) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index ae5e823ebc5..20f3b747556 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -20,22 +20,21 @@ namespace Microsoft.Android.Runtime; -sealed class JavaMarshalPeerManager : IDisposable +sealed class JavaMarshalRegisteredPeers : IDisposable { readonly Dictionary> RegisteredInstances = new (); readonly ConcurrentQueue CollectedContexts = new (); - readonly string ownerName; bool disposed; - public unsafe JavaMarshalPeerManager (string ownerName) + public JavaMarshalRegisteredPeers () { - this.ownerName = ownerName; - - var javaMarshalPeerManagerHandle = new GCHandle (this); - var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge ( - GCHandle.ToIntPtr (javaMarshalPeerManagerHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); - JavaMarshal.Initialize (mark_cross_references_ftn); + unsafe { + var registeredPeersHandle = new GCHandle (this); + var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge ( + GCHandle.ToIntPtr (registeredPeersHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); + JavaMarshal.Initialize (mark_cross_references_ftn); + } } public void Dispose () @@ -46,49 +45,41 @@ public void Dispose () void ThrowIfDisposed () { if (disposed) - throw new ObjectDisposedException (ownerName); + throw new ObjectDisposedException (nameof (JavaMarshalRegisteredPeers)); } - public 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 unsafe void CollectPeers () + public void CollectPeers () { ThrowIfDisposed (); - while (CollectedContexts.TryDequeue (out IntPtr contextPtr)) { - Debug.Assert (contextPtr != IntPtr.Zero, "CollectedContexts should not contain null pointers."); - HandleContext* context = (HandleContext*)contextPtr; - - lock (RegisteredInstances) { - Remove (context); - } + unsafe { + while (CollectedContexts.TryDequeue (out IntPtr contextPtr)) { + Debug.Assert (contextPtr != IntPtr.Zero, "CollectedContexts should not contain null pointers."); + HandleContext* context = (HandleContext*)contextPtr; - HandleContext.Free (ref context); - } + lock (RegisteredInstances) { + Remove (context); + } - void Remove (HandleContext* context) - { - int key = context->PeerIdentityHashCode; - if (!RegisteredInstances.TryGetValue (key, out List? peers)) - return; + HandleContext.Free (ref context); + } - for (int i = peers.Count - 1; i >= 0; i--) { - var peer = peers [i]; - if (peer.BelongsToContext (context)) { - peers.RemoveAt (i); + void Remove (HandleContext* context) + { + int key = context->PeerIdentityHashCode; + if (!RegisteredInstances.TryGetValue (key, out List? peers)) + return; + + for (int i = peers.Count - 1; i >= 0; i--) { + var peer = peers [i]; + if (peer.BelongsToContext (context)) { + peers.RemoveAt (i); + } } - } - if (peers.Count == 0) { - RegisteredInstances.Remove (key); + if (peers.Count == 0) { + RegisteredInstances.Remove (key); + } } } } @@ -417,13 +408,13 @@ static unsafe void BridgeProcessingStarted (MarkCrossReferencesArgs* mcr) } [UnmanagedCallersOnly] - static unsafe void BridgeProcessingFinished (IntPtr javaMarshalPeerManagerHandle, MarkCrossReferencesArgs* mcr) + static unsafe void BridgeProcessingFinished (IntPtr registeredPeersHandle, MarkCrossReferencesArgs* mcr) { if (mcr == null) { throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); } - JavaMarshalPeerManager instance = GCHandle.FromIntPtr (javaMarshalPeerManagerHandle).Target; + JavaMarshalRegisteredPeers instance = GCHandle.FromIntPtr (registeredPeersHandle).Target; ReadOnlySpan handlesToFree = instance.ProcessCollectedContexts (mcr); @@ -663,47 +654,52 @@ sealed class CoreClrJavaMarshalValueManager : JniRuntime.ReflectionJniValueManag static readonly Type[] JIConstructorSignature = [typeof (JniObjectReference).MakeByRefType (), typeof (JniObjectReferenceOptions)]; static readonly Type[] XAConstructorSignature = [typeof (IntPtr), typeof (JniHandleOwnership)]; - readonly JavaMarshalPeerManager peerManager = new (nameof (CoreClrJavaMarshalValueManager)); + readonly JavaMarshalRegisteredPeers registeredPeers = new (); protected override void Dispose (bool disposing) { - peerManager.Dispose (); + registeredPeers.Dispose (); base.Dispose (disposing); } public override void WaitForGCBridgeProcessing () { - peerManager.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 () { - peerManager.CollectPeers (); + registeredPeers.CollectPeers (); } public override void AddPeer (IJavaPeerable value) { - peerManager.AddPeer (value); + registeredPeers.AddPeer (value); } public override IJavaPeerable? PeekPeer (JniObjectReference reference) { - return peerManager.PeekPeer (reference); + return registeredPeers.PeekPeer (reference); } public override void RemovePeer (IJavaPeerable value) { - peerManager.RemovePeer (value); + registeredPeers.RemovePeer (value); } public override void FinalizePeer (IJavaPeerable value) { - peerManager.FinalizePeer (value); + registeredPeers.FinalizePeer (value); } public override List GetSurfacedPeers () { - return peerManager.GetSurfacedPeers (); + return registeredPeers.GetSurfacedPeers (); } public override IJavaPeerable? CreatePeer ( @@ -854,7 +850,7 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t { var proxy = value as JavaProxyThrowable; if (proxy != null) { - result = proxy.InnerException; + result = proxy.Exception; return true; } return base.TryUnboxPeerObject (value, out result); @@ -867,47 +863,52 @@ sealed class TrimmableTypeMapValueManager : JniRuntime.JniValueManager const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; - readonly JavaMarshalPeerManager peerManager = new (nameof (TrimmableTypeMapValueManager)); + readonly JavaMarshalRegisteredPeers registeredPeers = new (); protected override void Dispose (bool disposing) { - peerManager.Dispose (); + registeredPeers.Dispose (); base.Dispose (disposing); } public override void WaitForGCBridgeProcessing () { - peerManager.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 () { - peerManager.CollectPeers (); + registeredPeers.CollectPeers (); } public override void AddPeer (IJavaPeerable value) { - peerManager.AddPeer (value); + registeredPeers.AddPeer (value); } public override IJavaPeerable? PeekPeer (JniObjectReference reference) { - return peerManager.PeekPeer (reference); + return registeredPeers.PeekPeer (reference); } public override void RemovePeer (IJavaPeerable value) { - peerManager.RemovePeer (value); + registeredPeers.RemovePeer (value); } public override void FinalizePeer (IJavaPeerable value) { - peerManager.FinalizePeer (value); + registeredPeers.FinalizePeer (value); } public override List GetSurfacedPeers () { - return peerManager.GetSurfacedPeers (); + return registeredPeers.GetSurfacedPeers (); } public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) @@ -1231,36 +1232,16 @@ public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniVa [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize) - { - if (IsJniValueType) { - return sourceValue; - } - return base.CreateParameterFromManagedExpression (context, sourceValue, synchronize); - } + => throw new UnreachableException ( + $"{nameof (CreateParameterFromManagedExpression)} should not be called in the trimmable typemap path. " + + "Generated marshal methods use pregenerated value marshaling."); [RequiresDynamicCode (ExpressionRequiresUnreferencedCode)] [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) - { - if (IsJniValueType) { - return sourceValue; - } - if (typeof (T) == typeof (string)) { - return CreateStringReturnValueFromManagedExpression (context, sourceValue); - } - return base.CreateReturnValueFromManagedExpression (context, sourceValue); - } - - Expression CreateStringReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) - { - Func createString = JniEnvironment.Strings.NewString; - - var reference = Expression.Variable (typeof (JniObjectReference), sourceValue.Name + "_ref"); - context.LocalVariables.Add (reference); - context.CreationStatements.Add (Expression.Assign (reference, Expression.Call (createString.GetMethodInfo (), sourceValue))); - context.CleanupStatements.Add (DisposeObjectReference (reference)); - return ReturnObjectReferenceToJni (context, sourceValue.Name, reference); - } + => throw new UnreachableException ( + $"{nameof (CreateReturnValueFromManagedExpression)} should not be called in the trimmable typemap path. " + + "Generated marshal methods use pregenerated value marshaling."); } static void DisposeReferenceState (ref JniValueMarshalerState state) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index e345afdc7f3..a8e5980341f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -18,7 +18,6 @@ class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; static readonly Type[] EmptyTypeArray = []; - static readonly Dictionary ArrayTypes = []; static readonly Dictionary JavaObjectArrayTypes = []; static readonly Dictionary PrimitiveArrayTypes = []; readonly ConcurrentDictionary _typeSignatureCache = new (); @@ -28,8 +27,8 @@ class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager static TrimmableTypeMapTypeManager () { - AddKnownArrayTypes (); - AddKnownArrayTypes (); + AddKnownJavaObjectArrayTypes (); + AddKnownJavaObjectArrayTypes (); AddKnownPrimitiveArrayTypes (); AddKnownPrimitiveArrayTypes (); @@ -331,10 +330,6 @@ static bool TryMakeArrayType (Type elementType, int rank, [NotNullWhen (true)] o static bool TryMakeArrayType (Type elementType, [NotNullWhen (true)] out Type? arrayType) { - if (ArrayTypes.TryGetValue (elementType, out arrayType)) { - return true; - } - return TrimmableTypeMap.Instance.TryGetArrayType (elementType, out arrayType); } @@ -363,10 +358,10 @@ static void AddKnownPrimitiveArrayTypes< [DynamicallyAccessedMembers (Constructors)] TArray> () { - AddKnownArrayTypes (); - AddKnownArrayTypes> (); - AddKnownArrayTypes> (); - AddKnownArrayTypes (); + AddKnownJavaObjectArrayTypes (); + AddKnownJavaObjectArrayTypes> (); + AddKnownJavaObjectArrayTypes> (); + AddKnownJavaObjectArrayTypes (); PrimitiveArrayTypes [typeof (T)] = [ typeof (T[]), typeof (JavaArray), @@ -375,13 +370,10 @@ PrimitiveArrayTypes [typeof (T)] = [ ]; } - static void AddKnownArrayTypes< + static void AddKnownJavaObjectArrayTypes< [DynamicallyAccessedMembers (Constructors)] T> () { - ArrayTypes [typeof (T)] = typeof (T[]); - ArrayTypes [typeof (T[])] = typeof (T[][]); - ArrayTypes [typeof (T[][])] = typeof (T[][][]); JavaObjectArrayTypes [typeof (T)] = typeof (JavaObjectArray); JavaObjectArrayTypes [typeof (JavaObjectArray)] = typeof (JavaObjectArray>); } diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 6c3b43a0332..838470a434b 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -15,29 +15,24 @@ @@ -46,21 +41,12 @@ The trimmable variant uses a distinct output jar so MSBuild up-to-date checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\GetThis.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CrossReferenceBridge.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualBase.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualDerived.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\CallNonvirtualDerived2.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassBase1.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassBase2.java" /> - <_TrimmableJavaInteropDesktopFixture Include="$(MSBuildThisFileDirectory)..\..\..\external\Java.Interop\tests\Java.Interop-Tests\java\net\dot\jni\test\RenameClassDerived.java" /> - - diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java deleted file mode 100644 index 2dc73430987..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualBase implements GCUserPeerable { - - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualBase () { - } - - boolean methodInvoked; - public void method () { - System.out.println ("CallNonvirtualBase.method() invoked!"); - methodInvoked = true; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java deleted file mode 100644 index 9f7f11e831b..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualDerived - extends CallNonvirtualBase - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualDerived () { - } - - boolean methodInvoked; - public void method () { - System.out.println ("CallNonvirtualDerived.method() invoked!"); - methodInvoked = true; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java deleted file mode 100644 index 3ded295588a..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualDerived2 - extends CallNonvirtualDerived - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualDerived2 () { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java deleted file mode 100644 index dd6c4b762ea..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -// Android trimmable typemap variant of the Java.Interop CrossReferenceBridge -// fixture. The desktop JVM fixture calls net.dot.jni.ManagedPeer.construct() -// from its constructor; the trimmable Android runtime intentionally does not -// ship ManagedPeer, and managed peer construction is handled by generated -// typemap proxies instead. -public class CrossReferenceBridge implements GCUserPeerable { - - ArrayList managedReferences = new ArrayList(); - - public CrossReferenceBridge () { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java deleted file mode 100644 index 5715e2651b3..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase1 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase1 () { - System.out.println("RenameClassBase.()"); - } - - public int hashCode () { - System.out.println("RenameClassBase1.hashCode()"); - return 16; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java deleted file mode 100644 index cda0752a806..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase2 - extends RenameClassBase1 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase2 () { - System.out.println("RenameClassBase.()"); - } - - public int hashCode () { - System.out.println("RenameClassBase2.hashCode()"); - return 32; - } - - public int myNewHashCode() { - System.out.println("RenameClassBase2.myNewHashCode()"); - return 33; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java deleted file mode 100644 index e6b53d82c53..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassDerived - extends RenameClassBase2 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassDerived () { - System.out.println("RenameClassDerived.()"); - } - - public int hashCode () { - System.out.println("RenameClassDerived.hashCode()"); - return 64; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index febd15a295c..a021ea0f2ee 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -52,8 +52,6 @@ want tests tagged [Category("Intune")] to run. --> - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs index a0d6864172c..2f76e15dca3 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/TestInstrumentation.cs @@ -30,10 +30,7 @@ protected override IEnumerable? ExcludedCategories { if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { categories.Add ("NativeTypeMap"); categories.Add ("Export"); - // Java.Interop tests in this category exercise APIs that are unsupported - // by design under the trimmable typemap, such as obsolete - // marshal-member-builder paths or runtime swaps that are not part of - // the generated/AOT-safe trimmable runtime path. + categories.Add ("TrimmableTypeMapUnsupported"); } // Build-time flags flow in via runtimeconfig.json properties diff --git a/tests/TestRunner.Core/TestInstrumentation.cs b/tests/TestRunner.Core/TestInstrumentation.cs index 379241df172..f9cf928c426 100644 --- a/tests/TestRunner.Core/TestInstrumentation.cs +++ b/tests/TestRunner.Core/TestInstrumentation.cs @@ -151,18 +151,11 @@ TestFilter BuildNUnitFilter () } if (!noExclusions) { - // Exclude categories from two sources: - // 1. The ExcludedCategories subclass property - // 2. `ExcludeCategories` from runtimeconfig.json, set from the MSBuild - // property of the same name. - var excludes = new List (); if (ExcludedCategories is not null) { - excludes.AddRange (ExcludedCategories); - } - excludes.AddRange (GetConfiguredCategories ("ExcludeCategories")); - foreach (var cat in excludes) { - filterElements.Add (new XElement ("not", new XElement ("cat", cat))); - Log.Info (LogTag, $"Excluding category: {cat}"); + foreach (var cat in ExcludedCategories) { + filterElements.Add (new XElement ("not", new XElement ("cat", cat))); + Log.Info (LogTag, $"Excluding category: {cat}"); + } } if (ExcludedTestNames is not null) { @@ -226,16 +219,6 @@ List GetListExtra (string key) .ToList (); } - static List GetConfiguredCategories (string key) - { - var value = AppContext.GetData (key) as string; - if (value is null) { - return []; - } - return value.Split ([';', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .ToList (); - } - static void CountResults (ITestResult result, ref int passed, ref int failed, ref int skipped) { if (result.Test.IsSuite) { From 7496a3281a9b73ae1723a38241a730ea1ddd2fb8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 12:39:52 +0200 Subject: [PATCH 37/47] Simplify trimmable marshaler integration Keep Java.Interop reflection manager changes minimal and move trimmable-only primitive array handling to Android. Also removes the unproven ExportFieldAttribute RUC annotation and tightens the trim-warning test filter accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../JavaMarshalValueManager.cs | 234 +++++++++++++++++- .../TrimmableTypeMapTypeManager.cs | 37 +++ .../TrimmableTypeMapBuildTests.cs | 7 +- .../Java.Interop/ExportFieldAttribute.cs | 4 - 5 files changed, 270 insertions(+), 14 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index df2aa0a78bd..0208f3ad4c3 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit df2aa0a78bd02588797b5b66b6aebf10e1c8de54 +Subproject commit 0208f3ad4c38027dd5453779b00df8cc55604308 diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 20f3b747556..5ff3febd49f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -1123,8 +1123,7 @@ static bool TryCreateJavaArrayWrapper ( Type? targetType, [NotNullWhen (true)] out object? value) { - if (targetType != null && TryGetPrimitiveArrayValueMarshaler (targetType, out var marshaler)) { - value = marshaler.CreateValue (ref reference, options, targetType); + if (targetType != null && TryCreatePrimitiveArrayWrapper (ref reference, options, targetType, out value)) { return true; } @@ -1132,6 +1131,225 @@ static bool TryCreateJavaArrayWrapper ( return false; } + delegate TArray PrimitiveArrayFactory (ref JniObjectReference reference, JniObjectReferenceOptions options); + + readonly struct PrimitiveArrayArgumentState + { + public readonly bool DisposeArray; + + public PrimitiveArrayArgumentState (bool disposeArray) + { + DisposeArray = disposeArray; + } + } + + abstract class PrimitiveArrayHandler + { + public abstract bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value); + + public abstract bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state); + + public abstract bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize); + + public abstract bool IsTargetType (Type targetType); + } + + sealed class PrimitiveArrayHandler : PrimitiveArrayHandler + where TArray : global::Java.Interop.JavaArray + { + readonly PrimitiveArrayFactory createFromReference; + readonly Func create; + readonly Func, TArray> createCopy; + + public PrimitiveArrayHandler (PrimitiveArrayFactory createFromReference, Func create, Func, TArray> createCopy) + { + this.createFromReference = createFromReference; + this.create = create; + this.createCopy = createCopy; + } + + public override bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value) + { + if (!IsTargetType (targetType)) { + value = null; + return false; + } + + var array = createFromReference (ref reference, options); + if (targetType == typeof (T[]) || IsCompatibleListType (targetType)) { + try { + value = array.ToArray (); + return true; + } finally { + array.Dispose (); + } + } + + value = array; + return true; + } + + public override bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + { + if (value is TArray array) { + state = new JniValueMarshalerState (array); + return true; + } + + if (value is not IList list) { + state = new JniValueMarshalerState (); + return false; + } + + synchronize = GetCopyDirection (synchronize); + var copy = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; + var marshaledArray = copy ? createCopy (list) : create (list.Count); + state = new JniValueMarshalerState (marshaledArray, new PrimitiveArrayArgumentState (disposeArray: true)); + return true; + } + + public override bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + if (state.PeerableValue is not TArray source) { + return false; + } + + synchronize = GetCopyDirection (synchronize); + if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList destination) { + for (int i = 0; i < source.Length; i++) { + destination [i] = source [i]; + } + } + + if (state.Extra is PrimitiveArrayArgumentState { DisposeArray: true }) { + source.Dispose (); + } + + state = new JniValueMarshalerState (); + return true; + } + + public override bool IsTargetType (Type targetType) + { + return targetType == typeof (global::Java.Interop.JavaArray) || + targetType == typeof (global::Java.Interop.JavaPrimitiveArray) || + targetType == typeof (TArray) || + targetType == typeof (T[]) || + IsCompatibleListType (targetType); + } + + static bool IsCompatibleListType (Type targetType) + { + return targetType.IsGenericType && + targetType.GetGenericTypeDefinition () == typeof (IList<>) && + targetType.IsAssignableFrom (typeof (IList)); + } + } + + static readonly PrimitiveArrayHandler[] PrimitiveArrayHandlers = new PrimitiveArrayHandler [] { + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaBooleanArray (ref h, o), + length => new JavaBooleanArray (length), + list => new JavaBooleanArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSByteArray (ref h, o), + length => new JavaSByteArray (length), + list => new JavaSByteArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaCharArray (ref h, o), + length => new JavaCharArray (length), + list => new JavaCharArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt16Array (ref h, o), + length => new JavaInt16Array (length), + list => new JavaInt16Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt32Array (ref h, o), + length => new JavaInt32Array (length), + list => new JavaInt32Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt64Array (ref h, o), + length => new JavaInt64Array (length), + list => new JavaInt64Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSingleArray (ref h, o), + length => new JavaSingleArray (length), + list => new JavaSingleArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaDoubleArray (ref h, o), + length => new JavaDoubleArray (length), + list => new JavaDoubleArray (list)), + }; + + static bool TryCreatePrimitiveArrayWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryCreateWrapper (ref reference, options, targetType, out value)) { + return true; + } + } + + value = null; + return false; + } + + static bool TryCreatePrimitiveArrayArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryCreateArgumentState (value, synchronize, out state)) { + return true; + } + } + + state = new JniValueMarshalerState (); + return false; + } + + static bool TryDestroyPrimitiveArrayArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryDestroyArgumentState (value, ref state, synchronize)) { + return true; + } + } + + return false; + } + + static bool IsPrimitiveArrayTargetType (Type targetType) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.IsTargetType (targetType)) { + return true; + } + } + + return false; + } + + static ParameterAttributes GetCopyDirection (ParameterAttributes value) + { + const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; + if ((value & inout) != 0) + return value & inout; + return inout; + } + protected override JniValueMarshaler GetValueMarshalerCore (Type type) { EnsureNotDisposed (); @@ -1148,8 +1366,8 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) return TrimmableValueMarshaler.Instance; if (type == typeof (object)) return ObjectValueMarshaler; - if (TryGetPrimitiveArrayValueMarshaler (type, out var primitiveArrayMarshaler)) - return primitiveArrayMarshaler; + if (IsPrimitiveArrayTargetType (type)) + return TrimmableValueMarshaler.Instance; if (type == typeof (IJavaPeerable) || typeof (IJavaPeerable).IsAssignableFrom (type)) return PeerableValueMarshaler; @@ -1160,7 +1378,7 @@ protected override JniValueMarshaler GetValueMarshalerCore (Type type) { EnsureNotDisposed (); var type = typeof (T); - if (type.IsArray && !TryGetPrimitiveArrayValueMarshaler (type, out _)) { + if (type.IsArray || IsPrimitiveArrayTargetType (type)) { return TrimmableValueMarshaler.Instance; } @@ -1215,6 +1433,9 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState if (value == null) { return new JniValueMarshalerState (); } + if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { + return primitiveArrayState; + } if (value is IJavaPeerable peerable) { return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); } @@ -1227,6 +1448,9 @@ public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) { + if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { + return; + } DisposeReferenceState (ref state); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index a8e5980341f..e8324e4c589 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -167,6 +167,43 @@ static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signatur } } + static bool TryGetPrimitiveArrayTypeSignature (Type type, out JniTypeSignature signature) + { + if (TryGetPrimitiveArrayTypeSignature (type, "Z", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "B", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "C", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "S", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "I", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "J", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "F", out signature)) + return true; + if (TryGetPrimitiveArrayTypeSignature (type, "D", out signature)) + return true; + + signature = default; + return false; + } + + static bool TryGetPrimitiveArrayTypeSignature< + T, + TArray> (Type type, string jniSimpleReference, out JniTypeSignature signature) + where TArray : JavaArray + { + if (type == typeof (JavaArray) || type == typeof (JavaPrimitiveArray) || type == typeof (TArray)) { + signature = new JniTypeSignature (jniSimpleReference, arrayRank: 1, keyword: true); + return true; + } + + signature = default; + return false; + } + [return: DynamicallyAccessedMembers (Methods | Constructors | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 3ae3e1a44fd..b33ca8c0dd3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -563,16 +563,15 @@ class ExportShapes : Java.Lang.Object { // trimmable typemap assembly or the [Export] source file. // The regex requires ": warning IL" to avoid matching CSC command lines // that mention IL codes in /nowarn switches. - // Exclude IL2026 about ExportAttribute/ExportFieldAttribute constructors - // themselves — those are expected (the attributes carry [RequiresUnreferencedCode]). + // Exclude IL2026 about ExportAttribute constructors themselves — those + // are expected (the attribute carries [RequiresUnreferencedCode]). var ilWarningRegex = new Regex (@":\s*warning\s+(IL[23]\d{3})\b", RegexOptions.Compiled); var offending = new List (); foreach (var line in builder.LastBuildOutput) { if (!ilWarningRegex.IsMatch (line)) { continue; } - if ((line.Contains ("ExportAttribute", StringComparison.Ordinal) || - line.Contains ("ExportFieldAttribute", StringComparison.Ordinal)) + if (line.Contains ("ExportAttribute", StringComparison.Ordinal) && line.Contains ("RequiresUnreferencedCode", StringComparison.Ordinal)) { continue; } diff --git a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs index cd596409b30..c7db9433b8d 100644 --- a/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs +++ b/src/Xamarin.Android.NamingCustomAttributes/Java.Interop/ExportFieldAttribute.cs @@ -7,9 +7,6 @@ namespace Java.Interop { [AttributeUsage (AttributeTargets.Method, AllowMultiple=false, Inherited=false)] -#if !NETSTANDARD2_0 - [RequiresUnreferencedCode ("[ExportFieldAttribute] uses dynamic features.")] -#endif #if !JCW_ONLY_TYPE_NAMES public #endif // !JCW_ONLY_TYPE_NAMES @@ -24,4 +21,3 @@ public ExportFieldAttribute (string name) public string Name {get; set;} } } - From f2c59bde49993c3cf31ff650838919ff54a494b7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 13:08:33 +0200 Subject: [PATCH 38/47] Re-enable trimmable GC bridge coverage Use an Android trimmable CrossReferenceBridge Java fixture that preserves GCUserPeerable without relying on desktop Java.Interop ManagedPeer.construct, and exclude reflection-manager-only Java.Interop tests from the generated trimmable Android lane. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop-Tests.NET.csproj | 4 +++ .../Java.Interop-Tests.targets | 20 ++++++------ .../dot/jni/test/CrossReferenceBridge.java | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java diff --git a/external/Java.Interop b/external/Java.Interop index 0208f3ad4c3..4aae0bd35b3 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 0208f3ad4c38027dd5453779b00df8cc55604308 +Subproject commit 4aae0bd35b31fa547ef20767504a0d41f31a5c8a diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj index 79ccfecab84..20ae5d2f0b9 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj @@ -40,6 +40,10 @@ + + + + @@ -42,12 +42,14 @@ checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> + managedReferences = new ArrayList(); + + public CrossReferenceBridge () { + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} From 298f26234a68f3fe18573b7344f0c2a0ec135f99 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 13:18:37 +0200 Subject: [PATCH 39/47] Re-enable trimmable replacement method lookup test Add Android trimmable RenameClass fixture variants without ManagedPeer.construct so JniPeerMembersTests.ReplacementTypeUsedForMethodLookup can exercise replacement-type method lookup on device. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop-Tests.targets | 23 ++++++------ .../net/dot/jni/test/RenameClassBase1.java | 30 ++++++++++++++++ .../net/dot/jni/test/RenameClassBase2.java | 36 +++++++++++++++++++ .../net/dot/jni/test/RenameClassDerived.java | 31 ++++++++++++++++ 5 files changed, 109 insertions(+), 13 deletions(-) create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java diff --git a/external/Java.Interop b/external/Java.Interop index 4aae0bd35b3..67b9796744b 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 4aae0bd35b31fa547ef20767504a0d41f31a5c8a +Subproject commit 67b9796744b7480625ef7c736ea35506356c0eea diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index 845615f7fc2..c17374079e0 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -15,7 +15,7 @@ @@ -41,14 +41,13 @@ The trimmable variant uses a distinct output jar so MSBuild up-to-date checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> + <_TrimmableJavaInteropTestFixture Include="GetThis;CrossReferenceBridge;RenameClassBase1;RenameClassBase2;RenameClassDerived" Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " /> - - diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java new file mode 100644 index 00000000000..5715e2651b3 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java @@ -0,0 +1,30 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassBase1 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassBase1 () { + System.out.println("RenameClassBase.()"); + } + + public int hashCode () { + System.out.println("RenameClassBase1.hashCode()"); + return 16; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java new file mode 100644 index 00000000000..cda0752a806 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java @@ -0,0 +1,36 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassBase2 + extends RenameClassBase1 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassBase2 () { + System.out.println("RenameClassBase.()"); + } + + public int hashCode () { + System.out.println("RenameClassBase2.hashCode()"); + return 32; + } + + public int myNewHashCode() { + System.out.println("RenameClassBase2.myNewHashCode()"); + return 33; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java new file mode 100644 index 00000000000..e6b53d82c53 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java @@ -0,0 +1,31 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class RenameClassDerived + extends RenameClassBase2 + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public RenameClassDerived () { + System.out.println("RenameClassDerived.()"); + } + + public int hashCode () { + System.out.println("RenameClassDerived.hashCode()"); + return 64; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} From 7ab96595358f407c84649e65f6318e540036260f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 13:24:05 +0200 Subject: [PATCH 40/47] Re-enable trimmable method binding coverage Add Android trimmable CallNonvirtual fixture variants without ManagedPeer.construct so MethodBindingTests can validate virtual dispatch behavior under the trimmable typemap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop-Tests.targets | 2 +- .../net/dot/jni/test/CallNonvirtualBase.java | 29 +++++++++++++++++ .../dot/jni/test/CallNonvirtualDerived.java | 31 +++++++++++++++++++ .../dot/jni/test/CallNonvirtualDerived2.java | 25 +++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java create mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java diff --git a/external/Java.Interop b/external/Java.Interop index 67b9796744b..96ad3f94fa7 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 67b9796744b7480625ef7c736ea35506356c0eea +Subproject commit 96ad3f94fa708e1c1839ce8b98559ec64f6ba7c1 diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets index c17374079e0..40d184dfa6c 100644 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets @@ -41,7 +41,7 @@ The trimmable variant uses a distinct output jar so MSBuild up-to-date checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> - <_TrimmableJavaInteropTestFixture Include="GetThis;CrossReferenceBridge;RenameClassBase1;RenameClassBase2;RenameClassDerived" Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " /> + <_TrimmableJavaInteropTestFixture Include="GetThis;CrossReferenceBridge;RenameClassBase1;RenameClassBase2;RenameClassDerived;CallNonvirtualBase;CallNonvirtualDerived;CallNonvirtualDerived2" Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " /> diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java new file mode 100644 index 00000000000..2dc73430987 --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java @@ -0,0 +1,29 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualBase implements GCUserPeerable { + + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualBase () { + } + + boolean methodInvoked; + public void method () { + System.out.println ("CallNonvirtualBase.method() invoked!"); + methodInvoked = true; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java new file mode 100644 index 00000000000..9f7f11e831b --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java @@ -0,0 +1,31 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualDerived + extends CallNonvirtualBase + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualDerived () { + } + + boolean methodInvoked; + public void method () { + System.out.println ("CallNonvirtualDerived.method() invoked!"); + methodInvoked = true; + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java new file mode 100644 index 00000000000..3ded295588a --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java @@ -0,0 +1,25 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallNonvirtualDerived2 + extends CallNonvirtualDerived + implements GCUserPeerable +{ + ArrayList managedReferences = new ArrayList(); + + public CallNonvirtualDerived2 () { + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } +} From 12946a7e931ee7b6d7d2e4a93ef0bd7ca328cead Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 13:33:00 +0200 Subject: [PATCH 41/47] Remove JavaProxyThrowable Exception wrapper Use the existing InnerException member directly in throwable unwrapping paths instead of adding a redundant Exception property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 2 +- src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs | 2 -- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 42523569e86..9c68e122258 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -60,7 +60,7 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f var peeked = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); if (peeked is JavaProxyThrowable proxyThrowable) { JniObjectReference.Dispose (ref reference, options); - return proxyThrowable.Exception; + return proxyThrowable.InnerException; } var peekedExc = peeked as Exception; if (peekedExc == null) { diff --git a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs index f46ad52f191..630d23b9677 100644 --- a/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs +++ b/src/Mono.Android/Android.Runtime/JavaProxyThrowable.cs @@ -10,9 +10,7 @@ namespace Android.Runtime { sealed class JavaProxyThrowable : Java.Lang.Error { - public readonly Exception InnerException; - public Exception Exception => InnerException; JavaProxyThrowable (string message, Exception innerException) : base (message) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 5ff3febd49f..68d8e64bd0e 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -850,7 +850,7 @@ protected override bool TryUnboxPeerObject (IJavaPeerable value, [NotNullWhen (t { var proxy = value as JavaProxyThrowable; if (proxy != null) { - result = proxy.Exception; + result = proxy.InnerException; return true; } return base.TryUnboxPeerObject (value, out result); From 1cae90bbd5fb30825436f2cf620b04773c526a0f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 14:03:27 +0200 Subject: [PATCH 42/47] Remove ManagedPeer workarounds from trimmable PR Keep the PR focused by reverting NativeAOT manager wiring and ManagedTypeManager cleanup, removing Android-local ManagedPeer fixture replacements, and dropping ManagedPeer absence assertions. Unsupported ManagedPeer-dependent Java.Interop tests remain skipped by category. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop/JreRuntime.cs | 17 ++++-- .../ManagedTypeManager.cs | 54 ++++++++++++---- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 2 - .../TrimmableTypeMapBuildTests.cs | 61 ------------------- src/java-runtime/java-runtime.targets | 2 +- .../Java.Interop-Tests.NET.csproj | 4 -- .../Java.Interop-Tests.targets | 25 ++++---- .../net/dot/jni/test/CallNonvirtualBase.java | 29 --------- .../dot/jni/test/CallNonvirtualDerived.java | 31 ---------- .../dot/jni/test/CallNonvirtualDerived2.java | 25 -------- .../dot/jni/test/CrossReferenceBridge.java | 31 ---------- .../net/dot/jni/test/RenameClassBase1.java | 30 --------- .../net/dot/jni/test/RenameClassBase2.java | 36 ----------- .../net/dot/jni/test/RenameClassDerived.java | 31 ---------- 15 files changed, 69 insertions(+), 311 deletions(-) delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java delete mode 100644 tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java diff --git a/external/Java.Interop b/external/Java.Interop index 96ad3f94fa7..23f9e07061b 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 96ad3f94fa708e1c1839ce8b98559ec64f6ba7c1 +Subproject commit 23f9e07061b800df8652dfaa33330c748574865b diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index f9fcb52b08a..5891149578f 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -57,11 +57,11 @@ static NativeAotRuntimeOptions CreateJreVM (NativeAotRuntimeOptions builder) string.IsNullOrEmpty (builder.JvmLibraryPath)) throw new InvalidOperationException ($"Member `{nameof (NativeAotRuntimeOptions)}.{nameof (NativeAotRuntimeOptions.JvmLibraryPath)}` must be set."); - if (!RuntimeFeature.TrimmableTypeMap) - throw new NotSupportedException ($"Native AOT builds require using {nameof (RuntimeFeature.TrimmableTypeMap)}."); +#if NET + builder.TypeManager ??= CreateDefaultTypeManager (); +#endif // NET - builder.TypeManager ??= new TrimmableTypeMapTypeManager (); - builder.ValueManager ??= new TrimmableTypeMapValueManager (); + builder.ValueManager ??= new JavaMarshalValueManager (); builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) @@ -75,6 +75,15 @@ internal protected JreRuntime (NativeAotRuntimeOptions builder) { } + static JniRuntime.JniTypeManager CreateDefaultTypeManager () + { + if (RuntimeFeature.TrimmableTypeMap) { + return new TrimmableTypeMapTypeManager (); + } + + return new ManagedTypeManager (); + } + public override string? GetCurrentManagedThreadName () { return Thread.CurrentThread.Name; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 2e9dc1187bc..454bab0e1bb 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -7,38 +7,67 @@ namespace Microsoft.Android.Runtime; -[RequiresDynamicCode ("This type manager is reflection-backed and is not compatible with Native AOT.")] -[RequiresUnreferencedCode ("This type manager is reflection-backed and is not trimming-compatible.")] -class ManagedTypeManager : JniRuntime.ReflectionJniTypeManager { +class ManagedTypeManager : JniRuntime.JniTypeManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + public ManagedTypeManager () + { + } + [return: DynamicallyAccessedMembers (Constructors)] - protected override Type? GetInvokerTypeCore ([DynamicallyAccessedMembers (Constructors)] Type type) + protected override Type? GetInvokerTypeCore ( + [DynamicallyAccessedMembers (Constructors)] + 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 type.Assembly.GetType (type + suffix) ?? base.GetInvokerTypeCore (type); + return AssemblyGetType (type.Assembly, 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); - string suffixDefinitionName = definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt); - Type? suffixDefinition = definition.Assembly.GetType (suffixDefinitionName); + Type? suffixDefinition = AssemblyGetType (definition.Assembly, + definition.FullName.Substring (0, bt) + suffix + definition.FullName.Substring (bt)); if (suffixDefinition == null) return base.GetInvokerTypeCore (type); - return suffixDefinition.MakeGenericType (arguments); + return MakeGenericType (suffixDefinition, arguments); } + // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` + [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", "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) + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + ReadOnlySpan methods) { if (methods.IsEmpty) { base.RegisterNativeMembers (nativeClass, type, methods); @@ -99,6 +128,7 @@ public override void RegisterNativeMembers ( } } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { // Base class contains built-in mappings (e.g. java/lang/String → System.String) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index c622c2c231d..d615ca36376 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -49,8 +49,6 @@ - - - - @@ -41,13 +41,12 @@ The trimmable variant uses a distinct output jar so MSBuild up-to-date checks cannot reuse a non-trimmable jar with the desktop GetThis class. --> - <_TrimmableJavaInteropTestFixture Include="GetThis;CrossReferenceBridge;RenameClassBase1;RenameClassBase2;RenameClassDerived;CallNonvirtualBase;CallNonvirtualDerived;CallNonvirtualDerived2" Condition=" '$(_AndroidTypeMapImplementation)' == 'trimmable' " /> - diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java deleted file mode 100644 index 2dc73430987..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualBase.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualBase implements GCUserPeerable { - - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualBase () { - } - - boolean methodInvoked; - public void method () { - System.out.println ("CallNonvirtualBase.method() invoked!"); - methodInvoked = true; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java deleted file mode 100644 index 9f7f11e831b..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualDerived - extends CallNonvirtualBase - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualDerived () { - } - - boolean methodInvoked; - public void method () { - System.out.println ("CallNonvirtualDerived.method() invoked!"); - methodInvoked = true; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java deleted file mode 100644 index 3ded295588a..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallNonvirtualDerived2.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class CallNonvirtualDerived2 - extends CallNonvirtualDerived - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public CallNonvirtualDerived2 () { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java deleted file mode 100644 index 7fc24ee5a08..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CrossReferenceBridge.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -// Variant of CrossReferenceBridge used by the Mono.Android.NET-Tests trimmable typemap lane. -// -// The desktop-JVM variant (../../../java/net/dot/jni/test/CrossReferenceBridge.java) -// calls net.dot.jni.ManagedPeer.construct() from its constructor. That native -// method is only registered by the Java.Interop test JVM and throws -// UnsatisfiedLinkError on Android. The managed CrossReferenceBridge peer is -// constructed by the normal Android JavaObject path, so this fixture only needs -// to implement GCUserPeerable for GC bridge cross-reference tracking. -public class CrossReferenceBridge implements GCUserPeerable { - - ArrayList managedReferences = new ArrayList(); - - public CrossReferenceBridge () { - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java deleted file mode 100644 index 5715e2651b3..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase1.java +++ /dev/null @@ -1,30 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase1 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase1 () { - System.out.println("RenameClassBase.()"); - } - - public int hashCode () { - System.out.println("RenameClassBase1.hashCode()"); - return 16; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java deleted file mode 100644 index cda0752a806..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassBase2.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassBase2 - extends RenameClassBase1 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassBase2 () { - System.out.println("RenameClassBase.()"); - } - - public int hashCode () { - System.out.println("RenameClassBase2.hashCode()"); - return 32; - } - - public int myNewHashCode() { - System.out.println("RenameClassBase2.myNewHashCode()"); - return 33; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java deleted file mode 100644 index e6b53d82c53..00000000000 --- a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/RenameClassDerived.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.dot.jni.test; - -import java.util.ArrayList; - -import net.dot.jni.GCUserPeerable; - -public class RenameClassDerived - extends RenameClassBase2 - implements GCUserPeerable -{ - ArrayList managedReferences = new ArrayList(); - - public RenameClassDerived () { - System.out.println("RenameClassDerived.()"); - } - - public int hashCode () { - System.out.println("RenameClassDerived.hashCode()"); - return 64; - } - - public void jiAddManagedReference (java.lang.Object obj) - { - managedReferences.add (obj); - } - - public void jiClearManagedReferences () - { - managedReferences.clear (); - } -} From a53fb70e2d391454b10a5137da5039c538890585 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 14:16:31 +0200 Subject: [PATCH 43/47] Document built-in type signature mapping choice Record the benchmark finding that the current Type.GetTypeCode plus explicit nullable checks path is zero-allocation, and avoid Nullable.GetUnderlyingType because it allocates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index e8324e4c589..a00375af959 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -93,6 +93,8 @@ static Type GetUnderlyingType (Type type, out int rank) static bool TryGetBuiltInTypeSignature (Type type, out JniTypeSignature signature) { + // Keep the hybrid Type.GetTypeCode + explicit nullable checks. Nullable.GetUnderlyingType () + // allocates a Type[] via GetGenericArguments (), and this path is otherwise allocation-free. if (GetKeywordTypeName (type) is string keywordTypeName) { signature = new JniTypeSignature (keywordTypeName, 0, keyword: true); return true; From 25a05260bbb0ff5d09c2fc075d0c29c6dfc2ee78 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 15:17:41 +0200 Subject: [PATCH 44/47] Split Java marshal value manager types Move each top-level Java marshal value manager type into its own source file and route JavaObjectArray state creation through JniValueManager so the trimmable path no longer needs a value-marshaler implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../CoreClrJavaMarshalValueManager.cs | 223 +++ .../JavaMarshalRegisteredPeers.cs | 464 ++++++ .../JavaMarshalValueManager.cs | 1477 ----------------- .../JavaMarshalValueManagerHelper.cs | 65 + ...peMapValueManager.PrimitiveArrayHandler.cs | 229 +++ .../TrimmableTypeMapValueManager.cs | 395 +++++ .../TrimmableValueMarshalerHelper.cs | 44 + src/Mono.Android/Mono.Android.csproj | 7 +- 9 files changed, 1427 insertions(+), 1479 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManagerHelper.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs diff --git a/external/Java.Interop b/external/Java.Interop index 23f9e07061b..cf298e6232b 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 23f9e07061b800df8652dfaa33330c748574865b +Subproject commit cf298e6232b50c33ffcc00a78d5994a829799456 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..34f12e53eba --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/CoreClrJavaMarshalValueManager.cs @@ -0,0 +1,223 @@ +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)]; + + readonly JavaMarshalRegisteredPeers registeredPeers = new (); + + protected override void Dispose (bool disposing) + { + registeredPeers.Dispose (); + 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 () + { + registeredPeers.CollectPeers (); + } + + public override void AddPeer (IJavaPeerable value) + { + registeredPeers.AddPeer (value); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + return registeredPeers.PeekPeer (reference); + } + + public override void RemovePeer (IJavaPeerable value) + { + registeredPeers.RemovePeer (value); + } + + public override void FinalizePeer (IJavaPeerable value) + { + registeredPeers.FinalizePeer (value); + } + + public override List GetSurfacedPeers () + { + return registeredPeers.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); + + [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; + } + + 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/JavaMarshalRegisteredPeers.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs new file mode 100644 index 00000000000..229dd090a9f --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalRegisteredPeers.cs @@ -0,0 +1,464 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Java; +using Android.Runtime; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +// Originally from: https://github.com/dotnet/java-interop/blob/9b1d8781e8e322849d05efac32119c913b21c192/src/Java.Runtime.Environment/Java.Interop/ManagedValueManager.cs +sealed class JavaMarshalRegisteredPeers : IDisposable +{ + readonly Dictionary> RegisteredInstances = new (); + readonly ConcurrentQueue CollectedContexts = new (); + + bool disposed; + + public JavaMarshalRegisteredPeers () + { + unsafe { + var registeredPeersHandle = new GCHandle (this); + var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge ( + GCHandle.ToIntPtr (registeredPeersHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); + JavaMarshal.Initialize (mark_cross_references_ftn); + } + } + + public void Dispose () + { + disposed = true; + } + + void ThrowIfDisposed () + { + if (disposed) + throw new ObjectDisposedException (nameof (JavaMarshalRegisteredPeers)); + } + + public void CollectPeers () + { + ThrowIfDisposed (); + + unsafe { + while (CollectedContexts.TryDequeue (out IntPtr contextPtr)) { + Debug.Assert (contextPtr != IntPtr.Zero, "CollectedContexts should not contain null pointers."); + HandleContext* context = (HandleContext*)contextPtr; + + lock (RegisteredInstances) { + Remove (context); + } + + HandleContext.Free (ref context); + } + + void Remove (HandleContext* context) + { + int key = context->PeerIdentityHashCode; + if (!RegisteredInstances.TryGetValue (key, out List? peers)) + return; + + for (int i = peers.Count - 1; i >= 0; i--) { + var peer = peers [i]; + if (peer.BelongsToContext (context)) { + peers.RemoveAt (i); + } + } + + if (peers.Count == 0) { + RegisteredInstances.Remove (key); + } + } + } + } + + public void AddPeer (IJavaPeerable value) + { + ThrowIfDisposed (); + + // Remove any collected contexts before adding a new peer. + CollectPeers (); + + 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 ReferenceTrackingHandle (value)]; + RegisteredInstances.Add (key, peers); + return; + } + + for (int i = peers.Count - 1; i >= 0; i--) { + ReferenceTrackingHandle peer = peers [i]; + if (peer.Target is not IJavaPeerable target) + continue; + if (!JniEnvironment.Types.IsSameObject (target.PeerReference, value.PeerReference)) + continue; + if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { + peer.Dispose (); + peers [i] = new ReferenceTrackingHandle (value); + } else { + WarnNotReplacing (key, value, target); + } + GC.KeepAlive (target); + return; + } + + peers.Add (new ReferenceTrackingHandle (value)); + } + } + + void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) + { + JniEnvironment.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 IJavaPeerable? PeekPeer (JniObjectReference reference) + { + ThrowIfDisposed (); + + if (!reference.IsValid) + return null; + + int key = JniEnvironment.References.GetIdentityHashCode (reference); + + lock (RegisteredInstances) { + if (!RegisteredInstances.TryGetValue (key, out List? peers)) + return null; + + for (int i = peers.Count - 1; i >= 0; i--) { + if (peers [i].Target is IJavaPeerable peer + && JniEnvironment.Types.IsSameObject (reference, peer.PeerReference)) + { + return peer; + } + } + + if (peers.Count == 0) + RegisteredInstances.Remove (key); + } + return null; + } + + public void RemovePeer (IJavaPeerable value) + { + ThrowIfDisposed (); + + // Remove any collected contexts before modifying RegisteredInstances + CollectPeers (); + + if (value == null) + throw new ArgumentNullException (nameof (value)); + + lock (RegisteredInstances) { + int key = value.JniIdentityHashCode; + if (!RegisteredInstances.TryGetValue (key, out List? peers)) + return; + + for (int i = peers.Count - 1; i >= 0; i--) { + ReferenceTrackingHandle peer = peers [i]; + IJavaPeerable? target = peer.Target; + if (ReferenceEquals (value, target)) { + peers.RemoveAt (i); + peer.Dispose (); + } + GC.KeepAlive (target); + } + if (peers.Count == 0) + RegisteredInstances.Remove (key); + } + } + + public void FinalizePeer (IJavaPeerable value) + { + var h = value.PeerReference; + var o = JniEnvironment.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 List GetSurfacedPeers () + { + ThrowIfDisposed (); + + // Remove any collected contexts before iterating over all the registered instances + CollectPeers (); + + lock (RegisteredInstances) { + var peers = new List (RegisteredInstances.Count); + foreach (var (identityHashCode, referenceTrackingHandles) in RegisteredInstances) { + foreach (var peer in referenceTrackingHandles) { + if (peer.Target is IJavaPeerable target) { + peers.Add (new JniSurfacedPeerInfo (identityHashCode, new WeakReference (target))); + } + } + } + return peers; + } + } + + unsafe struct ReferenceTrackingHandle : IDisposable + { + WeakReference _weakReference; + HandleContext* _context; + + public bool BelongsToContext (HandleContext* context) + => _context == context; + + public ReferenceTrackingHandle (IJavaPeerable peer) + { + _context = HandleContext.Alloc (peer); + _weakReference = new (peer); + } + + public IJavaPeerable? Target + => _weakReference.TryGetTarget (out var target) ? target : null; + + public void Dispose () + { + if (_context == null) + return; + + IJavaPeerable? target = Target; + + GCHandle handle = HandleContext.GetAssociatedGCHandle (_context); + HandleContext.Free (ref _context); + _weakReference.SetTarget (null); + if (handle.IsAllocated) { + handle.Free (); + } + + // Make sure the target is not collected before we finish disposing + GC.KeepAlive (target); + } + } + + [StructLayout (LayoutKind.Sequential)] + unsafe struct HandleContext + { + static readonly nuint Size = (nuint)Marshal.SizeOf (); + static readonly Dictionary referenceTrackingHandles = new (); + + int identityHashCode; + IntPtr controlBlock; + + public int PeerIdentityHashCode => identityHashCode; + public bool IsCollected + { + get + { + if (controlBlock == IntPtr.Zero) + throw new InvalidOperationException ("HandleContext control block is not initialized."); + + return ((JniObjectReferenceControlBlock*) controlBlock)->handle == IntPtr.Zero; + } + } + + // This is an internal mirror of the Java.Interop.JniObjectReferenceControlBlock + private struct JniObjectReferenceControlBlock + { + public IntPtr handle; + public int handle_type; + public IntPtr weak_handle; + public int refs_added; + } + + public static GCHandle GetAssociatedGCHandle (HandleContext* context) + { + lock (referenceTrackingHandles) { + if (!referenceTrackingHandles.TryGetValue ((IntPtr) context, out GCHandle handle)) { + throw new InvalidOperationException ("Unknown reference tracking handle."); + } + + return handle; + } + } + + public static unsafe void EnsureAllContextsAreOurs (MarkCrossReferencesArgs* mcr) + { +// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.ComponentCount' is only supported on: 'android'. +// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.Components' is only supported on: 'android'. +// This call site is reachable on all platforms. 'StronglyConnectedComponent.Count' is only supported on: 'android'. +// This call site is reachable on all platforms. 'StronglyConnectedComponent.Contexts' is only supported on: 'android'. +#pragma warning disable CA1416 + + lock (referenceTrackingHandles) { + for (nuint i = 0; i < mcr->ComponentCount; i++) { + StronglyConnectedComponent component = mcr->Components [i]; + EnsureAllContextsInComponentAreOurs (component); + } + } + + static void EnsureAllContextsInComponentAreOurs (StronglyConnectedComponent component) + { + for (nuint i = 0; i < component.Count; i++) { + EnsureContextIsOurs ((IntPtr)component.Contexts [i]); + } + } + + static void EnsureContextIsOurs (IntPtr context) + { + if (!referenceTrackingHandles.ContainsKey (context)) { + throw new InvalidOperationException ("Unknown reference tracking handle."); + } + } + +#pragma warning restore CA1416 + } + + public static HandleContext* Alloc (IJavaPeerable peer) + { + var context = (HandleContext*) NativeMemory.AllocZeroed (1, Size); + if (context == null) { + throw new OutOfMemoryException ("Failed to allocate memory for HandleContext."); + } + + context->identityHashCode = peer.JniIdentityHashCode; + context->controlBlock = peer.JniObjectReferenceControlBlock; + + GCHandle handle = JavaMarshal.CreateReferenceTrackingHandle (peer, context); + lock (referenceTrackingHandles) { + referenceTrackingHandles [(IntPtr) context] = handle; + } + + return context; + } + + public static void Free (ref HandleContext* context) + { + if (context == null) { + return; + } + + lock (referenceTrackingHandles) { + referenceTrackingHandles.Remove ((IntPtr)context); + } + + NativeMemory.Free (context); + context = null; + } + } + + [UnmanagedCallersOnly] + static unsafe void BridgeProcessingStarted (MarkCrossReferencesArgs* mcr) + { + if (mcr == null) { + throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); + } + + HandleContext.EnsureAllContextsAreOurs (mcr); + } + + [UnmanagedCallersOnly] + static unsafe void BridgeProcessingFinished (IntPtr registeredPeersHandle, MarkCrossReferencesArgs* mcr) + { + if (mcr == null) { + throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); + } + + JavaMarshalRegisteredPeers instance = GCHandle.FromIntPtr (registeredPeersHandle).Target; + + ReadOnlySpan handlesToFree = instance.ProcessCollectedContexts (mcr); + + +// This call site is reachable on all platforms. 'JavaMarshal.FinishCrossReferenceProcessing(MarkCrossReferencesArgs*, ReadOnlySpan)' is only supported on: 'android'. +#pragma warning disable CA1416 + JavaMarshal.FinishCrossReferenceProcessing (mcr, handlesToFree); +#pragma warning restore CA1416 + } + + unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) + { + List handlesToFree = []; + +// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.ComponentCount' is only supported on: 'android'. +// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.Components' is only supported on: 'android'. +// This call site is reachable on all platforms. 'StronglyConnectedComponent.Count' is only supported on: 'android'. +// This call site is reachable on all platforms. 'StronglyConnectedComponent.Contexts' is only supported on: 'android'. +#pragma warning disable CA1416 + + for (int i = 0; (nuint)i < mcr->ComponentCount; i++) { + StronglyConnectedComponent component = mcr->Components [i]; + for (int j = 0; (nuint)j < component.Count; j++) { + ProcessContext ((HandleContext*)component.Contexts [j]); + } + } + +#pragma warning restore CA1416 + + void ProcessContext (HandleContext* context) + { + if (context == null) { + throw new ArgumentNullException (nameof (context), "HandleContext should never be null."); + } + + // Ignore contexts which were not collected + if (!context->IsCollected) { + return; + } + + GCHandle handle = HandleContext.GetAssociatedGCHandle (context); + + // Note: modifying the RegisteredInstances dictionary while processing the collected contexts + // is tricky and can lead to deadlocks, so we remember which contexts were collected and we will free + // them later outside of the bridge processing loop. + CollectedContexts.Enqueue ((IntPtr)context); + + // important: we must not free the handle before passing it to JavaMarshal.FinishCrossReferenceProcessing + handlesToFree.Add (handle); + } + + return CollectionsMarshal.AsSpan (handlesToFree); + } + +} 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 68d8e64bd0e..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ /dev/null @@ -1,1477 +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.Concurrent; -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.Linq.Expressions; -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.Java; -using System.Threading; -using Android.Runtime; -using Java.Interop; -using Java.Interop.Expressions; - -namespace Microsoft.Android.Runtime; - -sealed class JavaMarshalRegisteredPeers : IDisposable -{ - readonly Dictionary> RegisteredInstances = new (); - readonly ConcurrentQueue CollectedContexts = new (); - - bool disposed; - - public JavaMarshalRegisteredPeers () - { - unsafe { - var registeredPeersHandle = new GCHandle (this); - var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge ( - GCHandle.ToIntPtr (registeredPeersHandle), &BridgeProcessingStarted, &BridgeProcessingFinished); - JavaMarshal.Initialize (mark_cross_references_ftn); - } - } - - public void Dispose () - { - disposed = true; - } - - void ThrowIfDisposed () - { - if (disposed) - throw new ObjectDisposedException (nameof (JavaMarshalRegisteredPeers)); - } - - public void CollectPeers () - { - ThrowIfDisposed (); - - unsafe { - while (CollectedContexts.TryDequeue (out IntPtr contextPtr)) { - Debug.Assert (contextPtr != IntPtr.Zero, "CollectedContexts should not contain null pointers."); - HandleContext* context = (HandleContext*)contextPtr; - - lock (RegisteredInstances) { - Remove (context); - } - - HandleContext.Free (ref context); - } - - void Remove (HandleContext* context) - { - int key = context->PeerIdentityHashCode; - if (!RegisteredInstances.TryGetValue (key, out List? peers)) - return; - - for (int i = peers.Count - 1; i >= 0; i--) { - var peer = peers [i]; - if (peer.BelongsToContext (context)) { - peers.RemoveAt (i); - } - } - - if (peers.Count == 0) { - RegisteredInstances.Remove (key); - } - } - } - } - - public void AddPeer (IJavaPeerable value) - { - ThrowIfDisposed (); - - // Remove any collected contexts before adding a new peer. - CollectPeers (); - - 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 ReferenceTrackingHandle (value)]; - RegisteredInstances.Add (key, peers); - return; - } - - for (int i = peers.Count - 1; i >= 0; i--) { - ReferenceTrackingHandle peer = peers [i]; - if (peer.Target is not IJavaPeerable target) - continue; - if (!JniEnvironment.Types.IsSameObject (target.PeerReference, value.PeerReference)) - continue; - if (target.JniManagedPeerState.HasFlag (JniManagedPeerStates.Replaceable)) { - peer.Dispose (); - peers [i] = new ReferenceTrackingHandle (value); - } else { - WarnNotReplacing (key, value, target); - } - GC.KeepAlive (target); - return; - } - - peers.Add (new ReferenceTrackingHandle (value)); - } - } - - void WarnNotReplacing (int key, IJavaPeerable ignoreValue, IJavaPeerable keepValue) - { - JniEnvironment.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 IJavaPeerable? PeekPeer (JniObjectReference reference) - { - ThrowIfDisposed (); - - if (!reference.IsValid) - return null; - - int key = JniEnvironment.References.GetIdentityHashCode (reference); - - lock (RegisteredInstances) { - if (!RegisteredInstances.TryGetValue (key, out List? peers)) - return null; - - for (int i = peers.Count - 1; i >= 0; i--) { - if (peers [i].Target is IJavaPeerable peer - && JniEnvironment.Types.IsSameObject (reference, peer.PeerReference)) - { - return peer; - } - } - - if (peers.Count == 0) - RegisteredInstances.Remove (key); - } - return null; - } - - public void RemovePeer (IJavaPeerable value) - { - ThrowIfDisposed (); - - // Remove any collected contexts before modifying RegisteredInstances - CollectPeers (); - - if (value == null) - throw new ArgumentNullException (nameof (value)); - - lock (RegisteredInstances) { - int key = value.JniIdentityHashCode; - if (!RegisteredInstances.TryGetValue (key, out List? peers)) - return; - - for (int i = peers.Count - 1; i >= 0; i--) { - ReferenceTrackingHandle peer = peers [i]; - IJavaPeerable? target = peer.Target; - if (ReferenceEquals (value, target)) { - peers.RemoveAt (i); - peer.Dispose (); - } - GC.KeepAlive (target); - } - if (peers.Count == 0) - RegisteredInstances.Remove (key); - } - } - - public void FinalizePeer (IJavaPeerable value) - { - var h = value.PeerReference; - var o = JniEnvironment.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 List GetSurfacedPeers () - { - ThrowIfDisposed (); - - // Remove any collected contexts before iterating over all the registered instances - CollectPeers (); - - lock (RegisteredInstances) { - var peers = new List (RegisteredInstances.Count); - foreach (var (identityHashCode, referenceTrackingHandles) in RegisteredInstances) { - foreach (var peer in referenceTrackingHandles) { - if (peer.Target is IJavaPeerable target) { - peers.Add (new JniSurfacedPeerInfo (identityHashCode, new WeakReference (target))); - } - } - } - return peers; - } - } - - unsafe struct ReferenceTrackingHandle : IDisposable - { - WeakReference _weakReference; - HandleContext* _context; - - public bool BelongsToContext (HandleContext* context) - => _context == context; - - public ReferenceTrackingHandle (IJavaPeerable peer) - { - _context = HandleContext.Alloc (peer); - _weakReference = new (peer); - } - - public IJavaPeerable? Target - => _weakReference.TryGetTarget (out var target) ? target : null; - - public void Dispose () - { - if (_context == null) - return; - - IJavaPeerable? target = Target; - - GCHandle handle = HandleContext.GetAssociatedGCHandle (_context); - HandleContext.Free (ref _context); - _weakReference.SetTarget (null); - if (handle.IsAllocated) { - handle.Free (); - } - - // Make sure the target is not collected before we finish disposing - GC.KeepAlive (target); - } - } - - [StructLayout (LayoutKind.Sequential)] - unsafe struct HandleContext - { - static readonly nuint Size = (nuint)Marshal.SizeOf (); - static readonly Dictionary referenceTrackingHandles = new (); - - int identityHashCode; - IntPtr controlBlock; - - public int PeerIdentityHashCode => identityHashCode; - public bool IsCollected - { - get - { - if (controlBlock == IntPtr.Zero) - throw new InvalidOperationException ("HandleContext control block is not initialized."); - - return ((JniObjectReferenceControlBlock*) controlBlock)->handle == IntPtr.Zero; - } - } - - // This is an internal mirror of the Java.Interop.JniObjectReferenceControlBlock - private struct JniObjectReferenceControlBlock - { - public IntPtr handle; - public int handle_type; - public IntPtr weak_handle; - public int refs_added; - } - - public static GCHandle GetAssociatedGCHandle (HandleContext* context) - { - lock (referenceTrackingHandles) { - if (!referenceTrackingHandles.TryGetValue ((IntPtr) context, out GCHandle handle)) { - throw new InvalidOperationException ("Unknown reference tracking handle."); - } - - return handle; - } - } - - public static unsafe void EnsureAllContextsAreOurs (MarkCrossReferencesArgs* mcr) - { -// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.ComponentCount' is only supported on: 'android'. -// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.Components' is only supported on: 'android'. -// This call site is reachable on all platforms. 'StronglyConnectedComponent.Count' is only supported on: 'android'. -// This call site is reachable on all platforms. 'StronglyConnectedComponent.Contexts' is only supported on: 'android'. -#pragma warning disable CA1416 - - lock (referenceTrackingHandles) { - for (nuint i = 0; i < mcr->ComponentCount; i++) { - StronglyConnectedComponent component = mcr->Components [i]; - EnsureAllContextsInComponentAreOurs (component); - } - } - - static void EnsureAllContextsInComponentAreOurs (StronglyConnectedComponent component) - { - for (nuint i = 0; i < component.Count; i++) { - EnsureContextIsOurs ((IntPtr)component.Contexts [i]); - } - } - - static void EnsureContextIsOurs (IntPtr context) - { - if (!referenceTrackingHandles.ContainsKey (context)) { - throw new InvalidOperationException ("Unknown reference tracking handle."); - } - } - -#pragma warning restore CA1416 - } - - public static HandleContext* Alloc (IJavaPeerable peer) - { - var context = (HandleContext*) NativeMemory.AllocZeroed (1, Size); - if (context == null) { - throw new OutOfMemoryException ("Failed to allocate memory for HandleContext."); - } - - context->identityHashCode = peer.JniIdentityHashCode; - context->controlBlock = peer.JniObjectReferenceControlBlock; - - GCHandle handle = JavaMarshal.CreateReferenceTrackingHandle (peer, context); - lock (referenceTrackingHandles) { - referenceTrackingHandles [(IntPtr) context] = handle; - } - - return context; - } - - public static void Free (ref HandleContext* context) - { - if (context == null) { - return; - } - - lock (referenceTrackingHandles) { - referenceTrackingHandles.Remove ((IntPtr)context); - } - - NativeMemory.Free (context); - context = null; - } - } - - [UnmanagedCallersOnly] - static unsafe void BridgeProcessingStarted (MarkCrossReferencesArgs* mcr) - { - if (mcr == null) { - throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); - } - - HandleContext.EnsureAllContextsAreOurs (mcr); - } - - [UnmanagedCallersOnly] - static unsafe void BridgeProcessingFinished (IntPtr registeredPeersHandle, MarkCrossReferencesArgs* mcr) - { - if (mcr == null) { - throw new ArgumentNullException (nameof (mcr), "MarkCrossReferencesArgs should never be null."); - } - - JavaMarshalRegisteredPeers instance = GCHandle.FromIntPtr (registeredPeersHandle).Target; - - ReadOnlySpan handlesToFree = instance.ProcessCollectedContexts (mcr); - - -// This call site is reachable on all platforms. 'JavaMarshal.FinishCrossReferenceProcessing(MarkCrossReferencesArgs*, ReadOnlySpan)' is only supported on: 'android'. -#pragma warning disable CA1416 - JavaMarshal.FinishCrossReferenceProcessing (mcr, handlesToFree); -#pragma warning restore CA1416 - } - - unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) - { - List handlesToFree = []; - -// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.ComponentCount' is only supported on: 'android'. -// This call site is reachable on all platforms. 'MarkCrossReferencesArgs.Components' is only supported on: 'android'. -// This call site is reachable on all platforms. 'StronglyConnectedComponent.Count' is only supported on: 'android'. -// This call site is reachable on all platforms. 'StronglyConnectedComponent.Contexts' is only supported on: 'android'. -#pragma warning disable CA1416 - - for (int i = 0; (nuint)i < mcr->ComponentCount; i++) { - StronglyConnectedComponent component = mcr->Components [i]; - for (int j = 0; (nuint)j < component.Count; j++) { - ProcessContext ((HandleContext*)component.Contexts [j]); - } - } - -#pragma warning restore CA1416 - - void ProcessContext (HandleContext* context) - { - if (context == null) { - throw new ArgumentNullException (nameof (context), "HandleContext should never be null."); - } - - // Ignore contexts which were not collected - if (!context->IsCollected) { - return; - } - - GCHandle handle = HandleContext.GetAssociatedGCHandle (context); - - // Note: modifying the RegisteredInstances dictionary while processing the collected contexts - // is tricky and can lead to deadlocks, so we remember which contexts were collected and we will free - // them later outside of the bridge processing loop. - CollectedContexts.Enqueue ((IntPtr)context); - - // important: we must not free the handle before passing it to JavaMarshal.FinishCrossReferenceProcessing - handlesToFree.Add (handle); - } - - return CollectionsMarshal.AsSpan (handlesToFree); - } - -} - -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; - } -} - -static class TrimmableValueMarshalerHelper -{ - public static bool IsPrimitiveJniValueType (Type type) - { - return type == typeof (bool) || - type == typeof (byte) || - type == typeof (sbyte) || - type == typeof (char) || - type == typeof (short) || - type == typeof (ushort) || - type == typeof (int) || - type == typeof (uint) || - type == typeof (long) || - type == typeof (ulong) || - type == typeof (float) || - type == typeof (double); - } - - public static JniArgumentValue CreatePrimitiveArgumentValue (object? value, Type type) - { - return value switch { - null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), - bool v => new JniArgumentValue (v), - byte v => new JniArgumentValue (v), - sbyte v => new JniArgumentValue (v), - char v => new JniArgumentValue (v), - short v => new JniArgumentValue (v), - ushort v => new JniArgumentValue (v), - int v => new JniArgumentValue (v), - uint v => new JniArgumentValue (v), - long v => new JniArgumentValue (v), - ulong v => new JniArgumentValue (v), - float v => new JniArgumentValue (v), - double v => new JniArgumentValue (v), - _ => throw new NotSupportedException ($"Type '{type.AssemblyQualifiedName}' is not a JNI primitive value type."), - }; - } - - public static bool TryGetPrimitiveValueMarshaler (Type type, [NotNullWhen (true)] out JniValueMarshaler? marshaler) - { - if (type == typeof (bool)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (bool?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (byte)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (sbyte)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (sbyte?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (char)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (char?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (short)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (short?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (int)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (int?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (long)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (long?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (float)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (float?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (double)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - if (type == typeof (double?)) { - marshaler = TrimmableTypeMapValueManager.TrimmableValueMarshaler.Instance; - return true; - } - - marshaler = null; - return false; - } -} - -[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)]; - - readonly JavaMarshalRegisteredPeers registeredPeers = new (); - - protected override void Dispose (bool disposing) - { - registeredPeers.Dispose (); - 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 () - { - registeredPeers.CollectPeers (); - } - - public override void AddPeer (IJavaPeerable value) - { - registeredPeers.AddPeer (value); - } - - public override IJavaPeerable? PeekPeer (JniObjectReference reference) - { - return registeredPeers.PeekPeer (reference); - } - - public override void RemovePeer (IJavaPeerable value) - { - registeredPeers.RemovePeer (value); - } - - public override void FinalizePeer (IJavaPeerable value) - { - registeredPeers.FinalizePeer (value); - } - - public override List GetSurfacedPeers () - { - return registeredPeers.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); - - [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; - } - - 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); - } -} - -sealed class TrimmableTypeMapValueManager : JniRuntime.JniValueManager -{ - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); - const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; - - readonly JavaMarshalRegisteredPeers registeredPeers = new (); - - protected override void Dispose (bool disposing) - { - registeredPeers.Dispose (); - 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 () - { - registeredPeers.CollectPeers (); - } - - public override void AddPeer (IJavaPeerable value) - { - registeredPeers.AddPeer (value); - } - - public override IJavaPeerable? PeekPeer (JniObjectReference reference) - { - return registeredPeers.PeekPeer (reference); - } - - public override void RemovePeer (IJavaPeerable value) - { - registeredPeers.RemovePeer (value); - } - - public override void FinalizePeer (IJavaPeerable value) - { - registeredPeers.FinalizePeer (value); - } - - public override List GetSurfacedPeers () - { - return registeredPeers.GetSurfacedPeers (); - } - - public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) - { - throw new PlatformNotSupportedException ("Activating Java peers through the value manager is not supported when TrimmableTypeMap is enabled."); - } - - protected override void ConstructPeerCore ( - IJavaPeerable peer, - ref JniObjectReference reference, - JniObjectReferenceOptions options) - { - if (peer == null) - throw new ArgumentNullException (nameof (peer)); - - var newRef = peer.PeerReference; - if (newRef.IsValid) { - JniObjectReference.Dispose (ref reference, options); - - // Instance was already added, don't add again - if (peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable)) { - return; - } - var orig = newRef; - newRef = orig.NewGlobalRef (); - JniObjectReference.Dispose (ref orig); - } else if (options == JniObjectReferenceOptions.None) { - // `reference` is likely *InvalidJniObjectReference, and can't be touched - return; - } else if (!reference.IsValid) { - throw new ArgumentException ("JNI Object Reference is invalid.", nameof (reference)); - } else { - newRef = reference; - - if ((options & JniObjectReferenceOptions.Copy) == JniObjectReferenceOptions.Copy) { - newRef = reference.NewGlobalRef (); - } - - JniObjectReference.Dispose (ref reference, options); - } - - peer.SetPeerReference (newRef); - peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); - - var o = Runtime.ObjectReferenceManager; - if (o.LogGlobalReferenceMessages) { - o.WriteGlobalReferenceLine ("Created PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}, Java.Type={4}", - newRef.ToString (), - peer.JniIdentityHashCode.ToString ("x", CultureInfo.InvariantCulture), - RuntimeHelpers.GetHashCode (peer).ToString ("x", CultureInfo.InvariantCulture), - peer.GetType ().FullName, - JniEnvironment.Types.GetJniTypeNameFromInstance (newRef)); - } - - if ((options & DoNotRegisterTarget) != DoNotRegisterTarget) { - AddPeer (peer); - } - } - - public override IJavaPeerable? CreatePeer ( - ref JniObjectReference reference, - JniObjectReferenceOptions transfer, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - EnsureNotDisposed (); - - if (!reference.IsValid) { - return null; - } - - 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 (targetType is not null && resolvedTargetType is not null) { - if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { - throw new ArgumentException ( - $"Could not determine Java type corresponding to '{targetType.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); - } - } - - [return: MaybeNull] - protected override T CreateValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) - { - return GetValueCore (ref reference, options, targetType); - } - - protected override object? CreateValueCore ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) - { - return GetValueCore (ref reference, options, targetType); - } - - [return: MaybeNull] - protected override T GetValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) - { - EnsureNotDisposed (); - if (!reference.IsValid) { - return default (T); - } - - if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) { - throw new ArgumentException ( - string.Format (CultureInfo.InvariantCulture, "Requested runtime '{0}' value of '{1}' is not compatible with requested compile-time type T of '{2}'.", - nameof (targetType), - targetType, - typeof (T)), - nameof (targetType)); - } - - var value = GetValueCore (ref reference, options, targetType ?? typeof (T)); - if (value is null) { - return default (T); - } - return (T) value; - } - - protected override object? GetValueCore ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType = null) - { - EnsureNotDisposed (); - if (!reference.IsValid) { - return null; - } - - var existing = PeekValue (reference); - if (existing != null && (targetType == null || targetType.IsAssignableFrom (existing.GetType ()))) { - JniObjectReference.Dispose (ref reference, options); - return existing; - } - - if (TryCreateJavaArrayWrapper (ref reference, options, targetType, out var arrayWrapper)) { - return arrayWrapper; - } - - if (targetType != null && typeof (IJavaPeerable).IsAssignableFrom (targetType)) { - return CreatePeer (ref reference, options, targetType); - } - - var transfer = ToJniHandleOwnership (reference, options); - var value = JavaConvert.FromJniHandle (reference.Handle, transfer, GetValueConversionTargetType (targetType)); - if (transfer != JniHandleOwnership.DoNotTransfer) { - reference = default; - } - return value; - } - - [return: DynamicallyAccessedMembers (Constructors)] - static Type? GetValueConversionTargetType ([DynamicallyAccessedMembers (Constructors)] Type? targetType) - { - if (targetType == typeof (sbyte?)) { - return typeof (sbyte); - } - - return targetType; - } - - static bool TryCreateJavaArrayWrapper ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType, - [NotNullWhen (true)] out object? value) - { - if (targetType != null && TryCreatePrimitiveArrayWrapper (ref reference, options, targetType, out value)) { - return true; - } - - value = null; - return false; - } - - delegate TArray PrimitiveArrayFactory (ref JniObjectReference reference, JniObjectReferenceOptions options); - - readonly struct PrimitiveArrayArgumentState - { - public readonly bool DisposeArray; - - public PrimitiveArrayArgumentState (bool disposeArray) - { - DisposeArray = disposeArray; - } - } - - abstract class PrimitiveArrayHandler - { - public abstract bool TryCreateWrapper ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - [NotNullWhen (true)] out object? value); - - public abstract bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state); - - public abstract bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize); - - public abstract bool IsTargetType (Type targetType); - } - - sealed class PrimitiveArrayHandler : PrimitiveArrayHandler - where TArray : global::Java.Interop.JavaArray - { - readonly PrimitiveArrayFactory createFromReference; - readonly Func create; - readonly Func, TArray> createCopy; - - public PrimitiveArrayHandler (PrimitiveArrayFactory createFromReference, Func create, Func, TArray> createCopy) - { - this.createFromReference = createFromReference; - this.create = create; - this.createCopy = createCopy; - } - - public override bool TryCreateWrapper ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - [NotNullWhen (true)] out object? value) - { - if (!IsTargetType (targetType)) { - value = null; - return false; - } - - var array = createFromReference (ref reference, options); - if (targetType == typeof (T[]) || IsCompatibleListType (targetType)) { - try { - value = array.ToArray (); - return true; - } finally { - array.Dispose (); - } - } - - value = array; - return true; - } - - public override bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) - { - if (value is TArray array) { - state = new JniValueMarshalerState (array); - return true; - } - - if (value is not IList list) { - state = new JniValueMarshalerState (); - return false; - } - - synchronize = GetCopyDirection (synchronize); - var copy = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; - var marshaledArray = copy ? createCopy (list) : create (list.Count); - state = new JniValueMarshalerState (marshaledArray, new PrimitiveArrayArgumentState (disposeArray: true)); - return true; - } - - public override bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - if (state.PeerableValue is not TArray source) { - return false; - } - - synchronize = GetCopyDirection (synchronize); - if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList destination) { - for (int i = 0; i < source.Length; i++) { - destination [i] = source [i]; - } - } - - if (state.Extra is PrimitiveArrayArgumentState { DisposeArray: true }) { - source.Dispose (); - } - - state = new JniValueMarshalerState (); - return true; - } - - public override bool IsTargetType (Type targetType) - { - return targetType == typeof (global::Java.Interop.JavaArray) || - targetType == typeof (global::Java.Interop.JavaPrimitiveArray) || - targetType == typeof (TArray) || - targetType == typeof (T[]) || - IsCompatibleListType (targetType); - } - - static bool IsCompatibleListType (Type targetType) - { - return targetType.IsGenericType && - targetType.GetGenericTypeDefinition () == typeof (IList<>) && - targetType.IsAssignableFrom (typeof (IList)); - } - } - - static readonly PrimitiveArrayHandler[] PrimitiveArrayHandlers = new PrimitiveArrayHandler [] { - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaBooleanArray (ref h, o), - length => new JavaBooleanArray (length), - list => new JavaBooleanArray (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSByteArray (ref h, o), - length => new JavaSByteArray (length), - list => new JavaSByteArray (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaCharArray (ref h, o), - length => new JavaCharArray (length), - list => new JavaCharArray (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt16Array (ref h, o), - length => new JavaInt16Array (length), - list => new JavaInt16Array (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt32Array (ref h, o), - length => new JavaInt32Array (length), - list => new JavaInt32Array (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt64Array (ref h, o), - length => new JavaInt64Array (length), - list => new JavaInt64Array (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSingleArray (ref h, o), - length => new JavaSingleArray (length), - list => new JavaSingleArray (list)), - new PrimitiveArrayHandler ( - (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaDoubleArray (ref h, o), - length => new JavaDoubleArray (length), - list => new JavaDoubleArray (list)), - }; - - static bool TryCreatePrimitiveArrayWrapper ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type targetType, - [NotNullWhen (true)] out object? value) - { - foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryCreateWrapper (ref reference, options, targetType, out value)) { - return true; - } - } - - value = null; - return false; - } - - static bool TryCreatePrimitiveArrayArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) - { - foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryCreateArgumentState (value, synchronize, out state)) { - return true; - } - } - - state = new JniValueMarshalerState (); - return false; - } - - static bool TryDestroyPrimitiveArrayArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryDestroyArgumentState (value, ref state, synchronize)) { - return true; - } - } - - return false; - } - - static bool IsPrimitiveArrayTargetType (Type targetType) - { - foreach (var handler in PrimitiveArrayHandlers) { - if (handler.IsTargetType (targetType)) { - return true; - } - } - - return false; - } - - static ParameterAttributes GetCopyDirection (ParameterAttributes value) - { - const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; - if ((value & inout) != 0) - return value & inout; - return inout; - } - - protected override JniValueMarshaler GetValueMarshalerCore (Type type) - { - EnsureNotDisposed (); - if (type == null) { - throw new ArgumentNullException (nameof (type)); - } - if (type.ContainsGenericParameters) { - throw new ArgumentException ("Generic type definitions are not supported.", nameof (type)); - } - - if (TrimmableValueMarshalerHelper.TryGetPrimitiveValueMarshaler (type, out var primitiveMarshaler)) - return primitiveMarshaler; - if (type == typeof (string)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (object)) - return ObjectValueMarshaler; - if (IsPrimitiveArrayTargetType (type)) - return TrimmableValueMarshaler.Instance; - if (type == typeof (IJavaPeerable) || typeof (IJavaPeerable).IsAssignableFrom (type)) - return PeerableValueMarshaler; - - return ObjectValueMarshaler; - } - - protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () - { - EnsureNotDisposed (); - var type = typeof (T); - if (type.IsArray || IsPrimitiveArrayTargetType (type)) { - return TrimmableValueMarshaler.Instance; - } - - var marshaler = GetValueMarshaler (type); - if (marshaler is JniValueMarshaler typedMarshaler) { - return typedMarshaler; - } - return CreateDelegatingValueMarshaler (marshaler); - } - - static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, JniObjectReferenceOptions options) - { - const JniObjectReferenceOptions DisposeSource = (JniObjectReferenceOptions)(1 << 1); - if ((options & DisposeSource) != DisposeSource) { - return JniHandleOwnership.DoNotTransfer; - } - return reference.Type switch { - JniObjectReferenceType.Local => JniHandleOwnership.TransferLocalRef, - JniObjectReferenceType.Global => JniHandleOwnership.TransferGlobalRef, - _ => JniHandleOwnership.DoNotTransfer, - }; - } - - internal sealed class TrimmableValueMarshaler<[DynamicallyAccessedMembers (Constructors)] T> : JniValueMarshaler - { - public static readonly TrimmableValueMarshaler Instance = new (); - - public override bool IsJniValueType => TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (typeof (T)); - - public override Type MarshalType => IsJniValueType ? typeof (T) : base.MarshalType; - - [return: MaybeNull] - public override T CreateGenericValue ( - ref JniObjectReference reference, - JniObjectReferenceOptions options, - [DynamicallyAccessedMembers (Constructors)] - Type? targetType) - { - return JniEnvironment.Runtime.ValueManager.GetValue (ref reference, options, targetType); - } - - public override JniValueMarshalerState CreateGenericArgumentState ([MaybeNull] T value, ParameterAttributes synchronize = ParameterAttributes.In) - { - if (IsJniValueType) { - return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, typeof (T))); - } - return CreateGenericObjectReferenceArgumentState (value, synchronize); - } - - public override JniValueMarshalerState CreateGenericObjectReferenceArgumentState ([MaybeNull] T value, ParameterAttributes synchronize) - { - if (value == null) { - return new JniValueMarshalerState (); - } - if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { - return primitiveArrayState; - } - if (value is IJavaPeerable peerable) { - return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); - } - - var handle = JavaConvert.ToLocalJniHandle (value); - return handle == IntPtr.Zero - ? new JniValueMarshalerState () - : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); - } - - public override void DestroyGenericArgumentState ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { - return; - } - DisposeReferenceState (ref state); - } - - [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] - public override Expression CreateParameterFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue, ParameterAttributes synchronize) - => throw new UnreachableException ( - $"{nameof (CreateParameterFromManagedExpression)} should not be called in the trimmable typemap path. " + - "Generated marshal methods use pregenerated value marshaling."); - - [RequiresDynamicCode (ExpressionRequiresUnreferencedCode)] - [RequiresUnreferencedCode (ExpressionRequiresUnreferencedCode)] - public override Expression CreateReturnValueFromManagedExpression (JniValueMarshalerContext context, ParameterExpression sourceValue) - => throw new UnreachableException ( - $"{nameof (CreateReturnValueFromManagedExpression)} should not be called in the trimmable typemap path. " + - "Generated marshal methods use pregenerated value marshaling."); - } - - static void DisposeReferenceState (ref JniValueMarshalerState state) - { - var r = state.ReferenceValue; - JniObjectReference.Dispose (ref r); - state = new JniValueMarshalerState (); - } -} 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/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs new file mode 100644 index 00000000000..1567f71b8f3 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +sealed partial class TrimmableTypeMapValueManager +{ + delegate TArray PrimitiveArrayFactory (ref JniObjectReference reference, JniObjectReferenceOptions options); + + readonly struct PrimitiveArrayArgumentState + { + public readonly bool DisposeArray; + + public PrimitiveArrayArgumentState (bool disposeArray) + { + DisposeArray = disposeArray; + } + } + + abstract class PrimitiveArrayHandler + { + public abstract bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value); + + public abstract bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state); + + public abstract bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize); + + public abstract bool IsTargetType (Type targetType); + } + + sealed class PrimitiveArrayHandler : PrimitiveArrayHandler + where TArray : global::Java.Interop.JavaArray + { + readonly PrimitiveArrayFactory createFromReference; + readonly Func create; + readonly Func, TArray> createCopy; + + public PrimitiveArrayHandler (PrimitiveArrayFactory createFromReference, Func create, Func, TArray> createCopy) + { + this.createFromReference = createFromReference; + this.create = create; + this.createCopy = createCopy; + } + + public override bool TryCreateWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value) + { + if (!IsTargetType (targetType)) { + value = null; + return false; + } + + var array = createFromReference (ref reference, options); + if (targetType == typeof (T[]) || IsCompatibleListType (targetType)) { + try { + value = array.ToArray (); + return true; + } finally { + array.Dispose (); + } + } + + value = array; + return true; + } + + public override bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + { + if (value is TArray array) { + state = new JniValueMarshalerState (array); + return true; + } + + if (value is not IList list) { + state = new JniValueMarshalerState (); + return false; + } + + synchronize = GetCopyDirection (synchronize); + var copy = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; + var marshaledArray = copy ? createCopy (list) : create (list.Count); + state = new JniValueMarshalerState (marshaledArray, new PrimitiveArrayArgumentState (disposeArray: true)); + return true; + } + + public override bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + if (state.PeerableValue is not TArray source) { + return false; + } + + synchronize = GetCopyDirection (synchronize); + if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList destination) { + for (int i = 0; i < source.Length; i++) { + destination [i] = source [i]; + } + } + + if (state.Extra is PrimitiveArrayArgumentState { DisposeArray: true }) { + source.Dispose (); + } + + state = new JniValueMarshalerState (); + return true; + } + + public override bool IsTargetType (Type targetType) + { + return targetType == typeof (global::Java.Interop.JavaArray) || + targetType == typeof (global::Java.Interop.JavaPrimitiveArray) || + targetType == typeof (TArray) || + targetType == typeof (T[]) || + IsCompatibleListType (targetType); + } + + static bool IsCompatibleListType (Type targetType) + { + return targetType.IsGenericType && + targetType.GetGenericTypeDefinition () == typeof (IList<>) && + targetType.IsAssignableFrom (typeof (IList)); + } + } + + static readonly PrimitiveArrayHandler[] PrimitiveArrayHandlers = new PrimitiveArrayHandler [] { + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaBooleanArray (ref h, o), + length => new JavaBooleanArray (length), + list => new JavaBooleanArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSByteArray (ref h, o), + length => new JavaSByteArray (length), + list => new JavaSByteArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaCharArray (ref h, o), + length => new JavaCharArray (length), + list => new JavaCharArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt16Array (ref h, o), + length => new JavaInt16Array (length), + list => new JavaInt16Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt32Array (ref h, o), + length => new JavaInt32Array (length), + list => new JavaInt32Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt64Array (ref h, o), + length => new JavaInt64Array (length), + list => new JavaInt64Array (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSingleArray (ref h, o), + length => new JavaSingleArray (length), + list => new JavaSingleArray (list)), + new PrimitiveArrayHandler ( + (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaDoubleArray (ref h, o), + length => new JavaDoubleArray (length), + list => new JavaDoubleArray (list)), + }; + + static bool TryCreatePrimitiveArrayWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type targetType, + [NotNullWhen (true)] out object? value) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryCreateWrapper (ref reference, options, targetType, out value)) { + return true; + } + } + + value = null; + return false; + } + + static bool TryCreatePrimitiveArrayArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryCreateArgumentState (value, synchronize, out state)) { + return true; + } + } + + state = new JniValueMarshalerState (); + return false; + } + + static bool TryDestroyPrimitiveArrayArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.TryDestroyArgumentState (value, ref state, synchronize)) { + return true; + } + } + + return false; + } + + static bool IsPrimitiveArrayTargetType (Type targetType) + { + foreach (var handler in PrimitiveArrayHandlers) { + if (handler.IsTargetType (targetType)) { + return true; + } + } + + return false; + } + + static ParameterAttributes GetCopyDirection (ParameterAttributes value) + { + const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; + if ((value & inout) != 0) + return value & inout; + return inout; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs new file mode 100644 index 00000000000..b50267f47f4 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -0,0 +1,395 @@ +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; + +sealed partial class TrimmableTypeMapValueManager : JniRuntime.JniValueManager +{ + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + const JniObjectReferenceOptions DoNotRegisterTarget = (JniObjectReferenceOptions)(1 << 2); + const string ExpressionRequiresUnreferencedCode = "System.Linq.Expression usage may trim away required code."; + + readonly JavaMarshalRegisteredPeers registeredPeers = new (); + + protected override void Dispose (bool disposing) + { + registeredPeers.Dispose (); + 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 () + { + registeredPeers.CollectPeers (); + } + + public override void AddPeer (IJavaPeerable value) + { + registeredPeers.AddPeer (value); + } + + public override IJavaPeerable? PeekPeer (JniObjectReference reference) + { + return registeredPeers.PeekPeer (reference); + } + + public override void RemovePeer (IJavaPeerable value) + { + registeredPeers.RemovePeer (value); + } + + public override void FinalizePeer (IJavaPeerable value) + { + registeredPeers.FinalizePeer (value); + } + + public override List GetSurfacedPeers () + { + return registeredPeers.GetSurfacedPeers (); + } + + public override void ActivatePeer (JniObjectReference reference, [DynamicallyAccessedMembers (Constructors)] Type type, ConstructorInfo cinfo, object?[]? argumentValues) + { + throw new PlatformNotSupportedException ("Activating Java peers through the value manager is not supported when TrimmableTypeMap is enabled."); + } + + protected override void ConstructPeerCore ( + IJavaPeerable peer, + ref JniObjectReference reference, + JniObjectReferenceOptions options) + { + if (peer == null) + throw new ArgumentNullException (nameof (peer)); + + var newRef = peer.PeerReference; + if (newRef.IsValid) { + JniObjectReference.Dispose (ref reference, options); + + // Instance was already added, don't add again + if (peer.JniManagedPeerState.HasFlag (JniManagedPeerStates.Activatable)) { + return; + } + var orig = newRef; + newRef = orig.NewGlobalRef (); + JniObjectReference.Dispose (ref orig); + } else if (options == JniObjectReferenceOptions.None) { + // `reference` is likely *InvalidJniObjectReference, and can't be touched + return; + } else if (!reference.IsValid) { + throw new ArgumentException ("JNI Object Reference is invalid.", nameof (reference)); + } else { + newRef = reference; + + if ((options & JniObjectReferenceOptions.Copy) == JniObjectReferenceOptions.Copy) { + newRef = reference.NewGlobalRef (); + } + + JniObjectReference.Dispose (ref reference, options); + } + + peer.SetPeerReference (newRef); + peer.SetJniIdentityHashCode (JniEnvironment.References.GetIdentityHashCode (newRef)); + + var o = Runtime.ObjectReferenceManager; + if (o.LogGlobalReferenceMessages) { + o.WriteGlobalReferenceLine ("Created PeerReference={0} IdentityHashCode=0x{1} Instance=0x{2} Instance.Type={3}, Java.Type={4}", + newRef.ToString (), + peer.JniIdentityHashCode.ToString ("x", CultureInfo.InvariantCulture), + RuntimeHelpers.GetHashCode (peer).ToString ("x", CultureInfo.InvariantCulture), + peer.GetType ().FullName, + JniEnvironment.Types.GetJniTypeNameFromInstance (newRef)); + } + + if ((options & DoNotRegisterTarget) != DoNotRegisterTarget) { + AddPeer (peer); + } + } + + public override IJavaPeerable? CreatePeer ( + ref JniObjectReference reference, + JniObjectReferenceOptions transfer, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType) + { + EnsureNotDisposed (); + + if (!reference.IsValid) { + return null; + } + + 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 (targetType is not null && resolvedTargetType is not null) { + if (!typeMap.TryGetJniNameForManagedType (targetType, out var targetJniName)) { + throw new ArgumentException ( + $"Could not determine Java type corresponding to '{targetType.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); + } + } + + [return: MaybeNull] + protected override T CreateValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + return GetValueCore (ref reference, options, targetType); + } + + protected override object? CreateValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + return GetValueCore (ref reference, options, targetType); + } + + [return: MaybeNull] + protected override T GetValueCore<[DynamicallyAccessedMembers (Constructors)] T> ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + EnsureNotDisposed (); + if (!reference.IsValid) { + return default (T); + } + + if (targetType != null && !typeof (T).IsAssignableFrom (targetType)) { + throw new ArgumentException ( + string.Format (CultureInfo.InvariantCulture, "Requested runtime '{0}' value of '{1}' is not compatible with requested compile-time type T of '{2}'.", + nameof (targetType), + targetType, + typeof (T)), + nameof (targetType)); + } + + var value = GetValueCore (ref reference, options, targetType ?? typeof (T)); + if (value is null) { + return default (T); + } + return (T) value; + } + + protected override object? GetValueCore ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + EnsureNotDisposed (); + if (!reference.IsValid) { + return null; + } + + var existing = PeekValue (reference); + if (existing != null && (targetType == null || targetType.IsAssignableFrom (existing.GetType ()))) { + JniObjectReference.Dispose (ref reference, options); + return existing; + } + + if (TryCreateJavaArrayWrapper (ref reference, options, targetType, out var arrayWrapper)) { + return arrayWrapper; + } + + if (targetType != null && typeof (IJavaPeerable).IsAssignableFrom (targetType)) { + return CreatePeer (ref reference, options, targetType); + } + + var transfer = ToJniHandleOwnership (reference, options); + var value = JavaConvert.FromJniHandle (reference.Handle, transfer, GetValueConversionTargetType (targetType)); + if (transfer != JniHandleOwnership.DoNotTransfer) { + reference = default; + } + return value; + } + + [return: DynamicallyAccessedMembers (Constructors)] + static Type? GetValueConversionTargetType ([DynamicallyAccessedMembers (Constructors)] Type? targetType) + { + if (targetType == typeof (sbyte?)) { + return typeof (sbyte); + } + + return targetType; + } + + static bool TryCreateJavaArrayWrapper ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType, + [NotNullWhen (true)] out object? value) + { + if (targetType != null && TryCreatePrimitiveArrayWrapper (ref reference, options, targetType, out value)) { + return true; + } + + value = null; + return false; + } + + protected override JniValueMarshalerState CreateValueMarshalerStateCore (Type type, object? value, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (value == null) { + return new JniValueMarshalerState (); + } + if (TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (type)) { + return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, type)); + } + return CreateObjectReferenceValueMarshalerStateCore (type, value, synchronize); + } + + protected override JniValueMarshalerState CreateValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([MaybeNull] T value, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (value == null) { + return new JniValueMarshalerState (); + } + if (TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (typeof (T))) { + return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, typeof (T))); + } + return CreateObjectReferenceValueMarshalerStateCore (value, synchronize); + } + + protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore (Type type, object? value, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (value == null) { + return new JniValueMarshalerState (); + } + if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { + return primitiveArrayState; + } + if (value is IJavaPeerable peerable) { + return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); + } + + var handle = JavaConvert.ToLocalJniHandle (value); + return handle == IntPtr.Zero + ? new JniValueMarshalerState () + : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); + } + + protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([MaybeNull] T value, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (value == null) { + return new JniValueMarshalerState (); + } + if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { + return primitiveArrayState; + } + if (value is IJavaPeerable peerable) { + return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); + } + + var handle = JavaConvert.ToLocalJniHandle (value); + return handle == IntPtr.Zero + ? new JniValueMarshalerState () + : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); + } + + protected override void DestroyValueMarshalerStateCore (Type type, object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { + return; + } + DisposeReferenceState (ref state); + } + + protected override void DestroyValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + { + EnsureNotDisposed (); + if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { + return; + } + DisposeReferenceState (ref state); + } + + protected override JniValueMarshaler GetValueMarshalerCore (Type type) + { + throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); + } + + protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () + { + throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); + } + + static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, JniObjectReferenceOptions options) + { + const JniObjectReferenceOptions DisposeSource = (JniObjectReferenceOptions)(1 << 1); + if ((options & DisposeSource) != DisposeSource) { + return JniHandleOwnership.DoNotTransfer; + } + return reference.Type switch { + JniObjectReferenceType.Local => JniHandleOwnership.TransferLocalRef, + JniObjectReferenceType.Global => JniHandleOwnership.TransferGlobalRef, + _ => JniHandleOwnership.DoNotTransfer, + }; + } + + static void DisposeReferenceState (ref JniValueMarshalerState state) + { + var r = state.ReferenceValue; + JniObjectReference.Dispose (ref r); + state = new JniValueMarshalerState (); + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs new file mode 100644 index 00000000000..29225418f61 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +static class TrimmableValueMarshalerHelper +{ + public static bool IsPrimitiveJniValueType (Type type) + { + return type == typeof (bool) || + type == typeof (byte) || + type == typeof (sbyte) || + type == typeof (char) || + type == typeof (short) || + type == typeof (ushort) || + type == typeof (int) || + type == typeof (uint) || + type == typeof (long) || + type == typeof (ulong) || + type == typeof (float) || + type == typeof (double); + } + + public static JniArgumentValue CreatePrimitiveArgumentValue (object? value, Type type) + { + return value switch { + null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), + bool v => new JniArgumentValue (v), + byte v => new JniArgumentValue (v), + sbyte v => new JniArgumentValue (v), + char v => new JniArgumentValue (v), + short v => new JniArgumentValue (v), + ushort v => new JniArgumentValue (v), + int v => new JniArgumentValue (v), + uint v => new JniArgumentValue (v), + long v => new JniArgumentValue (v), + ulong v => new JniArgumentValue (v), + float v => new JniArgumentValue (v), + double v => new JniArgumentValue (v), + _ => throw new NotSupportedException ($"Type '{type.AssemblyQualifiedName}' is not a JNI primitive value type."), + }; + } +} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index b1ffc25501b..42e64059669 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -365,11 +365,16 @@ - + + + + + + From a5b39d10b9ace5649e6457bd017ee054d630549c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 17:08:14 +0200 Subject: [PATCH 45/47] Simplify trimmable object array marshaling Remove unused trimmable value-marshaler helper code and keep only the object-reference state path needed for JavaObjectArray element assignment. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- src/Mono.Android/Java.Interop/JavaConvert.cs | 27 +++++ ...peMapValueManager.PrimitiveArrayHandler.cs | 58 +++------- .../TrimmableTypeMapValueManager.cs | 106 +++--------------- .../TrimmableValueMarshalerHelper.cs | 44 -------- src/Mono.Android/Mono.Android.csproj | 1 - 6 files changed, 59 insertions(+), 179 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs diff --git a/external/Java.Interop b/external/Java.Interop index cf298e6232b..26a56153520 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit cf298e6232b50c33ffcc00a78d5994a829799456 +Subproject commit 26a5615352043f776792869e16469eaa61968f85 diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index ba46b155c00..42be1b849d6 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -12,6 +12,8 @@ namespace Java.Interop { static class JavaConvert { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + // Mirrors JniObjectReference.DisposeSource; JniObjectReferenceOptions only exposes it through CopyAndDispose. + const JniObjectReferenceOptions DisposeSource = (JniObjectReferenceOptions)(1 << 1); static Dictionary> JniHandleConverters = new Dictionary>() { { typeof (bool), (handle, transfer) => { @@ -295,6 +297,31 @@ public static T? FromJniHandle< return (T?) Convert.ChangeType (v, typeof (T), CultureInfo.InvariantCulture); } + internal static object? FromObjectReference ( + ref JniObjectReference reference, + JniObjectReferenceOptions options, + [DynamicallyAccessedMembers (Constructors)] + Type? targetType = null) + { + JniHandleOwnership transfer; + if ((options & DisposeSource) != DisposeSource) { + transfer = JniHandleOwnership.DoNotTransfer; + } else { + transfer = reference.Type switch { + JniObjectReferenceType.Local => JniHandleOwnership.TransferLocalRef, + JniObjectReferenceType.Global => JniHandleOwnership.TransferGlobalRef, + _ => JniHandleOwnership.DoNotTransfer, + }; + } + + var value = FromJniHandle (reference.Handle, transfer, targetType); + if (transfer != JniHandleOwnership.DoNotTransfer) { + reference = default; + } + + return value; + } + public static object? FromJniHandle ( IntPtr handle, JniHandleOwnership transfer, diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs index 1567f71b8f3..8c903697949 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Java.Interop; namespace Microsoft.Android.Runtime; @@ -29,24 +28,22 @@ public abstract bool TryCreateWrapper ( Type targetType, [NotNullWhen (true)] out object? value); - public abstract bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state); + public abstract bool TryCreateArgumentState (object value, out JniValueMarshalerState state); - public abstract bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize); + public abstract bool TryDestroyArgumentState (ref JniValueMarshalerState state); public abstract bool IsTargetType (Type targetType); } sealed class PrimitiveArrayHandler : PrimitiveArrayHandler - where TArray : global::Java.Interop.JavaArray + where TArray : JavaArray { readonly PrimitiveArrayFactory createFromReference; - readonly Func create; readonly Func, TArray> createCopy; - public PrimitiveArrayHandler (PrimitiveArrayFactory createFromReference, Func create, Func, TArray> createCopy) + public PrimitiveArrayHandler (PrimitiveArrayFactory createFromReference, Func, TArray> createCopy) { this.createFromReference = createFromReference; - this.create = create; this.createCopy = createCopy; } @@ -76,7 +73,7 @@ public override bool TryCreateWrapper ( return true; } - public override bool TryCreateArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + public override bool TryCreateArgumentState (object value, out JniValueMarshalerState state) { if (value is TArray array) { state = new JniValueMarshalerState (array); @@ -88,26 +85,17 @@ public override bool TryCreateArgumentState (object value, ParameterAttributes s return false; } - synchronize = GetCopyDirection (synchronize); - var copy = (synchronize & ParameterAttributes.In) == ParameterAttributes.In; - var marshaledArray = copy ? createCopy (list) : create (list.Count); + var marshaledArray = createCopy (list); state = new JniValueMarshalerState (marshaledArray, new PrimitiveArrayArgumentState (disposeArray: true)); return true; } - public override bool TryDestroyArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + public override bool TryDestroyArgumentState (ref JniValueMarshalerState state) { if (state.PeerableValue is not TArray source) { return false; } - synchronize = GetCopyDirection (synchronize); - if ((synchronize & ParameterAttributes.Out) == ParameterAttributes.Out && value is IList destination) { - for (int i = 0; i < source.Length; i++) { - destination [i] = source [i]; - } - } - if (state.Extra is PrimitiveArrayArgumentState { DisposeArray: true }) { source.Dispose (); } @@ -118,8 +106,8 @@ public override bool TryDestroyArgumentState (object? value, ref JniValueMarshal public override bool IsTargetType (Type targetType) { - return targetType == typeof (global::Java.Interop.JavaArray) || - targetType == typeof (global::Java.Interop.JavaPrimitiveArray) || + return targetType == typeof (JavaArray) || + targetType == typeof (JavaPrimitiveArray) || targetType == typeof (TArray) || targetType == typeof (T[]) || IsCompatibleListType (targetType); @@ -133,40 +121,32 @@ static bool IsCompatibleListType (Type targetType) } } - static readonly PrimitiveArrayHandler[] PrimitiveArrayHandlers = new PrimitiveArrayHandler [] { + static readonly PrimitiveArrayHandler[] PrimitiveArrayHandlers = [ new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaBooleanArray (ref h, o), - length => new JavaBooleanArray (length), list => new JavaBooleanArray (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSByteArray (ref h, o), - length => new JavaSByteArray (length), list => new JavaSByteArray (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaCharArray (ref h, o), - length => new JavaCharArray (length), list => new JavaCharArray (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt16Array (ref h, o), - length => new JavaInt16Array (length), list => new JavaInt16Array (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt32Array (ref h, o), - length => new JavaInt32Array (length), list => new JavaInt32Array (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaInt64Array (ref h, o), - length => new JavaInt64Array (length), list => new JavaInt64Array (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaSingleArray (ref h, o), - length => new JavaSingleArray (length), list => new JavaSingleArray (list)), new PrimitiveArrayHandler ( (ref JniObjectReference h, JniObjectReferenceOptions o) => new JavaDoubleArray (ref h, o), - length => new JavaDoubleArray (length), list => new JavaDoubleArray (list)), - }; + ]; static bool TryCreatePrimitiveArrayWrapper ( ref JniObjectReference reference, @@ -185,10 +165,10 @@ static bool TryCreatePrimitiveArrayWrapper ( return false; } - static bool TryCreatePrimitiveArrayArgumentState (object value, ParameterAttributes synchronize, out JniValueMarshalerState state) + static bool TryCreatePrimitiveArrayArgumentState (object value, out JniValueMarshalerState state) { foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryCreateArgumentState (value, synchronize, out state)) { + if (handler.TryCreateArgumentState (value, out state)) { return true; } } @@ -197,10 +177,10 @@ static bool TryCreatePrimitiveArrayArgumentState (object value, ParameterAttribu return false; } - static bool TryDestroyPrimitiveArrayArgumentState (object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + static bool TryDestroyPrimitiveArrayArgumentState (ref JniValueMarshalerState state) { foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryDestroyArgumentState (value, ref state, synchronize)) { + if (handler.TryDestroyArgumentState (ref state)) { return true; } } @@ -218,12 +198,4 @@ static bool IsPrimitiveArrayTargetType (Type targetType) return false; } - - static ParameterAttributes GetCopyDirection (ParameterAttributes value) - { - const ParameterAttributes inout = ParameterAttributes.In | ParameterAttributes.Out; - if ((value & inout) != 0) - return value & inout; - return inout; - } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index b50267f47f4..a92cc9016ca 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -250,12 +250,7 @@ protected override void ConstructPeerCore ( return CreatePeer (ref reference, options, targetType); } - var transfer = ToJniHandleOwnership (reference, options); - var value = JavaConvert.FromJniHandle (reference.Handle, transfer, GetValueConversionTargetType (targetType)); - if (transfer != JniHandleOwnership.DoNotTransfer) { - reference = default; - } - return value; + return JavaConvert.FromObjectReference (ref reference, options, GetValueConversionTargetType (targetType)); } [return: DynamicallyAccessedMembers (Constructors)] @@ -283,60 +278,21 @@ static bool TryCreateJavaArrayWrapper ( return false; } - protected override JniValueMarshalerState CreateValueMarshalerStateCore (Type type, object? value, ParameterAttributes synchronize) - { - EnsureNotDisposed (); - if (value == null) { - return new JniValueMarshalerState (); - } - if (TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (type)) { - return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, type)); - } - return CreateObjectReferenceValueMarshalerStateCore (type, value, synchronize); - } - - protected override JniValueMarshalerState CreateValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([MaybeNull] T value, ParameterAttributes synchronize) - { - EnsureNotDisposed (); - if (value == null) { - return new JniValueMarshalerState (); - } - if (TrimmableValueMarshalerHelper.IsPrimitiveJniValueType (typeof (T))) { - return new JniValueMarshalerState (TrimmableValueMarshalerHelper.CreatePrimitiveArgumentValue (value, typeof (T))); - } - return CreateObjectReferenceValueMarshalerStateCore (value, synchronize); - } - - protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore (Type type, object? value, ParameterAttributes synchronize) - { - EnsureNotDisposed (); - if (value == null) { - return new JniValueMarshalerState (); - } - if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { - return primitiveArrayState; - } - if (value is IJavaPeerable peerable) { - return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); - } - - var handle = JavaConvert.ToLocalJniHandle (value); - return handle == IntPtr.Zero - ? new JniValueMarshalerState () - : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); - } - - protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([MaybeNull] T value, ParameterAttributes synchronize) + protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore ( + [DynamicallyAccessedMembers (Constructors)] + Type type, + object? value) { - EnsureNotDisposed (); if (value == null) { return new JniValueMarshalerState (); } - if (TryCreatePrimitiveArrayArgumentState (value, synchronize, out var primitiveArrayState)) { + if (TryCreatePrimitiveArrayArgumentState (value, out var primitiveArrayState)) { return primitiveArrayState; } if (value is IJavaPeerable peerable) { - return PeerableValueMarshaler.CreateObjectReferenceArgumentState (peerable, synchronize); + return peerable.PeerReference.IsValid + ? new JniValueMarshalerState (peerable.PeerReference.NewLocalRef ()) + : new JniValueMarshalerState (); } var handle = JavaConvert.ToLocalJniHandle (value); @@ -345,51 +301,21 @@ protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerSta : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); } - protected override void DestroyValueMarshalerStateCore (Type type, object? value, ref JniValueMarshalerState state, ParameterAttributes synchronize) + protected override void DestroyValueMarshalerStateCore (ref JniValueMarshalerState state) { - EnsureNotDisposed (); - if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { + if (TryDestroyPrimitiveArrayArgumentState (ref state)) { return; } - DisposeReferenceState (ref state); - } - protected override void DestroyValueMarshalerStateCore<[DynamicallyAccessedMembers (Constructors)] T> ([AllowNull] T value, ref JniValueMarshalerState state, ParameterAttributes synchronize) - { - EnsureNotDisposed (); - if (TryDestroyPrimitiveArrayArgumentState (value, ref state, synchronize)) { - return; - } - DisposeReferenceState (ref state); + var r = state.ReferenceValue; + JniObjectReference.Dispose (ref r); + state = new JniValueMarshalerState (); } protected override JniValueMarshaler GetValueMarshalerCore (Type type) - { - throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); - } + => throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); protected override JniValueMarshaler GetValueMarshalerCore<[DynamicallyAccessedMembers (Constructors)] T> () - { - throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); - } - - static JniHandleOwnership ToJniHandleOwnership (JniObjectReference reference, JniObjectReferenceOptions options) - { - const JniObjectReferenceOptions DisposeSource = (JniObjectReferenceOptions)(1 << 1); - if ((options & DisposeSource) != DisposeSource) { - return JniHandleOwnership.DoNotTransfer; - } - return reference.Type switch { - JniObjectReferenceType.Local => JniHandleOwnership.TransferLocalRef, - JniObjectReferenceType.Global => JniHandleOwnership.TransferGlobalRef, - _ => JniHandleOwnership.DoNotTransfer, - }; - } + => throw new NotSupportedException ($"{nameof (GetValueMarshalerCore)} should not be called in the trimmable typemap path."); - static void DisposeReferenceState (ref JniValueMarshalerState state) - { - var r = state.ReferenceValue; - JniObjectReference.Dispose (ref r); - state = new JniValueMarshalerState (); - } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs deleted file mode 100644 index 29225418f61..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableValueMarshalerHelper.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -static class TrimmableValueMarshalerHelper -{ - public static bool IsPrimitiveJniValueType (Type type) - { - return type == typeof (bool) || - type == typeof (byte) || - type == typeof (sbyte) || - type == typeof (char) || - type == typeof (short) || - type == typeof (ushort) || - type == typeof (int) || - type == typeof (uint) || - type == typeof (long) || - type == typeof (ulong) || - type == typeof (float) || - type == typeof (double); - } - - public static JniArgumentValue CreatePrimitiveArgumentValue (object? value, Type type) - { - return value switch { - null => throw new ArgumentNullException (nameof (value), "Value cannot be null for primitive JNI value types."), - bool v => new JniArgumentValue (v), - byte v => new JniArgumentValue (v), - sbyte v => new JniArgumentValue (v), - char v => new JniArgumentValue (v), - short v => new JniArgumentValue (v), - ushort v => new JniArgumentValue (v), - int v => new JniArgumentValue (v), - uint v => new JniArgumentValue (v), - long v => new JniArgumentValue (v), - ulong v => new JniArgumentValue (v), - float v => new JniArgumentValue (v), - double v => new JniArgumentValue (v), - _ => throw new NotSupportedException ($"Type '{type.AssemblyQualifiedName}' is not a JNI primitive value type."), - }; - } -} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 42e64059669..f0ff68095d4 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -374,7 +374,6 @@ - From 5dff3a8bd0ee0355b3860942c30862c9a59aee9e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 18:38:10 +0200 Subject: [PATCH 46/47] Return object references from trimmable marshaling Update the trimmable value manager to return standalone local JNI references for JavaObjectArray element assignment and remove the remaining marshaler-state cleanup path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- ...peMapValueManager.PrimitiveArrayHandler.cs | 61 +++++-------------- .../TrimmableTypeMapValueManager.cs | 27 +++----- 3 files changed, 25 insertions(+), 65 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index 26a56153520..fcd73dbcc62 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit 26a5615352043f776792869e16469eaa61968f85 +Subproject commit fcd73dbcc62a854f293004a8ca9748266e7d1603 diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs index 8c903697949..66c51e2e9ca 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.PrimitiveArrayHandler.cs @@ -9,16 +9,6 @@ sealed partial class TrimmableTypeMapValueManager { delegate TArray PrimitiveArrayFactory (ref JniObjectReference reference, JniObjectReferenceOptions options); - readonly struct PrimitiveArrayArgumentState - { - public readonly bool DisposeArray; - - public PrimitiveArrayArgumentState (bool disposeArray) - { - DisposeArray = disposeArray; - } - } - abstract class PrimitiveArrayHandler { public abstract bool TryCreateWrapper ( @@ -28,9 +18,7 @@ public abstract bool TryCreateWrapper ( Type targetType, [NotNullWhen (true)] out object? value); - public abstract bool TryCreateArgumentState (object value, out JniValueMarshalerState state); - - public abstract bool TryDestroyArgumentState (ref JniValueMarshalerState state); + public abstract bool TryCreateObjectReference (object value, out JniObjectReference reference); public abstract bool IsTargetType (Type targetType); } @@ -73,35 +61,29 @@ public override bool TryCreateWrapper ( return true; } - public override bool TryCreateArgumentState (object value, out JniValueMarshalerState state) + public override bool TryCreateObjectReference (object value, out JniObjectReference reference) { if (value is TArray array) { - state = new JniValueMarshalerState (array); + reference = array.PeerReference.IsValid + ? array.PeerReference.NewLocalRef () + : new JniObjectReference (); return true; } if (value is not IList list) { - state = new JniValueMarshalerState (); + reference = new JniObjectReference (); return false; } var marshaledArray = createCopy (list); - state = new JniValueMarshalerState (marshaledArray, new PrimitiveArrayArgumentState (disposeArray: true)); - return true; - } - - public override bool TryDestroyArgumentState (ref JniValueMarshalerState state) - { - if (state.PeerableValue is not TArray source) { - return false; - } - - if (state.Extra is PrimitiveArrayArgumentState { DisposeArray: true }) { - source.Dispose (); + try { + reference = marshaledArray.PeerReference.IsValid + ? marshaledArray.PeerReference.NewLocalRef () + : new JniObjectReference (); + return true; + } finally { + marshaledArray.Dispose (); } - - state = new JniValueMarshalerState (); - return true; } public override bool IsTargetType (Type targetType) @@ -165,26 +147,15 @@ static bool TryCreatePrimitiveArrayWrapper ( return false; } - static bool TryCreatePrimitiveArrayArgumentState (object value, out JniValueMarshalerState state) - { - foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryCreateArgumentState (value, out state)) { - return true; - } - } - - state = new JniValueMarshalerState (); - return false; - } - - static bool TryDestroyPrimitiveArrayArgumentState (ref JniValueMarshalerState state) + static bool TryCreatePrimitiveArrayObjectReference (object value, out JniObjectReference reference) { foreach (var handler in PrimitiveArrayHandlers) { - if (handler.TryDestroyArgumentState (ref state)) { + if (handler.TryCreateObjectReference (value, out reference)) { return true; } } + reference = new JniObjectReference (); return false; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index a92cc9016ca..941399fdefe 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -278,38 +278,27 @@ static bool TryCreateJavaArrayWrapper ( return false; } - protected override JniValueMarshalerState CreateObjectReferenceValueMarshalerStateCore ( + protected override JniObjectReference CreateObjectReferenceArgumentCore ( [DynamicallyAccessedMembers (Constructors)] Type type, object? value) { if (value == null) { - return new JniValueMarshalerState (); + return new JniObjectReference (); } - if (TryCreatePrimitiveArrayArgumentState (value, out var primitiveArrayState)) { - return primitiveArrayState; + if (TryCreatePrimitiveArrayObjectReference (value, out var primitiveArrayReference)) { + return primitiveArrayReference; } if (value is IJavaPeerable peerable) { return peerable.PeerReference.IsValid - ? new JniValueMarshalerState (peerable.PeerReference.NewLocalRef ()) - : new JniValueMarshalerState (); + ? peerable.PeerReference.NewLocalRef () + : new JniObjectReference (); } var handle = JavaConvert.ToLocalJniHandle (value); return handle == IntPtr.Zero - ? new JniValueMarshalerState () - : new JniValueMarshalerState (new JniObjectReference (handle, JniObjectReferenceType.Local)); - } - - protected override void DestroyValueMarshalerStateCore (ref JniValueMarshalerState state) - { - if (TryDestroyPrimitiveArrayArgumentState (ref state)) { - return; - } - - var r = state.ReferenceValue; - JniObjectReference.Dispose (ref r); - state = new JniValueMarshalerState (); + ? new JniObjectReference () + : new JniObjectReference (handle, JniObjectReferenceType.Local); } protected override JniValueMarshaler GetValueMarshalerCore (Type type) From f928f9918be885a1ee0f65312df999b43a8a5a49 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 12 Jun 2026 18:53:17 +0200 Subject: [PATCH 47/47] Clarify trimmable local reference ownership Update the trimmable value manager override to match the CreateLocalObjectReferenceArgument naming from Java.Interop. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index fcd73dbcc62..e5f8b41a9cf 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit fcd73dbcc62a854f293004a8ca9748266e7d1603 +Subproject commit e5f8b41a9cf01d4266ae641736ba95e8cdc7a1a8 diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs index 941399fdefe..1b0a319f0ab 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapValueManager.cs @@ -278,7 +278,7 @@ static bool TryCreateJavaArrayWrapper ( return false; } - protected override JniObjectReference CreateObjectReferenceArgumentCore ( + protected override JniObjectReference CreateLocalObjectReferenceArgumentCore ( [DynamicallyAccessedMembers (Constructors)] Type type, object? value)