diff --git a/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in b/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in index fee83c88b9c..5f28384bdc6 100644 --- a/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in +++ b/src/Mono.Android.Runtime/Properties/AssemblyInfo.cs.in @@ -19,3 +19,5 @@ using System.Runtime.Versioning; [assembly: SupportedOSPlatform("Android@MIN_API_LEVEL@.0")] [assembly: InternalsVisibleTo("Mono.Android, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo("Microsoft.Android.Runtime.NativeAOT, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] +[assembly: InternalsVisibleTo("Mono.Android.NET-Tests, PublicKey=0024000004800000940000000602000000240000525341310004000011000000438ac2a5acfbf16cbd2b2b47a62762f273df9cb2795ceccdf77d10bf508e69e7a362ea7a45455bbf3ac955e1f2e2814f144e5d817efc4c6502cc012df310783348304e3ae38573c6d658c234025821fda87a0be8a0d504df564e2c93b2b878925f42503e9d54dfef9f9586d9e6f38a305769587b1de01f6c0410328b2c9733db")] diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index cb05c9da9b1..adc0ad709a3 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -11,6 +11,7 @@ using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; using System.Diagnostics.CodeAnalysis; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; #if JAVA_INTEROP namespace Android.Runtime { diff --git a/src/Mono.Android/Android.Runtime/JNIEnv.cs b/src/Mono.Android/Android.Runtime/JNIEnv.cs index 433d1a0d53e..140e1af82cc 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnv.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnv.cs @@ -14,6 +14,7 @@ using Java.Interop; using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; namespace Android.Runtime { public static partial class JNIEnv { diff --git a/src/Mono.Android/Java.Interop/TypeManager.cs b/src/Mono.Android/Java.Interop/TypeManager.cs index f856818e09f..3d9c68015e0 100644 --- a/src/Mono.Android/Java.Interop/TypeManager.cs +++ b/src/Mono.Android/Java.Interop/TypeManager.cs @@ -9,6 +9,7 @@ using Android.Runtime; using Microsoft.Android.Runtime; +using RuntimeFeature = Microsoft.Android.Runtime.RuntimeFeature; namespace Java.Interop { diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index c8c2c87a071..7b5c6f23afd 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -368,7 +368,6 @@ - diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs index 7cb25bd7540..62c35ac7724 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest2.cs @@ -532,15 +532,15 @@ static IEnumerable Get_BuildHasTrimmerWarningsData () } else { AddTestData (runtime, "", new string [0], true); } - AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, 2); + AddTestData (runtime, "SuppressTrimAnalysisWarnings=false", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 3); AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, false, 1); - AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, 2); + AddTestData (runtime, "TrimMode=full", new string [] { "IL2055" }, true, runtime == AndroidRuntime.NativeAOT ? 2 : 2); AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, false); if (runtime == AndroidRuntime.NativeAOT) { AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 2); } else { - AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 3); + AddTestData (runtime, "IsAotCompatible=true", new string [] { "IL2055", "IL3050" }, true, 4); } } @@ -562,6 +562,8 @@ void AddTestData (AndroidRuntime runtime, string properties, string [] codes, bo [TestCaseSource (nameof (Get_BuildHasTrimmerWarningsData))] public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, string [] codes, bool isRelease, int? totalWarnings = null) { + const int maxWarningLinesToShow = 25; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } @@ -601,10 +603,38 @@ public void BuildHasTrimmerWarnings (AndroidRuntime runtime, string properties, b.AssertHasNoWarnings (); } else { totalWarnings ??= codes.Length; - Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, $"{totalWarnings} Warning(s)"), $"Should receive {totalWarnings} warnings"); + + string warningSummaryLine = b.LastBuildOutput.LastOrDefault (line => line.Contains ("Warning(s)", StringComparison.Ordinal)) ?? ""; + var actualWarnings = GetWarningCount (warningSummaryLine); + + var allWarningLines = b.LastBuildOutput + .Where (line => line.Contains (": warning ", StringComparison.OrdinalIgnoreCase)) + .Take (maxWarningLinesToShow) + .ToArray (); + Assert.AreEqual ( + totalWarnings.Value, + actualWarnings, + $"{b.BuildLogFile} should have {totalWarnings} warnings. Summary line: '{warningSummaryLine}'. " + + $"Warnings found ({allWarningLines.Length} shown):{Environment.NewLine}{string.Join (Environment.NewLine, allWarningLines)}" + ); foreach (var code in codes) { - Assert.True (StringAssertEx.ContainsText (b.LastBuildOutput, code), $"Should receive {code} warning"); + Assert.True ( + StringAssertEx.ContainsText (b.LastBuildOutput, code), + $"{b.BuildLogFile} should contain warning {code}. Summary line: '{warningSummaryLine}'. " + + $"Warnings found ({allWarningLines.Length} shown):{Environment.NewLine}{string.Join (Environment.NewLine, allWarningLines)}" + ); + } + } + + static int GetWarningCount (string warningSummaryLine) + { + string [] tokens = warningSummaryLine.Split (new [] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + for (int i = 1; i < tokens.Length; i++) { + if (tokens [i] == "Warning(s)" && int.TryParse (tokens [i - 1], out var warningCount)) { + return warningCount; + } } + return -1; } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs index 7013ebc9367..1e06b5f52c7 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/LlvmIrGeneratorTests.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.Build.Utilities; using NUnit.Framework; +using Xamarin.Android.Build.Tasks; using Xamarin.Android.Tasks.LLVMIR; using Xamarin.Android.Tools; @@ -79,5 +80,32 @@ public void GeneratedIR_FunctionWithWhitespaceParameterName_ProducesValidOutput Assert.That (output, Does.Not.Contain ("%\t)"), "Generated LLVM IR should not contain 'ptr noundef %\\t)' pattern"); Assert.That (output, Does.Contain ("@test_function"), "Generated LLVM IR should contain the function name"); } + + /// + /// Regression test for the NativeAOT link failure in https://github.com/dotnet/android/pull/11669. + /// + /// `Android.Runtime.JNIEnvInit.Initialize` (used only by MonoVM/CoreCLR) has a `[LibraryImport]` + /// p/invoke to the native `xamarin_app_init` function, which becomes a direct native reference + /// under NativeAOT (`xa-internal-api` is a `DirectPInvoke`). Since `Initialize` is force-preserved + /// by the ILLink descriptor and is never trimmed, the reference reaches the linker even though + /// NativeAOT never generates the marshal methods native code that defines `xamarin_app_init`. + /// The NativeAOT jni-init assembler source must therefore emit a no-op definition so the linker + /// can resolve the symbol. + /// + [Test] + public void NativeAotJniInit_EmitsNoOpXamarinAppInit () + { + var log = new TaskLoggingHelper (new MockBuildEngine (TestContext.Out, [], [], []), "test"); + var composer = new NativeAotJniInitNativeAssemblyGenerator (log, runtimeComponentsJniOnLoadHandlers: null, customJniOnLoadHandlers: null); + LlvmIrModule module = composer.Construct (); + + var generator = LlvmIrGenerator.Create (AndroidTargetArch.Arm64, "jniinit.ll"); + using var writer = new StringWriter (); + generator.Generate (writer, module); + + string output = writer.ToString (); + Assert.That (output, Does.Contain ("define"), "Generated LLVM IR should contain a function definition"); + Assert.That (output, Does.Contain ("@xamarin_app_init"), "Generated LLVM IR should define xamarin_app_init so the NativeAOT linker can resolve JNIEnvInit.Initialize's DirectPInvoke"); + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs index a865295d8d9..834ebf31a50 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/NativeAotJniInitNativeAssemblyGenerator.cs @@ -29,6 +29,8 @@ protected override void Construct (LlvmIrModule module) CollectHandlers (customJniOnLoadHandlers); JniOnLoadNativeAssemblerHelper.GenerateJniOnLoadHandlerCode (jniOnLoadNames, module); + DefineNoOpXamarinAppInit (module); + void CollectHandlers (List? handlers) { if (handlers == null || handlers.Count == 0) { @@ -44,4 +46,24 @@ void CollectHandlers (List? handlers) } } } + + // The managed method `Android.Runtime.JNIEnvInit.Initialize` (used only by MonoVM and CoreCLR) has a + // `[LibraryImport]` p/invoke to the native `xamarin_app_init` function, which becomes a direct native + // reference because the `xa-internal-api` "library" is registered as a `DirectPInvoke` for NativeAOT. + // That symbol is only ever defined by the MonoVM/CoreCLR marshal methods native code, which is never + // generated for NativeAOT. `Initialize` is unreachable under NativeAOT (which uses + // `InitializeNativeAotRuntime` instead), but it is force-preserved by the ILLink descriptor + // (`PreserveLists/Mono.Android.xml`), so the unresolved reference reaches the linker and fails with + // XA3007 "undefined symbol: xamarin_app_init". Emit an empty definition to satisfy the linker; it is + // never actually called under NativeAOT. + static void DefineNoOpXamarinAppInit (LlvmIrModule module) + { + var parameters = new List { + new LlvmIrFunctionParameter (typeof (IntPtr), "env"), + new LlvmIrFunctionParameter (typeof (IntPtr), "fn"), + }; + var func = new LlvmIrFunction ("xamarin_app_init", typeof (void), parameters); + func.Body.Ret (typeof (void)); + module.Add (func); + } }