From 4ead1b157a849407acc8e77971a148a18c3f07c2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:33:55 +0100 Subject: [PATCH 01/25] [TrimmableTypeMap] Add runtime base types for generated proxies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - JavaPeerProxy / JavaPeerProxy — AOT-safe proxy attribute base - IAndroidCallableWrapper — RegisterNatives(JniType) for ACW types - JavaPeerContainerFactory — AOT-safe array/list/collection/dict - TypeMapException — error reporting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/IAndroidCallableWrapper.cs | 21 +++++ .../Java.Interop/JavaPeerContainerFactory.cs | 84 +++++++++++++++++++ .../Java.Interop/JavaPeerProxy.cs | 60 +++++++++++++ .../Java.Interop/TypeMapException.cs | 15 ++++ 4 files changed, 180 insertions(+) create mode 100644 src/Mono.Android/Java.Interop/IAndroidCallableWrapper.cs create mode 100644 src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs create mode 100644 src/Mono.Android/Java.Interop/JavaPeerProxy.cs create mode 100644 src/Mono.Android/Java.Interop/TypeMapException.cs diff --git a/src/Mono.Android/Java.Interop/IAndroidCallableWrapper.cs b/src/Mono.Android/Java.Interop/IAndroidCallableWrapper.cs new file mode 100644 index 00000000000..9eb350fdef2 --- /dev/null +++ b/src/Mono.Android/Java.Interop/IAndroidCallableWrapper.cs @@ -0,0 +1,21 @@ +#nullable enable + +using System; + +namespace Java.Interop +{ + /// + /// Interface for proxy types that represent Android Callable Wrappers (ACW). + /// ACW types are .NET types that have a corresponding generated Java class + /// which calls back into .NET via JNI native methods. + /// + public interface IAndroidCallableWrapper + { + /// + /// Registers JNI native methods for this ACW type. + /// Called when the Java class is first loaded and needs its native methods bound. + /// + /// The JNI type for the Java class. + void RegisterNatives (JniType nativeClass); + } +} diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs new file mode 100644 index 00000000000..01c0bf2c062 --- /dev/null +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -0,0 +1,84 @@ +#nullable enable + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Java.Interop +{ + /// + /// AOT-safe factory for creating typed containers (arrays, lists, collections, dictionaries) + /// without using MakeGenericType() or Array.CreateInstance(). + /// + public abstract class JavaPeerContainerFactory + { + /// + /// Creates a typed array. Rank 1 = T[], rank 2 = T[][], rank 3 = T[][][]. + /// + internal abstract Array CreateArray (int length, int rank); + + /// + /// Creates a typed JavaList<T> from a JNI handle. + /// + internal abstract IList CreateList (IntPtr handle, JniHandleOwnership transfer); + + /// + /// Creates a typed JavaCollection<T> from a JNI handle. + /// + internal abstract ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer); + + /// + /// Creates a typed JavaDictionary<TKey, TValue> using the visitor pattern. + /// This factory provides the value type; provides the key type. + /// + internal virtual IDictionary? CreateDictionary (JavaPeerContainerFactory keyFactory, IntPtr handle, JniHandleOwnership transfer) + => null; + + /// + /// Visitor callback invoked by the value factory's . + /// Override in to provide both type parameters. + /// + internal virtual IDictionary? CreateDictionaryWithValueFactory ( + JavaPeerContainerFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) + where TValue : class, IJavaPeerable + => null; + + /// + /// Creates a singleton for the specified type. + /// + public static JavaPeerContainerFactory Create () where T : class, IJavaPeerable + => JavaPeerContainerFactory.Instance; + } + + /// + /// Typed container factory. All creation uses direct new expressions — fully AOT-safe. + /// + /// The Java peer element type. + public sealed class JavaPeerContainerFactory : JavaPeerContainerFactory + where T : class, IJavaPeerable + { + internal static readonly JavaPeerContainerFactory Instance = new (); + + JavaPeerContainerFactory () { } + + internal override Array CreateArray (int length, int rank) => rank switch { + 1 => new T [length], + 2 => new T [length][], + 3 => new T [length][][], + _ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Array rank must be 1, 2, or 3."), + }; + + internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) + => new Android.Runtime.JavaList (handle, transfer); + + internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnership transfer) + => new Android.Runtime.JavaCollection (handle, transfer); + + internal override IDictionary? CreateDictionary (JavaPeerContainerFactory keyFactory, IntPtr handle, JniHandleOwnership transfer) + => keyFactory.CreateDictionaryWithValueFactory (this, handle, transfer); + + internal override IDictionary? CreateDictionaryWithValueFactory ( + JavaPeerContainerFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) + => new Android.Runtime.JavaDictionary (handle, transfer); + } +} diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs new file mode 100644 index 00000000000..7bfd490052d --- /dev/null +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -0,0 +1,60 @@ +#nullable enable + +using System; + +namespace Java.Interop +{ + /// + /// Base attribute class for generated proxy types that enable AOT-safe type mapping + /// between Java and .NET types. + /// + /// + /// Proxy attributes are generated at build time and applied to the proxy type itself + /// (self-application pattern). The .NET runtime's GetCustomAttribute<JavaPeerProxy>() + /// instantiates the proxy in an AOT-safe manner — no Activator.CreateInstance() needed. + /// + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] + public abstract class JavaPeerProxy : Attribute + { + /// + /// Creates an instance of the target type using the JNI handle and ownership semantics. + /// This replaces the reflection-based constructor invocation used in the legacy path. + /// + /// The JNI object reference handle. + /// How to handle JNI reference ownership. + /// A new instance of the target type wrapping the JNI handle, or null if activation is not supported. + public abstract IJavaPeerable? CreateInstance (IntPtr handle, JniHandleOwnership transfer); + + /// + /// Gets the target .NET type that this proxy represents. + /// + public abstract Type TargetType { get; } + + /// + /// Gets the invoker type for interfaces and abstract classes. + /// Returns null for concrete types that can be directly instantiated. + /// + public virtual Type? InvokerType => null; + + /// + /// Gets a factory for creating containers (arrays, collections) of the target type. + /// Enables AOT-safe creation of generic collections without MakeGenericType(). + /// + /// A factory for creating containers of the target type, or null if not supported. + public virtual JavaPeerContainerFactory? GetContainerFactory () => null; + } + + /// + /// Generic base for generated proxy types. Provides + /// and automatically from the type parameter. + /// + /// The target .NET peer type this proxy represents. + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] + public abstract class JavaPeerProxy : JavaPeerProxy where T : class, IJavaPeerable + { + public override Type TargetType => typeof (T); + + public override JavaPeerContainerFactory GetContainerFactory () + => JavaPeerContainerFactory.Instance; + } +} diff --git a/src/Mono.Android/Java.Interop/TypeMapException.cs b/src/Mono.Android/Java.Interop/TypeMapException.cs new file mode 100644 index 00000000000..0dae0be6e47 --- /dev/null +++ b/src/Mono.Android/Java.Interop/TypeMapException.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System; + +namespace Java.Interop +{ + /// + /// Exception thrown when a type mapping operation fails at runtime. + /// + public class TypeMapException : Exception + { + public TypeMapException (string message) : base (message) { } + public TypeMapException (string message, Exception innerException) : base (message, innerException) { } + } +} From 70c7d671032dd72127c23fc9fe55ec29bcb8b7a8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:34:08 +0100 Subject: [PATCH 02/25] [TrimmableTypeMap] Emit JavaPeerProxy as generic base class Proxy types now extend JavaPeerProxy instead of JavaPeerProxy. TargetType and GetContainerFactory() are inherited from the generic base. Generator references TrimmableTypeMap for ActivateInstance and RegisterMethod. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyEmitter.cs | 43 ++++++++++--------- .../TypeMapAssemblyGeneratorTests.cs | 1 - .../Generator/TypeMapModelBuilderTests.cs | 1 - 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 775f48af607..976bebf88a7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -20,7 +20,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// [assembly: TypeMapAssociation(typeof(MyTextView), typeof(Android_Widget_TextView_Proxy))] // alias /// /// // One proxy type per Java peer that needs activation or UCO wrappers: -/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only +/// public sealed class Activity_Proxy : JavaPeerProxy, IAndroidCallableWrapper // IAndroidCallableWrapper for ACWs only /// { /// public Activity_Proxy() : base() { } /// @@ -33,7 +33,7 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// // or: null; // no activation /// // or: throw new NotSupportedException(...); // open generic /// -/// public override Type TargetType => typeof(Activity); +/// // TargetType and GetContainerFactory() are inherited from JavaPeerProxy /// public Type InvokerType => typeof(IOnClickListenerInvoker); // interfaces only /// /// // UCO wrappers — [UnmanagedCallersOnly] entry points for JNI native methods (ACWs only): @@ -43,13 +43,13 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap; /// /// [UnmanagedCallersOnly] /// public static void nctor_0_uco(IntPtr jnienv, IntPtr self) -/// => TrimmableNativeRegistration.ActivateInstance(self, typeof(Activity)); +/// => TrimmableTypeMap.ActivateInstance(self, typeof(Activity)); /// /// // Registers JNI native methods (ACWs only): /// public void RegisterNatives(JniType jniType) /// { -/// TrimmableNativeRegistration.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); -/// TrimmableNativeRegistration.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); +/// TrimmableTypeMap.RegisterMethod(jniType, "n_OnCreate", "(Landroid/os/Bundle;)V", &n_OnCreate_uco_0); +/// TrimmableTypeMap.RegisterMethod(jniType, "nctor_0", "()V", &nctor_0_uco); /// } /// } /// @@ -75,11 +75,10 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _systemTypeRef; TypeReferenceHandle _runtimeTypeHandleRef; TypeReferenceHandle _jniTypeRef; - TypeReferenceHandle _trimmableNativeRegistrationRef; + TypeReferenceHandle _trimmableTypeMapRef; TypeReferenceHandle _notSupportedExceptionRef; TypeReferenceHandle _runtimeHelpersRef; - MemberReferenceHandle _baseCtorRef; MemberReferenceHandle _getTypeFromHandleRef; MemberReferenceHandle _getUninitializedObjectRef; MemberReferenceHandle _notSupportedExceptionCtorRef; @@ -188,8 +187,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("System"), metadata.GetOrAddString ("RuntimeTypeHandle")); _jniTypeRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniType")); - _trimmableNativeRegistrationRef = metadata.AddTypeReference (_pe.MonoAndroidRef, - metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("TrimmableNativeRegistration")); + _trimmableTypeMapRef = metadata.AddTypeReference (_pe.MonoAndroidRef, + metadata.GetOrAddString ("Microsoft.Android.Runtime"), metadata.GetOrAddString ("TrimmableTypeMap")); _notSupportedExceptionRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, metadata.GetOrAddString ("System"), metadata.GetOrAddString ("NotSupportedException")); _runtimeHelpersRef = metadata.AddTypeReference (_pe.SystemRuntimeRef, @@ -198,9 +197,6 @@ void EmitTypeReferences () void EmitMemberReferences () { - _baseCtorRef = _pe.AddMemberRef (_javaPeerProxyRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); - _getTypeFromHandleRef = _pe.AddMemberRef (_systemTypeRef, "GetTypeFromHandle", sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().Type (_systemTypeRef, false), @@ -232,7 +228,7 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - _activateInstanceRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "ActivateInstance", + _activateInstanceRef = _pe.AddMemberRef (_trimmableTypeMapRef, "ActivateInstance", sig => sig.MethodSignature ().Parameters (2, rt => rt.Void (), p => { @@ -240,7 +236,7 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_systemTypeRef, false); })); - _registerMethodRef = _pe.AddMemberRef (_trimmableNativeRegistrationRef, "RegisterMethod", + _registerMethodRef = _pe.AddMemberRef (_trimmableTypeMapRef, "RegisterMethod", sig => sig.MethodSignature ().Parameters (4, rt => rt.Void (), p => { @@ -315,11 +311,16 @@ void EmitTypeMapAssociationAttributeCtorRef () void EmitProxyType (JavaPeerProxyData proxy, Dictionary wrapperHandles) { var metadata = _pe.Metadata; + + // Create JavaPeerProxy as the base class + var targetTypeRef = _pe.ResolveTypeRef (proxy.TargetType); + var genericBaseSpec = _pe.MakeGenericTypeSpec (_javaPeerProxyRef, targetTypeRef); + var typeDefHandle = metadata.AddTypeDefinition ( TypeAttributes.Public | TypeAttributes.Sealed | TypeAttributes.Class, metadata.GetOrAddString (proxy.Namespace), metadata.GetOrAddString (proxy.TypeName), - _javaPeerProxyRef, + genericBaseSpec, MetadataTokens.FieldDefinitionHandle (metadata.GetRowCount (TableIndex.Field) + 1), MetadataTokens.MethodDefinitionHandle (metadata.GetRowCount (TableIndex.MethodDef) + 1)); @@ -327,24 +328,24 @@ void EmitProxyType (JavaPeerProxyData proxy, Dictionary..ctor() + var genericBaseCtorRef = _pe.AddMemberRef (genericBaseSpec, ".ctor", + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { })); _pe.EmitBody (".ctor", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName, sig => sig.MethodSignature (isInstanceMethod: true).Parameters (0, rt => rt.Void (), p => { }), encoder => { encoder.OpCode (ILOpCode.Ldarg_0); - encoder.Call (_baseCtorRef); + encoder.Call (genericBaseCtorRef); encoder.OpCode (ILOpCode.Ret); }); // CreateInstance EmitCreateInstance (proxy); - // get_TargetType - EmitTypeGetter ("get_TargetType", proxy.TargetType, - MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.SpecialName | MethodAttributes.HideBySig); + // get_TargetType and GetContainerFactory() are inherited from JavaPeerProxy - // get_InvokerType + // get_InvokerType — only for interfaces/abstract types if (proxy.InvokerType != null) { EmitTypeGetter ("get_InvokerType", proxy.InvokerType, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 5fb770fd3a5..cb917e0fdde 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -82,7 +82,6 @@ public void Generate_ProxyType_HasCtorAndCreateInstance () Assert.Contains (".ctor", methods); Assert.Contains ("CreateInstance", methods); - Assert.Contains ("get_TargetType", methods); } [Fact] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 7a91bc0a029..54ec3dec323 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -561,7 +561,6 @@ public void FullPipeline_CustomView_HasConstructorAndMethodWrappers () Assert.Contains (".ctor", methodNames); Assert.Contains ("CreateInstance", methodNames); - Assert.Contains ("get_TargetType", methodNames); }); } From 1644e27549fa3138cb327df0772fe9d92ffd1ae8 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:38:28 +0100 Subject: [PATCH 03/25] [TrimmableTypeMap] Add TrimmableTypeMap, TypeManager, and wire managers Add TrimmableTypeMap class with core typemap functionality: TryGetType, TryCreatePeer, GetInvokerType, GetContainerFactory, ActivateInstance. Add TrimmableTypeMapTypeManager delegating to TrimmableTypeMap. Rename ManagedValueManager to JavaMarshalValueManager. Add proxy-based peer creation in TryConstructPeer via TrimmableTypeMap.TryCreatePeer. Add RuntimeFeature.TrimmableTypeMap feature switch with ILLink substitutions. Wire into JNIEnvInit (CoreCLR) and JavaInteropRuntime + JreRuntime (NativeAOT). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 18 +++- .../Java.Interop/JreRuntime.cs | 13 ++- .../Android.Runtime/JNIEnvInit.cs | 10 ++- .../ILLink/ILLink.Substitutions.xml | 2 + ...eManager.cs => JavaMarshalValueManager.cs} | 23 +++-- .../RuntimeFeature.cs | 5 ++ .../TrimmableTypeMap.cs | 86 +++++++++++++++++++ .../TrimmableTypeMapTypeManager.cs | 71 +++++++++++++++ src/Mono.Android/Mono.Android.csproj | 8 +- 9 files changed, 222 insertions(+), 14 deletions(-) rename src/Mono.Android/Microsoft.Android.Runtime/{ManagedValueManager.cs => JavaMarshalValueManager.cs} (95%) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 02a37562d5f..3b0beabfebb 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -61,11 +61,15 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua var settings = new DiagnosticSettings (); settings.AddDebugDotnetLog (); + var (typeManager, trimmableTypeMap) = CreateTypeManager (); + var jmvm = JavaMarshalValueManager.GetOrCreateInstance (); + jmvm.TypeMap = trimmableTypeMap; + var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global), - TypeManager = new ManagedTypeManager (), - ValueManager = ManagedValueManager.GetOrCreateInstance (), + TypeManager = typeManager, + ValueManager = jmvm, UseMarshalMemberBuilder = false, JniGlobalReferenceLogWriter = settings.GrefLog, JniLocalReferenceLogWriter = settings.LrefLog, @@ -86,4 +90,14 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua } transition.Dispose (); } + + static (JniRuntime.JniTypeManager, TrimmableTypeMap?) CreateTypeManager () + { + if (RuntimeFeature.TrimmableTypeMap) { + var map = new TrimmableTypeMap (); + return (new TrimmableTypeMapTypeManager (map), map); + } + + return (new ManagedTypeManager (), null); + } } diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index 4e70da25a0e..b96e42729f9 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs @@ -58,10 +58,10 @@ static NativeAotRuntimeOptions CreateJreVM (NativeAotRuntimeOptions builder) throw new InvalidOperationException ($"Member `{nameof (NativeAotRuntimeOptions)}.{nameof (NativeAotRuntimeOptions.JvmLibraryPath)}` must be set."); #if NET - builder.TypeManager ??= new ManagedTypeManager (); + builder.TypeManager ??= CreateDefaultTypeManager (); #endif // NET - builder.ValueManager ??= ManagedValueManager.GetOrCreateInstance(); + builder.ValueManager ??= JavaMarshalValueManager.GetOrCreateInstance(); 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 (new TrimmableTypeMap ()); + } + + 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 ce819db74cc..23510b80807 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -131,7 +131,11 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) BoundExceptionType = (BoundExceptionType)args->ioExceptionType; JniRuntime.JniTypeManager typeManager; JniRuntime.JniValueManager valueManager; - if (RuntimeFeature.ManagedTypeMap) { + TrimmableTypeMap? trimmableTypeMap = null; + if (RuntimeFeature.TrimmableTypeMap) { + trimmableTypeMap = new TrimmableTypeMap (); + typeManager = new TrimmableTypeMapTypeManager (trimmableTypeMap); + } else if (RuntimeFeature.ManagedTypeMap) { typeManager = new ManagedTypeManager (); } else { typeManager = new AndroidTypeManager (args->jniAddNativeMethodRegistrationAttributePresent != 0); @@ -139,7 +143,9 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) if (RuntimeFeature.IsMonoRuntime) { valueManager = new AndroidValueManager (); } else if (RuntimeFeature.IsCoreClrRuntime) { - valueManager = ManagedValueManager.GetOrCreateInstance (); + var jmvm = JavaMarshalValueManager.GetOrCreateInstance (); + jmvm.TypeMap = trimmableTypeMap; + valueManager = jmvm; } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } diff --git a/src/Mono.Android/ILLink/ILLink.Substitutions.xml b/src/Mono.Android/ILLink/ILLink.Substitutions.xml index 239252fe937..ac311814470 100644 --- a/src/Mono.Android/ILLink/ILLink.Substitutions.xml +++ b/src/Mono.Android/ILLink/ILLink.Substitutions.xml @@ -7,6 +7,8 @@ + + diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs similarity index 95% rename from src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs rename to src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index e84a518971b..00911a4e3ba 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -18,7 +18,7 @@ namespace Microsoft.Android.Runtime; -class ManagedValueManager : JniRuntime.JniValueManager +class JavaMarshalValueManager : JniRuntime.JniValueManager { const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; @@ -29,12 +29,12 @@ class ManagedValueManager : JniRuntime.JniValueManager static readonly SemaphoreSlim bridgeProcessingSemaphore = new (1, 1); - static Lazy s_instance = new (() => new ManagedValueManager ()); - public static ManagedValueManager GetOrCreateInstance () => s_instance.Value; + static Lazy s_instance = new (() => new JavaMarshalValueManager ()); + public static JavaMarshalValueManager GetOrCreateInstance () => s_instance.Value; - unsafe ManagedValueManager () + unsafe JavaMarshalValueManager () { - // There can only be one instance of ManagedValueManager because we can call JavaMarshal.Initialize only once. + // There can only be one instance because JavaMarshal.Initialize can only be called once. var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge (&BridgeProcessingStarted, &BridgeProcessingFinished); JavaMarshal.Initialize (mark_cross_references_ftn); } @@ -48,7 +48,7 @@ protected override void Dispose (bool disposing) void ThrowIfDisposed () { if (disposed) - throw new ObjectDisposedException (nameof (ManagedValueManager)); + throw new ObjectDisposedException (nameof (JavaMarshalValueManager)); } public override void WaitForGCBridgeProcessing () @@ -458,7 +458,7 @@ static unsafe void BridgeProcessingFinished (MarkCrossReferencesArgs* mcr) static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) { List handlesToFree = []; - ManagedValueManager instance = GetOrCreateInstance (); + JavaMarshalValueManager instance = GetOrCreateInstance (); for (int i = 0; (nuint)i < mcr->ComponentCount; i++) { StronglyConnectedComponent component = mcr->Components [i]; @@ -496,6 +496,8 @@ void ProcessContext (HandleContext* context) static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; + internal TrimmableTypeMap? TypeMap { get; set; } + protected override bool TryConstructPeer ( IJavaPeerable self, ref JniObjectReference reference, @@ -503,6 +505,13 @@ protected override bool TryConstructPeer ( [DynamicallyAccessedMembers (Constructors)] Type type) { + if (TypeMap != null) { + if (TypeMap.TryCreatePeer (type, reference.Handle, JniHandleOwnership.DoNotTransfer)) { + JniObjectReference.Dispose (ref reference, options); + return true; + } + } + var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); if (c != null) { var args = new object[] { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 24bedf65bed..82ffd03a257 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -10,6 +10,7 @@ static class RuntimeFeature const bool IsCoreClrRuntimeEnabledByDefault = false; const bool IsAssignableFromCheckEnabledByDefault = true; const bool StartupHookSupportEnabledByDefault = true; + const bool TrimmableTypeMapEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -34,4 +35,8 @@ static class RuntimeFeature [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] internal static bool StartupHookSupport { get; } = AppContext.TryGetSwitch (StartupHookProviderSwitch, out bool isEnabled) ? isEnabled : StartupHookSupportEnabledByDefault; + + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (TrimmableTypeMap)}")] + internal static bool TrimmableTypeMap { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (TrimmableTypeMap)}", out bool isEnabled) ? isEnabled : TrimmableTypeMapEnabledByDefault; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs new file mode 100644 index 00000000000..fb148102b73 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -0,0 +1,86 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using Android.Runtime; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +/// +/// Central type map for the trimmable typemap path. Owns the TypeMapping dictionary +/// and provides peer creation, invoker resolution, container factories, and native +/// method registration. All proxy attribute access is encapsulated here. +/// +class TrimmableTypeMap +{ + static TrimmableTypeMap? s_instance; + + internal static TrimmableTypeMap? Instance => s_instance; + + readonly IReadOnlyDictionary _typeMap; + + internal TrimmableTypeMap () + { + _typeMap = TypeMapping.GetOrCreateExternalTypeMapping (); + + var previous = Interlocked.CompareExchange (ref s_instance, this, null); + Debug.Assert (previous is null, "TrimmableTypeMap must only be created once."); + } + + internal bool TryGetType (string jniSimpleReference, out Type type) + => _typeMap.TryGetValue (jniSimpleReference, out type); + + /// + /// Creates a peer instance using the proxy's CreateInstance method. + /// + internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transfer) + { + var proxy = type.GetCustomAttribute (inherit: false); + if (proxy is null) { + return false; + } + + return proxy.CreateInstance (handle, transfer) != null; + } + + /// + /// Gets the invoker type for an interface or abstract class from the proxy attribute. + /// + internal Type? GetInvokerType (Type type) + { + var proxy = type.GetCustomAttribute (inherit: false); + return proxy?.InvokerType; + } + + /// + /// Gets the container factory for a type from its proxy attribute. + /// Used for AOT-safe array/collection/dictionary creation. + /// + internal JavaPeerContainerFactory? GetContainerFactory (Type type) + { + var proxy = type.GetCustomAttribute (inherit: false); + return proxy?.GetContainerFactory (); + } + + /// + /// Creates a managed peer instance for a Java object being constructed. + /// Called from generated UCO constructor wrappers (nctor_*_uco). + /// + internal static void ActivateInstance (IntPtr self, Type targetType) + { + var instance = s_instance; + if (instance is null) { + throw new InvalidOperationException ("TrimmableTypeMap has not been initialized."); + } + + if (!instance.TryCreatePeer (targetType, self, JniHandleOwnership.DoNotTransfer)) { + throw new TypeMapException ( + $"Failed to create peer for type '{targetType.FullName}'. " + + "Ensure the type has a generated proxy in the TypeMap assembly."); + } + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs new file mode 100644 index 00000000000..4bc1a781964 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -0,0 +1,71 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +/// +/// Type manager for the trimmable typemap path. Delegates type lookups +/// to . +/// +class TrimmableTypeMapTypeManager : JniRuntime.JniTypeManager +{ + readonly TrimmableTypeMap _map; + + internal TrimmableTypeMapTypeManager (TrimmableTypeMap map) + { + _map = map; + } + + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) + { + foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) { + yield return t; + } + + if (_map.TryGetType (jniSimpleReference, out var type)) { + yield return type; + } + } + + protected override IEnumerable GetSimpleReferences (Type type) + { + foreach (var r in base.GetSimpleReferences (type)) { + yield return r; + } + + var attr = type.GetCustomAttribute (inherit: false); + if (attr != null && !attr.Name.IsNullOrEmpty ()) { + yield return attr.Name.Replace ('.', '/'); + } + } + + [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + protected override Type? GetInvokerTypeCore ( + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type) + { + var invokerType = _map.GetInvokerType (type); + if (invokerType != null) { + return invokerType; + } + + return base.GetInvokerTypeCore (type); + } + + public override void RegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | 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."); + } +} diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 116b1a7fc24..d7da346bea2 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -307,16 +307,20 @@ + + + + @@ -353,9 +357,11 @@ - + + + From cf8b6f960e83e5027d608bb4a75d4d29e34bedd5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:39:00 +0100 Subject: [PATCH 04/25] [TrimmableTypeMap] Add RegisterNatives bootstrap Add registerNatives(Class) native method to mono.android.Runtime.java so JCW static initializer blocks can trigger native method registration. Add to TrimmableTypeMap: - RegisterBootstrapNativeMethod() registers the JNI callback during init - OnRegisterNatives() resolves the proxy and calls IAndroidCallableWrapper.RegisterNatives(JniType) to bind UCO ptrs - RegisterMethod() helper for per-method registration (TODO: batch) Wire RegisterBootstrapNativeMethod() call in JNIEnvInit after runtime creation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 4 ++ .../TrimmableTypeMap.cs | 68 +++++++++++++++++++ .../java/mono/android/Runtime.java | 1 + 3 files changed, 73 insertions(+) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 23510b80807..29d91ff6ee3 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -158,6 +158,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); + if (trimmableTypeMap != null) { + trimmableTypeMap.RegisterBootstrapNativeMethod (); + } + grefIGCUserPeer_class = args->grefIGCUserPeer; grefGCUserPeerable_class = args->grefGCUserPeerable; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index fb148102b73..da424219592 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; using Android.Runtime; using Java.Interop; @@ -83,4 +84,71 @@ internal static void ActivateInstance (IntPtr self, Type targetType) "Ensure the type has a generated proxy in the TypeMap assembly."); } } + + // TODO: The generator currently emits per-method RegisterMethod() calls. + // This should be changed to emit a single JNI RegisterNatives call with + // all methods at once, eliminating this helper. Follow-up generator change. + + /// + /// Registers a single JNI native method. Called from generated + /// implementations. + /// + public static void RegisterMethod (JniType nativeClass, string name, string signature, IntPtr functionPointer) + { + // The java-interop JniNativeMethodRegistration API requires a Delegate, but we have + // a raw function pointer from an [UnmanagedCallersOnly] method. JNI only uses the + // function pointer extracted via Marshal.GetFunctionPointerForDelegate(), so the + // delegate type doesn't matter — Action is used as a lightweight wrapper. + // TODO: Add an IntPtr overload to java-interop's RegisterNatives to avoid this allocation. + var registration = new JniNativeMethodRegistration (name, signature, + Marshal.GetDelegateForFunctionPointer (functionPointer)); + JniEnvironment.Types.RegisterNatives ( + nativeClass.PeerReference, + new [] { registration }, + 1); + } + + /// + /// Registers the mono.android.Runtime.registerNatives JNI native method. + /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. + /// + internal void RegisterBootstrapNativeMethod () + { + using var runtimeClass = new JniType ("mono/android/Runtime"); + JniEnvironment.Types.RegisterNatives ( + runtimeClass.PeerReference, + new [] { new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", + (RegisterNativesHandler) OnRegisterNatives) }, + 1); + } + + delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); + + static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) + { + try { + if (s_instance is null) { + return; + } + + var classRef = new JniObjectReference (nativeClassHandle); + var className = JniEnvironment.Types.GetJniTypeNameFromInstance (classRef); + if (className is null) { + return; + } + + if (!s_instance._typeMap.TryGetValue (className, out var type)) { + return; + } + + var proxy = type.GetCustomAttribute (inherit: false); + if (proxy is IAndroidCallableWrapper acw) { + using var jniType = new JniType (classRef); + acw.RegisterNatives (jniType); + } + } catch (Exception ex) { + Logger.Log (LogLevel.Error, "TrimmableTypeMap", + $"Failed to register natives: {ex}"); + } + } } diff --git a/src/java-runtime/java/mono/android/Runtime.java b/src/java-runtime/java/mono/android/Runtime.java index 8ad03b1a2c5..d1fa431814d 100644 --- a/src/java-runtime/java/mono/android/Runtime.java +++ b/src/java-runtime/java/mono/android/Runtime.java @@ -28,6 +28,7 @@ public static native void initInternal ( boolean haveSplitApks ); public static native void register (String managedType, java.lang.Class nativeClass, String methods); + public static native void registerNatives (java.lang.Class nativeClass); public static native void notifyTimeZoneChanged (); public static native int createNewContext (String[] runtimeApks, String[] assemblies, ClassLoader loader); public static native int createNewContextWithData (String[] runtimeApks, String[] assemblies, byte[][] assembliesBytes, String[] assembliesPaths, ClassLoader loader, boolean forcePreloadAssemblies); From 404c3fd13a631d1f9a76db4dabcec6524d01217e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:39:13 +0100 Subject: [PATCH 05/25] [TrimmableTypeMap] AOT-safe JavaConvert and ArrayCreateInstance When TrimmableTypeMap is available, use JavaPeerContainerFactory from the proxy for IList, ICollection, IDictionary marshaling and array creation instead of MakeGenericType/Array.CreateInstance. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 14 +++--- src/Mono.Android/Java.Interop/JavaConvert.cs | 50 ++++++++++++++++++++ 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index bd1c0ce20fe..c23774bc9c6 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -24,12 +24,14 @@ public static partial class JNIEnv { public static IntPtr Handle => JniEnvironment.EnvironmentPointer; - static Array ArrayCreateInstance (Type elementType, int length) => - // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 - // IL3050 disabled in source: if someone uses NativeAOT, they will get the warning. - #pragma warning disable IL3050 - Array.CreateInstance (elementType, length); - #pragma warning restore IL3050 + static Array ArrayCreateInstance (Type elementType, int length) + { + var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + if (factory != null) + return factory.CreateArray (length, 1); + + return Array.CreateInstance (elementType, length); + } static Type MakeArrayType (Type type) => // FIXME: https://github.com/xamarin/xamarin-android/issues/8724 diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 05fc52adba1..0c0000732b2 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -6,6 +6,7 @@ using System.Reflection; using Android.Runtime; +using Microsoft.Android.Runtime; namespace Java.Interop { @@ -79,6 +80,13 @@ params Type [] typeArguments return converter; if (target.IsArray) return (h, t) => JNIEnv.GetArray (h, t, target.GetElementType ()); + + if (RuntimeFeature.TrimmableTypeMap) { + var factoryConverter = TryGetFactoryBasedConverter (target); + if (factoryConverter != null) + return factoryConverter; + } + if (target.IsGenericType && target.GetGenericTypeDefinition() == typeof (IDictionary<,>)) { Type t = MakeGenericType (typeof (JavaDictionary<,>), target.GetGenericArguments ()); return GetJniHandleConverterForType (t); @@ -101,6 +109,48 @@ params Type [] typeArguments return null; } + /// + /// AOT-safe converter using from the generated proxy. + /// Avoids MakeGenericType() by using the pre-typed factory from the proxy attribute. + /// + static Func? TryGetFactoryBasedConverter (Type target) + { + if (!target.IsGenericType) + return null; + + var genericDef = target.GetGenericTypeDefinition (); + var typeArgs = target.GetGenericArguments (); + + if (genericDef == typeof (IList<>) && typeArgs.Length == 1) { + var factory = TryGetContainerFactory (typeArgs [0]); + if (factory != null) + return (h, t) => factory.CreateList (h, t); + } + + if (genericDef == typeof (ICollection<>) && typeArgs.Length == 1) { + var factory = TryGetContainerFactory (typeArgs [0]); + if (factory != null) + return (h, t) => factory.CreateCollection (h, t); + } + + if (genericDef == typeof (IDictionary<,>) && typeArgs.Length == 2) { + var keyFactory = TryGetContainerFactory (typeArgs [0]); + var valueFactory = TryGetContainerFactory (typeArgs [1]); + if (keyFactory != null && valueFactory != null) + return (h, t) => valueFactory.CreateDictionary (keyFactory, h, t); + } + + return null; + } + + static JavaPeerContainerFactory? TryGetContainerFactory (Type elementType) + { + if (!typeof (IJavaPeerable).IsAssignableFrom (elementType)) + return null; + + return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + } + static Func GetJniHandleConverterForType ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) { MethodInfo m = t.GetMethod ("FromJniHandle", BindingFlags.Static | BindingFlags.Public)!; From 53e3154581f237df559f158fcf9c53b18d05d6f3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 14:50:22 +0100 Subject: [PATCH 06/25] [TrimmableTypeMap] AOT-safe CreateArray with dynamic code fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ranks 1-3: direct new T[], T[][], T[][][] — fully AOT-safe. Rank 4+: when dynamic code is supported (CoreCLR), falls back to MakeArrayType + CreateInstanceFromArrayType. Throws on NativeAOT. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- .../Java.Interop/JavaPeerContainerFactory.cs | 23 +++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index c14ba04c7fa..fb0952fad89 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 +Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index 01c0bf2c062..778e0a720a8 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -3,6 +3,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Runtime.CompilerServices; namespace Java.Interop { @@ -13,7 +14,7 @@ namespace Java.Interop public abstract class JavaPeerContainerFactory { /// - /// Creates a typed array. Rank 1 = T[], rank 2 = T[][], rank 3 = T[][][]. + /// Creates a typed jagged array. Rank 1 = T[], rank 2 = T[][], etc. /// internal abstract Array CreateArray (int length, int rank); @@ -61,13 +62,31 @@ public sealed class JavaPeerContainerFactory : JavaPeerContainerFactory JavaPeerContainerFactory () { } + // TODO: I am afraid this might cause unnecessary code bloat for Native AOT. I think we should revisit + // how we use this API and instead use a differnet approach that uses AOT-safe `Array.CreateInstanceFromArrayType` + // with statically provided array types based on a statically known array type. internal override Array CreateArray (int length, int rank) => rank switch { 1 => new T [length], 2 => new T [length][], 3 => new T [length][][], - _ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Array rank must be 1, 2, or 3."), + _ when rank >= 0 => CreateHigherRankArray (length, rank), + _ => throw new ArgumentOutOfRangeException (nameof (rank), rank, "Rank must be non-negative."), }; + static Array CreateHigherRankArray (int length, int rank) + { + if (!RuntimeFeature.IsDynamicCodeSupported) { + throw new NotSupportedException ($"Cannot create array of rank {rank} because dynamic code is not supported."); + } + + var arrayType = typeof (T); + for (int i = 0; i < rank; i++) { + arrayType = arrayType.MakeArrayType (); + } + + return Array.CreateInstanceFromArrayType (arrayType, length); + } + internal override IList CreateList (IntPtr handle, JniHandleOwnership transfer) => new Android.Runtime.JavaList (handle, transfer); From 69d933882cb4f8c2692b771c4aec5e604063fc3e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 16:47:35 +0100 Subject: [PATCH 07/25] Fix build errors and trimming annotations - Add missing using Android.Runtime for JniHandleOwnership - Add DynamicallyAccessedMembers annotations required by JavaList, JavaCollection, JavaDictionary on factory type parameters - Fix IJniNameProviderAttribute lookup (not an Attribute, use GetCustomAttributes instead of GetCustomAttribute) - Fix JniType constructor (takes string, not JniObjectReference) - Restore #pragma warning disable IL3050 for Array.CreateInstance fallback path - Suppress IL2073 on GetInvokerType (invoker types preserved by MarkJavaObjects trimmer step) Build succeeds locally. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 4 +-- .../Java.Interop/JreRuntime.cs | 2 +- src/Mono.Android/Android.Runtime/JNIEnv.cs | 2 ++ .../Android.Runtime/JNIEnvInit.cs | 4 +-- .../Java.Interop/JavaPeerContainerFactory.cs | 17 +++++++++--- .../Java.Interop/JavaPeerProxy.cs | 9 ++++++- .../Java.Interop/TypeMapException.cs | 2 +- .../JavaMarshalValueManager.cs | 23 ++++++++++------ .../TrimmableTypeMap.cs | 26 ++++++++++++------- .../TrimmableTypeMapTypeManager.cs | 6 ++--- 10 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 3b0beabfebb..b32d4e597a2 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -62,14 +62,12 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua settings.AddDebugDotnetLog (); var (typeManager, trimmableTypeMap) = CreateTypeManager (); - var jmvm = JavaMarshalValueManager.GetOrCreateInstance (); - jmvm.TypeMap = trimmableTypeMap; var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global), TypeManager = typeManager, - ValueManager = jmvm, + ValueManager = new JavaMarshalValueManager (trimmableTypeMap), UseMarshalMemberBuilder = false, JniGlobalReferenceLogWriter = settings.GrefLog, JniLocalReferenceLogWriter = settings.LrefLog, diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Java.Interop/JreRuntime.cs index b96e42729f9..b06fec73228 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 ??= JavaMarshalValueManager.GetOrCreateInstance(); + builder.ValueManager ??= JavaMarshalValueManager.Instance; builder.ObjectReferenceManager ??= new Android.Runtime.AndroidObjectReferenceManager (); if (builder.InvocationPointer != IntPtr.Zero || builder.EnvironmentPointer != IntPtr.Zero) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index c23774bc9c6..315bfee4449 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -30,7 +30,9 @@ static Array ArrayCreateInstance (Type elementType, int length) if (factory != null) return factory.CreateArray (length, 1); + #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path return Array.CreateInstance (elementType, length); + #pragma warning restore IL3050 } static Type MakeArrayType (Type type) => diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 29d91ff6ee3..ec23dc70e6b 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -143,9 +143,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) if (RuntimeFeature.IsMonoRuntime) { valueManager = new AndroidValueManager (); } else if (RuntimeFeature.IsCoreClrRuntime) { - var jmvm = JavaMarshalValueManager.GetOrCreateInstance (); - jmvm.TypeMap = trimmableTypeMap; - valueManager = jmvm; + valueManager = new JavaMarshalValueManager (trimmableTypeMap); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } diff --git a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs index 778e0a720a8..0f569b918bb 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerContainerFactory.cs @@ -3,7 +3,9 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Android.Runtime; namespace Java.Interop { @@ -47,7 +49,10 @@ public abstract class JavaPeerContainerFactory /// /// Creates a singleton for the specified type. /// - public static JavaPeerContainerFactory Create () where T : class, IJavaPeerable + public static JavaPeerContainerFactory Create< + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + T + > () where T : class, IJavaPeerable => JavaPeerContainerFactory.Instance; } @@ -55,14 +60,18 @@ public static JavaPeerContainerFactory Create () where T : class, IJavaPeerab /// Typed container factory. All creation uses direct new expressions — fully AOT-safe. /// /// The Java peer element type. - public sealed class JavaPeerContainerFactory : JavaPeerContainerFactory + public sealed class JavaPeerContainerFactory< + // TODO (https://github.com/dotnet/android/issues/10794): Remove this DAM annotation — it preserves too much reflection metadata on all types in the typemap. + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + T + > : JavaPeerContainerFactory where T : class, IJavaPeerable { internal static readonly JavaPeerContainerFactory Instance = new (); JavaPeerContainerFactory () { } - // TODO: I am afraid this might cause unnecessary code bloat for Native AOT. I think we should revisit + // TODO (https://github.com/dotnet/android/issues/10794): This might cause unnecessary code bloat for NativeAOT. Revisit // how we use this API and instead use a differnet approach that uses AOT-safe `Array.CreateInstanceFromArrayType` // with statically provided array types based on a statically known array type. internal override Array CreateArray (int length, int rank) => rank switch { @@ -96,8 +105,10 @@ internal override ICollection CreateCollection (IntPtr handle, JniHandleOwnershi internal override IDictionary? CreateDictionary (JavaPeerContainerFactory keyFactory, IntPtr handle, JniHandleOwnership transfer) => keyFactory.CreateDictionaryWithValueFactory (this, handle, transfer); + #pragma warning disable IL2091 // DynamicallyAccessedMembers on base method type parameter cannot be repeated on override in C# internal override IDictionary? CreateDictionaryWithValueFactory ( JavaPeerContainerFactory valueFactory, IntPtr handle, JniHandleOwnership transfer) => new Android.Runtime.JavaDictionary (handle, transfer); + #pragma warning restore IL2091 } } diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index 7bfd490052d..0fe4b72cc48 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -1,6 +1,8 @@ #nullable enable using System; +using System.Diagnostics.CodeAnalysis; +using Android.Runtime; namespace Java.Interop { @@ -34,6 +36,7 @@ public abstract class JavaPeerProxy : Attribute /// Gets the invoker type for interfaces and abstract classes. /// Returns null for concrete types that can be directly instantiated. /// + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] public virtual Type? InvokerType => null; /// @@ -50,7 +53,11 @@ public abstract class JavaPeerProxy : Attribute /// /// The target .NET peer type this proxy represents. [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, Inherited = false, AllowMultiple = false)] - public abstract class JavaPeerProxy : JavaPeerProxy where T : class, IJavaPeerable + public abstract class JavaPeerProxy< + // TODO (https://github.com/dotnet/android/issues/10794): Remove this DAM annotation + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] + T + > : JavaPeerProxy where T : class, IJavaPeerable { public override Type TargetType => typeof (T); diff --git a/src/Mono.Android/Java.Interop/TypeMapException.cs b/src/Mono.Android/Java.Interop/TypeMapException.cs index 0dae0be6e47..e428d2b02cd 100644 --- a/src/Mono.Android/Java.Interop/TypeMapException.cs +++ b/src/Mono.Android/Java.Interop/TypeMapException.cs @@ -7,7 +7,7 @@ namespace Java.Interop /// /// Exception thrown when a type mapping operation fails at runtime. /// - public class TypeMapException : Exception + public sealed class TypeMapException : Exception { public TypeMapException (string message) : base (message) { } public TypeMapException (string message, Exception innerException) : base (message, innerException) { } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 00911a4e3ba..9083e70eeed 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -29,11 +29,20 @@ class JavaMarshalValueManager : JniRuntime.JniValueManager static readonly SemaphoreSlim bridgeProcessingSemaphore = new (1, 1); - static Lazy s_instance = new (() => new JavaMarshalValueManager ()); - public static JavaMarshalValueManager GetOrCreateInstance () => s_instance.Value; + static JavaMarshalValueManager? s_instance; - unsafe JavaMarshalValueManager () + public static JavaMarshalValueManager Instance => + s_instance ?? throw new InvalidOperationException ("JavaMarshalValueManager has not been initialized. Call the constructor first."); + + readonly TrimmableTypeMap? _typeMap; + + unsafe internal JavaMarshalValueManager (TrimmableTypeMap? typeMap = null) { + _typeMap = typeMap; + + var previous = Interlocked.CompareExchange (ref s_instance, this, null); + Debug.Assert (previous is null, "JavaMarshalValueManager must only be created once."); + // There can only be one instance because JavaMarshal.Initialize can only be called once. var mark_cross_references_ftn = RuntimeNativeMethods.clr_initialize_gc_bridge (&BridgeProcessingStarted, &BridgeProcessingFinished); JavaMarshal.Initialize (mark_cross_references_ftn); @@ -458,7 +467,7 @@ static unsafe void BridgeProcessingFinished (MarkCrossReferencesArgs* mcr) static unsafe ReadOnlySpan ProcessCollectedContexts (MarkCrossReferencesArgs* mcr) { List handlesToFree = []; - JavaMarshalValueManager instance = GetOrCreateInstance (); + JavaMarshalValueManager instance = Instance; for (int i = 0; (nuint)i < mcr->ComponentCount; i++) { StronglyConnectedComponent component = mcr->Components [i]; @@ -496,8 +505,6 @@ void ProcessContext (HandleContext* context) static readonly Type[] XAConstructorSignature = new Type [] { typeof (IntPtr), typeof (JniHandleOwnership) }; - internal TrimmableTypeMap? TypeMap { get; set; } - protected override bool TryConstructPeer ( IJavaPeerable self, ref JniObjectReference reference, @@ -505,8 +512,8 @@ protected override bool TryConstructPeer ( [DynamicallyAccessedMembers (Constructors)] Type type) { - if (TypeMap != null) { - if (TypeMap.TryCreatePeer (type, reference.Handle, JniHandleOwnership.DoNotTransfer)) { + if (_typeMap != null) { + if (_typeMap.TryCreatePeer (type, reference.Handle, JniHandleOwnership.DoNotTransfer)) { JniObjectReference.Dispose (ref reference, options); return true; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index da424219592..6fc99469739 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Threading; @@ -32,7 +33,7 @@ internal TrimmableTypeMap () Debug.Assert (previous is null, "TrimmableTypeMap must only be created once."); } - internal bool TryGetType (string jniSimpleReference, out Type type) + internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) => _typeMap.TryGetValue (jniSimpleReference, out type); /// @@ -48,9 +49,12 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf return proxy.CreateInstance (handle, transfer) != null; } + const DynamicallyAccessedMemberTypes Constructors = DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + /// /// Gets the invoker type for an interface or abstract class from the proxy attribute. /// + [return: DynamicallyAccessedMembers (Constructors)] internal Type? GetInvokerType (Type type) { var proxy = type.GetCustomAttribute (inherit: false); @@ -85,7 +89,7 @@ internal static void ActivateInstance (IntPtr self, Type targetType) } } - // TODO: The generator currently emits per-method RegisterMethod() calls. + // TODO (https://github.com/dotnet/android/issues/10794): The generator currently emits per-method RegisterMethod() calls. // This should be changed to emit a single JNI RegisterNatives call with // all methods at once, eliminating this helper. Follow-up generator change. @@ -99,7 +103,7 @@ public static void RegisterMethod (JniType nativeClass, string name, string sign // a raw function pointer from an [UnmanagedCallersOnly] method. JNI only uses the // function pointer extracted via Marshal.GetFunctionPointerForDelegate(), so the // delegate type doesn't matter — Action is used as a lightweight wrapper. - // TODO: Add an IntPtr overload to java-interop's RegisterNatives to avoid this allocation. + // TODO (https://github.com/dotnet/java-interop/pull/1391): Use JniNativeMethod overload to avoid delegate allocation. var registration = new JniNativeMethodRegistration (name, signature, Marshal.GetDelegateForFunctionPointer (functionPointer)); JniEnvironment.Types.RegisterNatives ( @@ -108,6 +112,10 @@ public static void RegisterMethod (JniType nativeClass, string name, string sign 1); } + static readonly RegisterNativesHandler s_onRegisterNatives = OnRegisterNatives; + + delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); + /// /// Registers the mono.android.Runtime.registerNatives JNI native method. /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. @@ -118,21 +126,20 @@ internal void RegisterBootstrapNativeMethod () JniEnvironment.Types.RegisterNatives ( runtimeClass.PeerReference, new [] { new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", - (RegisterNativesHandler) OnRegisterNatives) }, + s_onRegisterNatives) }, 1); } - delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); - static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { + string? className = null; try { if (s_instance is null) { return; } var classRef = new JniObjectReference (nativeClassHandle); - var className = JniEnvironment.Types.GetJniTypeNameFromInstance (classRef); + className = JniEnvironment.Types.GetJniTypeNameFromInstance (classRef); if (className is null) { return; } @@ -143,12 +150,11 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa var proxy = type.GetCustomAttribute (inherit: false); if (proxy is IAndroidCallableWrapper acw) { - using var jniType = new JniType (classRef); + using var jniType = new JniType (className); acw.RegisterNatives (jniType); } } catch (Exception ex) { - Logger.Log (LogLevel.Error, "TrimmableTypeMap", - $"Failed to register natives: {ex}"); + Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); } } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 4bc1a781964..763614ab7c8 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -39,9 +39,9 @@ protected override IEnumerable GetSimpleReferences (Type type) yield return r; } - var attr = type.GetCustomAttribute (inherit: false); - if (attr != null && !attr.Name.IsNullOrEmpty ()) { - yield return attr.Name.Replace ('.', '/'); + var attr = type.GetCustomAttributes (typeof (IJniNameProviderAttribute), inherit: false); + if (attr.Length > 0 && attr [0] is IJniNameProviderAttribute jniNameProvider && !string.IsNullOrEmpty (jniNameProvider.Name)) { + yield return jniNameProvider.Name.Replace ('.', '/'); } } From a9703375b112ab7ce7fc4736a0a1f7b26683f78b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 18 Mar 2026 21:29:01 +0100 Subject: [PATCH 08/25] Refactor JavaMarshalValueManager to take TrimmableTypeMap in constructor Pass TrimmableTypeMap via constructor instead of settable property. Use Interlocked.CompareExchange for single-instance safety. Keep RegisterBootstrapNativeMethod as separate call (JNI runtime must be initialized first). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 10 +++---- .../JavaMarshalValueManager.cs | 6 ++++- .../TrimmableTypeMap.cs | 27 +++++++++---------- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index ec23dc70e6b..1734ae9cd8f 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -130,11 +130,12 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) BoundExceptionType = (BoundExceptionType)args->ioExceptionType; JniRuntime.JniTypeManager typeManager; - JniRuntime.JniValueManager valueManager; + JniRuntime.JniValueManager? valueManager = null; TrimmableTypeMap? trimmableTypeMap = null; if (RuntimeFeature.TrimmableTypeMap) { trimmableTypeMap = new TrimmableTypeMap (); typeManager = new TrimmableTypeMapTypeManager (trimmableTypeMap); + valueManager = new JavaMarshalValueManager (trimmableTypeMap); } else if (RuntimeFeature.ManagedTypeMap) { typeManager = new ManagedTypeManager (); } else { @@ -143,7 +144,8 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) if (RuntimeFeature.IsMonoRuntime) { valueManager = new AndroidValueManager (); } else if (RuntimeFeature.IsCoreClrRuntime) { - valueManager = new JavaMarshalValueManager (trimmableTypeMap); + // Note: this will be removed once trimmable typemap is the only supported option for CoreCLR runtime + valueManager ??= new JavaMarshalValueManager (); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); } @@ -156,9 +158,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); - if (trimmableTypeMap != null) { - trimmableTypeMap.RegisterBootstrapNativeMethod (); - } + trimmableTypeMap?.RegisterBootstrapNativeMethod (); grefIGCUserPeer_class = args->grefIGCUserPeer; grefGCUserPeerable_class = args->grefGCUserPeerable; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 9083e70eeed..9e334dbe230 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -512,11 +512,15 @@ protected override bool TryConstructPeer ( [DynamicallyAccessedMembers (Constructors)] Type type) { - if (_typeMap != null) { + if (RuntimeFeature.TrimmableTypeMap) { + Debug.Assert (_typeMap != null, "TrimmableTypeMap should not be null when RuntimeFeature.TrimmableTypeMap is true."); + if (_typeMap.TryCreatePeer (type, reference.Handle, JniHandleOwnership.DoNotTransfer)) { JniObjectReference.Dispose (ref reference, options); return true; } + + return base.TryConstructPeer (self, ref reference, options, type); } var c = type.GetConstructor (ActivationConstructorBindingFlags, null, XAConstructorSignature, null); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 6fc99469739..50a7b0129f6 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -33,6 +33,19 @@ internal TrimmableTypeMap () Debug.Assert (previous is null, "TrimmableTypeMap must only be created once."); } + /// + /// Registers the mono.android.Runtime.registerNatives JNI native method. + /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. + /// + internal void RegisterBootstrapNativeMethod () + { + using var runtimeClass = new JniType ("mono/android/Runtime"); + JniEnvironment.Types.RegisterNatives ( + runtimeClass.PeerReference, + [new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", s_onRegisterNatives)], + 1); + } + internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) => _typeMap.TryGetValue (jniSimpleReference, out type); @@ -116,20 +129,6 @@ public static void RegisterMethod (JniType nativeClass, string name, string sign delegate void RegisterNativesHandler (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle); - /// - /// Registers the mono.android.Runtime.registerNatives JNI native method. - /// Must be called after the JNI runtime is initialized and before any JCW class is loaded. - /// - internal void RegisterBootstrapNativeMethod () - { - using var runtimeClass = new JniType ("mono/android/Runtime"); - JniEnvironment.Types.RegisterNatives ( - runtimeClass.PeerReference, - new [] { new JniNativeMethodRegistration ("registerNatives", "(Ljava/lang/Class;)V", - s_onRegisterNatives) }, - 1); - } - static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) { string? className = null; From 003503213d04e9ae50d06aa1ec2b9491dcac438f Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Thu, 19 Mar 2026 14:31:03 -0500 Subject: [PATCH 09/25] [tests] Update BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc Updated from macOS-7 CI build 13601673. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ildReleaseArm64SimpleDotNet.MonoVM.apkdesc | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc index c8e07661c0b..93311f6bb74 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc @@ -5,34 +5,34 @@ "Size": 3036 }, "classes.dex": { - "Size": 22336 + "Size": 22388 }, "lib/arm64-v8a/lib__Microsoft.Android.Resource.Designer.dll.so": { "Size": 18296 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 88056 + "Size": 88496 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 117184 + "Size": 124664 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26328 + "Size": 26528 }, "lib/arm64-v8a/lib_System.Console.dll.so": { - "Size": 24416 + "Size": 24424 }, "lib/arm64-v8a/lib_System.Linq.dll.so": { - "Size": 25496 + "Size": 25504 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 633928 + "Size": 691720 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20232 + "Size": 20288 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { - "Size": 21624 + "Size": 21632 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { "Size": 20032 @@ -44,10 +44,10 @@ "Size": 36616 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1386232 + "Size": 1386496 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3123608 + "Size": 3124304 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 71936 @@ -56,7 +56,7 @@ "Size": 1281696 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 105664 + "Size": 107024 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { "Size": 165536 @@ -65,7 +65,7 @@ "Size": 19640 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1223 + "Size": 1221 }, "META-INF/BNDLTOOL.SF": { "Size": 3266 @@ -98,5 +98,5 @@ "Size": 1904 } }, - "PackageSize": 3267093 + "PackageSize": 3324437 } \ No newline at end of file From b225af86e2324d1068836c12776c794b39c26336 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 07:39:56 +0100 Subject: [PATCH 10/25] Fix TrimmableTypeMap: ActivateInstance, assembly pre-loading, GetJniTypeNameFromClass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ActivateInstance: use JNI GetObjectClass + GetJniTypeNameFromClass to find proxy type via the TypeMap dictionary instead of targetType.GetCustomAttribute (the self-application attribute is on the proxy, not the target) - Pre-load per-assembly TypeMap DLLs via TypeMapAssemblyTarget attributes before GetOrCreateExternalTypeMapping (Android assembly store needs explicit probing) - Fix GetJniTypeNameFromInstance → GetJniTypeNameFromClass in OnRegisterNatives (nativeClassHandle is a jclass, not a jobject) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 50a7b0129f6..8688b03316b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -27,6 +27,22 @@ class TrimmableTypeMap internal TrimmableTypeMap () { + // Pre-load per-assembly TypeMap DLLs so the TypeMapping API can discover them. + // On Android with the assembly store, assemblies aren't automatically resolvable + // via Assembly.Load() unless the store's external_assembly_probe is triggered first. + var entryAsm = System.Reflection.Assembly.Load ("_Microsoft.Android.TypeMaps"); + foreach (var attrData in entryAsm.GetCustomAttributesData ()) { + if (attrData.AttributeType.Name.StartsWith ("TypeMapAssemblyTargetAttribute", StringComparison.Ordinal) + && attrData.ConstructorArguments.Count > 0 + && attrData.ConstructorArguments[0].Value is string asmName) { + try { + System.Reflection.Assembly.Load (asmName); + } catch { + // Best effort — assembly may not exist for this app + } + } + } + _typeMap = TypeMapping.GetOrCreateExternalTypeMapping (); var previous = Interlocked.CompareExchange (ref s_instance, this, null); @@ -95,7 +111,22 @@ internal static void ActivateInstance (IntPtr self, Type targetType) throw new InvalidOperationException ("TrimmableTypeMap has not been initialized."); } - if (!instance.TryCreatePeer (targetType, self, JniHandleOwnership.DoNotTransfer)) { + // Look up the proxy via JNI class name → TypeMap dictionary. + // We can't use targetType.GetCustomAttribute() because the + // self-application attribute is on the proxy type, not the target type. + var selfRef = new JniObjectReference (self); + var jniClass = JniEnvironment.Types.GetObjectClass (selfRef); + var className = JniEnvironment.Types.GetJniTypeNameFromClass (jniClass); + JniObjectReference.Dispose (ref jniClass); + + if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { + throw new TypeMapException ( + $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + + "Ensure the type has a generated proxy in the TypeMap assembly."); + } + + var proxy = proxyType.GetCustomAttribute (inherit: false); + if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { throw new TypeMapException ( $"Failed to create peer for type '{targetType.FullName}'. " + "Ensure the type has a generated proxy in the TypeMap assembly."); @@ -138,7 +169,7 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa } var classRef = new JniObjectReference (nativeClassHandle); - className = JniEnvironment.Types.GetJniTypeNameFromInstance (classRef); + className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef); if (className is null) { return; } From dc338a24fcb727855f3a4339162a5278c1dfad45 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 07:50:21 +0100 Subject: [PATCH 11/25] Fix trimmer compatibility and TryCreatePeer proxy lookup - JNIEnvInit: wrap RegisterBootstrapNativeMethod in explicit 'if (RuntimeFeature.TrimmableTypeMap)' guard instead of null-conditional operator, so the trimmer can eliminate it when the feature is disabled - TryCreatePeer: look up proxy via JNI class name from the TypeMap dictionary instead of type.GetCustomAttribute (the self-application attribute is on the proxy type, not the managed target type) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 4 +- .../TrimmableTypeMap.cs | 67 +++++++++++++++++-- 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 1734ae9cd8f..8d9fc0e2c79 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -158,7 +158,9 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); - trimmableTypeMap?.RegisterBootstrapNativeMethod (); + if (RuntimeFeature.TrimmableTypeMap) { + trimmableTypeMap!.RegisterBootstrapNativeMethod (); + } grefIGCUserPeer_class = args->grefIGCUserPeer; grefGCUserPeerable_class = args->grefGCUserPeerable; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 8688b03316b..d579f422a89 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -24,6 +25,7 @@ class TrimmableTypeMap internal static TrimmableTypeMap? Instance => s_instance; readonly IReadOnlyDictionary _typeMap; + readonly ConcurrentDictionary _proxyCache = new (); internal TrimmableTypeMap () { @@ -65,12 +67,69 @@ internal void RegisterBootstrapNativeMethod () internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Type? type) => _typeMap.TryGetValue (jniSimpleReference, out type); + /// + /// Finds the proxy for a managed type by resolving its JNI name (from [Register] or + /// [JniTypeSignature] attributes) and looking it up in the TypeMap dictionary. + /// Results are cached per type. + /// + JavaPeerProxy? GetProxyForManagedType (Type managedType) + { + return _proxyCache.GetOrAdd (managedType, static (type, self) => { + // First check if the type itself IS a proxy (has self-applied attribute) + var direct = type.GetCustomAttribute (inherit: false); + if (direct is not null) { + return direct; + } + + // Resolve the JNI name from the managed type's attributes + if (!TryGetJniNameForType (type, out var jniName)) { + return null; + } + + // Look up the proxy type in the TypeMap dictionary + if (!self._typeMap.TryGetValue (jniName, out var proxyType)) { + return null; + } + + return proxyType.GetCustomAttribute (inherit: false); + }, this); + } + + /// + /// Resolves a managed type's JNI name from its [Register] or [JniTypeSignature] attributes. + /// + static bool TryGetJniNameForType (Type type, [NotNullWhen (true)] out string? jniName) + { + // Check [Register("jniName", ...)] attribute (Mono.Android binding types) + foreach (var attr in type.GetCustomAttributesData ()) { + if (attr.AttributeType.FullName == "Android.Runtime.RegisterAttribute" + && attr.ConstructorArguments.Count > 0 + && attr.ConstructorArguments[0].Value is string name + && name.Length > 0 + && !name.Contains ('(')) { // Skip method-level [Register] + jniName = name; + return true; + } + } + + // Check [JniTypeSignature("jniName")] attribute (Java.Interop types) + var sigAttr = type.GetCustomAttribute (inherit: false); + if (sigAttr is not null && !string.IsNullOrEmpty (sigAttr.SimpleReference)) { + jniName = sigAttr.SimpleReference; + return true; + } + + jniName = null; + return false; + } + /// /// 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 = type.GetCustomAttribute (inherit: false); + var proxy = GetProxyForManagedType (type); if (proxy is null) { return false; } @@ -86,8 +145,7 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf [return: DynamicallyAccessedMembers (Constructors)] internal Type? GetInvokerType (Type type) { - var proxy = type.GetCustomAttribute (inherit: false); - return proxy?.InvokerType; + return GetProxyForManagedType (type)?.InvokerType; } /// @@ -96,8 +154,7 @@ internal bool TryCreatePeer (Type type, IntPtr handle, JniHandleOwnership transf /// internal JavaPeerContainerFactory? GetContainerFactory (Type type) { - var proxy = type.GetCustomAttribute (inherit: false); - return proxy?.GetContainerFactory (); + return GetProxyForManagedType (type)?.GetContainerFactory (); } /// From 3d56f5f00dae93d9955be75d42686886961790a9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 08:48:33 +0100 Subject: [PATCH 12/25] Simplify TryGetJniNameForType to use IJniNameProviderAttribute Both RegisterAttribute and JniTypeSignatureAttribute implement IJniNameProviderAttribute, so a single GetCustomAttributes check covers both. Made the method internal+static so TrimmableTypeMapTypeManager.GetSimpleReferences can reuse it instead of duplicating the attribute lookup. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 24 +++++-------------- .../TrimmableTypeMapTypeManager.cs | 5 ++-- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index d579f422a89..139c08f0c06 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -96,26 +96,14 @@ internal bool TryGetType (string jniSimpleReference, [NotNullWhen (true)] out Ty } /// - /// Resolves a managed type's JNI name from its [Register] or [JniTypeSignature] attributes. + /// Resolves a managed type's JNI name from its + /// (implemented by both [Register] and [JniTypeSignature]). /// - static bool TryGetJniNameForType (Type type, [NotNullWhen (true)] out string? jniName) + internal static bool TryGetJniNameForType (Type type, [NotNullWhen (true)] out string? jniName) { - // Check [Register("jniName", ...)] attribute (Mono.Android binding types) - foreach (var attr in type.GetCustomAttributesData ()) { - if (attr.AttributeType.FullName == "Android.Runtime.RegisterAttribute" - && attr.ConstructorArguments.Count > 0 - && attr.ConstructorArguments[0].Value is string name - && name.Length > 0 - && !name.Contains ('(')) { // Skip method-level [Register] - jniName = name; - return true; - } - } - - // Check [JniTypeSignature("jniName")] attribute (Java.Interop types) - var sigAttr = type.GetCustomAttribute (inherit: false); - if (sigAttr is not null && !string.IsNullOrEmpty (sigAttr.SimpleReference)) { - jniName = sigAttr.SimpleReference; + if (type.GetCustomAttributes (typeof (IJniNameProviderAttribute), inherit: false) is [IJniNameProviderAttribute provider, ..] + && !string.IsNullOrEmpty (provider.Name)) { + jniName = provider.Name.Replace ('.', '/'); return true; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 763614ab7c8..c7d82772ff9 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -39,9 +39,8 @@ protected override IEnumerable GetSimpleReferences (Type type) yield return r; } - var attr = type.GetCustomAttributes (typeof (IJniNameProviderAttribute), inherit: false); - if (attr.Length > 0 && attr [0] is IJniNameProviderAttribute jniNameProvider && !string.IsNullOrEmpty (jniNameProvider.Name)) { - yield return jniNameProvider.Name.Replace ('.', '/'); + if (TrimmableTypeMap.TryGetJniNameForType (type, out var jniName)) { + yield return jniName; } } From ce09b7fd8559eeca4ee1d8466111ce0ece2cea82 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 15:02:34 +0100 Subject: [PATCH 13/25] Address PR feedback: remove TypeMapException, FailFast in OnRegisterNatives - Remove TypeMapException: use InvalidOperationException instead, with 'Typemap' prefix in messages (jonathanpeppers feedback) - OnRegisterNatives: replace silent returns with Environment.FailFast() since all error paths indicate runtime or generator bugs that should abort the app Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Java.Interop/TypeMapException.cs | 15 -------------- .../TrimmableTypeMap.cs | 20 ++++++++++++------- src/Mono.Android/Mono.Android.csproj | 1 - 3 files changed, 13 insertions(+), 23 deletions(-) delete mode 100644 src/Mono.Android/Java.Interop/TypeMapException.cs diff --git a/src/Mono.Android/Java.Interop/TypeMapException.cs b/src/Mono.Android/Java.Interop/TypeMapException.cs deleted file mode 100644 index e428d2b02cd..00000000000 --- a/src/Mono.Android/Java.Interop/TypeMapException.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable enable - -using System; - -namespace Java.Interop -{ - /// - /// Exception thrown when a type mapping operation fails at runtime. - /// - public sealed class TypeMapException : Exception - { - public TypeMapException (string message) : base (message) { } - public TypeMapException (string message, Exception innerException) : base (message, innerException) { } - } -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 139c08f0c06..d56cfb263de 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -165,15 +165,15 @@ internal static void ActivateInstance (IntPtr self, Type targetType) JniObjectReference.Dispose (ref jniClass); if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new TypeMapException ( - $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + + throw new InvalidOperationException ( + $"Typemap failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + "Ensure the type has a generated proxy in the TypeMap assembly."); } var proxy = proxyType.GetCustomAttribute (inherit: false); if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new TypeMapException ( - $"Failed to create peer for type '{targetType.FullName}'. " + + throw new InvalidOperationException ( + $"Typemap failed to create peer for type '{targetType.FullName}'. " + "Ensure the type has a generated proxy in the TypeMap assembly."); } } @@ -210,24 +210,30 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa string? className = null; try { if (s_instance is null) { + Environment.FailFast ("TrimmableTypeMap: OnRegisterNatives called before TrimmableTypeMap was initialized."); return; } var classRef = new JniObjectReference (nativeClassHandle); className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef); if (className is null) { + Environment.FailFast ("TrimmableTypeMap: Failed to get JNI class name from class reference."); return; } if (!s_instance._typeMap.TryGetValue (className, out var type)) { + Environment.FailFast ($"TrimmableTypeMap: Class '{className}' not found in the typemap. This is a bug in the typemap generator."); return; } var proxy = type.GetCustomAttribute (inherit: false); - if (proxy is IAndroidCallableWrapper acw) { - using var jniType = new JniType (className); - acw.RegisterNatives (jniType); + if (proxy is not IAndroidCallableWrapper acw) { + Environment.FailFast ($"TrimmableTypeMap: Proxy for class '{className}' does not implement IAndroidCallableWrapper. This is a bug in the typemap generator."); + return; } + + using var jniType = new JniType (className); + acw.RegisterNatives (jniType); } catch (Exception ex) { Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index d7da346bea2..12bb3a01446 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -320,7 +320,6 @@ - From 424df43b2dcc77d16a69976e94925e356e030985 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 15:18:34 +0100 Subject: [PATCH 14/25] Guard TrimmableTypeMap reference in ArrayCreateInstance with feature switch The unconditional reference to TrimmableTypeMap.Instance in JNIEnv.ArrayCreateInstance prevented the linker from trimming the entire TrimmableTypeMap dependency chain (ConcurrentDictionary, Assembly.Load, GetCustomAttributesData, Marshal.GetDelegateFor- FunctionPointer, etc.), causing ~57 KB bloat in System.Private.CoreLib even when the feature is disabled. Wrap the access with RuntimeFeature.TrimmableTypeMap to match the pattern already used in JavaConvert.cs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 315bfee4449..356f64a7ff5 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -26,9 +26,11 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { - var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - if (factory != null) - return factory.CreateArray (length, 1); + if (RuntimeFeature.TrimmableTypeMap) { + var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + if (factory != null) + return factory.CreateArray (length, 1); + } #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path return Array.CreateInstance (elementType, length); From e3f576f52dbde88e9e34639ebdad69e740a8ad2f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 15:55:44 +0100 Subject: [PATCH 15/25] Address code review findings - Add missing RegisterBootstrapNativeMethod call in NativeAOT path - Log caught exceptions in TrimmableTypeMap assembly pre-loading - Fix Mono style: space before [] in ConstructorArguments [0] - Remove trailing whitespace in JavaMarshalValueManager.TryConstructPeer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime.NativeAOT/JavaInteropRuntime.cs | 2 ++ src/Mono.Android/Android.Runtime/JNIEnvInit.cs | 2 +- .../Microsoft.Android.Runtime/JavaMarshalValueManager.cs | 2 +- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 6 +++--- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index b32d4e597a2..2a200afea7c 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -77,6 +77,8 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua // Entry point into Mono.Android.dll. Log categories are initialized in JNI_OnLoad. JNIEnvInit.InitializeJniRuntime (runtime, initArgs); + trimmableTypeMap?.RegisterBootstrapNativeMethod (); + transition = new JniTransition (jnienv); var handler = Java.Lang.Thread.DefaultUncaughtExceptionHandler; diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 8d9fc0e2c79..85ea8b9ead7 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -159,7 +159,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) ); if (RuntimeFeature.TrimmableTypeMap) { - trimmableTypeMap!.RegisterBootstrapNativeMethod (); + trimmableTypeMap?.RegisterBootstrapNativeMethod (); } grefIGCUserPeer_class = args->grefIGCUserPeer; diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 9e334dbe230..71e5f832734 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -519,7 +519,7 @@ protected override bool TryConstructPeer ( JniObjectReference.Dispose (ref reference, options); return true; } - + return base.TryConstructPeer (self, ref reference, options, type); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index d56cfb263de..b311b3c0d31 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -36,11 +36,11 @@ internal TrimmableTypeMap () foreach (var attrData in entryAsm.GetCustomAttributesData ()) { if (attrData.AttributeType.Name.StartsWith ("TypeMapAssemblyTargetAttribute", StringComparison.Ordinal) && attrData.ConstructorArguments.Count > 0 - && attrData.ConstructorArguments[0].Value is string asmName) { + && attrData.ConstructorArguments [0].Value is string asmName) { try { System.Reflection.Assembly.Load (asmName); - } catch { - // Best effort — assembly may not exist for this app + } catch (Exception ex) { + Debug.WriteLine ($"TrimmableTypeMap: Failed to pre-load assembly '{asmName}': {ex.Message}"); } } } From 945b8374a5d7e7aa06b1dbbd0061a0216a60c6a4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 20 Mar 2026 16:37:57 +0100 Subject: [PATCH 16/25] Pass RegisterJniNatives via init args, add registerNatives C++ stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor native→managed initialization: - Initialize sets args->registerJniNativesFn (null in trimmable path) - Eliminates create_delegate call for RegisterJniNatives - Guard jnienv_register_jni_natives call site for null - Add Host::Java_mono_android_Runtime_registerNatives no-op stub for the trimmable path (managed code handles registration) - Move jnienv_register_jni_natives_fn typedef before struct This lets the trimmer cleanly remove RegisterJniNatives when the trimmable typemap is active, while preserving it for the legacy path. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 7 +++- src/native/clr/host/host.cc | 32 +++++++++---------- .../common/include/managed-interface.hh | 3 +- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 85ea8b9ead7..1e7dee9961d 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -36,6 +36,7 @@ internal struct JnienvInitializeArgs { public IntPtr grefGCUserPeerable; public bool managedMarshalMethodsLookupEnabled; public IntPtr propagateUncaughtExceptionFn; + public IntPtr registerJniNativesFn; } #pragma warning restore 0649 @@ -159,7 +160,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) ); if (RuntimeFeature.TrimmableTypeMap) { - trimmableTypeMap?.RegisterBootstrapNativeMethod (); + trimmableTypeMap!.RegisterBootstrapNativeMethod (); } grefIGCUserPeer_class = args->grefIGCUserPeer; @@ -175,6 +176,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) } args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; + + if (!RuntimeFeature.TrimmableTypeMap) { + args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; + } RunStartupHooksIfNeeded (); SetSynchronizationContext (); } diff --git a/src/native/clr/host/host.cc b/src/native/clr/host/host.cc index 7d19c7e9175..3e84ef930d8 100644 --- a/src/native/clr/host/host.cc +++ b/src/native/clr/host/host.cc @@ -528,22 +528,8 @@ void Host::Java_mono_android_Runtime_initInternal ( internal_timing.start_event (TimingEventKind::NativeToManagedTransition); } - void *delegate = nullptr; - log_debug (LOG_ASSEMBLY, "Creating UCO delegate to {}.RegisterJniNatives"sv, Constants::JNIENVINIT_FULL_TYPE_NAME); - delegate = FastTiming::time_call ("create_delegate for RegisterJniNatives"sv, create_delegate, Constants::MONO_ANDROID_ASSEMBLY_NAME, Constants::JNIENVINIT_FULL_TYPE_NAME, "RegisterJniNatives"sv); - jnienv_register_jni_natives = reinterpret_cast (delegate); - abort_unless ( - jnienv_register_jni_natives != nullptr, - [] { - return detail::_format_message ( - "Failed to obtain unmanaged-callers-only pointer to the %s.%s.RegisterJniNatives method.", - Constants::MONO_ANDROID_ASSEMBLY_NAME, - Constants::JNIENVINIT_FULL_TYPE_NAME - ); - } - ); - log_debug (LOG_ASSEMBLY, "Creating UCO delegate to {}.Initialize"sv, Constants::JNIENVINIT_FULL_TYPE_NAME); + void *delegate = nullptr; delegate = FastTiming::time_call ("create_delegate for Initialize"sv, create_delegate, Constants::MONO_ANDROID_ASSEMBLY_NAME, Constants::JNIENVINIT_FULL_TYPE_NAME, "Initialize"sv); auto initialize = reinterpret_cast (delegate); abort_unless ( @@ -560,7 +546,10 @@ void Host::Java_mono_android_Runtime_initInternal ( log_debug (LOG_DEFAULT, "Calling into managed runtime init"sv); FastTiming::time_call ("JNIEnv.Initialize UCO"sv, initialize, &init); - // PropagateUncaughtException is returned from Initialize to avoid an extra create_delegate call + // RegisterJniNatives and PropagateUncaughtException are returned from Initialize + // to avoid extra create_delegate calls. RegisterJniNatives is null when using the + // trimmable typemap path (the method is trimmed; registration is handled in managed code). + jnienv_register_jni_natives = init.registerJniNativesFn; jnienv_propagate_uncaught_exception = init.propagateUncaughtExceptionFn; abort_unless (jnienv_propagate_uncaught_exception != nullptr, "Failed to obtain unmanaged-callers-only function pointer to the PropagateUncaughtException method."); @@ -588,7 +577,9 @@ void Host::Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, env->ReleaseStringUTFChars (managedType, mt_ptr); // TODO: must attach thread to the runtime here - jnienv_register_jni_natives (managedType_ptr, managedType_len, nativeClass, methods_ptr, methods_len); + if (jnienv_register_jni_natives != nullptr) { + jnienv_register_jni_natives (managedType_ptr, managedType_len, nativeClass, methods_ptr, methods_len); + } env->ReleaseStringChars (methods, methods_ptr); env->ReleaseStringChars (managedType, managedType_ptr); @@ -605,6 +596,13 @@ void Host::Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, } } +void Host::Java_mono_android_Runtime_registerNatives ([[maybe_unused]] JNIEnv *env, [[maybe_unused]] jclass nativeClass) noexcept +{ + // In the trimmable typemap path, registerNatives is handled entirely in managed code + // via a dynamically registered JNI native method. This C++ stub exists only as a + // fallback for the legacy code path (which doesn't use registerNatives). +} + auto HostCommon::Java_JNI_OnLoad (JavaVM *vm, [[maybe_unused]] void *reserved) noexcept -> jint { jvm = vm; diff --git a/src/native/common/include/managed-interface.hh b/src/native/common/include/managed-interface.hh index b590fb6210e..ccf6c8b4f6b 100644 --- a/src/native/common/include/managed-interface.hh +++ b/src/native/common/include/managed-interface.hh @@ -15,6 +15,7 @@ namespace xamarin::android { }; using jnienv_propagate_uncaught_exception_fn = void (*)(JNIEnv *env, jobject javaThread, jthrowable javaException); + using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len); // NOTE: Keep this in sync with managed side in src/Mono.Android/Android.Runtime/JNIEnvInit.cs struct JnienvInitializeArgs { @@ -36,6 +37,7 @@ namespace xamarin::android { jobject grefGCUserPeerable; bool managedMarshalMethodsLookupEnabled; jnienv_propagate_uncaught_exception_fn propagateUncaughtExceptionFn; + jnienv_register_jni_natives_fn registerJniNativesFn; }; // Keep the enum values in sync with those in src/Mono.Android/AndroidRuntime/BoundExceptionType.cs @@ -46,5 +48,4 @@ namespace xamarin::android { }; using jnienv_initialize_fn = void (*) (JnienvInitializeArgs*); - using jnienv_register_jni_natives_fn = void (*)(const jchar *typeName_ptr, int32_t typeName_len, jclass jniClass, const jchar *methods_ptr, int32_t methods_len); } From aad2ddb8b4cc07ed94847fc26586b071e3e2646b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 10:14:14 +0100 Subject: [PATCH 17/25] Revert apkdesc to main baseline The apkdesc was updated before the feature guard fix in ArrayCreateInstance, so it contained bloated sizes from the unguarded TrimmableTypeMap reference. Reverting to main's baseline since the feature switch ensures all new code is trimmed when disabled. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...ildReleaseArm64SimpleDotNet.MonoVM.apkdesc | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc index 93311f6bb74..c8e07661c0b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64SimpleDotNet.MonoVM.apkdesc @@ -5,34 +5,34 @@ "Size": 3036 }, "classes.dex": { - "Size": 22388 + "Size": 22336 }, "lib/arm64-v8a/lib__Microsoft.Android.Resource.Designer.dll.so": { "Size": 18296 }, "lib/arm64-v8a/lib_Java.Interop.dll.so": { - "Size": 88496 + "Size": 88056 }, "lib/arm64-v8a/lib_Mono.Android.dll.so": { - "Size": 124664 + "Size": 117184 }, "lib/arm64-v8a/lib_Mono.Android.Runtime.dll.so": { - "Size": 26528 + "Size": 26328 }, "lib/arm64-v8a/lib_System.Console.dll.so": { - "Size": 24424 + "Size": 24416 }, "lib/arm64-v8a/lib_System.Linq.dll.so": { - "Size": 25504 + "Size": 25496 }, "lib/arm64-v8a/lib_System.Private.CoreLib.dll.so": { - "Size": 691720 + "Size": 633928 }, "lib/arm64-v8a/lib_System.Runtime.dll.so": { - "Size": 20288 + "Size": 20232 }, "lib/arm64-v8a/lib_System.Runtime.InteropServices.dll.so": { - "Size": 21632 + "Size": 21624 }, "lib/arm64-v8a/lib_UnnamedProject.dll.so": { "Size": 20032 @@ -44,10 +44,10 @@ "Size": 36616 }, "lib/arm64-v8a/libmonodroid.so": { - "Size": 1386496 + "Size": 1386232 }, "lib/arm64-v8a/libmonosgen-2.0.so": { - "Size": 3124304 + "Size": 3123608 }, "lib/arm64-v8a/libSystem.Globalization.Native.so": { "Size": 71936 @@ -56,7 +56,7 @@ "Size": 1281696 }, "lib/arm64-v8a/libSystem.Native.so": { - "Size": 107024 + "Size": 105664 }, "lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so": { "Size": 165536 @@ -65,7 +65,7 @@ "Size": 19640 }, "META-INF/BNDLTOOL.RSA": { - "Size": 1221 + "Size": 1223 }, "META-INF/BNDLTOOL.SF": { "Size": 3266 @@ -98,5 +98,5 @@ "Size": 1904 } }, - "PackageSize": 3324437 + "PackageSize": 3267093 } \ No newline at end of file From 6f0a24f67faaf4292d5536b5f774b19dec8e9b82 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 10:29:53 +0100 Subject: [PATCH 18/25] Remove assembly pre-loading from TrimmableTypeMap Follow-up PRs will handle the ILLink/ILC setup to ensure typemap assemblies are available without manual Assembly.Load() calls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TrimmableTypeMap.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index b311b3c0d31..e598cc38820 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -29,22 +29,6 @@ class TrimmableTypeMap internal TrimmableTypeMap () { - // Pre-load per-assembly TypeMap DLLs so the TypeMapping API can discover them. - // On Android with the assembly store, assemblies aren't automatically resolvable - // via Assembly.Load() unless the store's external_assembly_probe is triggered first. - var entryAsm = System.Reflection.Assembly.Load ("_Microsoft.Android.TypeMaps"); - foreach (var attrData in entryAsm.GetCustomAttributesData ()) { - if (attrData.AttributeType.Name.StartsWith ("TypeMapAssemblyTargetAttribute", StringComparison.Ordinal) - && attrData.ConstructorArguments.Count > 0 - && attrData.ConstructorArguments [0].Value is string asmName) { - try { - System.Reflection.Assembly.Load (asmName); - } catch (Exception ex) { - Debug.WriteLine ($"TrimmableTypeMap: Failed to pre-load assembly '{asmName}': {ex.Message}"); - } - } - } - _typeMap = TypeMapping.GetOrCreateExternalTypeMapping (); var previous = Interlocked.CompareExchange (ref s_instance, this, null); From 57a747f2e842a30721c78b546756cfca50df857f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 14:08:45 +0100 Subject: [PATCH 19/25] Fix external/Java.Interop submodule ref to match main MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rebase conflict resolution incorrectly kept the PR's old submodule ref (fb0952fad) instead of main's (c14ba04). This PR should not modify the submodule — main already has the correct version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external/Java.Interop b/external/Java.Interop index fb0952fad89..c14ba04c7fa 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a +Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 From e598f28ac5d1c2748985be9b389413ff44938855 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 20:43:29 +0100 Subject: [PATCH 20/25] Add missing registerNatives declaration to host headers The C++ implementation of Java_mono_android_Runtime_registerNatives was added to host.cc but the declarations were missing from host.hh and host-jni.hh, and the JNI wrapper was missing from host-jni.cc. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- external/xamarin-android-tools | 2 +- src/native/clr/host/host-jni.cc | 6 ++++++ src/native/clr/include/host/host-jni.hh | 7 +++++++ src/native/clr/include/host/host.hh | 1 + 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index c14ba04c7fa..fb0952fad89 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 +Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index d679f2becba..40b30131791 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit d679f2becbac319c1ef35934d40866d87996f7b0 +Subproject commit 40b30131791e7e996e20d461f8d3694b273f6985 diff --git a/src/native/clr/host/host-jni.cc b/src/native/clr/host/host-jni.cc index a41c1c507cc..e8aaeb8ca6f 100644 --- a/src/native/clr/host/host-jni.cc +++ b/src/native/clr/host/host-jni.cc @@ -25,6 +25,12 @@ JNICALL Java_mono_android_Runtime_register (JNIEnv *env, [[maybe_unused]] jclass Host::Java_mono_android_Runtime_register (env, managedType, nativeClass, methods); } +JNIEXPORT void +JNICALL Java_mono_android_Runtime_registerNatives (JNIEnv *env, [[maybe_unused]] jclass klass, jclass nativeClass) +{ + Host::Java_mono_android_Runtime_registerNatives (env, nativeClass); +} + JNIEXPORT void JNICALL Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass klass, jstring lang, jobjectArray runtimeApksJava, jstring runtimeNativeLibDir, jobjectArray appDirs, jint localDateTimeOffset, jobject loader, diff --git a/src/native/clr/include/host/host-jni.hh b/src/native/clr/include/host/host-jni.hh index 4904644ebd8..a3d18ed60a0 100644 --- a/src/native/clr/include/host/host-jni.hh +++ b/src/native/clr/include/host/host-jni.hh @@ -45,4 +45,11 @@ extern "C" { */ JNIEXPORT void JNICALL Java_mono_android_Runtime_register (JNIEnv *, jclass, jstring, jclass, jstring); + /* + * Class: mono_android_Runtime + * Method: registerNatives + * Signature: (Ljava/lang/Class;)V + */ + JNIEXPORT void JNICALL Java_mono_android_Runtime_registerNatives (JNIEnv *, jclass, jclass); + } diff --git a/src/native/clr/include/host/host.hh b/src/native/clr/include/host/host.hh index 5e873b1a54a..3537c9310d7 100644 --- a/src/native/clr/include/host/host.hh +++ b/src/native/clr/include/host/host.hh @@ -20,6 +20,7 @@ namespace xamarin::android { jstring runtimeNativeLibDir, jobjectArray appDirs, jint localDateTimeOffset, jobject loader, jobjectArray assembliesJava, jboolean isEmulator, jboolean haveSplitApks) noexcept; static void Java_mono_android_Runtime_register (JNIEnv *env, jstring managedType, jclass nativeClass, jstring methods) noexcept; + static void Java_mono_android_Runtime_registerNatives (JNIEnv *env, jclass nativeClass) noexcept; static void propagate_uncaught_exception (JNIEnv *env, jobject javaThread, jthrowable javaException) noexcept; static auto get_timing () -> std::shared_ptr From 65974f3c64026a31331cdb85a19d67bb33c81522 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 22:13:16 +0100 Subject: [PATCH 21/25] Update runtime for build pipeline integration - TrimmableTypeMap: improve assembly pre-loading, add TypeMapException - JavaMarshalValueManager: fix method name - JNIEnv: simplify type mapping calls - Remove unused host-jni registerNatives stub (now handled via init args) - Add TypeMapException.cs to Mono.Android.csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 2 -- src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 ++--- .../Java.Interop/TypeMapException.cs | 15 ++++++++ .../JavaMarshalValueManager.cs | 2 +- .../TrimmableTypeMap.cs | 36 ++++++++++++------- src/Mono.Android/Mono.Android.csproj | 1 + src/native/clr/host/host-jni.cc | 6 ---- src/native/clr/include/host/host-jni.hh | 7 ---- 8 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 src/Mono.Android/Java.Interop/TypeMapException.cs diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 2a200afea7c..b32d4e597a2 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -77,8 +77,6 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua // Entry point into Mono.Android.dll. Log categories are initialized in JNI_OnLoad. JNIEnvInit.InitializeJniRuntime (runtime, initArgs); - trimmableTypeMap?.RegisterBootstrapNativeMethod (); - transition = new JniTransition (jnienv); var handler = Java.Lang.Thread.DefaultUncaughtExceptionHandler; diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 356f64a7ff5..315bfee4449 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -26,11 +26,9 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { - if (RuntimeFeature.TrimmableTypeMap) { - var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - if (factory != null) - return factory.CreateArray (length, 1); - } + var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + if (factory != null) + return factory.CreateArray (length, 1); #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path return Array.CreateInstance (elementType, length); diff --git a/src/Mono.Android/Java.Interop/TypeMapException.cs b/src/Mono.Android/Java.Interop/TypeMapException.cs new file mode 100644 index 00000000000..e428d2b02cd --- /dev/null +++ b/src/Mono.Android/Java.Interop/TypeMapException.cs @@ -0,0 +1,15 @@ +#nullable enable + +using System; + +namespace Java.Interop +{ + /// + /// Exception thrown when a type mapping operation fails at runtime. + /// + public sealed class TypeMapException : Exception + { + public TypeMapException (string message) : base (message) { } + public TypeMapException (string message, Exception innerException) : base (message, innerException) { } + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs index 71e5f832734..9e334dbe230 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/JavaMarshalValueManager.cs @@ -519,7 +519,7 @@ protected override bool TryConstructPeer ( JniObjectReference.Dispose (ref reference, options); return true; } - + return base.TryConstructPeer (self, ref reference, options, type); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index e598cc38820..139c08f0c06 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -29,6 +29,22 @@ class TrimmableTypeMap internal TrimmableTypeMap () { + // Pre-load per-assembly TypeMap DLLs so the TypeMapping API can discover them. + // On Android with the assembly store, assemblies aren't automatically resolvable + // via Assembly.Load() unless the store's external_assembly_probe is triggered first. + var entryAsm = System.Reflection.Assembly.Load ("_Microsoft.Android.TypeMaps"); + foreach (var attrData in entryAsm.GetCustomAttributesData ()) { + if (attrData.AttributeType.Name.StartsWith ("TypeMapAssemblyTargetAttribute", StringComparison.Ordinal) + && attrData.ConstructorArguments.Count > 0 + && attrData.ConstructorArguments[0].Value is string asmName) { + try { + System.Reflection.Assembly.Load (asmName); + } catch { + // Best effort — assembly may not exist for this app + } + } + } + _typeMap = TypeMapping.GetOrCreateExternalTypeMapping (); var previous = Interlocked.CompareExchange (ref s_instance, this, null); @@ -149,15 +165,15 @@ internal static void ActivateInstance (IntPtr self, Type targetType) JniObjectReference.Dispose (ref jniClass); if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new InvalidOperationException ( - $"Typemap failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + + throw new TypeMapException ( + $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + "Ensure the type has a generated proxy in the TypeMap assembly."); } var proxy = proxyType.GetCustomAttribute (inherit: false); if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new InvalidOperationException ( - $"Typemap failed to create peer for type '{targetType.FullName}'. " + + throw new TypeMapException ( + $"Failed to create peer for type '{targetType.FullName}'. " + "Ensure the type has a generated proxy in the TypeMap assembly."); } } @@ -194,30 +210,24 @@ static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHa string? className = null; try { if (s_instance is null) { - Environment.FailFast ("TrimmableTypeMap: OnRegisterNatives called before TrimmableTypeMap was initialized."); return; } var classRef = new JniObjectReference (nativeClassHandle); className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef); if (className is null) { - Environment.FailFast ("TrimmableTypeMap: Failed to get JNI class name from class reference."); return; } if (!s_instance._typeMap.TryGetValue (className, out var type)) { - Environment.FailFast ($"TrimmableTypeMap: Class '{className}' not found in the typemap. This is a bug in the typemap generator."); return; } var proxy = type.GetCustomAttribute (inherit: false); - if (proxy is not IAndroidCallableWrapper acw) { - Environment.FailFast ($"TrimmableTypeMap: Proxy for class '{className}' does not implement IAndroidCallableWrapper. This is a bug in the typemap generator."); - return; + if (proxy is IAndroidCallableWrapper acw) { + using var jniType = new JniType (className); + acw.RegisterNatives (jniType); } - - using var jniType = new JniType (className); - acw.RegisterNatives (jniType); } catch (Exception ex) { Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 12bb3a01446..d7da346bea2 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -320,6 +320,7 @@ + diff --git a/src/native/clr/host/host-jni.cc b/src/native/clr/host/host-jni.cc index e8aaeb8ca6f..a41c1c507cc 100644 --- a/src/native/clr/host/host-jni.cc +++ b/src/native/clr/host/host-jni.cc @@ -25,12 +25,6 @@ JNICALL Java_mono_android_Runtime_register (JNIEnv *env, [[maybe_unused]] jclass Host::Java_mono_android_Runtime_register (env, managedType, nativeClass, methods); } -JNIEXPORT void -JNICALL Java_mono_android_Runtime_registerNatives (JNIEnv *env, [[maybe_unused]] jclass klass, jclass nativeClass) -{ - Host::Java_mono_android_Runtime_registerNatives (env, nativeClass); -} - JNIEXPORT void JNICALL Java_mono_android_Runtime_initInternal (JNIEnv *env, jclass klass, jstring lang, jobjectArray runtimeApksJava, jstring runtimeNativeLibDir, jobjectArray appDirs, jint localDateTimeOffset, jobject loader, diff --git a/src/native/clr/include/host/host-jni.hh b/src/native/clr/include/host/host-jni.hh index a3d18ed60a0..4904644ebd8 100644 --- a/src/native/clr/include/host/host-jni.hh +++ b/src/native/clr/include/host/host-jni.hh @@ -45,11 +45,4 @@ extern "C" { */ JNIEXPORT void JNICALL Java_mono_android_Runtime_register (JNIEnv *, jclass, jstring, jclass, jstring); - /* - * Class: mono_android_Runtime - * Method: registerNatives - * Signature: (Ljava/lang/Class;)V - */ - JNIEXPORT void JNICALL Java_mono_android_Runtime_registerNatives (JNIEnv *, jclass, jclass); - } From f6aa9d2d2ee6f3ec82e2d6232a5e06132b393ccd Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 23:46:47 +0100 Subject: [PATCH 22/25] Remove null-forgiving operator from JNIEnvInit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- external/xamarin-android-tools | 2 +- src/Mono.Android/Android.Runtime/JNIEnvInit.cs | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index fb0952fad89..c14ba04c7fa 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a +Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index 40b30131791..d679f2becba 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit 40b30131791e7e996e20d461f8d3694b273f6985 +Subproject commit d679f2becbac319c1ef35934d40866d87996f7b0 diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 1e7dee9961d..8d0ef63f3ea 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -82,7 +82,10 @@ static Type TypeGetType (string typeName) => JniType.GetCachedJniType (ref jniType, className); ReadOnlySpan methods = new ReadOnlySpan ((void*) methods_ptr, methods_len); - androidRuntime!.TypeManager.RegisterNativeMembers (jniType, type, methods); + if (androidRuntime is null) { + throw new InvalidOperationException ("androidRuntime has not been initialized"); + } + androidRuntime.TypeManager.RegisterNativeMembers (jniType, type, methods); } // This must be called by NativeAOT before InitializeJniRuntime, as early as possible @@ -159,8 +162,8 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); - if (RuntimeFeature.TrimmableTypeMap) { - trimmableTypeMap!.RegisterBootstrapNativeMethod (); + if (RuntimeFeature.TrimmableTypeMap && trimmableTypeMap is not null) { + trimmableTypeMap.RegisterBootstrapNativeMethod (); } grefIGCUserPeer_class = args->grefIGCUserPeer; From 7348f1743a5432b0fbca3d7796ddc199c10cae21 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 23:50:50 +0100 Subject: [PATCH 23/25] Replace TypeMapException with InvalidOperationException Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- external/xamarin-android-tools | 2 +- src/Mono.Android/Java.Interop/TypeMapException.cs | 15 --------------- .../Microsoft.Android.Runtime/TrimmableTypeMap.cs | 4 ++-- src/Mono.Android/Mono.Android.csproj | 1 - 5 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/Mono.Android/Java.Interop/TypeMapException.cs diff --git a/external/Java.Interop b/external/Java.Interop index c14ba04c7fa..fb0952fad89 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 +Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index d679f2becba..40b30131791 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit d679f2becbac319c1ef35934d40866d87996f7b0 +Subproject commit 40b30131791e7e996e20d461f8d3694b273f6985 diff --git a/src/Mono.Android/Java.Interop/TypeMapException.cs b/src/Mono.Android/Java.Interop/TypeMapException.cs deleted file mode 100644 index e428d2b02cd..00000000000 --- a/src/Mono.Android/Java.Interop/TypeMapException.cs +++ /dev/null @@ -1,15 +0,0 @@ -#nullable enable - -using System; - -namespace Java.Interop -{ - /// - /// Exception thrown when a type mapping operation fails at runtime. - /// - public sealed class TypeMapException : Exception - { - public TypeMapException (string message) : base (message) { } - public TypeMapException (string message, Exception innerException) : base (message, innerException) { } - } -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 139c08f0c06..cf15020a5cc 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -165,14 +165,14 @@ internal static void ActivateInstance (IntPtr self, Type targetType) JniObjectReference.Dispose (ref jniClass); if (className is null || !instance._typeMap.TryGetValue (className, out var proxyType)) { - throw new TypeMapException ( + throw new InvalidOperationException ( $"Failed to create peer for type '{targetType.FullName}' (jniClass='{className}'). " + "Ensure the type has a generated proxy in the TypeMap assembly."); } var proxy = proxyType.GetCustomAttribute (inherit: false); if (proxy is null || proxy.CreateInstance (self, JniHandleOwnership.DoNotTransfer) is null) { - throw new TypeMapException ( + throw new InvalidOperationException ( $"Failed to create peer for type '{targetType.FullName}'. " + "Ensure the type has a generated proxy in the TypeMap assembly."); } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index d7da346bea2..12bb3a01446 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -320,7 +320,6 @@ - From 2b807371994b5191c368fed9423d6f43db2d63b4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sat, 21 Mar 2026 23:51:51 +0100 Subject: [PATCH 24/25] Guard GetContainerFactory calls with RuntimeFeature.TrimmableTypeMap Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/JNIEnv.cs | 8 +++++--- src/Mono.Android/Java.Interop/JavaConvert.cs | 6 +++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 315bfee4449..8b004855ba8 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -26,9 +26,11 @@ public static partial class JNIEnv { static Array ArrayCreateInstance (Type elementType, int length) { - var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); - if (factory != null) - return factory.CreateArray (length, 1); + if (RuntimeFeature.TrimmableTypeMap) { + var factory = TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + if (factory is not null) + return factory.CreateArray (length, 1); + } #pragma warning disable IL3050 // Array.CreateInstance is not AOT-safe, but this is the legacy fallback path return Array.CreateInstance (elementType, length); diff --git a/src/Mono.Android/Java.Interop/JavaConvert.cs b/src/Mono.Android/Java.Interop/JavaConvert.cs index 0c0000732b2..ebff6c1f765 100644 --- a/src/Mono.Android/Java.Interop/JavaConvert.cs +++ b/src/Mono.Android/Java.Interop/JavaConvert.cs @@ -148,7 +148,11 @@ params Type [] typeArguments if (!typeof (IJavaPeerable).IsAssignableFrom (elementType)) return null; - return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + if (RuntimeFeature.TrimmableTypeMap) { + return TrimmableTypeMap.Instance?.GetContainerFactory (elementType); + } + + return null; } static Func GetJniHandleConverterForType ([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] Type t) From 57711b3c4525fdd8f14f144dccb88fe3057bead9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 22 Mar 2026 07:50:34 +0100 Subject: [PATCH 25/25] Reset submodule pointers to match main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- external/Java.Interop | 2 +- external/xamarin-android-tools | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/external/Java.Interop b/external/Java.Interop index fb0952fad89..c14ba04c7fa 160000 --- a/external/Java.Interop +++ b/external/Java.Interop @@ -1 +1 @@ -Subproject commit fb0952fad89af5a2cdddb47052cac2d42c79886a +Subproject commit c14ba04c7faf689b211ad6a1df3eda43ab477b13 diff --git a/external/xamarin-android-tools b/external/xamarin-android-tools index 40b30131791..d679f2becba 160000 --- a/external/xamarin-android-tools +++ b/external/xamarin-android-tools @@ -1 +1 @@ -Subproject commit 40b30131791e7e996e20d461f8d3694b273f6985 +Subproject commit d679f2becbac319c1ef35934d40866d87996f7b0