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..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,25 +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.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); + // Entry point into Mono.Android.dll for NativeAOT-specific JNI runtime initialization. + JNIEnvInit.InitializeNativeAotRuntime (runtime, initArgs); transition = new JniTransition (jnienv); @@ -87,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/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 7547c5ac38f..432cddf6e05 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,14 @@ 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) + /// 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, + ISet? frameworkAssemblyNames = null) { if (peers is null) { throw new ArgumentNullException (nameof (peers)); @@ -96,7 +96,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri peersForName.Sort ((a, b) => StringComparer.Ordinal.Compare (a.ManagedTypeName, b.ManagedTypeName)); } - EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames); + EmitPeers (model, jniName, peersForName, assemblyName, usedProxyNames, forceUnconditionalEntries, frameworkAssemblyNames); if (maxArrayRank > 0) { EmitArrayEntries (model, jniName, peersForName, maxArrayRank); @@ -125,7 +125,8 @@ 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, + ISet? frameworkAssemblyNames) { bool isAliasGroup = peersForName.Count > 1; @@ -141,7 +142,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, frameworkAssemblyNames); model.Entries.Add (entry); // Emit a TypeMapAssociation for every entry that has a proxy. @@ -176,7 +177,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, frameworkAssemblyNames)); // Link each alias type to the alias holder for trimming model.Associations.Add (new TypeMapAssociationData { @@ -189,14 +190,14 @@ 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); + || peersForName.Any (p => IsUnconditionalEntry (p, forceUnconditionalEntries: false, frameworkAssemblyNames)); model.Entries.Add (new TypeMapAttributeData { JniName = jniName, ProxyTypeReference = holderRef, @@ -230,20 +231,20 @@ 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, bool forceUnconditionalEntries, ISet? frameworkAssemblyNames) { - // Essential runtime types needed by the Java interop runtime + if (forceUnconditionalEntries) { + return true; + } + if (EssentialRuntimeTypes.Contains (peer.JavaName)) { return true; } - // 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) { + if (!peer.DoNotGenerateAcw && !peer.IsInterface && !IsFrameworkAssembly (peer, frameworkAssemblyNames)) { return true; } - // Types marked unconditional by the scanner (component attributes: Activity, Service, etc.) if (peer.IsUnconditional) { return true; } @@ -251,6 +252,11 @@ static bool IsUnconditionalEntry (JavaPeerInfo peer) return false; } + 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) { if (asmName != null && !string.Equals (asmName, outputAssemblyName, StringComparison.Ordinal)) { @@ -395,7 +401,8 @@ static void BuildNativeRegistrations (JavaPeerProxyData proxy) } static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? proxy, - string outputAssemblyName, string jniName) + string outputAssemblyName, string jniName, bool forceUnconditionalEntries, + ISet? frameworkAssemblyNames) { string proxyRef; if (proxy != null) { @@ -404,13 +411,8 @@ 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); - string? targetRef = null; - if (!isUnconditional) { - targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); - } + bool isUnconditional = IsUnconditionalEntry (peer, forceUnconditionalEntries, frameworkAssemblyNames); + 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 48ca89f45bc..fa3583ae258 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyGenerator.cs @@ -18,6 +18,17 @@ 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; + + /// + /// 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 . /// @@ -28,9 +39,20 @@ 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) { - var model = ModelBuilder.Build (peers, assemblyName + ".dll", assemblyName, maxArrayRank); + 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 090075d73bf..fba2f07c725 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. @@ -63,7 +72,12 @@ public TrimmableTypeMapResult Execute ( PropagateDeferredRegistrationToBaseClasses (allPeers); PropagateCannotRegisterToDescendants (allPeers); - var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse, maxArrayRank); + var generatedAssemblies = GenerateTypeMapAssemblies ( + allPeers, + systemRuntimeVersion, + useSharedTypemapUniverse, + maxArrayRank, + frameworkAssemblyNames); var jcwPeers = allPeers.Where (p => !frameworkAssemblyNames.Contains (p.AssemblyName) || p.JavaName.StartsWith ("mono/", StringComparison.Ordinal)).ToList (); @@ -154,7 +168,12 @@ 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, + HashSet frameworkAssemblyNames) { List<(string AssemblyName, List Peers)> peersByAssembly; @@ -178,7 +197,10 @@ List GenerateTypeMapAssemblies (List allPeers, var generatedAssemblies = new List (); var perAssemblyNames = new List (); - var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion); + var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion) { + ForceUnconditionalEntries = ForceUnconditionalEntries, + FrameworkAssemblyNames = frameworkAssemblyNames, + }; foreach (var (assemblyName, peers) in peersByAssembly) { string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; perAssemblyNames.Add (typeMapAssemblyName); diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs index 73d20f6397b..1d9848cd4db 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntimeInternal.cs @@ -19,6 +19,8 @@ static AndroidRuntimeInternal () mono_unhandled_exception = MonoUnhandledException; } 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 20b635cfb70..ebfc81cc39b 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -138,6 +138,8 @@ internal static void PropagateUncaughtException (IntPtr env, IntPtr javaThreadPt MonoDroidUnhandledException (innerException ?? javaException); } 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"); } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index 2e8898e4616..d8ec6c1e612 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -97,65 +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); } // 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) { + 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); + RegisterTrimmableTypeMapNativeMethodsIfNeeded (); SetSynchronizationContext (); } + // 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); + InitializeCommonState (*args); + InitializeTrimmableTypeMapDataIfNeeded (); - gref_gc_threshold = args->grefGcThreshold; - - jniRemappingInUse = args->jniRemappingInUse; - MarshalMethodsEnabled = args->marshalMethodsEnabled; - java_class_loader = args->grefLoader; - - BoundExceptionType = (BoundExceptionType)args->ioExceptionType; - if (RuntimeFeature.TrimmableTypeMap) { - InitializeTrimmableTypeMapData (); - } - - 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, @@ -165,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; @@ -196,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)] 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..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 @@ -4,17 +4,26 @@ <_TrimmableRuntimeProviderJavaName Condition=" '$(_TrimmableRuntimeProviderJavaName)' == '' ">net.dot.jni.nativeaot.NativeAotRuntimeProvider + <_TrimmableTypeMapForceUnconditionalEntries Condition=" '$(_TrimmableTypeMapForceUnconditionalEntries)' == '' ">false - + BeforeTargets="_AndroidComputeIlcCompileInputs" + DependsOnTargets="_GenerateTrimmableTypeMap"> + + $(_TypeMapAssemblyName) + - - + <_TrimmableTypeMapIlcAssemblies Include="$(_TypeMapOutputDirectory)*.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/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 e499a05dcfa..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 @@ -20,12 +20,12 @@ <_TypeMapBaseOutputDir>$(_TypeMapBaseOutputDir.Replace('\','/')) <_TypeMapOutputDirectory>$(_TypeMapBaseOutputDir)typemap/ <_TypeMapJavaOutputDirectory>$(_TypeMapBaseOutputDir)typemap/java - <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' and '$(PublishAot)' == 'true' ">3 <_AndroidTrimmableTypeMapMaxArrayRank Condition=" '$(_AndroidTrimmableTypeMapMaxArrayRank)' == '' ">0 + <_TrimmableTypeMapForceUnconditionalEntries Condition=" '$(_TrimmableTypeMapForceUnconditionalEntries)' == '' ">true @@ -68,6 +68,7 @@ [Required] public ITaskItem [] ResolvedAssemblies { get; set; } = []; + public ITaskItem [] ResolvedFrameworkAssemblies { get; set; } = []; [Required] public string OutputDirectory { get; set; } = ""; [Required] @@ -68,13 +69,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; } @@ -89,8 +91,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); @@ -100,11 +109,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; @@ -125,7 +138,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)) { @@ -137,11 +152,11 @@ public override bool RunTask () systemRuntimeVersion, frameworkAssemblyNames, useSharedTypemapUniverse: !Debug, - manifestConfig, - manifestTemplate, + manifestConfig: manifestConfig, + manifestTemplate: manifestTemplate, maxArrayRank: MaxArrayRank); - 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 @@ -196,6 +211,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/src/native/nativeaot/host/host.cc b/src/native/nativeaot/host/host.cc index bf43f0f47de..b19bc9750ac 100644 --- a/src/native/nativeaot/host/host.cc +++ b/src/native/nativeaot/host/host.cc @@ -68,5 +68,25 @@ 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"); + 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 ()); + initArgs->grefIGCUserPeer = env->NewGlobalRef (lrefIGCUserPeer); + initArgs->grefGCUserPeerable = env->NewGlobalRef (lrefGCUserPeerable); + + env->DeleteLocalRef (lrefIGCUserPeer); + env->DeleteLocalRef (lrefGCUserPeerable); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index 049bc74a654..ea892d787b5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -10,10 +10,16 @@ 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: 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,18 @@ 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); + } + [Fact] public void Build_McwBinding_IsTrimmable () { @@ -182,6 +200,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 () {