From 085cc2b225971b63b0784994563f6c6ec874e75c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 19:02:42 +0200 Subject: [PATCH 01/13] [NativeAOT] Initialize trimmable typemap runtime Wire the trimmable typemap into NativeAOT startup and ILC inputs so generated typemap assemblies are included in the NativeAOT closure and runtime state is initialized before managed peer creation. Also add an explicit NativeAOT runtime feature switch for runtime code paths that should not be treated as MonoVM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 2 ++ .../Android.Runtime/AndroidRuntimeInternal.cs | 2 +- src/Mono.Android/Android.Runtime/JNIEnv.cs | 2 +- .../Android.Runtime/JNIEnvInit.cs | 30 +++++++++++++++++++ .../RuntimeFeature.cs | 5 ++++ .../Microsoft.Android.Sdk.NativeAOT.targets | 4 +++ ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 14 +++++---- src/native/nativeaot/host/host.cc | 10 +++++++ 8 files changed, 62 insertions(+), 7 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 47bc0e0a673..f3a8e5fbe51 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -57,6 +57,7 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua // This needs to be called first, since it sets up locations, environment variables, logging etc XA_Host_NativeAOT_OnInit (language, filesDir, cacheDir, ref initArgs); JNIEnvInit.InitializeJniRuntimeEarly (initArgs); + JNIEnvInit.InitializeNativeAotTrimmableTypeMapData (); var settings = new DiagnosticSettings (); settings.AddDebugDotnetLog (); @@ -75,6 +76,7 @@ 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); + JNIEnvInit.RegisterNativeAotTrimmableTypeMapNativeMethods (); transition = new JniTransition (jnienv); diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs index 73d20f6397b..84499d19c7c 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs @@ -17,7 +17,7 @@ static AndroidRuntimeInternal () { if (RuntimeFeature.IsMonoRuntime) { mono_unhandled_exception = MonoUnhandledException; - } else if (RuntimeFeature.IsCoreClrRuntime) { + } else if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime) { mono_unhandled_exception = CoreClrUnhandledException; } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 20b635cfb70..dc007e2d534 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -136,7 +136,7 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt if (RuntimeFeature.IsMonoRuntime) { MonoDroidUnhandledException (innerException ?? javaException); - } else if (RuntimeFeature.IsCoreClrRuntime) { + } else if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime) { ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent (innerException ?? javaException); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 2e8898e4616..e2892335816 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -105,15 +105,45 @@ internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args) Logger.SetLogCategories ((LogCategories)args.logCategories); } + internal static void InitializeNativeAotTrimmableTypeMapData () + { + if (RuntimeFeature.TrimmableTypeMap) { + InitializeTrimmableTypeMapData (); + } + } + // NOTE: should have different name than `Initialize` to avoid: // * Assertion at /__w/1/s/src/mono/mono/metadata/icall.c:6258, condition `!only_unmanaged_callers_only' not met internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args) { + gref_gc_threshold = args.grefGcThreshold; + + jniRemappingInUse = args.jniRemappingInUse; + MarshalMethodsEnabled = args.marshalMethodsEnabled; + java_class_loader = args.grefLoader; + + BoundExceptionType = (BoundExceptionType)args.ioExceptionType; + androidRuntime = runtime; JniRuntime.SetCurrent (runtime); + + grefIGCUserPeer_class = args.grefIGCUserPeer; + grefGCUserPeerable_class = args.grefGCUserPeerable; + + PropagateExceptions = args.brokenExceptionTransitions == 0; + + JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args.packageNamingPolicy; + SetSynchronizationContext (); } + internal static void RegisterNativeAotTrimmableTypeMapNativeMethods () + { + if (RuntimeFeature.TrimmableTypeMap) { + TrimmableTypeMap.RegisterNativeMethods (); + } + } + [UnmanagedCallersOnly] internal static unsafe void Initialize (JnienvInitializeArgs* args) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 0493790657d..5d20f9e5ac4 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -8,6 +8,7 @@ static class RuntimeFeature const bool ManagedTypeMapEnabledByDefault = false; const bool IsMonoRuntimeEnabledByDefault = true; const bool IsCoreClrRuntimeEnabledByDefault = false; + const bool IsNativeAotRuntimeEnabledByDefault = false; const bool IsAssignableFromCheckEnabledByDefault = true; const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; @@ -28,6 +29,10 @@ static class RuntimeFeature internal static bool IsCoreClrRuntime { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsCoreClrRuntime)}", out bool isEnabled) ? isEnabled : IsCoreClrRuntimeEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsNativeAotRuntime)}")] + internal static bool IsNativeAotRuntime { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsNativeAotRuntime)}", out bool isEnabled) ? isEnabled : IsNativeAotRuntimeEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (IsAssignableFromCheck)}")] internal static bool IsAssignableFromCheck { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (IsAssignableFromCheck)}", out bool isEnabled) ? isEnabled : IsAssignableFromCheckEnabledByDefault; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets index a6b22f41b7d..be03d887905 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.NativeAOT.targets @@ -50,6 +50,10 @@ This file contains the NativeAOT-specific MSBuild logic for .NET for Android. Value="false" Trim="true" /> + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index cc96c228bc7..450f48c2053 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -6,15 +6,19 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider - + BeforeTargets="_AndroidComputeIlcCompileInputs" + DependsOnTargets="_GenerateTrimmableTypeMap"> + + $(_TypeMapAssemblyName) + - - + <_TrimmableTypeMapIlcAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + + diff --git a/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index bf43f0f47de..732ee24a743 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -68,5 +68,15 @@ void Host::OnInit (jstring language, jstring filesDir, jstring cacheDir, JnienvI // We expect the struct to be initialized by the managed land the way it sees fit, we set only the // fields we support. + jclass lrefIGCUserPeer = env->FindClass ("mono/android/IGCUserPeer"); + jclass lrefGCUserPeerable = env->FindClass ("net/dot/jni/GCUserPeerable"); + abort_unless (lrefIGCUserPeer != nullptr && lrefGCUserPeerable != nullptr, "Failed to load GC user peer classes"); + initArgs->logCategories = log_categories; + initArgs->grefGcThreshold = static_cast(AndroidSystem::get_gref_gc_threshold ()); + initArgs->grefIGCUserPeer = env->NewGlobalRef (lrefIGCUserPeer); + initArgs->grefGCUserPeerable = env->NewGlobalRef (lrefGCUserPeerable); + + env->DeleteLocalRef (lrefIGCUserPeer); + env->DeleteLocalRef (lrefGCUserPeerable); } From bfb27ac8f71f84fa30bc912fb54f7b03de37a0ab Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 19:27:55 +0200 Subject: [PATCH 02/13] [NativeAOT] Use conditional trimmable typemap entries Make ForceUnconditionalEntries configurable and disable it for NativeAOT trimmable typemap testing so framework bindings can be conditionally rooted. Keep the existing workaround enabled by default for other trimmable typemap configurations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 30 +++++++++---------- .../Generator/TypeMapAssemblyGenerator.cs | 4 +-- .../TrimmableTypeMapGenerator.cs | 9 +++--- ...id.Sdk.TypeMap.Trimmable.NativeAOT.targets | 1 + ...soft.Android.Sdk.TypeMap.Trimmable.targets | 3 +- .../Tasks/GenerateTrimmableTypeMap.cs | 6 ++-- .../Generator/TypeMapModelBuilderTests.cs | 15 ++++++++-- 7 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 7547c5ac38f..c736a1b2054 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,13 +16,6 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; - // Workaround for https://github.com/dotnet/runtime/issues/127004 - // When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the - // trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute - // references the same type. Set to false once the runtime bug is fixed to re-enable - // 3-arg conditional entries that allow unused framework bindings to be trimmed away. - const bool ForceUnconditionalEntries = true; - static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -44,7 +37,7 @@ static class ModelBuilder /// Emit per-rank array TypeMap entries + __ArrayMapRank{N} sentinels /// for ranks 1... 0 disables array entry emission. /// - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0) + public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0, bool forceUnconditionalEntries = true) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -98,6 +91,11 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); + if (maxArrayRank > 0) { + EmitArrayEntries (model, jniName, peersForName, maxArrayRank); + } + EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries); + if (maxArrayRank > 0) { EmitArrayEntries (model, jniName, peersForName, maxArrayRank); } @@ -125,7 +123,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri } static void EmitPeers (TypeMapAssemblyData model, string jniName, - List peersForName, string assemblyName, HashSet usedProxyNames) + List peersForName, string assemblyName, HashSet usedProxyNames, bool forceUnconditionalEntries) { bool isAliasGroup = peersForName.Count > 1; @@ -141,7 +139,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - var entry = BuildEntry (peer, proxy, assemblyName, jniName); + var entry = BuildEntry (peer, proxy, assemblyName, jniName, forceUnconditionalEntries); model.Entries.Add (entry); // Emit a TypeMapAssociation for every entry that has a proxy. @@ -176,7 +174,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName)); + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName, forceUnconditionalEntries)); // Link each alias type to the alias holder for trimming model.Associations.Add (new TypeMapAssociationData { @@ -189,12 +187,12 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, } // Base JNI name entry → alias holder (self-referencing trim target, kept alive by associations) - // When ForceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just + // When forceUnconditionalEntries is true we MUST emit this as 2-arg (unconditional) just // like BuildEntry does: dotnet/runtime#127004 strips the TypeMapAssociation that keeps the // holder alive when a TypeMap entry references the same type, leaving the dictionary key // missing at runtime and breaking hierarchy lookups for essential types like // java/lang/String and java/lang/Object. - bool aliasBaseUnconditional = ForceUnconditionalEntries + bool aliasBaseUnconditional = forceUnconditionalEntries || EssentialRuntimeTypes.Contains (jniName) || peersForName.Any (IsUnconditionalEntry); model.Entries.Add (new TypeMapAttributeData { @@ -395,7 +393,7 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, - string outputAssemblyName, string jniName) + string outputAssemblyName, string jniName, bool forceUnconditionalEntries) { string proxyRef; if (proxy != null) { @@ -404,9 +402,9 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } - // When ForceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap + // When forceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap // attributes to work around https://github.com/dotnet/runtime/issues/127004. - bool isUnconditional = ForceUnconditionalEntries || IsUnconditionalEntry (peer); + bool isUnconditional = forceUnconditionalEntries || IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 48ca89f45bc..e223e2b0e3b 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -28,9 +28,9 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// /// Max rank for per-rank array TypeMap entries. 0 disables. - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0) + public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0, bool forceUnconditionalEntries = true) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, forceUnconditionalEntries); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 090075d73bf..bd343f1f2a1 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -39,7 +39,8 @@ public TrimmableTypeMapResult Execute ( bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, - int maxArrayRank = 0) + int maxArrayRank = 0, + bool forceUnconditionalEntries = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -63,7 +64,7 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank); + var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, forceUnconditionalEntries); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -154,7 +155,7 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank) + List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank, bool forceUnconditionalEntries) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -183,7 +184,7 @@ List GenerateTypeMapAssemblies (List allPeers, string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank, forceUnconditionalEntries); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 450f48c2053..bbc31d83f03 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -4,6 +4,7 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider + <_TrimmableTypeMapForceUnconditionalEntries Condition=" '$(_TrimmableTypeMapForceUnconditionalEntries)' == '' ">false <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 + <_TrimmableTypeMapForceUnconditionalEntries Condition=" '$(_TrimmableTypeMapForceUnconditionalEntries)' == '' ">true @@ -84,6 +84,7 @@ NeedsInternet="$(AndroidNeedsInternetPermission)" EmbedAssemblies="$(EmbedAssembliesIntoApk)" MaxArrayRank="$(_AndroidTrimmableTypeMapMaxArrayRank)" + ForceUnconditionalEntries="$(_TrimmableTypeMapForceUnconditionalEntries)" ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index d3797bbddb5..a6950b7d3b3 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -68,13 +68,14 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public bool Debug { get; set; } public bool NeedsInternet { get; set; } public bool EmbedAssemblies { get; set; } - /// /// Maximum array rank for which the generator emits per-rank __ArrayMapRank{N} /// sentinels and TypeMap entries. 0 disables. Set via /// $(_AndroidTrimmableTypeMapMaxArrayRank). /// public int MaxArrayRank { get; set; } + + public bool ForceUnconditionalEntries { get; set; } = true; public string? ManifestPlaceholders { get; set; } public string? CheckedBuild { get; set; } public string? ApplicationJavaClass { get; set; } @@ -139,7 +140,8 @@ public override bool RunTask () useSharedTypemapUniverse: !Debug, manifestConfig, manifestTemplate, - maxArrayRank: MaxArrayRank); + maxArrayRank: MaxArrayRank, + forceUnconditionalEntries: ForceUnconditionalEntries); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 049bc74a654..c36254fdeba 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -10,10 +10,10 @@ namespace Microsoft.Android.Sdk.TrimmableTypeMap.Tests; public class ModelBuilderTests : FixtureTestBase { - static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null) + static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null, bool forceUnconditionalEntries = true) { var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); - return ModelBuilder.Build (peers, outputPath, assemblyName); + return ModelBuilder.Build (peers, outputPath, assemblyName, forceUnconditionalEntries); } static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3) @@ -182,6 +182,17 @@ public void Build_McwBinding_IsTrimmable () Assert.Null (model.Entries [0].TargetTypeReference); } + [Fact] + public void Build_McwBinding_IsConditional_WhenForceUnconditionalEntriesDisabled () + { + var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true }; + var model = BuildModel (new [] { peer }, forceUnconditionalEntries: false); + + Assert.Single (model.Entries); + Assert.False (model.Entries [0].IsUnconditional); + Assert.Equal ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference); + } + [Fact] public void Build_UnconditionalScannedType_IsUnconditional () { From d6aeefaba0717e5be0baf7d5333a66459bb97fad Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 19:49:35 +0200 Subject: [PATCH 03/13] [NativeAOT] Avoid exporting framework typemap assemblies Keep generated framework typemap assemblies as ILC references for type-map metadata, but do not pass them as UnmanagedEntryPointsAssembly inputs. This avoids treating framework typemap assemblies as unmanaged-entrypoint roots while preserving app typemap exports. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index bbc31d83f03..2a8085ccac4 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -18,8 +18,10 @@ <_TrimmableTypeMapIlcAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblies Include="@(_TrimmableTypeMapIlcAssemblies)" + Exclude="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapOutputDirectory)_Java.Interop.TypeMap.dll;$(_TypeMapOutputDirectory)_Mono.Android.TypeMap.dll" /> - + From e5908fc4e2ed90f387b8850d3c060c73d06fc1b5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 19:52:43 +0200 Subject: [PATCH 04/13] [NativeAOT] Simplify trimmable typemap configuration Clarify the ForceUnconditionalEntries plumbing and make the NativeAOT unmanaged-entrypoint assembly filtering easier to read without changing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 18 ++++++++---------- .../Generator/TypeMapAssemblyGenerator.cs | 9 ++++++++- .../TrimmableTypeMapGenerator.cs | 14 ++++++++++++-- ...oid.Sdk.TypeMap.Trimmable.NativeAOT.targets | 6 ++++-- .../Tasks/GenerateTrimmableTypeMap.cs | 4 ++-- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index c736a1b2054..6e1b74a21f2 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -37,7 +37,13 @@ static class ModelBuilder /// Emit per-rank array TypeMap entries + __ArrayMapRank{N} sentinels /// for ranks 1... 0 disables array entry emission. /// - public static TypeMapAssemblyData Build (IReadOnlyList peers, string outputPath, string? assemblyName = null, int maxArrayRank = 0, bool forceUnconditionalEntries = true) + /// True to emit all TypeMap entries as unconditional 2-arg attributes. + public static TypeMapAssemblyData Build ( + IReadOnlyList peers, + string outputPath, + string? assemblyName = null, + int maxArrayRank = 0, + bool forceUnconditionalEntries = true) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -89,11 +95,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); - - if (maxArrayRank > 0) { - EmitArrayEntries (model, jniName, peersForName, maxArrayRank); - } EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries); if (maxArrayRank > 0) { @@ -405,10 +406,7 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr // When forceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap // attributes to work around https://github.com/dotnet/runtime/issues/127004. bool isUnconditional = forceUnconditionalEntries || IsUnconditionalEntry (peer); - string? targetRef = null; - if (!isUnconditional) { - targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); - } + string? targetRef = isUnconditional ? null : AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); return new TypeMapAttributeData { JniName = jniName, diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index e223e2b0e3b..423475a9eb9 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -28,7 +28,14 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// /// Max rank for per-rank array TypeMap entries. 0 disables. - public void Generate (IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, int maxArrayRank = 0, bool forceUnconditionalEntries = true) + /// True to emit all TypeMap entries as unconditional 2-arg attributes. + public void Generate ( + IReadOnlyList peers, + Stream stream, + string assemblyName, + bool useSharedTypemapUniverse = false, + int maxArrayRank = 0, + bool forceUnconditionalEntries = true) { var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, forceUnconditionalEntries); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index bd343f1f2a1..12296b633cf 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -64,7 +64,12 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, forceUnconditionalEntries); + var generatedAssemblies = GenerateTypeMapAssemblies ( + allPeers, + systemRuntimeVersion, + useSharedTypemapUniverse, + maxArrayRank, + forceUnconditionalEntries); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -155,7 +160,12 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes return (peers, manifestInfo); } - List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank, bool forceUnconditionalEntries) + List GenerateTypeMapAssemblies ( + List allPeers, + Version systemRuntimeVersion, + bool useSharedTypemapUniverse, + int maxArrayRank, + bool forceUnconditionalEntries) { List<(string AssemblyName, List Peers)> peersByAssembly; diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets index 2a8085ccac4..be3b88b929c 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.NativeAOT.targets @@ -18,8 +18,10 @@ <_TrimmableTypeMapIlcAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> - <_TrimmableTypeMapUnmanagedEntryPointAssemblies Include="@(_TrimmableTypeMapIlcAssemblies)" - Exclude="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll;$(_TypeMapOutputDirectory)_Java.Interop.TypeMap.dll;$(_TypeMapOutputDirectory)_Mono.Android.TypeMap.dll" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblies Include="@(_TrimmableTypeMapIlcAssemblies)" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="$(_TypeMapOutputDirectory)$(_TypeMapAssemblyName).dll" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="$(_TypeMapOutputDirectory)_Java.Interop.TypeMap.dll" /> + <_TrimmableTypeMapUnmanagedEntryPointAssemblies Remove="$(_TypeMapOutputDirectory)_Mono.Android.TypeMap.dll" /> diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index a6950b7d3b3..4fc8f361619 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -138,8 +138,8 @@ public override bool RunTask () systemRuntimeVersion, frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, - manifestConfig, - manifestTemplate, + manifestConfig: manifestConfig, + manifestTemplate: manifestTemplate, maxArrayRank: MaxArrayRank, forceUnconditionalEntries: ForceUnconditionalEntries); From ea508240d3f7789f96bf1fe3fe4df5b17fdd1f1b Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 19:58:30 +0200 Subject: [PATCH 05/13] [NativeAOT] Handle GC peer class lookup failures Check each NativeAOT GC peer FindClass result before continuing so a failed lookup does not leave a pending JNI exception while another JNI lookup runs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/nativeaot/host/host.cc | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index 732ee24a743..b19bc9750ac 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -69,8 +69,18 @@ void Host::OnInit (jstring language, jstring filesDir, jstring cacheDir, JnienvI // We expect the struct to be initialized by the managed land the way it sees fit, we set only the // fields we support. jclass lrefIGCUserPeer = env->FindClass ("mono/android/IGCUserPeer"); + if (lrefIGCUserPeer == nullptr) [[unlikely]] { + env->ExceptionDescribe (); + env->ExceptionClear (); + abort_unless (false, "Failed to load mono/android/IGCUserPeer class"); + } + jclass lrefGCUserPeerable = env->FindClass ("net/dot/jni/GCUserPeerable"); - abort_unless (lrefIGCUserPeer != nullptr && lrefGCUserPeerable != nullptr, "Failed to load GC user peer classes"); + if (lrefGCUserPeerable == nullptr) [[unlikely]] { + env->ExceptionDescribe (); + env->ExceptionClear (); + abort_unless (false, "Failed to load net/dot/jni/GCUserPeerable class"); + } initArgs->logCategories = log_categories; initArgs->grefGcThreshold = static_cast(AndroidSystem::get_gref_gc_threshold ()); From e12293b7e294b31f4ed6124af3fc28a5c7315936 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 5 May 2026 23:56:43 +0200 Subject: [PATCH 06/13] [TrimmableTypeMap] Treat framework ACWs as conditional NativeAOT trimmable typemap generation scans framework assemblies so runtime-needed framework peers remain available, but framework ACW implementors should not be blanket unconditional roots. Classify framework inputs before model generation and emit framework ACWs as conditional trim-target entries. Add summary logging and regression coverage for duplicate MSBuild input items where only one copy carries framework metadata. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 16 ++++ .../Generator/ModelBuilder.cs | 85 ++++++++++++++----- .../ITrimmableTypeMapLogger.cs | 1 + .../TrimmableTypeMapGenerator.cs | 33 ++++++- ...soft.Android.Sdk.TypeMap.Trimmable.targets | 1 + .../Tasks/GenerateTrimmableTypeMap.cs | 28 ++++-- .../Tasks/GenerateTrimmableTypeMapTests.cs | 70 +++++++++++++++ .../TrimmableTypeMapGeneratorTests.cs | 11 +++ .../Generator/TypeMapModelBuilderTests.cs | 21 ++++- 9 files changed, 236 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index b9126586bf4..440b0000a9d 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -79,6 +79,22 @@ sealed record TypeMapAttributeData /// public string? TargetTypeReference { get; init; } + /// + /// Human-readable explanation for why this entry is unconditional or conditional. + /// + public required string InclusionReason { get; init; } + + /// + /// Managed type that caused this entry to be emitted, when the entry maps directly to a peer. + /// Alias-holder entries and synthetic entries may leave this unset. + /// + public string? SourceManagedTypeName { get; init; } + + /// + /// Assembly containing , when known. + /// + public string? SourceAssemblyName { get; init; } + /// /// True for 2-arg unconditional entries (ACW types, essential runtime types). /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 6e1b74a21f2..06a470d8642 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,6 +16,8 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; + readonly record struct InclusionDecision (bool IsUnconditional, string Reason); + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -43,7 +45,8 @@ public static TypeMapAssemblyData Build ( string outputPath, string? assemblyName = null, int maxArrayRank = 0, - bool forceUnconditionalEntries = true) + bool forceUnconditionalEntries = true, + ISet? frameworkAssemblyNames = null) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -95,7 +98,7 @@ public static TypeMapAssemblyData Build ( peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries); + EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries, frameworkAssemblyNames); if (maxArrayRank > 0) { EmitArrayEntries (model, jniName, peersForName, maxArrayRank); @@ -124,7 +127,8 @@ public static TypeMapAssemblyData Build ( } static void EmitPeers (TypeMapAssemblyData model, string jniName, - List peersForName, string assemblyName, HashSet usedProxyNames, bool forceUnconditionalEntries) + List peersForName, string assemblyName, HashSet usedProxyNames, bool forceUnconditionalEntries, + ISet? frameworkAssemblyNames) { bool isAliasGroup = peersForName.Count > 1; @@ -140,7 +144,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - var entry = BuildEntry (peer, proxy, assemblyName, jniName, forceUnconditionalEntries); + var entry = BuildEntry (peer, proxy, assemblyName, jniName, forceUnconditionalEntries, frameworkAssemblyNames); model.Entries.Add (entry); // Emit a TypeMapAssociation for every entry that has a proxy. @@ -175,7 +179,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName, forceUnconditionalEntries)); + model.Entries.Add (BuildEntry (peer, proxy, assemblyName, entryJniName, forceUnconditionalEntries, frameworkAssemblyNames)); // Link each alias type to the alias holder for trimming model.Associations.Add (new TypeMapAssociationData { @@ -195,11 +199,15 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // java/lang/String and java/lang/Object. bool aliasBaseUnconditional = forceUnconditionalEntries || EssentialRuntimeTypes.Contains (jniName) - || peersForName.Any (IsUnconditionalEntry); + || peersForName.Any (p => IsUnconditionalEntry (p, frameworkAssemblyNames)); + string aliasBaseReason = GetAliasBaseInclusionReason (jniName, peersForName, forceUnconditionalEntries, frameworkAssemblyNames); model.Entries.Add (new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = holderRef, TargetTypeReference = aliasBaseUnconditional ? null : holderRef, + InclusionReason = aliasBaseReason, + SourceManagedTypeName = $"{holderNamespace}.{holderTypeName}", + SourceAssemblyName = assemblyName, }); model.AliasHolders.Add (new AliasHolderData { @@ -229,25 +237,56 @@ static void AddProxyAssociation (TypeMapAssemblyData model, string managedTypeNa /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. /// Unconditional types are always preserved by the trimmer. /// - static bool IsUnconditionalEntry (JavaPeerInfo peer) + static bool IsUnconditionalEntry (JavaPeerInfo peer, ISet? frameworkAssemblyNames) { - // Essential runtime types needed by the Java interop runtime + return GetInclusionDecision (peer, forceUnconditionalEntries: false, frameworkAssemblyNames).IsUnconditional; + } + + static InclusionDecision GetInclusionDecision (JavaPeerInfo peer, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) + { + if (forceUnconditionalEntries) { + return new InclusionDecision (true, "force-unconditional entries are enabled"); + } + if (EssentialRuntimeTypes.Contains (peer.JavaName)) { - return true; + return new InclusionDecision (true, "essential Java interop runtime type"); } - // User-defined ACW types (not MCW bindings, not interfaces) are unconditional - // because Android can instantiate them from Java at any time. - if (!peer.DoNotGenerateAcw && !peer.IsInterface) { - return true; + if (!peer.DoNotGenerateAcw && !peer.IsInterface && !IsFrameworkAssembly (peer, frameworkAssemblyNames)) { + return new InclusionDecision (true, "user ACW type can be instantiated from Java"); } - // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.) if (peer.IsUnconditional) { - return true; + return new InclusionDecision (true, "scanner marked the peer unconditional"); + } + + if (!peer.DoNotGenerateAcw && !peer.IsInterface) { + return new InclusionDecision (false, "conditional framework ACW candidate"); } - return false; + return new InclusionDecision (false, "conditional trim-target entry"); + } + + static string GetAliasBaseInclusionReason (string jniName, List peersForName, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) + { + if (forceUnconditionalEntries) { + return "alias holder is unconditional because force-unconditional entries are enabled"; + } + if (EssentialRuntimeTypes.Contains (jniName)) { + return "alias holder is unconditional because the JNI name is an essential Java interop runtime type"; + } + foreach (var peer in peersForName) { + var decision = GetInclusionDecision (peer, forceUnconditionalEntries: false, frameworkAssemblyNames); + if (decision.IsUnconditional) { + return $"alias holder is unconditional because {peer.ManagedTypeName} is unconditional: {decision.Reason}"; + } + } + return "alias holder is conditional on the holder type"; + } + + static bool IsFrameworkAssembly (JavaPeerInfo peer, ISet? frameworkAssemblyNames) + { + return frameworkAssemblyNames is not null && frameworkAssemblyNames.Contains (peer.AssemblyName); } static void AddIfCrossAssembly (SortedSet set, string? asmName, string outputAssemblyName) @@ -394,7 +433,8 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, - string outputAssemblyName, string jniName, bool forceUnconditionalEntries) + string outputAssemblyName, string jniName, bool forceUnconditionalEntries, + ISet? frameworkAssemblyNames) { string proxyRef; if (proxy != null) { @@ -403,15 +443,17 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } - // When forceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap - // attributes to work around https://github.com/dotnet/runtime/issues/127004. - bool isUnconditional = forceUnconditionalEntries || IsUnconditionalEntry (peer); + var inclusion = GetInclusionDecision (peer, forceUnconditionalEntries, frameworkAssemblyNames); + bool isUnconditional = inclusion.IsUnconditional; string? targetRef = isUnconditional ? null : AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); return new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = proxyRef, TargetTypeReference = targetRef, + InclusionReason = inclusion.Reason, + SourceManagedTypeName = peer.ManagedTypeName, + SourceAssemblyName = peer.AssemblyName, }; } @@ -444,6 +486,9 @@ static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -165,7 +166,8 @@ List GenerateTypeMapAssemblies ( Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank, - bool forceUnconditionalEntries) + bool forceUnconditionalEntries, + HashSet frameworkAssemblyNames) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -189,12 +191,21 @@ List GenerateTypeMapAssemblies ( var generatedAssemblies = new List (); var perAssemblyNames = new List (); - var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion); foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); + var model = ModelBuilder.Build ( + peers, + typeMapAssemblyName + ".dll", + typeMapAssemblyName, + maxArrayRank: maxArrayRank, + forceUnconditionalEntries: forceUnconditionalEntries, + frameworkAssemblyNames: frameworkAssemblyNames); + LogTypeMapAssemblyDetails (typeMapAssemblyName, model); + var stream = new MemoryStream (); - generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank, forceUnconditionalEntries); + var emitter = new TypeMapAssemblyEmitter (systemRuntimeVersion); + emitter.Emit (model, stream, useSharedTypemapUniverse); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); @@ -209,6 +220,20 @@ List GenerateTypeMapAssemblies ( return generatedAssemblies; } + void LogTypeMapAssemblyDetails (string typeMapAssemblyName, TypeMapAssemblyData model) + { + int unconditionalCount = model.Entries.Count (e => e.IsUnconditional); + int conditionalCount = model.Entries.Count - unconditionalCount; + logger.LogGeneratedTypeMapAssemblySummary ( + typeMapAssemblyName, + model.Entries.Count, + unconditionalCount, + conditionalCount, + model.ProxyTypes.Count, + model.Associations.Count, + model.AliasHolders.Count); + } + /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 511a9a33156..47dfcc96729 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -68,6 +68,7 @@ log.LogMessage (MessageImportance.Low, $"Found {typeCount} Application/Instrumentation types for deferred registration."); public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => log.LogMessage (MessageImportance.Low, $" {assemblyName}: {typeCount} types"); + public void LogGeneratedTypeMapAssemblySummary (string assemblyName, int entryCount, int unconditionalEntryCount, int conditionalEntryCount, int proxyTypeCount, int associationCount, int aliasHolderCount) => + log.LogMessage (MessageImportance.Low, $" {assemblyName}: TypeMap entries={entryCount} unconditional={unconditionalEntryCount} conditional={conditionalEntryCount} proxies={proxyTypeCount} associations={associationCount} alias-holders={aliasHolderCount}"); public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => log.LogMessage (MessageImportance.Low, $" Root: {assemblyReferenceCount} per-assembly refs"); public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => @@ -43,6 +45,7 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; + public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; [Required] public string OutputDirectory { get; set; } = ""; [Required] @@ -90,8 +93,15 @@ public void LogManifestReferencedTypeNotFoundWarning (string javaTypeName) => public override bool RunTask () { var systemRuntimeVersion = ParseTargetFrameworkVersion (TargetFrameworkVersion); - var assemblyPaths = ResolvedAssemblies.Select (i => i.ItemSpec).Distinct ().ToList (); - // TODO(#10792): populate with framework assembly names to skip JCW generation for pre-compiled framework types + var frameworkAssemblyPaths = new HashSet ( + ResolvedFrameworkAssemblies.Select (i => Path.GetFullPath (i.ItemSpec)), + StringComparer.OrdinalIgnoreCase); + var assemblyInputs = ResolvedAssemblies + .GroupBy (i => Path.GetFullPath (i.ItemSpec), StringComparer.OrdinalIgnoreCase) + .Select (g => ( + Path: g.Key, + IsFrameworkAssembly: frameworkAssemblyPaths.Contains (g.Key) || g.Any (IsFrameworkAssemblyItem))) + .ToList (); var frameworkAssemblyNames = new HashSet (StringComparer.OrdinalIgnoreCase); Directory.CreateDirectory (OutputDirectory); @@ -101,11 +111,15 @@ public override bool RunTask () var assemblies = new List<(string Name, PEReader Reader)> (); TrimmableTypeMapResult? result = null; try { - foreach (var path in assemblyPaths) { + foreach (var (path, isFrameworkAssembly) in assemblyInputs) { var peReader = new PEReader (File.OpenRead (path)); peReaders.Add (peReader); var mdReader = peReader.GetMetadataReader (); - assemblies.Add ((mdReader.GetString (mdReader.GetAssemblyDefinition ().Name), peReader)); + var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); + assemblies.Add ((assemblyName, peReader)); + if (isFrameworkAssembly) { + frameworkAssemblyNames.Add (assemblyName); + } } ManifestConfig? manifestConfig = null; @@ -143,7 +157,7 @@ public override bool RunTask () maxArrayRank: MaxArrayRank, forceUnconditionalEntries: ForceUnconditionalEntries); - GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyPaths); + GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); // Write manifest to disk if generated @@ -198,6 +212,10 @@ public override bool RunTask () return !Log.HasLoggedErrors; } + static bool IsFrameworkAssemblyItem (ITaskItem item) => + string.Equals (item.GetMetadata ("FrameworkAssembly"), bool.TrueString, StringComparison.OrdinalIgnoreCase) || + MonoAndroidHelper.IsFrameworkAssembly (item); + ITaskItem [] WriteAssembliesToDisk (IReadOnlyList assemblies, IReadOnlyList assemblyPaths) { // Build a map from assembly name -> source path for timestamp comparison diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index f5954e7e77d..eb894dd33ff 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using NUnit.Framework; @@ -77,6 +79,38 @@ public void Execute_WithMonoAndroid_ProducesOutputs () } } + [Test] + public void Execute_DuplicateFrameworkAssemblyItem_MakesFrameworkAcwConditional () + { + var path = Path.Combine ("temp", TestName); + var outputDir = Path.Combine (Root, path, "typemap"); + var javaDir = Path.Combine (Root, path, "java"); + + var monoAndroidItem = FindMonoAndroidDll (); + if (monoAndroidItem is null) { + Assert.Ignore ("Mono.Android.dll not found; skipping."); + return; + } + + var frameworkMonoAndroidItem = new TaskItem (monoAndroidItem.ItemSpec); + frameworkMonoAndroidItem.SetMetadata ("FrameworkAssembly", "True"); + + var task = CreateTask (new [] { monoAndroidItem, frameworkMonoAndroidItem }, outputDir, javaDir); + task.ForceUnconditionalEntries = false; + + Assert.IsTrue (task.Execute (), "Task should succeed."); + + var typeMapPath = task.GeneratedAssemblies + .Select (i => i.ItemSpec) + .First (p => p.Contains ("_Mono.Android.TypeMap.dll")); + var entry = ReadTypeMapAttribute ( + typeMapPath, + "mono/android/media/tv/TvView_OnUnhandledInputEventListenerImplementor"); + + Assert.AreEqual (3, entry.ParameterCount); + Assert.AreEqual ("Android.Media.TV.TvView+IOnUnhandledInputEventListenerImplementor, Mono.Android", entry.TrimTarget); + } + [Test] public void Execute_SecondRun_OutputsAreUpToDate () { @@ -200,5 +234,41 @@ GenerateTrimmableTypeMap CreateTask (ITaskItem [] assemblies, string outputDir, item.SetMetadata ("HasMonoAndroidReference", "True"); return item; } + + static (int ParameterCount, string? TrimTarget) ReadTypeMapAttribute (string assemblyPath, string javaName) + { + using var peReader = new PEReader (File.OpenRead (assemblyPath)); + var mdReader = peReader.GetMetadataReader (); + foreach (var customAttributeHandle in mdReader.GetAssemblyDefinition ().GetCustomAttributes ()) { + var customAttribute = mdReader.GetCustomAttribute (customAttributeHandle); + var parameterCount = GetConstructorParameterCount (mdReader, customAttribute.Constructor); + var blobReader = mdReader.GetBlobReader (customAttribute.Value); + if (blobReader.ReadUInt16 () != 1) { + continue; + } + var attributeJavaName = blobReader.ReadSerializedString (); + blobReader.ReadSerializedString (); + var trimTarget = parameterCount >= 3 ? blobReader.ReadSerializedString () : null; + if (attributeJavaName == javaName) { + return (parameterCount, trimTarget); + } + } + Assert.Fail ($"TypeMapAttribute for '{javaName}' was not found in '{assemblyPath}'."); + return default; + } + + static int GetConstructorParameterCount (MetadataReader mdReader, EntityHandle constructor) + { + BlobReader signatureReader; + if (constructor.Kind == HandleKind.MemberReference) { + var memberReference = mdReader.GetMemberReference ((MemberReferenceHandle) constructor); + signatureReader = mdReader.GetBlobReader (memberReference.Signature); + } else { + var methodDefinition = mdReader.GetMethodDefinition ((MethodDefinitionHandle) constructor); + signatureReader = mdReader.GetBlobReader (methodDefinition.Signature); + } + signatureReader.ReadSignatureHeader (); + return signatureReader.ReadCompressedInteger (); + } } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index c9f070dc146..75efac9beaa 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -24,6 +24,8 @@ public void LogDeferredRegistrationTypesInfo (int typeCount) => logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => logMessages.Add ($" {assemblyName}: {typeCount} types"); + public void LogGeneratedTypeMapAssemblySummary (string assemblyName, int entryCount, int unconditionalEntryCount, int conditionalEntryCount, int proxyTypeCount, int associationCount, int aliasHolderCount) => + logMessages.Add ($" {assemblyName}: TypeMap entries={entryCount} unconditional={unconditionalEntryCount} conditional={conditionalEntryCount} proxies={proxyTypeCount} associations={associationCount} alias-holders={aliasHolderCount}"); public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => @@ -72,6 +74,15 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } + [Fact] + public void Execute_LogsTypeMapEntrySummary () + { + using var peReader = CreateTestFixturePEReader (); + CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); + + Assert.Contains (logMessages, m => m.Contains ("_TestFixtures.TypeMap: TypeMap entries=", StringComparison.Ordinal)); + } + [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index c36254fdeba..af54bf84ff0 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -13,7 +13,13 @@ public class ModelBuilderTests : FixtureTestBase static TypeMapAssemblyData BuildModel (IReadOnlyList peers, string? assemblyName = null, bool forceUnconditionalEntries = true) { var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); - return ModelBuilder.Build (peers, outputPath, assemblyName, forceUnconditionalEntries); + return ModelBuilder.Build (peers, outputPath, assemblyName, forceUnconditionalEntries: forceUnconditionalEntries); + } + + static TypeMapAssemblyData BuildModel (IReadOnlyList peers, ISet frameworkAssemblyNames, string? assemblyName = null, bool forceUnconditionalEntries = true) + { + var outputPath = Path.Combine (Path.GetTempPath (), (assemblyName ?? "TestTypeMap") + ".dll"); + return ModelBuilder.Build (peers, outputPath, assemblyName, forceUnconditionalEntries: forceUnconditionalEntries, frameworkAssemblyNames: frameworkAssemblyNames); } static TypeMapAssemblyData BuildModelWithArrays (IReadOnlyList peers, string? assemblyName = null, int maxArrayRank = 3) @@ -168,6 +174,19 @@ public void Build_UserAcwType_IsUnconditional () Assert.Null (mainEntry.TargetTypeReference); } + [Fact] + public void Build_FrameworkAcwType_IsConditional_WhenForceUnconditionalEntriesDisabled () + { + var peer = MakeAcwPeer ("mono/android/media/tv/TvView_OnUnhandledInputEventListenerImplementor", + "Android.Media.TV.TvView+IOnUnhandledInputEventListenerImplementor", "Mono.Android"); + var model = BuildModel (new [] { peer }, new HashSet (StringComparer.OrdinalIgnoreCase) { "Mono.Android" }, forceUnconditionalEntries: false); + + var entry = Assert.Single (model.Entries); + Assert.False (entry.IsUnconditional); + Assert.Equal ("Android.Media.TV.TvView+IOnUnhandledInputEventListenerImplementor, Mono.Android", entry.TargetTypeReference); + Assert.Equal ("conditional framework ACW candidate", entry.InclusionReason); + } + [Fact] public void Build_McwBinding_IsTrimmable () { From 988eb036884e4550e2efa582d5d434b51c49e3b4 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:01:42 +0200 Subject: [PATCH 07/13] [TrimmableTypeMap] Remove inclusion decision diagnostics Remove the diagnostic InclusionDecision/InclusionReason plumbing from the typemap model while keeping the conditional vs. unconditional entry behavior unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/Model/TypeMapAssemblyData.cs | 16 ------ .../Generator/ModelBuilder.cs | 55 +++---------------- .../Generator/TypeMapModelBuilderTests.cs | 1 - 3 files changed, 8 insertions(+), 64 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs index 440b0000a9d..b9126586bf4 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/Model/TypeMapAssemblyData.cs @@ -79,22 +79,6 @@ sealed record TypeMapAttributeData /// public string? TargetTypeReference { get; init; } - /// - /// Human-readable explanation for why this entry is unconditional or conditional. - /// - public required string InclusionReason { get; init; } - - /// - /// Managed type that caused this entry to be emitted, when the entry maps directly to a peer. - /// Alias-holder entries and synthetic entries may leave this unset. - /// - public string? SourceManagedTypeName { get; init; } - - /// - /// Assembly containing , when known. - /// - public string? SourceAssemblyName { get; init; } - /// /// True for 2-arg unconditional entries (ACW types, essential runtime types). /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 06a470d8642..432cddf6e05 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,8 +16,6 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; - readonly record struct InclusionDecision (bool IsUnconditional, string Reason); - static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -199,15 +197,11 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, // java/lang/String and java/lang/Object. bool aliasBaseUnconditional = forceUnconditionalEntries || EssentialRuntimeTypes.Contains (jniName) - || peersForName.Any (p => IsUnconditionalEntry (p, frameworkAssemblyNames)); - string aliasBaseReason = GetAliasBaseInclusionReason (jniName, peersForName, forceUnconditionalEntries, frameworkAssemblyNames); + || peersForName.Any (p => IsUnconditionalEntry (p, forceUnconditionalEntries: false, frameworkAssemblyNames)); model.Entries.Add (new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = holderRef, TargetTypeReference = aliasBaseUnconditional ? null : holderRef, - InclusionReason = aliasBaseReason, - SourceManagedTypeName = $"{holderNamespace}.{holderTypeName}", - SourceAssemblyName = assemblyName, }); model.AliasHolders.Add (new AliasHolderData { @@ -237,51 +231,25 @@ static void AddProxyAssociation (TypeMapAssemblyData model, string managedTypeNa /// Determines whether a type should use the unconditional (2-arg) TypeMap attribute. /// Unconditional types are always preserved by the trimmer. /// - static bool IsUnconditionalEntry (JavaPeerInfo peer, ISet? frameworkAssemblyNames) - { - return GetInclusionDecision (peer, forceUnconditionalEntries: false, frameworkAssemblyNames).IsUnconditional; - } - - static InclusionDecision GetInclusionDecision (JavaPeerInfo peer, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) + static bool IsUnconditionalEntry (JavaPeerInfo peer, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) { if (forceUnconditionalEntries) { - return new InclusionDecision (true, "force-unconditional entries are enabled"); + return true; } if (EssentialRuntimeTypes.Contains (peer.JavaName)) { - return new InclusionDecision (true, "essential Java interop runtime type"); + return true; } if (!peer.DoNotGenerateAcw && !peer.IsInterface && !IsFrameworkAssembly (peer, frameworkAssemblyNames)) { - return new InclusionDecision (true, "user ACW type can be instantiated from Java"); + return true; } if (peer.IsUnconditional) { - return new InclusionDecision (true, "scanner marked the peer unconditional"); + return true; } - if (!peer.DoNotGenerateAcw && !peer.IsInterface) { - return new InclusionDecision (false, "conditional framework ACW candidate"); - } - - return new InclusionDecision (false, "conditional trim-target entry"); - } - - static string GetAliasBaseInclusionReason (string jniName, List peersForName, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) - { - if (forceUnconditionalEntries) { - return "alias holder is unconditional because force-unconditional entries are enabled"; - } - if (EssentialRuntimeTypes.Contains (jniName)) { - return "alias holder is unconditional because the JNI name is an essential Java interop runtime type"; - } - foreach (var peer in peersForName) { - var decision = GetInclusionDecision (peer, forceUnconditionalEntries: false, frameworkAssemblyNames); - if (decision.IsUnconditional) { - return $"alias holder is unconditional because {peer.ManagedTypeName} is unconditional: {decision.Reason}"; - } - } - return "alias holder is conditional on the holder type"; + return false; } static bool IsFrameworkAssembly (JavaPeerInfo peer, ISet? frameworkAssemblyNames) @@ -443,17 +411,13 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } - var inclusion = GetInclusionDecision (peer, forceUnconditionalEntries, frameworkAssemblyNames); - bool isUnconditional = inclusion.IsUnconditional; + bool isUnconditional = IsUnconditionalEntry (peer, forceUnconditionalEntries, frameworkAssemblyNames); string? targetRef = isUnconditional ? null : AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); return new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = proxyRef, TargetTypeReference = targetRef, - InclusionReason = inclusion.Reason, - SourceManagedTypeName = peer.ManagedTypeName, - SourceAssemblyName = peer.AssemblyName, }; } @@ -486,9 +450,6 @@ static void EmitArrayEntries (TypeMapAssemblyData model, string jniName, List Date: Wed, 6 May 2026 07:12:41 +0200 Subject: [PATCH 08/13] [TrimmableTypeMap] Move force-unconditional flag to generator state Keep force-unconditional entries as the default generator behavior, with the MSBuild task setting the init-only generator option from its task property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyGenerator.cs | 11 +++++++---- .../TrimmableTypeMapGenerator.cs | 16 +++++++++++----- .../Tasks/GenerateTrimmableTypeMap.cs | 7 ++++--- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 423475a9eb9..3e6c5def4dc 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -18,6 +18,11 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) _systemRuntimeVersion = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); } + /// + /// True to emit all TypeMap entries as unconditional 2-arg attributes. + /// + public bool ForceUnconditionalEntries { get; init; } = true; + /// /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to . /// @@ -28,16 +33,14 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// When true, uses Java.Lang.Object as the shared anchor type. When false, emits a per-assembly anchor. /// /// Max rank for per-rank array TypeMap entries. 0 disables. - /// True to emit all TypeMap entries as unconditional 2-arg attributes. public void Generate ( IReadOnlyList peers, Stream stream, string assemblyName, bool useSharedTypemapUniverse = false, - int maxArrayRank = 0, - bool forceUnconditionalEntries = true) + int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, forceUnconditionalEntries); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, ForceUnconditionalEntries); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 304832c1f39..9970738eb21 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -27,6 +27,15 @@ public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) this.logger = logger ?? throw new ArgumentNullException (nameof (logger)); } + /// + /// Workaround for https://github.com/dotnet/runtime/issues/127004. + /// When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the + /// trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute + /// references the same type. Set to false once the runtime bug is fixed to re-enable + /// 3-arg conditional entries that allow unused framework bindings to be trimmed away. + /// + public bool ForceUnconditionalEntries { get; init; } = true; + /// /// Runs the full generation pipeline: scan assemblies, generate typemap /// assemblies, generate JCW Java sources, and optionally generate a merged manifest. @@ -39,8 +48,7 @@ public TrimmableTypeMapResult Execute ( bool useSharedTypemapUniverse = false, ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, - int maxArrayRank = 0, - bool forceUnconditionalEntries = true) + int maxArrayRank = 0) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); @@ -69,7 +77,6 @@ public TrimmableTypeMapResult Execute ( systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank, - forceUnconditionalEntries, frameworkAssemblyNames); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) @@ -166,7 +173,6 @@ List GenerateTypeMapAssemblies ( Version systemRuntimeVersion, bool useSharedTypemapUniverse, int maxArrayRank, - bool forceUnconditionalEntries, HashSet frameworkAssemblyNames) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -199,7 +205,7 @@ List GenerateTypeMapAssemblies ( typeMapAssemblyName + ".dll", typeMapAssemblyName, maxArrayRank: maxArrayRank, - forceUnconditionalEntries: forceUnconditionalEntries, + forceUnconditionalEntries: ForceUnconditionalEntries, frameworkAssemblyNames: frameworkAssemblyNames); LogTypeMapAssemblyDetails (typeMapAssemblyName, model); diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 3f4d4cfb642..8579809c7eb 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -140,7 +140,9 @@ public override bool RunTask () ApplicationJavaClass: ApplicationJavaClass); } - var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)); + var generator = new TrimmableTypeMapGenerator (new MSBuildTrimmableTypeMapLogger (Log)) { + ForceUnconditionalEntries = ForceUnconditionalEntries, + }; XDocument? manifestTemplate = null; if (!ManifestTemplate.IsNullOrEmpty () && File.Exists (ManifestTemplate)) { @@ -154,8 +156,7 @@ public override bool RunTask () useSharedTypemapUniverse: !Debug, manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, - maxArrayRank: MaxArrayRank, - forceUnconditionalEntries: ForceUnconditionalEntries); + maxArrayRank: MaxArrayRank); GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); GeneratedJavaFiles = WriteJavaSourcesToDisk (result.GeneratedJavaSources); From cd9bce7e6badcc6e44d26eb0da63dc1763edde30 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:12:41 +0200 Subject: [PATCH 09/13] [NativeAOT] Split CoreCLR and NativeAOT runtime branches Keep the CoreCLR and NativeAOT runtime checks separate instead of combining them, even where the current branch bodies are identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs | 4 +++- src/Mono.Android/Android.Runtime/JNIEnv.cs | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs index 84499d19c7c..1d9848cd4db 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs @@ -17,7 +17,9 @@ static AndroidRuntimeInternal () { if (RuntimeFeature.IsMonoRuntime) { mono_unhandled_exception = MonoUnhandledException; - } else if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime) { + } else if (RuntimeFeature.IsCoreClrRuntime) { + mono_unhandled_exception = CoreClrUnhandledException; + } else if (RuntimeFeature.IsNativeAotRuntime) { mono_unhandled_exception = CoreClrUnhandledException; } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index dc007e2d534..ebfc81cc39b 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -136,7 +136,9 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt if (RuntimeFeature.IsMonoRuntime) { MonoDroidUnhandledException (innerException ?? javaException); - } else if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime) { + } else if (RuntimeFeature.IsCoreClrRuntime) { + ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent (innerException ?? javaException); + } else if (RuntimeFeature.IsNativeAotRuntime) { ExceptionHandling.RaiseAppDomainUnhandledExceptionEvent (innerException ?? javaException); } else { throw new NotSupportedException ("Internal error: unknown runtime not supported"); From 91c6eb7cd95436b9138afdd8e5b7e6337c526f2e Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:15:43 +0200 Subject: [PATCH 10/13] [TrimmableTypeMap] Remove assembly summary logging Drop the extra generated typemap assembly summary logger and keep the existing per-assembly type count logging. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ITrimmableTypeMapLogger.cs | 1 - .../TrimmableTypeMapGenerator.cs | 15 --------------- .../Tasks/GenerateTrimmableTypeMap.cs | 2 -- .../Generator/TrimmableTypeMapGeneratorTests.cs | 11 ----------- 4 files changed, 29 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 814875009db..94e046c732f 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -7,7 +7,6 @@ public interface ITrimmableTypeMapLogger void LogGeneratingJcwFilesInfo (int jcwPeerCount, int totalPeerCount); void LogDeferredRegistrationTypesInfo (int typeCount); void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount); - void LogGeneratedTypeMapAssemblySummary (string assemblyName, int entryCount, int unconditionalEntryCount, int conditionalEntryCount, int proxyTypeCount, int associationCount, int aliasHolderCount); void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount); void LogGeneratedTypeMapAssembliesInfo (int assemblyCount); void LogGeneratedJcwFilesInfo (int sourceCount); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 9970738eb21..d9db1a32e92 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -207,7 +207,6 @@ List GenerateTypeMapAssemblies ( maxArrayRank: maxArrayRank, forceUnconditionalEntries: ForceUnconditionalEntries, frameworkAssemblyNames: frameworkAssemblyNames); - LogTypeMapAssemblyDetails (typeMapAssemblyName, model); var stream = new MemoryStream (); var emitter = new TypeMapAssemblyEmitter (systemRuntimeVersion); @@ -226,20 +225,6 @@ List GenerateTypeMapAssemblies ( return generatedAssemblies; } - void LogTypeMapAssemblyDetails (string typeMapAssemblyName, TypeMapAssemblyData model) - { - int unconditionalCount = model.Entries.Count (e => e.IsUnconditional); - int conditionalCount = model.Entries.Count - unconditionalCount; - logger.LogGeneratedTypeMapAssemblySummary ( - typeMapAssemblyName, - model.Entries.Count, - unconditionalCount, - conditionalCount, - model.ProxyTypes.Count, - model.Associations.Count, - model.AliasHolders.Count); - } - /// /// Groups peers by assembly, merging cross-assembly aliases into a single group. /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index 8579809c7eb..9e20c8cef71 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -27,8 +27,6 @@ public void LogDeferredRegistrationTypesInfo (int typeCount) => log.LogMessage (MessageImportance.Low, $"Found {typeCount} Application/Instrumentation types for deferred registration."); public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => log.LogMessage (MessageImportance.Low, $" {assemblyName}: {typeCount} types"); - public void LogGeneratedTypeMapAssemblySummary (string assemblyName, int entryCount, int unconditionalEntryCount, int conditionalEntryCount, int proxyTypeCount, int associationCount, int aliasHolderCount) => - log.LogMessage (MessageImportance.Low, $" {assemblyName}: TypeMap entries={entryCount} unconditional={unconditionalEntryCount} conditional={conditionalEntryCount} proxies={proxyTypeCount} associations={associationCount} alias-holders={aliasHolderCount}"); public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => log.LogMessage (MessageImportance.Low, $" Root: {assemblyReferenceCount} per-assembly refs"); public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index 75efac9beaa..c9f070dc146 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -24,8 +24,6 @@ public void LogDeferredRegistrationTypesInfo (int typeCount) => logMessages.Add ($"Found {typeCount} Application/Instrumentation types for deferred registration."); public void LogGeneratedTypeMapAssemblyInfo (string assemblyName, int typeCount) => logMessages.Add ($" {assemblyName}: {typeCount} types"); - public void LogGeneratedTypeMapAssemblySummary (string assemblyName, int entryCount, int unconditionalEntryCount, int conditionalEntryCount, int proxyTypeCount, int associationCount, int aliasHolderCount) => - logMessages.Add ($" {assemblyName}: TypeMap entries={entryCount} unconditional={unconditionalEntryCount} conditional={conditionalEntryCount} proxies={proxyTypeCount} associations={associationCount} alias-holders={aliasHolderCount}"); public void LogGeneratedRootTypeMapInfo (int assemblyReferenceCount) => logMessages.Add ($" Root: {assemblyReferenceCount} per-assembly refs"); public void LogGeneratedTypeMapAssembliesInfo (int assemblyCount) => @@ -74,15 +72,6 @@ public void Execute_WithTestFixtures_ProducesOutputs () Assert.Contains (result.GeneratedAssemblies, a => a.Name == "_TestFixtures.TypeMap"); } - [Fact] - public void Execute_LogsTypeMapEntrySummary () - { - using var peReader = CreateTestFixturePEReader (); - CreateGenerator ().Execute (new List<(string, PEReader)> { ("TestFixtures", peReader) }, new Version (11, 0), new HashSet ()); - - Assert.Contains (logMessages, m => m.Contains ("_TestFixtures.TypeMap: TypeMap entries=", StringComparison.Ordinal)); - } - [Fact] public void Execute_CollectsDeferredRegistrationTypes_ForAllApplicationAndInstrumentationSubtypes () { From 473e2d04785a23511e156c4b5c74955684362aff Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:18:34 +0200 Subject: [PATCH 11/13] [TrimmableTypeMap] Use type map assembly generator wrapper Let TypeMapAssemblyGenerator carry framework assembly classification as init state so TrimmableTypeMapGenerator can keep using the wrapper instead of building models and invoking the emitter directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyGenerator.cs | 8 +++++++- .../TrimmableTypeMapGenerator.cs | 15 +++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 3e6c5def4dc..609a7c7575a 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -23,6 +23,12 @@ public TypeMapAssemblyGenerator (Version systemRuntimeVersion) /// public bool ForceUnconditionalEntries { get; init; } = true; + /// + /// Assembly names that should be treated as framework assemblies when deciding whether + /// ACW entries are unconditional. + /// + public ISet? FrameworkAssemblyNames { get; init; } + /// /// Generates a TypeMap PE assembly from the given Java peer info records and writes it to . /// @@ -40,7 +46,7 @@ public void Generate ( bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, ForceUnconditionalEntries); + var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, ForceUnconditionalEntries, FrameworkAssemblyNames); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index d9db1a32e92..4ecdab74853 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -200,17 +200,12 @@ List GenerateTypeMapAssemblies ( foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); - var model = ModelBuilder.Build ( - peers, - typeMapAssemblyName + ".dll", - typeMapAssemblyName, - maxArrayRank: maxArrayRank, - forceUnconditionalEntries: ForceUnconditionalEntries, - frameworkAssemblyNames: frameworkAssemblyNames); - var stream = new MemoryStream (); - var emitter = new TypeMapAssemblyEmitter (systemRuntimeVersion); - emitter.Emit (model, stream, useSharedTypemapUniverse); + var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion) { + ForceUnconditionalEntries = ForceUnconditionalEntries, + FrameworkAssemblyNames = frameworkAssemblyNames, + }; + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); From 5babb7bfd0989735c6b281871e51f64f4f616e77 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 07:45:42 +0200 Subject: [PATCH 12/13] [TrimmableTypeMap] Simplify assembly generator reuse Reuse the per-assembly TypeMapAssemblyGenerator instance and make the ModelBuilder.Build arguments explicit for readability. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generator/TypeMapAssemblyGenerator.cs | 8 +++++++- .../TrimmableTypeMapGenerator.cs | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs index 609a7c7575a..fa3583ae258 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -46,7 +46,13 @@ public void Generate ( bool useSharedTypemapUniverse = false, int maxArrayRank = 0) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank, ForceUnconditionalEntries, FrameworkAssemblyNames); + var model = ModelBuilder.Build ( + peers, + assemblyName + ".dll", + assemblyName, + maxArrayRank: maxArrayRank, + forceUnconditionalEntries: ForceUnconditionalEntries, + frameworkAssemblyNames: FrameworkAssemblyNames); var emitter = new TypeMapAssemblyEmitter (_systemRuntimeVersion); emitter.Emit (model, stream, useSharedTypemapUniverse); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 4ecdab74853..fba2f07c725 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -197,14 +197,14 @@ List GenerateTypeMapAssemblies ( var generatedAssemblies = new List (); var perAssemblyNames = new List (); + var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion) { + ForceUnconditionalEntries = ForceUnconditionalEntries, + FrameworkAssemblyNames = frameworkAssemblyNames, + }; foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion) { - ForceUnconditionalEntries = ForceUnconditionalEntries, - FrameworkAssemblyNames = frameworkAssemblyNames, - }; generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse, maxArrayRank); stream.Position = 0; generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); From 725cbb3790ce2f35518f6b46e980f841036ea678 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 6 May 2026 10:49:14 +0200 Subject: [PATCH 13/13] [NativeAOT] Consolidate JNI runtime initialization Route NativeAOT JNI runtime setup through a dedicated initialization entry point so common state, trimmable typemap initialization, and native method registration happen in order after the runtime is current. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../JavaInteropRuntime.cs | 23 +-- .../Android.Runtime/JNIEnvInit.cs | 161 +++++++++--------- 2 files changed, 87 insertions(+), 97 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 f3a8e5fbe51..b5b38cedb86 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -56,27 +56,23 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua // This needs to be called first, since it sets up locations, environment variables, logging etc XA_Host_NativeAOT_OnInit (language, filesDir, cacheDir, ref initArgs); - JNIEnvInit.InitializeJniRuntimeEarly (initArgs); - JNIEnvInit.InitializeNativeAotTrimmableTypeMapData (); + JNIEnvInit.InitializeLogCategories (initArgs); var settings = new DiagnosticSettings (); settings.AddDebugDotnetLog (); - var typeManager = CreateTypeManager (); - var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, ClassLoader = new JniObjectReference (classLoader, JniObjectReferenceType.Global), - TypeManager = typeManager, - ValueManager = new JavaMarshalValueManager (), + TypeManager = JNIEnvInit.CreateTypeManager (initArgs), + ValueManager = JNIEnvInit.CreateValueManager (), JniGlobalReferenceLogWriter = settings.GrefLog, JniLocalReferenceLogWriter = settings.LrefLog, }; runtime = options.CreateJreVM (); - // Entry point into Mono.Android.dll. Log categories are initialized in JNI_OnLoad. - JNIEnvInit.InitializeJniRuntime (runtime, initArgs); - JNIEnvInit.RegisterNativeAotTrimmableTypeMapNativeMethods (); + // Entry point into Mono.Android.dll for NativeAOT-specific JNI runtime initialization. + JNIEnvInit.InitializeNativeAotRuntime (runtime, initArgs); transition = new JniTransition (jnienv); @@ -89,13 +85,4 @@ static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader, IntPtr langua } transition.Dispose (); } - - static JniRuntime.JniTypeManager CreateTypeManager () - { - if (RuntimeFeature.TrimmableTypeMap) { - return new TrimmableTypeMapTypeManager (); - } - - return new ManagedTypeManager (); - } } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index e2892335816..d8ec6c1e612 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -97,95 +97,50 @@ internal static void NativeAotInitializeMaxGrefGet () } } - // This is needed to initialize e.g. logging before anything else (useful with e.g. gref - // logging where runtime creation causes several grefs to be created and logged without - // stack traces because logging categories on the managed side aren't yet set) - internal static void InitializeJniRuntimeEarly (JnienvInitializeArgs args) + internal static void InitializeLogCategories (JnienvInitializeArgs args) { Logger.SetLogCategories ((LogCategories)args.logCategories); } - internal static void InitializeNativeAotTrimmableTypeMapData () - { - if (RuntimeFeature.TrimmableTypeMap) { - InitializeTrimmableTypeMapData (); - } - } - // NOTE: should have different name than `Initialize` to avoid: // * Assertion at /__w/1/s/src/mono/mono/metadata/icall.c:6258, condition `!only_unmanaged_callers_only' not met - internal static void InitializeJniRuntime (JniRuntime runtime, JnienvInitializeArgs args) + // Only used for NativeAOT. MonoVM and CoreCLR use Initialize(). + internal static void InitializeNativeAotRuntime (JniRuntime runtime, JnienvInitializeArgs args) { - gref_gc_threshold = args.grefGcThreshold; - - jniRemappingInUse = args.jniRemappingInUse; - MarshalMethodsEnabled = args.marshalMethodsEnabled; - java_class_loader = args.grefLoader; - - BoundExceptionType = (BoundExceptionType)args.ioExceptionType; + if (!RuntimeFeature.IsNativeAotRuntime) { + throw new NotSupportedException ("JNIEnvInit.InitializeNativeAotRuntime can only be used to initialize NativeAOT."); + } + if (RuntimeFeature.IsMonoRuntime || RuntimeFeature.IsCoreClrRuntime) { + throw new NotSupportedException ("Internal error: NativeAOT cannot be enabled with MonoVM or CoreCLR."); + } + InitializeCommonState (args); + InitializeTrimmableTypeMapDataIfNeeded (); androidRuntime = runtime; JniRuntime.SetCurrent (runtime); - - grefIGCUserPeer_class = args.grefIGCUserPeer; - grefGCUserPeerable_class = args.grefGCUserPeerable; - - PropagateExceptions = args.brokenExceptionTransitions == 0; - - JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args.packageNamingPolicy; - + RegisterTrimmableTypeMapNativeMethodsIfNeeded (); SetSynchronizationContext (); } - internal static void RegisterNativeAotTrimmableTypeMapNativeMethods () - { - if (RuntimeFeature.TrimmableTypeMap) { - TrimmableTypeMap.RegisterNativeMethods (); - } - } - + // Only used for MonoVM and CoreCLR. NativeAOT uses InitializeNativeAotRuntime(). [UnmanagedCallersOnly] internal static unsafe void Initialize (JnienvInitializeArgs* args) { - // Should not be allowed - if (RuntimeFeature.IsMonoRuntime && RuntimeFeature.IsCoreClrRuntime) { - throw new NotSupportedException ("Internal error: both RuntimeFeature.IsMonoRuntime and RuntimeFeature.IsCoreClrRuntime are enabled"); + if (RuntimeFeature.IsNativeAotRuntime) { + throw new NotSupportedException ("JNIEnvInit.Initialize cannot be used to initialize NativeAOT."); + } + if (RuntimeFeature.IsMonoRuntime == RuntimeFeature.IsCoreClrRuntime) { + throw new NotSupportedException ("Internal error: exactly one of RuntimeFeature.IsMonoRuntime or RuntimeFeature.IsCoreClrRuntime must be enabled."); } IntPtr total_timing_sequence = IntPtr.Zero; IntPtr partial_timing_sequence = IntPtr.Zero; - Logger.SetLogCategories ((LogCategories)args->logCategories); - - gref_gc_threshold = args->grefGcThreshold; - - jniRemappingInUse = args->jniRemappingInUse; - MarshalMethodsEnabled = args->marshalMethodsEnabled; - java_class_loader = args->grefLoader; - - BoundExceptionType = (BoundExceptionType)args->ioExceptionType; - if (RuntimeFeature.TrimmableTypeMap) { - InitializeTrimmableTypeMapData (); - } + InitializeCommonState (*args); + InitializeTrimmableTypeMapDataIfNeeded (); - JniRuntime.JniTypeManager typeManager; - JniRuntime.JniValueManager? valueManager = null; - if (RuntimeFeature.TrimmableTypeMap) { - typeManager = new TrimmableTypeMapTypeManager (); - valueManager = new JavaMarshalValueManager (); - } else if (RuntimeFeature.ManagedTypeMap) { - typeManager = new ManagedTypeManager (); - } else { - typeManager = new AndroidTypeManager (args->jniAddNativeMethodRegistrationAttributePresent != 0); - } - if (RuntimeFeature.IsMonoRuntime) { - valueManager = new AndroidValueManager (); - } else if (RuntimeFeature.IsCoreClrRuntime) { - // 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"); - } + JniRuntime.JniTypeManager typeManager = CreateTypeManager (*args); + JniRuntime.JniValueManager valueManager = CreateValueManager (); androidRuntime = new AndroidRuntime ( args->env, args->javaVm, @@ -195,18 +150,7 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->jniAddNativeMethodRegistrationAttributePresent != 0 ); JniRuntime.SetCurrent (androidRuntime); - if (RuntimeFeature.TrimmableTypeMap) { - // TypeMapLoader.Initialize() only loads managed typemap data. Registering - // mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader. - TrimmableTypeMap.RegisterNativeMethods (); - } - - grefIGCUserPeer_class = args->grefIGCUserPeer; - grefGCUserPeerable_class = args->grefGCUserPeerable; - - PropagateExceptions = args->brokenExceptionTransitions == 0; - - JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args->packageNamingPolicy; + RegisterTrimmableTypeMapNativeMethodsIfNeeded (); if (args->managedMarshalMethodsLookupEnabled) { delegate* unmanaged getFunctionPointer = &ManagedMarshalMethodsLookupTable.GetFunctionPointer; @@ -226,6 +170,65 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) [UnmanagedCallConv (CallConvs = new[] { typeof (CallConvCdecl) })] private static unsafe partial void xamarin_app_init (IntPtr env, delegate* unmanaged get_function_pointer); + internal static JniRuntime.JniTypeManager CreateTypeManager (JnienvInitializeArgs args) + { + if (RuntimeFeature.TrimmableTypeMap) { + return new TrimmableTypeMapTypeManager (); + } + + if (RuntimeFeature.IsNativeAotRuntime || RuntimeFeature.ManagedTypeMap) { + return new ManagedTypeManager (); + } + + return new AndroidTypeManager (args.jniAddNativeMethodRegistrationAttributePresent != 0); + } + + internal static JniRuntime.JniValueManager CreateValueManager () + { + if (RuntimeFeature.IsMonoRuntime) { + return new AndroidValueManager (); + } + + if (RuntimeFeature.IsCoreClrRuntime || RuntimeFeature.IsNativeAotRuntime) { + return new JavaMarshalValueManager (); + } + + throw new NotSupportedException ("Internal error: unknown runtime not supported"); + } + + static void InitializeCommonState (JnienvInitializeArgs args) + { + Logger.SetLogCategories ((LogCategories)args.logCategories); + + gref_gc_threshold = args.grefGcThreshold; + jniRemappingInUse = args.jniRemappingInUse; + MarshalMethodsEnabled = args.marshalMethodsEnabled; + java_class_loader = args.grefLoader; + + BoundExceptionType = (BoundExceptionType)args.ioExceptionType; + grefIGCUserPeer_class = args.grefIGCUserPeer; + grefGCUserPeerable_class = args.grefGCUserPeerable; + PropagateExceptions = args.brokenExceptionTransitions == 0; + + JavaNativeTypeManager.PackageNamingPolicy = (PackageNamingPolicy)args.packageNamingPolicy; + } + + static void InitializeTrimmableTypeMapDataIfNeeded () + { + if (RuntimeFeature.TrimmableTypeMap) { + InitializeTrimmableTypeMapData (); + } + } + + static void RegisterTrimmableTypeMapNativeMethodsIfNeeded () + { + if (RuntimeFeature.TrimmableTypeMap) { + // TypeMapLoader.Initialize() only loads managed typemap data. Registering + // mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader. + TrimmableTypeMap.RegisterNativeMethods (); + } + } + // Separate method so the JIT doesn't try to resolve TypeMapLoader (from _Microsoft.Android.TypeMaps.dll) // when compiling JNIEnvInit.Initialize() in non-trimmable builds where that assembly isn't present. [MethodImpl (MethodImplOptions.NoInlining)]