From 3a8d50970882fb6decbfd685ddbaa436e7ee4b2f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 11:44:39 +0200 Subject: [PATCH 01/21] [TrimmableTypeMap] Runtime trimmable typemap support Rebuild the remaining runtime-only portion of PR 11090 on top of main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 6 + .../Android.Runtime/JNIEnvInit.cs | 2 + src/Mono.Android/Java.Interop/TypeManager.cs | 8 +- .../JavaMarshalValueManager.cs | 15 +- .../TrimmableTypeMap.cs | 173 +++++++++++++++++- .../TrimmableTypeMapTypeManager.cs | 4 +- 6 files changed, 186 insertions(+), 22 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 8b004855ba8..aa91db31463 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -442,6 +442,12 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) internal static unsafe string? TypemapManagedToJava (Type type) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + return TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName) ? jniName : null; + } + if (mvid_bytes == null) mvid_bytes = new byte[16]; diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 097c8e5d0ab..8bb98c4a993 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -110,6 +110,7 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args) internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args) { androidRuntime = runtime; + JniRuntime.SetCurrent (runtime); SetSynchronizationContext (); } @@ -159,6 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) valueManager, args->jniAddNativeMethodRegistrationAttributePresent != 0 ); + JniRuntime.SetCurrent (androidRuntime); if (RuntimeFeature.TrimmableTypeMap) { TrimmableTypeMap.Initialize (); diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 379558e3265..7152e911b3e 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -268,7 +268,13 @@ static Type monovm_typemap_java_to_managed (string java_type_name) return type; } - if (RuntimeFeature.IsMonoRuntime) { + if (RuntimeFeature.TrimmableTypeMap) { + // The trimmable typemap doesn't use the native typemap tables. + // Delegate to the managed TrimmableTypeMap instead. + if (!TrimmableTypeMap.Instance.TryGetType (class_name, out type)) { + return null; + } + } else if (RuntimeFeature.IsMonoRuntime) { type = monovm_typemap_java_to_managed (class_name); } else if (RuntimeFeature.IsCoreClrRuntime) { type = clr_typemap_java_to_managed (class_name); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e1b7e975059..fa7aa786925 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -509,16 +509,11 @@ void ProcessContext (HandleContext* context) { if (RuntimeFeature.TrimmableTypeMap) { var typeMap = TrimmableTypeMap.Instance; - if (typeMap is not null && targetType is not null) { - var proxy = typeMap.GetProxyForManagedType (targetType); - if (proxy is not null) { - var peer = proxy.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); - if (peer is not null) { - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); - JniObjectReference.Dispose (ref reference, transfer); - return peer; - } - } + var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); + if (peer is not null) { + peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + JniObjectReference.Dispose (ref reference, transfer); + return peer; } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b1c2b945429..b5383da77c0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -9,6 +9,7 @@ using System.Threading; using Android.Runtime; using Java.Interop; +using Java.Interop.Tools.TypeNameMappings; namespace Microsoft.Android.Runtime; @@ -29,6 +30,8 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; readonly ConcurrentDictionary _proxyCache = new (); + readonly ConcurrentDictionary _jniNameCache = new (); + readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () { @@ -69,7 +72,19 @@ unsafe void RegisterNatives () } internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) - => _typeMap.TryGetValue (jniSimpleReference, out type); + { + if (!_typeMap.TryGetValue (jniSimpleReference, out var mappedType)) { + type = null; + return false; + } + + // External typemap entries usually point at the generated proxy for ACW-backed types. + // Surface the real managed peer when possible so activation and virtual dispatch land + // on the user's override instead of the generated proxy type. + var proxy = mappedType.GetCustomAttribute (inherit: false); + type = proxy?.TargetType ?? mappedType; + return true; + } /// /// Finds the proxy for a managed type using the generated proxy type map. @@ -87,30 +102,170 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty return proxyType.GetCustomAttribute (inherit: false); } - internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) + internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) { - var proxy = GetProxyForManagedType (managedType); + if (_jniNameCache.TryGetValue (type, out jniName)) { + return jniName != null; + } + + var proxy = GetProxyForManagedType (type); if (proxy is not null) { jniName = proxy.JniName; + _jniNameCache [type] = jniName; + return true; + } + + if (TryGetJniNameForType (type, out jniName)) { + _jniNameCache [type] = jniName; return true; } + if (TryGetCompatJniNameForAndroidComponent (type, out jniName)) { + _jniNameCache [type] = jniName; + return true; + } + + // Prefer the JavaNativeTypeManager calculation for user/application types, + // as it matches the ACW generation rules used during the build. + if (typeof (IJavaPeerable).IsAssignableFrom (type)) { + jniName = JavaNativeTypeManager.ToJniName (type); + if (!string.IsNullOrEmpty (jniName) && jniName != "java/lang/Object") { + _jniNameCache [type] = jniName; + return true; + } + } + jniName = null; + _jniNameCache [type] = null; return false; } + internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) + => TryGetJniName (managedType, out jniName); + + internal JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) + { + if (handle == IntPtr.Zero) { + return null; + } + + var selfRef = new JniObjectReference (handle); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + + try { + while (jniClass.IsValid) { + var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + if (className != null) { + if (_peerProxyCache.TryGetValue (className, out var cached)) { + if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { + return cached; + } + } else if (_typeMap.TryGetValue (className, out var mappedType)) { + var proxy = mappedType.GetCustomAttribute (inherit: false); + _peerProxyCache [className] = proxy; + if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + return proxy; + } + } + } + + var super = JniEnvironment.Types.GetSuperclass (jniClass); + JniObjectReference.Dispose (ref jniClass); + jniClass = super; + } + } finally { + JniObjectReference.Dispose (ref jniClass); + } + + return null; + } + + internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) + { + var proxy = GetProxyForPeer (handle, targetType); + if (proxy is null && targetType is not null) { + proxy = GetProxyForManagedType (targetType); + // Verify the Java object is actually assignable to the target Java type + // before creating the peer. Without this, we'd create invalid peers + // (e.g., IAppendableInvoker wrapping a java.lang.Integer). + if (proxy is not null && TryGetJniName (targetType, out var targetJniName)) { + var selfRef = new JniObjectReference (handle); + var objClass = JniEnvironment.Types.GetObjectClass (selfRef); + var targetClass = JniEnvironment.Types.FindClass (targetJniName); + try { + if (!JniEnvironment.Types.IsAssignableFrom (objClass, targetClass)) { + proxy = null; + } + } finally { + JniObjectReference.Dispose (ref objClass); + JniObjectReference.Dispose (ref targetClass); + } + } + } + + return proxy?.CreateInstance (handle, transfer); + } + + static bool TryGetCompatJniNameForAndroidComponent (Type type, [NotNullWhen (true)] out string? jniName) + { + if (!IsAndroidComponentType (type)) { + jniName = null; + return false; + } + + var (typeName, parentJniName, ns) = GetCompatTypeNameParts (type); + jniName = parentJniName is not null + ? $"{parentJniName}_{typeName}" + : ns.Length == 0 + ? typeName + : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; + return true; + } + + static bool IsAndroidComponentType (Type type) + { + return type.IsDefined (typeof (global::Android.App.ActivityAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ApplicationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.InstrumentationAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.App.ServiceAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.BroadcastReceiverAttribute), inherit: false) || + type.IsDefined (typeof (global::Android.Content.ContentProviderAttribute), inherit: false); + } + + static (string TypeName, string? ParentJniName, string Namespace) GetCompatTypeNameParts (Type type) + { + var nameParts = new List { SanitizeTypeName (type.Name) }; + var current = type; + string? parentJniName = null; + + while (current.DeclaringType is Type parentType) { + if (TryGetJniNameForType (parentType, out var explicitJniName) || + TryGetCompatJniNameForAndroidComponent (parentType, out explicitJniName)) { + parentJniName = explicitJniName; + break; + } + + nameParts.Add (SanitizeTypeName (parentType.Name)); + current = parentType; + } + + nameParts.Reverse (); + return (string.Join ("_", nameParts), parentJniName, current.Namespace ?? ""); + } + + static string SanitizeTypeName (string name) + { + var tick = name.IndexOf ('`'); + return (tick >= 0 ? name.Substring (0, tick) : name).Replace ('+', '_'); + } + /// /// Creates a peer instance using the proxy's CreateInstance method. /// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance. /// internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer) { - var proxy = GetProxyForManagedType (type); - if (proxy is null) { - return false; - } - - return proxy.CreateInstance (handle, transfer) != null; + return CreatePeer (handle, transfer, type) != null; } const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 3f34ab3c62a..f2b573f0ab1 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -28,7 +28,7 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl protected override IEnumerable GetSimpleReferences (Type type) { - if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { + if (TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName)) { yield return jniName; yield break; } @@ -40,7 +40,7 @@ protected override IEnumerable GetSimpleReferences (Type 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)) { + if (TrimmableTypeMap.Instance.TryGetJniName (baseType, out var baseJniName)) { yield return baseJniName; yield break; } From d4152dff1e5f8ac6d6386af2ea448228f4d9458d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 11:53:56 +0200 Subject: [PATCH 02/21] [TrimmableTypeMap] Keep cached proxy-only JNI lookup Remove the accidental fallback chain from TryGetJniName while preserving the cache. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 74 ------------------- 1 file changed, 74 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b5383da77c0..32b77951ec4 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -9,7 +9,6 @@ using System.Threading; using Android.Runtime; using Java.Interop; -using Java.Interop.Tools.TypeNameMappings; namespace Microsoft.Android.Runtime; @@ -115,26 +114,6 @@ internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName return true; } - if (TryGetJniNameForType (type, out jniName)) { - _jniNameCache [type] = jniName; - return true; - } - - if (TryGetCompatJniNameForAndroidComponent (type, out jniName)) { - _jniNameCache [type] = jniName; - return true; - } - - // Prefer the JavaNativeTypeManager calculation for user/application types, - // as it matches the ACW generation rules used during the build. - if (typeof (IJavaPeerable).IsAssignableFrom (type)) { - jniName = JavaNativeTypeManager.ToJniName (type); - if (!string.IsNullOrEmpty (jniName) && jniName != "java/lang/Object") { - _jniNameCache [type] = jniName; - return true; - } - } - jniName = null; _jniNameCache [type] = null; return false; @@ -206,59 +185,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return proxy?.CreateInstance (handle, transfer); } - static bool TryGetCompatJniNameForAndroidComponent (Type type, [NotNullWhen (true)] out string? jniName) - { - if (!IsAndroidComponentType (type)) { - jniName = null; - return false; - } - - var (typeName, parentJniName, ns) = GetCompatTypeNameParts (type); - jniName = parentJniName is not null - ? $"{parentJniName}_{typeName}" - : ns.Length == 0 - ? typeName - : $"{ns.ToLowerInvariant ().Replace ('.', '/')}/{typeName}"; - return true; - } - - static bool IsAndroidComponentType (Type type) - { - return type.IsDefined (typeof (global::Android.App.ActivityAttribute), inherit: false) || - type.IsDefined (typeof (global::Android.App.ApplicationAttribute), inherit: false) || - type.IsDefined (typeof (global::Android.App.InstrumentationAttribute), inherit: false) || - type.IsDefined (typeof (global::Android.App.ServiceAttribute), inherit: false) || - type.IsDefined (typeof (global::Android.Content.BroadcastReceiverAttribute), inherit: false) || - type.IsDefined (typeof (global::Android.Content.ContentProviderAttribute), inherit: false); - } - - static (string TypeName, string? ParentJniName, string Namespace) GetCompatTypeNameParts (Type type) - { - var nameParts = new List { SanitizeTypeName (type.Name) }; - var current = type; - string? parentJniName = null; - - while (current.DeclaringType is Type parentType) { - if (TryGetJniNameForType (parentType, out var explicitJniName) || - TryGetCompatJniNameForAndroidComponent (parentType, out explicitJniName)) { - parentJniName = explicitJniName; - break; - } - - nameParts.Add (SanitizeTypeName (parentType.Name)); - current = parentType; - } - - nameParts.Reverse (); - return (string.Join ("_", nameParts), parentJniName, current.Namespace ?? ""); - } - - static string SanitizeTypeName (string name) - { - var tick = name.IndexOf ('`'); - return (tick >= 0 ? name.Substring (0, tick) : name).Replace ('+', '_'); - } - /// /// Creates a peer instance using the proxy's CreateInstance method. /// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance. From 5d16a1244a96699b5e93027d5d9a9de3118b6a89 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 12:31:33 +0200 Subject: [PATCH 03/21] [TrimmableTypeMap] Remove legacy TypeManager redirect Keep the trimmable peer path on TrimmableTypeMapTypeManager/JavaMarshalValueManager, make any legacy TypeManager lookup in trimmable mode fail fast, and tighten the proxy caches/state handling for parity with the existing activation flow. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Java.Interop/TypeManager.cs | 9 ++-- .../JavaMarshalValueManager.cs | 8 +++- .../TrimmableTypeMap.cs | 41 +++++++------------ 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index 7152e911b3e..cc0b22936bd 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -269,11 +269,10 @@ static Type monovm_typemap_java_to_managed (string java_type_name) } if (RuntimeFeature.TrimmableTypeMap) { - // The trimmable typemap doesn't use the native typemap tables. - // Delegate to the managed TrimmableTypeMap instead. - if (!TrimmableTypeMap.Instance.TryGetType (class_name, out type)) { - return null; - } + throw new System.Diagnostics.UnreachableException ( + $"{nameof (TypeManager)}.{nameof (GetJavaToManagedTypeCore)} should not be used when " + + $"{nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. The trimmable path should resolve " + + $"types through {nameof (TrimmableTypeMapTypeManager)}."); } else if (RuntimeFeature.IsMonoRuntime) { type = monovm_typemap_java_to_managed (class_name); } else if (RuntimeFeature.IsCoreClrRuntime) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index fa7aa786925..5bebc91e6c0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -508,10 +508,16 @@ void ProcessContext (HandleContext* context) Type? targetType) { if (RuntimeFeature.TrimmableTypeMap) { + // Prefer proxy-backed activation first, but keep the generic JniValueManager + // fallback for parity when there is no proxy match for the requested target type. var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); if (peer is not null) { - peer.SetJniManagedPeerState (peer.JniManagedPeerState | JniManagedPeerStates.Replaceable); + var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; + if (Android.Runtime.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { + peerState |= JniManagedPeerStates.Activatable; + } + peer.SetJniManagedPeerState (peerState); JniObjectReference.Dispose (ref reference, transfer); return peer; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 32b77951ec4..882a4de2d5b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -77,9 +77,10 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty return false; } - // External typemap entries usually point at the generated proxy for ACW-backed types. - // Surface the real managed peer when possible so activation and virtual dispatch land - // on the user's override instead of the generated proxy type. + // External typemap entries for ACW-backed types resolve to the generated proxy-bearing + // helper (including alias slots such as "jni/name[1]"). Surface the actual managed peer + // when a JavaPeerProxy attribute is present so activation and virtual dispatch land on + // the user's type instead of the generated helper. var proxy = mappedType.GetCustomAttribute (inherit: false); type = proxy?.TargetType ?? mappedType; return true; @@ -103,20 +104,8 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) { - if (_jniNameCache.TryGetValue (type, out jniName)) { - return jniName != null; - } - - var proxy = GetProxyForManagedType (type); - if (proxy is not null) { - jniName = proxy.JniName; - _jniNameCache [type] = jniName; - return true; - } - - jniName = null; - _jniNameCache [type] = null; - return false; + jniName = _jniNameCache.GetOrAdd (type, static (type, self) => self.GetProxyForManagedType (type)?.JniName, this); + return jniName is not null; } internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) @@ -135,16 +124,16 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - if (_peerProxyCache.TryGetValue (className, out var cached)) { - if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { - return cached; - } - } else if (_typeMap.TryGetValue (className, out var mappedType)) { - var proxy = mappedType.GetCustomAttribute (inherit: false); - _peerProxyCache [className] = proxy; - if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { - return proxy; + var cached = _peerProxyCache.GetOrAdd (className, static (name, self) => { + if (!self._typeMap.TryGetValue (name, out var mappedType)) { + return null; } + + return mappedType.GetCustomAttribute (inherit: false); + }, this); + + if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { + return cached; } } From 656ddcff657434ddd510b631b919158d06bd2db0 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 12:43:24 +0200 Subject: [PATCH 04/21] [TrimmableTypeMap] Fail fast on missing peer proxies Stop silently falling back when trimmable peer activation cannot resolve a generated JavaPeerProxy. In trimmable mode, missing proxy coverage should surface as an explicit runtime error so generator gaps are fixed at the source. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 5bebc91e6c0..b30e1a76172 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -508,8 +508,6 @@ void ProcessContext (HandleContext* context) Type? targetType) { if (RuntimeFeature.TrimmableTypeMap) { - // Prefer proxy-backed activation first, but keep the generic JniValueManager - // fallback for parity when there is no proxy match for the requested target type. var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); if (peer is not null) { @@ -521,6 +519,13 @@ void ProcessContext (HandleContext* context) JniObjectReference.Dispose (ref reference, transfer); return peer; } + + var targetName = targetType?.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."); } return base.CreatePeer (ref reference, transfer, targetType); From 5a96a7174d3f34e13cb94b9b99edec32d914d836 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 12:44:37 +0200 Subject: [PATCH 05/21] [TrimmableTypeMap] Remove duplicate JNI name helper Collapse the trimmable managed-to-JNI lookup back to a single internal API by keeping TryGetJniNameForManagedType() and removing the duplicate TryGetJniName() entry point. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 2 +- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 9 +++------ .../TrimmableTypeMapTypeManager.cs | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index aa91db31463..8104ee308a2 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -445,7 +445,7 @@ static unsafe IntPtr monovm_typemap_managed_to_java (Type type, byte* mvidptr) if (RuntimeFeature.TrimmableTypeMap) { // The trimmable typemap doesn't use the native typemap tables. // Delegate to the managed TrimmableTypeMap instead. - return TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName) ? jniName : null; + return TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName) ? jniName : null; } if (mvid_bytes == null) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 882a4de2d5b..b9dbe54c48c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -102,15 +102,12 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty return proxyType.GetCustomAttribute (inherit: false); } - internal bool TryGetJniName (Type type, [NotNullWhen (true)] out string? jniName) + internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { - jniName = _jniNameCache.GetOrAdd (type, static (type, self) => self.GetProxyForManagedType (type)?.JniName, this); + jniName = _jniNameCache.GetOrAdd (managedType, static (type, self) => self.GetProxyForManagedType (type)?.JniName, this); return jniName is not null; } - internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) - => TryGetJniName (managedType, out jniName); - internal JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { @@ -156,7 +153,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // Verify the Java object is actually assignable to the target Java type // before creating the peer. Without this, we'd create invalid peers // (e.g., IAppendableInvoker wrapping a java.lang.Integer). - if (proxy is not null && TryGetJniName (targetType, out var targetJniName)) { + if (proxy is not null && TryGetJniNameForManagedType (targetType, out var targetJniName)) { var selfRef = new JniObjectReference (handle); var objClass = JniEnvironment.Types.GetObjectClass (selfRef); var targetClass = JniEnvironment.Types.FindClass (targetJniName); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index f2b573f0ab1..3f34ab3c62a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -28,7 +28,7 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl protected override IEnumerable GetSimpleReferences (Type type) { - if (TrimmableTypeMap.Instance.TryGetJniName (type, out var jniName)) { + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (type, out var jniName)) { yield return jniName; yield break; } @@ -40,7 +40,7 @@ protected override IEnumerable GetSimpleReferences (Type 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.TryGetJniName (baseType, out var baseJniName)) { + if (TrimmableTypeMap.Instance.TryGetJniNameForManagedType (baseType, out var baseJniName)) { yield return baseJniName; yield break; } From aa33e560233aed9015912c54fbd458b0571662dd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 12:48:21 +0200 Subject: [PATCH 06/21] [TrimmableTypeMap] Prune internal peer helpers Reduce TrimmableTypeMap surface area by making purely local proxy helpers private and removing the unused TryCreatePeer() wrapper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b9dbe54c48c..456b0e811a4 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -90,7 +90,7 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty /// Finds the proxy for a managed type using the generated proxy type map. /// Results are cached per type. /// - internal JavaPeerProxy? GetProxyForManagedType (Type managedType) + JavaPeerProxy? GetProxyForManagedType (Type managedType) => _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type), this); JavaPeerProxy? ResolveProxyForManagedType (Type managedType) @@ -108,7 +108,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } - internal JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) + JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { return null; @@ -171,15 +171,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return proxy?.CreateInstance (handle, transfer); } - /// - /// Creates a peer instance using the proxy's CreateInstance method. - /// Given a managed type, resolves the JNI name, finds the proxy, and calls CreateInstance. - /// - internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer) - { - return CreatePeer (handle, transfer, type) != null; - } - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; /// From 47089ad238867b2bf9566a16fd65be7a884b655e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 12:53:23 +0200 Subject: [PATCH 07/21] [TrimmableTypeMap] Clarify Java object proxy lookup Rename GetProxyForPeer() to GetProxyForJavaObject() so the helper more clearly describes what it actually inspects and resolves. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 456b0e811a4..82ca6aa6a9d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -108,7 +108,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } - JavaPeerProxy? GetProxyForPeer (IntPtr handle, Type? targetType = null) + JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { return null; @@ -147,7 +147,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) { - var proxy = GetProxyForPeer (handle, targetType); + var proxy = GetProxyForJavaObject (handle, targetType); if (proxy is null && targetType is not null) { proxy = GetProxyForManagedType (targetType); // Verify the Java object is actually assignable to the target Java type From c4b567c0e1a0178b301b86fd8718441e65ab8a70 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 13:24:14 +0200 Subject: [PATCH 08/21] [TrimmableTypeMap] Restore CreatePeer guard semantics Preserve the inherited JniValueManager.CreatePeer() preconditions for disposed managers and invalid JNI references before the trimmable fail-fast proxy check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index b30e1a76172..d0c5c07d4a2 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -507,6 +507,12 @@ void ProcessContext (HandleContext* context) [DynamicallyAccessedMembers (Constructors)] Type? targetType) { + ThrowIfDisposed (); + + if (!reference.IsValid) { + return null; + } + if (RuntimeFeature.TrimmableTypeMap) { var typeMap = TrimmableTypeMap.Instance; var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); From 9c5672b9a0e6f2069d42361dde9adc2ec2924805 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:10:21 +0200 Subject: [PATCH 09/21] Fix trimmable typemap review issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 8 ++- .../TrimmableTypeMap.cs | 53 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index d0c5c07d4a2..63e8787bccf 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -527,7 +527,13 @@ void ProcessContext (HandleContext* context) } var targetName = targetType?.AssemblyQualifiedName ?? ""; - var javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + string? javaType = null; + try { + javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + } finally { + JniObjectReference.Dispose (ref reference, transfer); + } + throw new NotSupportedException ( $"No generated {nameof (JavaPeerProxy)} was found for Java type '{javaType}' " + $"with targetType '{targetName}' while {nameof (RuntimeFeature.TrimmableTypeMap)} is enabled. " + diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 82ca6aa6a9d..1eca558d499 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -28,9 +28,10 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - readonly ConcurrentDictionary _proxyCache = new (); - readonly ConcurrentDictionary _jniNameCache = new (); - readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); + // ConcurrentDictionary doesn't accept null values, so these caches only store hits. + readonly ConcurrentDictionary _proxyCache = new (); + readonly ConcurrentDictionary _jniNameCache = new (); + readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () { @@ -91,7 +92,18 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty /// Results are cached per type. /// JavaPeerProxy? GetProxyForManagedType (Type managedType) - => _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type), this); + { + if (_proxyCache.TryGetValue (managedType, out var cached)) { + return cached; + } + + var resolved = ResolveProxyForManagedType (managedType); + if (resolved is null) { + return null; + } + + return _proxyCache.GetOrAdd (managedType, resolved); + } JavaPeerProxy? ResolveProxyForManagedType (Type managedType) { @@ -104,8 +116,19 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { - jniName = _jniNameCache.GetOrAdd (managedType, static (type, self) => self.GetProxyForManagedType (type)?.JniName, this); - return jniName is not null; + if (_jniNameCache.TryGetValue (managedType, out var cached)) { + jniName = cached; + return true; + } + + var proxy = GetProxyForManagedType (managedType); + if (proxy is null) { + jniName = null; + return false; + } + + jniName = _jniNameCache.GetOrAdd (managedType, proxy.JniName); + return true; } JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) @@ -121,13 +144,13 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - var cached = _peerProxyCache.GetOrAdd (className, static (name, self) => { - if (!self._typeMap.TryGetValue (name, out var mappedType)) { - return null; + JavaPeerProxy? cached = null; + if (!_peerProxyCache.TryGetValue (className, out cached) && _typeMap.TryGetValue (className, out var mappedType)) { + var resolved = mappedType.GetCustomAttribute (inherit: false); + if (resolved is not null) { + cached = _peerProxyCache.GetOrAdd (className, resolved); } - - return mappedType.GetCustomAttribute (inherit: false); - }, this); + } if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { return cached; @@ -155,9 +178,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) // (e.g., IAppendableInvoker wrapping a java.lang.Integer). if (proxy is not null && TryGetJniNameForManagedType (targetType, out var targetJniName)) { var selfRef = new JniObjectReference (handle); - var objClass = JniEnvironment.Types.GetObjectClass (selfRef); - var targetClass = JniEnvironment.Types.FindClass (targetJniName); + var objClass = default (JniObjectReference); + var targetClass = default (JniObjectReference); try { + objClass = JniEnvironment.Types.GetObjectClass (selfRef); + targetClass = JniEnvironment.Types.FindClass (targetJniName); if (!JniEnvironment.Types.IsAssignableFrom (objClass, targetClass)) { proxy = null; } From 4a68393c319ec3335afe0b983dfff944975eeb70 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:16:16 +0200 Subject: [PATCH 10/21] Refine trimmable typemap cleanup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 36 +++++++++---------- .../TrimmableTypeMap.cs | 8 +---- 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 63e8787bccf..c39c2958bb1 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -514,30 +514,28 @@ void ProcessContext (HandleContext* context) } if (RuntimeFeature.TrimmableTypeMap) { - var typeMap = TrimmableTypeMap.Instance; - var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); - if (peer is not null) { - var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; - if (Android.Runtime.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { - peerState |= JniManagedPeerStates.Activatable; + try { + var typeMap = TrimmableTypeMap.Instance; + var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); + if (peer is not null) { + var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; + if (Android.Runtime.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { + peerState |= JniManagedPeerStates.Activatable; + } + peer.SetJniManagedPeerState (peerState); + return peer; } - peer.SetJniManagedPeerState (peerState); - JniObjectReference.Dispose (ref reference, transfer); - return peer; - } - var targetName = targetType?.AssemblyQualifiedName ?? ""; - string? javaType = null; - try { - javaType = JniEnvironment.Types.GetJniTypeNameFromInstance (reference); + var targetName = targetType?.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); } - - 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."); } return base.CreatePeer (ref reference, transfer, targetType); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 1eca558d499..ca9d7460bae 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -30,7 +30,6 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _proxyTypeMap; // ConcurrentDictionary doesn't accept null values, so these caches only store hits. readonly ConcurrentDictionary _proxyCache = new (); - readonly ConcurrentDictionary _jniNameCache = new (); readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); TrimmableTypeMap () @@ -116,18 +115,13 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { - if (_jniNameCache.TryGetValue (managedType, out var cached)) { - jniName = cached; - return true; - } - var proxy = GetProxyForManagedType (managedType); if (proxy is null) { jniName = null; return false; } - jniName = _jniNameCache.GetOrAdd (managedType, proxy.JniName); + jniName = proxy.JniName; return true; } From 2897a080357e40a0a0a80e907e3d60df06757062 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:42:44 +0200 Subject: [PATCH 11/21] Inline sentinel cache handling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 43 +++++++++++-------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index ca9d7460bae..a40b3f48278 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -20,6 +20,7 @@ namespace Microsoft.Android.Runtime; class TrimmableTypeMap { static readonly Lock s_initLock = new (); + static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; internal static TrimmableTypeMap Instance => @@ -28,7 +29,7 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // ConcurrentDictionary doesn't accept null values, so these caches only store hits. + // ConcurrentDictionary doesn't accept null values, so misses are cached with s_noPeerSentinel. readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); @@ -92,16 +93,8 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty /// JavaPeerProxy? GetProxyForManagedType (Type managedType) { - if (_proxyCache.TryGetValue (managedType, out var cached)) { - return cached; - } - - var resolved = ResolveProxyForManagedType (managedType); - if (resolved is null) { - return null; - } - - return _proxyCache.GetOrAdd (managedType, resolved); + var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type) ?? s_noPeerSentinel, this); + return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } JavaPeerProxy? ResolveProxyForManagedType (Type managedType) @@ -138,15 +131,9 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - JavaPeerProxy? cached = null; - if (!_peerProxyCache.TryGetValue (className, out cached) && _typeMap.TryGetValue (className, out var mappedType)) { - var resolved = mappedType.GetCustomAttribute (inherit: false); - if (resolved is not null) { - cached = _peerProxyCache.GetOrAdd (className, resolved); - } - } + var cached = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); - if (cached != null && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { + if (!ReferenceEquals (cached, s_noPeerSentinel) && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { return cached; } } @@ -162,6 +149,15 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } + JavaPeerProxy? ResolveProxyForJavaType (string className) + { + if (!_typeMap.TryGetValue (className, out var mappedType)) { + return null; + } + + return mappedType.GetCustomAttribute (inherit: false); + } + internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) { var proxy = GetProxyForJavaObject (handle, targetType); @@ -239,4 +235,13 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa } } + sealed class MissingJavaPeerProxy : JavaPeerProxy + { + public MissingJavaPeerProxy () : base ("", typeof (Java.Lang.Object), null) + { + } + + public override IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer) => null; + } + } From 7a7c18648e34523006c456e1d9359a4e93855dbe Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:47:31 +0200 Subject: [PATCH 12/21] Reuse managed proxy lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index a40b3f48278..d7e71e02969 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -151,11 +151,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) JavaPeerProxy? ResolveProxyForJavaType (string className) { - if (!_typeMap.TryGetValue (className, out var mappedType)) { + if (!TryGetType (className, out var managedType)) { return null; } - return mappedType.GetCustomAttribute (inherit: false); + return GetProxyForManagedType (managedType); } internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) From 48102b6a07a6d084505a72ab8485b25b9d7b7408 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:52:24 +0200 Subject: [PATCH 13/21] Rename typemap target lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 4 ++-- .../Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index d7e71e02969..78f7751ab58 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -71,7 +71,7 @@ unsafe void RegisterNatives () } } - internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) + internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) { if (!_typeMap.TryGetValue (jniSimpleReference, out var mappedType)) { type = null; @@ -151,7 +151,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) JavaPeerProxy? ResolveProxyForJavaType (string className) { - if (!TryGetType (className, out var managedType)) { + if (!TryGetTargetType (className, out var managedType)) { return null; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 3f34ab3c62a..50fb402b91f 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -21,7 +21,7 @@ protected override IEnumerable GetTypesForSimpleReference (string jniSimpl yield return t; } - if (TrimmableTypeMap.Instance.TryGetType (jniSimpleReference, out var type)) { + if (TrimmableTypeMap.Instance.TryGetTargetType (jniSimpleReference, out var type)) { yield return type; } } From 530b2b23eb2c4c69c31501bcdc722a6786d2e433 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:53:14 +0200 Subject: [PATCH 14/21] Extract Java proxy cache helper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 78f7751ab58..69ed961dc0b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -131,10 +131,9 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - var cached = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); - - if (!ReferenceEquals (cached, s_noPeerSentinel) && (targetType is null || targetType.IsAssignableFrom (cached.TargetType))) { - return cached; + var proxy = GetProxyForJavaType (className); + if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + return proxy; } } @@ -149,6 +148,12 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } + JavaPeerProxy? GetProxyForJavaType (string className) + { + var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); + return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; + } + JavaPeerProxy? ResolveProxyForJavaType (string className) { if (!TryGetTargetType (className, out var managedType)) { From 0bad30001e9ee737a2d47bb9ca65660be340d1a9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 14:59:15 +0200 Subject: [PATCH 15/21] Separate proxy lookup from activation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaMarshalValueManager.cs | 3 +- .../TrimmableTypeMap.cs | 54 +++++++++---------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index c39c2958bb1..d2166be6388 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -516,7 +516,8 @@ void ProcessContext (HandleContext* context) if (RuntimeFeature.TrimmableTypeMap) { try { var typeMap = TrimmableTypeMap.Instance; - var peer = typeMap.CreatePeer (reference.Handle, JniHandleOwnership.DoNotTransfer, targetType); + var proxy = typeMap.GetProxyForJavaObject (reference.Handle, targetType); + var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); if (peer is not null) { var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; if (Android.Runtime.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 69ed961dc0b..7b1e551cb17 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -118,7 +118,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return true; } - JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) + internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { return null; @@ -145,7 +145,29 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) JniObjectReference.Dispose (ref jniClass); } - return null; + if (targetType is null) { + return null; + } + + var proxy = GetProxyForManagedType (targetType); + // Verify the Java object is actually assignable to the target Java type + // before creating the peer. Without this, we'd create invalid peers + // (e.g., IAppendableInvoker wrapping a java.lang.Integer). + if (proxy is null || !TryGetJniNameForManagedType (targetType, out var targetJniName)) { + return null; + } + + var selfRef = new JniObjectReference (handle); + var objClass = default (JniObjectReference); + var targetClass = default (JniObjectReference); + try { + objClass = JniEnvironment.Types.GetObjectClass (selfRef); + targetClass = JniEnvironment.Types.FindClass (targetJniName); + return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null; + } finally { + JniObjectReference.Dispose (ref objClass); + JniObjectReference.Dispose (ref targetClass); + } } JavaPeerProxy? GetProxyForJavaType (string className) @@ -163,34 +185,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return GetProxyForManagedType (managedType); } - internal IJavaPeerable? CreatePeer (IntPtr handle, JniHandleOwnership transfer, Type? targetType = null) - { - var proxy = GetProxyForJavaObject (handle, targetType); - if (proxy is null && targetType is not null) { - proxy = GetProxyForManagedType (targetType); - // Verify the Java object is actually assignable to the target Java type - // before creating the peer. Without this, we'd create invalid peers - // (e.g., IAppendableInvoker wrapping a java.lang.Integer). - if (proxy is not null && TryGetJniNameForManagedType (targetType, out var targetJniName)) { - var selfRef = new JniObjectReference (handle); - var objClass = default (JniObjectReference); - var targetClass = default (JniObjectReference); - try { - objClass = JniEnvironment.Types.GetObjectClass (selfRef); - targetClass = JniEnvironment.Types.FindClass (targetJniName); - if (!JniEnvironment.Types.IsAssignableFrom (objClass, targetClass)) { - proxy = null; - } - } finally { - JniObjectReference.Dispose (ref objClass); - JniObjectReference.Dispose (ref targetClass); - } - } - } - - return proxy?.CreateInstance (handle, transfer); - } - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; /// From f6267000b019176d2fcb6c8477841371faaa2cb5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 15:07:35 +0200 Subject: [PATCH 16/21] Refactor Java proxy resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 105 +++++++++--------- 1 file changed, 51 insertions(+), 54 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 7b1e551cb17..45e9e411ca5 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -58,9 +58,6 @@ internal static void Initialize () } } - /// - /// Registers the mono.android.Runtime.registerNatives JNI native method. - /// unsafe void RegisterNatives () { using var runtimeClass = new JniType ("mono/android/Runtime"u8); @@ -87,16 +84,18 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] return true; } - /// - /// Finds the proxy for a managed type using the generated proxy type map. - /// Results are cached per type. - /// JavaPeerProxy? GetProxyForManagedType (Type managedType) { var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type) ?? s_noPeerSentinel, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } + JavaPeerProxy? GetProxyForJavaType (string className) + { + var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); + return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; + } + JavaPeerProxy? ResolveProxyForManagedType (Type managedType) { if (!_proxyTypeMap.TryGetValue (managedType, out var proxyType)) { @@ -108,14 +107,8 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) { - var proxy = GetProxyForManagedType (managedType); - if (proxy is null) { - jniName = null; - return false; - } - - jniName = proxy.JniName; - return true; + jniName = GetProxyForManagedType (managedType)?.JniName; + return jniName is not null; } internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) @@ -124,56 +117,60 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - var selfRef = new JniObjectReference (handle); - var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + return TryGetProxyFromHierarchy () ?? TryGetProxyFromTargetType (); - try { - while (jniClass.IsValid) { - var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); - if (className != null) { - var proxy = GetProxyForJavaType (className); - if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { - return proxy; + JavaPeerProxy? TryGetProxyFromHierarchy () + { + var selfRef = new JniObjectReference (handle); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + + try { + while (jniClass.IsValid) { + var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + if (className != null) { + var proxy = GetProxyForJavaType (className); + if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { + return proxy; + } } - } - var super = JniEnvironment.Types.GetSuperclass (jniClass); + var super = JniEnvironment.Types.GetSuperclass (jniClass); + JniObjectReference.Dispose (ref jniClass); + jniClass = super; + } + } finally { JniObjectReference.Dispose (ref jniClass); - jniClass = super; } - } finally { - JniObjectReference.Dispose (ref jniClass); - } - if (targetType is null) { return null; } - var proxy = GetProxyForManagedType (targetType); - // Verify the Java object is actually assignable to the target Java type - // before creating the peer. Without this, we'd create invalid peers - // (e.g., IAppendableInvoker wrapping a java.lang.Integer). - if (proxy is null || !TryGetJniNameForManagedType (targetType, out var targetJniName)) { - return null; - } + JavaPeerProxy? TryGetProxyFromTargetType () + { + if (targetType is null) { + return null; + } - var selfRef = new JniObjectReference (handle); - var objClass = default (JniObjectReference); - var targetClass = default (JniObjectReference); - try { - objClass = JniEnvironment.Types.GetObjectClass (selfRef); - targetClass = JniEnvironment.Types.FindClass (targetJniName); - return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null; - } finally { - JniObjectReference.Dispose (ref objClass); - JniObjectReference.Dispose (ref targetClass); - } - } + var proxy = GetProxyForManagedType (targetType); + // Verify the Java object is actually assignable to the target Java type + // before returning the fallback proxy. Without this, we'd create invalid peers + // (e.g., IAppendableInvoker wrapping a java.lang.Integer). + if (proxy is null || !TryGetJniNameForManagedType (targetType, out var targetJniName)) { + return null; + } - JavaPeerProxy? GetProxyForJavaType (string className) - { - var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); - return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; + var selfRef = new JniObjectReference (handle); + var objClass = default (JniObjectReference); + var targetClass = default (JniObjectReference); + try { + objClass = JniEnvironment.Types.GetObjectClass (selfRef); + targetClass = JniEnvironment.Types.FindClass (targetJniName); + return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null; + } finally { + JniObjectReference.Dispose (ref objClass); + JniObjectReference.Dispose (ref targetClass); + } + } } JavaPeerProxy? ResolveProxyForJavaType (string className) From 79ddbfbad1283a4696a2e13fe802776fb147e688 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 15:16:17 +0200 Subject: [PATCH 17/21] Inline typemap cache resolution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 45e9e411ca5..1fdc3e78327 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -86,23 +86,26 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] JavaPeerProxy? GetProxyForManagedType (Type managedType) { - var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => self.ResolveProxyForManagedType (type) ?? s_noPeerSentinel, this); - return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; - } + var proxy = _proxyCache.GetOrAdd (managedType, static (type, self) => { + if (!self._proxyTypeMap.TryGetValue (type, out var proxyType)) { + return s_noPeerSentinel; + } - JavaPeerProxy? GetProxyForJavaType (string className) - { - var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => self.ResolveProxyForJavaType (name) ?? s_noPeerSentinel, this); + return proxyType.GetCustomAttribute (inherit: false) ?? s_noPeerSentinel; + }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } - JavaPeerProxy? ResolveProxyForManagedType (Type managedType) + JavaPeerProxy? GetProxyForJavaType (string className) { - if (!_proxyTypeMap.TryGetValue (managedType, out var proxyType)) { - return null; - } + var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => { + if (!self.TryGetTargetType (name, out var managedType)) { + return s_noPeerSentinel; + } - return proxyType.GetCustomAttribute (inherit: false); + return self.GetProxyForManagedType (managedType) ?? s_noPeerSentinel; + }, this); + return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; } internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true)] out string? jniName) @@ -117,9 +120,10 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - return TryGetProxyFromHierarchy () ?? TryGetProxyFromTargetType (); + return TryGetProxyFromHierarchy (this, handle, targetType) ?? + (targetType is null ? null : TryGetProxyFromTargetType (this, handle, targetType)); - JavaPeerProxy? TryGetProxyFromHierarchy () + static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType) { var selfRef = new JniObjectReference (handle); var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); @@ -128,7 +132,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) while (jniClass.IsValid) { var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); if (className != null) { - var proxy = GetProxyForJavaType (className); + var proxy = self.GetProxyForJavaType (className); if (proxy != null && (targetType is null || targetType.IsAssignableFrom (proxy.TargetType))) { return proxy; } @@ -145,17 +149,13 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - JavaPeerProxy? TryGetProxyFromTargetType () + static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type targetType) { - if (targetType is null) { - return null; - } - - var proxy = GetProxyForManagedType (targetType); + var proxy = self.GetProxyForManagedType (targetType); // Verify the Java object is actually assignable to the target Java type // before returning the fallback proxy. Without this, we'd create invalid peers // (e.g., IAppendableInvoker wrapping a java.lang.Integer). - if (proxy is null || !TryGetJniNameForManagedType (targetType, out var targetJniName)) { + if (proxy is null || !self.TryGetJniNameForManagedType (targetType, out var targetJniName)) { return null; } @@ -173,15 +173,6 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } } - JavaPeerProxy? ResolveProxyForJavaType (string className) - { - if (!TryGetTargetType (className, out var managedType)) { - return null; - } - - return GetProxyForManagedType (managedType); - } - const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; /// From cb32b9149332a422ccf400b58f2f4f05f9e54c8b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 15:23:00 +0200 Subject: [PATCH 18/21] Tidy typemap helper flow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 1fdc3e78327..324fe3f68fe 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -29,7 +29,6 @@ class TrimmableTypeMap readonly IReadOnlyDictionary _typeMap; readonly IReadOnlyDictionary _proxyTypeMap; - // ConcurrentDictionary doesn't accept null values, so misses are cached with s_noPeerSentinel. readonly ConcurrentDictionary _proxyCache = new (); readonly ConcurrentDictionary _peerProxyCache = new (StringComparer.Ordinal); @@ -121,7 +120,7 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) } return TryGetProxyFromHierarchy (this, handle, targetType) ?? - (targetType is null ? null : TryGetProxyFromTargetType (this, handle, targetType)); + TryGetProxyFromTargetType (this, handle, targetType); static JavaPeerProxy? TryGetProxyFromHierarchy (TrimmableTypeMap self, IntPtr handle, Type? targetType) { @@ -149,8 +148,12 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } - static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type targetType) + static JavaPeerProxy? TryGetProxyFromTargetType (TrimmableTypeMap self, IntPtr handle, Type? targetType) { + if (targetType is null) { + return null; + } + var proxy = self.GetProxyForManagedType (targetType); // Verify the Java object is actually assignable to the target Java type // before returning the fallback proxy. Without this, we'd create invalid peers From af4a2df082d807330e9beafe297c4b5cb7d0aed3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 15:30:29 +0200 Subject: [PATCH 19/21] Fail fast on typemap aliases Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 324fe3f68fe..4b5a5a531b0 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -74,12 +74,15 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] return false; } - // External typemap entries for ACW-backed types resolve to the generated proxy-bearing - // helper (including alias slots such as "jni/name[1]"). Surface the actual managed peer - // when a JavaPeerProxy attribute is present so activation and virtual dispatch land on - // the user's type instead of the generated helper. var proxy = mappedType.GetCustomAttribute (inherit: false); - type = proxy?.TargetType ?? mappedType; + if (proxy is null) { + // Alias typemap entries (for example "jni/name[1]") are not implemented yet. + // Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788. + throw new NotImplementedException ( + $"Trimmable typemap alias handling is not implemented yet for '{jniSimpleReference}'."); + } + + type = proxy.TargetType; return true; } From 6df784cba06d965b829ad9f276b1316d6c8eb277 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 16:40:50 +0200 Subject: [PATCH 20/21] Fix Runtime helper qualification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index d2166be6388..2278e272461 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -520,7 +520,7 @@ void ProcessContext (HandleContext* context) var peer = proxy?.CreateInstance (reference.Handle, JniHandleOwnership.DoNotTransfer); if (peer is not null) { var peerState = peer.JniManagedPeerState | JniManagedPeerStates.Replaceable; - if (Android.Runtime.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { + if (global::Java.Interop.Runtime.IsGCUserPeer (peer.PeerReference.Handle)) { peerState |= JniManagedPeerStates.Activatable; } peer.SetJniManagedPeerState (peerState); From b9f3b1bd3f02c6b7967e6272be944fef27800a8f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 10 Apr 2026 16:53:32 +0200 Subject: [PATCH 21/21] Simplify Java proxy target lookup Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 4b5a5a531b0..6c5b51fef0d 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -69,21 +69,8 @@ unsafe void RegisterNatives () internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) { - if (!_typeMap.TryGetValue (jniSimpleReference, out var mappedType)) { - type = null; - return false; - } - - var proxy = mappedType.GetCustomAttribute (inherit: false); - if (proxy is null) { - // Alias typemap entries (for example "jni/name[1]") are not implemented yet. - // Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788. - throw new NotImplementedException ( - $"Trimmable typemap alias handling is not implemented yet for '{jniSimpleReference}'."); - } - - type = proxy.TargetType; - return true; + type = GetProxyForJavaType (jniSimpleReference)?.TargetType; + return type is not null; } JavaPeerProxy? GetProxyForManagedType (Type managedType) @@ -101,11 +88,19 @@ internal bool TryGetTargetType (string jniSimpleReference, [NotNullWhen (true)] JavaPeerProxy? GetProxyForJavaType (string className) { var proxy = _peerProxyCache.GetOrAdd (className, static (name, self) => { - if (!self.TryGetTargetType (name, out var managedType)) { + if (!self._typeMap.TryGetValue (name, out var mappedType)) { return s_noPeerSentinel; } - return self.GetProxyForManagedType (managedType) ?? s_noPeerSentinel; + var proxy = mappedType.GetCustomAttribute (inherit: false); + if (proxy is null) { + // Alias typemap entries (for example "jni/name[1]") are not implemented yet. + // Support for them will be added in a follow-up for https://github.com/dotnet/android/issues/10788. + throw new NotImplementedException ( + $"Trimmable typemap alias handling is not implemented yet for '{name}'."); + } + + return proxy; }, this); return ReferenceEquals (proxy, s_noPeerSentinel) ? null : proxy; }