From ac372529dacde87fae1e10e450485b576be8f891 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Mon, 9 Feb 2026 16:00:13 +0100 Subject: [PATCH 01/23] Basics --- .../GenerateNativeApplicationConfigSources.cs | 6 ++ ...pplicationConfigNativeAssemblyGenerator.cs | 2 + ...icationConfigNativeAssemblyGeneratorCLR.cs | 61 +++++++++++++++++++ .../Utilities/MonoAndroidHelper.cs | 24 ++++++-- .../Xamarin.Android.Common.targets | 24 ++++++++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 3d9eda66001..9fc2052a56b 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -28,6 +28,8 @@ public class GenerateNativeApplicationConfigSources : AndroidTask public ITaskItem[] ResolvedAssemblies { get; set; } = []; public ITaskItem[]? NativeLibraries { get; set; } + public ITaskItem[]? NativeLibrariesNoJniPreload { get; set; } + public ITaskItem[]? NativeLibrarysAlwaysJniPreload { get; set; } public ITaskItem[]? MonoComponents { get; set; } @@ -253,6 +255,8 @@ public override bool RunTask () NumberOfAssembliesInApk = assemblyCount, BundledAssemblyNameWidth = assemblyNameWidth, NativeLibraries = uniqueNativeLibraries, + NativeLibrariesNoJniPreload = NativeLibrariesNoJniPreload, + NativeLibrarysAlwaysJniPreload = NativeLibrarysAlwaysJniPreload, AndroidRuntimeJNIEnvToken = android_runtime_jnienv_class_token, JNIEnvInitializeToken = jnienv_initialize_method_token, JNIEnvRegisterJniNativesToken = jnienv_registerjninatives_method_token, @@ -279,6 +283,8 @@ public override bool RunTask () BundledAssemblyNameWidth = assemblyNameWidth, MonoComponents = (MonoComponent)monoComponents, NativeLibraries = uniqueNativeLibraries, + NativeLibrariesNoJniPreload = NativeLibrariesNoJniPreload, + NativeLibrarysAlwaysJniPreload = NativeLibrarysAlwaysJniPreload, HaveAssemblyStore = UseAssemblyStore, AndroidRuntimeJNIEnvToken = android_runtime_jnienv_class_token, JNIEnvInitializeToken = jnienv_initialize_method_token, diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs index 62b7a0361a7..28939e5a251 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs @@ -202,6 +202,8 @@ sealed class DsoCacheState public MonoComponent MonoComponents { get; set; } public PackageNamingPolicy PackageNamingPolicy { get; set; } public List NativeLibraries { get; set; } = []; + public ICollection? NativeLibrariesNoJniPreload { get; set; } + public ICollection? NativeLibrarysAlwaysJniPreload { get; set; } public bool MarshalMethodsEnabled { get; set; } public bool ManagedMarshalMethodsLookupEnabled { get; set; } public bool IgnoreSplitConfigs { get; set; } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs index 76ccaf37a0d..668269bedc5 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs @@ -6,6 +6,7 @@ using System.IO; using Java.Interop.Tools.TypeNameMappings; +using Microsoft.Android.Build.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Xamarin.Android.Tasks.LLVMIR; @@ -281,6 +282,8 @@ sealed class DsoCacheState public int JniRemappingReplacementMethodIndexEntryCount { get; set; } public PackageNamingPolicy PackageNamingPolicy { get; set; } public List NativeLibraries { get; set; } = []; + public ICollection? NativeLibrariesNoJniPreload { get; set; } + public ICollection? NativeLibrarysAlwaysJniPreload { get; set; } public bool MarshalMethodsEnabled { get; set; } public bool ManagedMarshalMethodsLookupEnabled { get; set; } public bool IgnoreSplitConfigs { get; set; } @@ -775,4 +778,62 @@ void MapStructures (LlvmIrModule module) runtimePropertyIndexEntryStructureInfo = module.MapStructure (); appEnvironmentVariableStructureInfo = module.MapStructure (); } + + internal static bool ShouldIgnoreForJniPreload (TaskLoggingHelper log, ICollection libsToIgnore, ITaskItem libItem) + { + if (libsToIgnore.Count == 0) { + return false; + } + + string? libFileName = GetFileName (log, libItem); + if (libFileName == null) { + return false; // We have no idea what it is, so let the caller handle the situation + } + + return !libsToIgnore.Contains (libFileName); + } + + internal static ICollection MakeJniPreloadIgnoreCollection (TaskLoggingHelper log, ICollection alwaysPreload, ICollection ignorePreload) + { + // There Can Be Only One, no matter what name casing is on the user's build OS. + var libsToIgnore = new HashSet (StringComparer.OrdinalIgnoreCase); + var neverIgnore = new HashSet (StringComparer.OrdinalIgnoreCase); + + string? fileName; + foreach (ITaskItem item in alwaysPreload) { + fileName = GetFileName (log, item); + if (fileName == null) { + continue; + } + + neverIgnore.Add (fileName); + } + + foreach (ITaskItem item in ignorePreload) { + fileName = GetFileName (log, item); + if (fileName == null) { + continue; + } + + if (neverIgnore.Contains (fileName)) { + log.LogDebugMessage ($"Native library '{item.ItemSpec}' cannot be ignored when preloading JNI native libraries."); + continue; + } + + libsToIgnore.Add (fileName); + } + + return libsToIgnore; + } + + static string? GetFileName (TaskLoggingHelper log, ITaskItem item) + { + var name = MonoAndroidHelper.GetNormalizedNativeLibraryName (item); + if (String.IsNullOrEmpty (name)) { + log.LogDebugMessage ($"Failed to convert item path '{item.ItemSpec}' to canonical native shared library name."); + return null; + } + + return name; + } } diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index ac4a8f888ea..b5a1d205e88 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -875,16 +875,28 @@ public static void LogTextStreamContents (TaskLoggingHelper log, string message, log.LogDebugMessage (reader.ReadToEnd ()); } - public static int GetMinimumApiLevel (AndroidTargetArch arch, AndroidRuntime runtime) + /// + /// Takes `libItem.ItemSpec` and transforms it to a file name of a native library. It will + /// remove any paths from `ItemSpec` and will make sure that the file ends with the `.so` + /// extension. Empty string is returned if there's nothing to process. String comparisons + /// are ordinal and case-insensitive. + /// + public static string GetNormalizedNativeLibraryName (ITaskItem libItem) { - int minValue = 0; + if (String.IsNullOrEmpty (libItem.ItemSpec)) { + return String.Empty; + } - Dictionary apiLevels = runtime == AndroidRuntime.MonoVM ? XABuildConfig.ArchToApiLevel : XABuildConfig.ArchToApiLevelNonMono; - if (!apiLevels.TryGetValue (arch, out minValue)) { - throw new InvalidOperationException ($"Unable to determine minimum API level for architecture {arch}"); + string ret = Path.GetFileName (libItem.ItemSpec); + if (String.IsNullOrEmpty (ret)) { + return String.Empty; } - return minValue; + if (!String.Equals (Path.GetExtension (ret), ".so", StringComparison.OrdinalIgnoreCase)) { + return "${ret}.so"; + } + + return ret; } } } diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 30e7b9d3813..d4d2349250f 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -291,6 +291,8 @@ Copyright (C) 2011-2012 Xamarin. All rights reserved. <_AndroidStripNativeLibraries Condition=" '$(AndroidStripNativeLibraries)' != '' And '$(AndroidStripNativeLibraries)' == 'true' ">true <_AndroidStripNativeLibraries Condition=" '$(_AndroidStripNativeLibraries)' != 'true' ">false + + false @@ -1693,10 +1695,32 @@ because xbuild doesn't support framework reference assemblies. OutputDirectory="$(_AndroidIntermediateJavaSourceDirectory)mono"> + + + + <_AndroidNativeLibraryAlwaysJniPreload Include="libSystem.Security.Cryptography.Native.Android.so" /> + + + <_AndroidNativeLibraryAlwaysJniPreload Include="libmonodroid.so" /> + + + + + + + Date: Mon, 9 Feb 2026 17:33:00 +0100 Subject: [PATCH 02/23] Slightly broken, tbc tomorrow --- ...icationConfigNativeAssemblyGeneratorCLR.cs | 31 +++++++++++++------ .../Xamarin.Android.Common.targets | 4 ++- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs index 668269bedc5..90165a0d135 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs @@ -686,6 +686,7 @@ DsoCacheState InitDSOCache () var nameMutations = new List (); var dsoNamesBlob = new LlvmIrStringBlob (); int nameMutationsCount = -1; + ICollection ignorePreload = MakeJniPreloadIgnoreCollection (Log, NativeLibrarysAlwaysJniPreload, NativeLibrariesNoJniPreload); for (int i = 0; i < dsos.Count; i++) { string name = dsos[i].name; @@ -693,7 +694,7 @@ DsoCacheState InitDSOCache () bool isJniLibrary = ELFHelper.IsJniLibrary (Log, dsos[i].item.ItemSpec); bool ignore = dsos[i].ignore; - bool ignore_for_preload = !DsoCacheJniPreloadIgnore.Contains (name); + bool ignore_for_preload = ShouldIgnoreForJniPreload (Log, ignorePreload, dsos[i].item); nameMutations.Clear(); AddNameMutations (name); @@ -721,7 +722,7 @@ DsoCacheState InitDSOCache () // We must add all aliases to the preloads indices array so that all of them have their handle // set when the library is preloaded. - if (entry.is_jni_library && ignore_for_preload) { + if (entry.is_jni_library && !ignore_for_preload) { jniPreloads.Add (entry); } @@ -793,20 +794,26 @@ internal static bool ShouldIgnoreForJniPreload (TaskLoggingHelper log, ICollecti return !libsToIgnore.Contains (libFileName); } - internal static ICollection MakeJniPreloadIgnoreCollection (TaskLoggingHelper log, ICollection alwaysPreload, ICollection ignorePreload) + internal static ICollection MakeJniPreloadIgnoreCollection (TaskLoggingHelper log, ICollection? alwaysPreload, ICollection? ignorePreload) { // There Can Be Only One, no matter what name casing is on the user's build OS. var libsToIgnore = new HashSet (StringComparer.OrdinalIgnoreCase); + if (ignorePreload == null || ignorePreload.Count == 0) { + return libsToIgnore; + } + var neverIgnore = new HashSet (StringComparer.OrdinalIgnoreCase); string? fileName; - foreach (ITaskItem item in alwaysPreload) { - fileName = GetFileName (log, item); - if (fileName == null) { - continue; - } + if (alwaysPreload != null) { + foreach (ITaskItem item in alwaysPreload) { + fileName = GetFileName (log, item); + if (fileName == null) { + continue; + } - neverIgnore.Add (fileName); + neverIgnore.Add (fileName); + } } foreach (ITaskItem item in ignorePreload) { @@ -828,7 +835,11 @@ internal static ICollection MakeJniPreloadIgnoreCollection (TaskLoggingH static string? GetFileName (TaskLoggingHelper log, ITaskItem item) { - var name = MonoAndroidHelper.GetNormalizedNativeLibraryName (item); + string? name = item.GetMetadata ("ArchiveFileName"); + if (String.IsNullOrEmpty (name)) { + name = MonoAndroidHelper.GetNormalizedNativeLibraryName (item); + } + if (String.IsNullOrEmpty (name)) { log.LogDebugMessage ($"Failed to convert item path '{item.ItemSpec}' to canonical native shared library name."); return null; diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index d4d2349250f..daa17fc536e 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1705,7 +1705,9 @@ because xbuild doesn't support framework reference assemblies. segfault --> <_AndroidNativeLibraryAlwaysJniPreload Include="libSystem.Security.Cryptography.Native.Android.so" /> - + <_AndroidNativeLibraryAlwaysJniPreload Include="libmonodroid.so" /> From 4498b97ef3a76151e9a3da1ac2675cc6e31caf34 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Tue, 10 Feb 2026 09:55:19 +0100 Subject: [PATCH 03/23] Update src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets Co-authored-by: Jonathan Peppers --- src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index daa17fc536e..94ba0a122f0 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1711,7 +1711,7 @@ because xbuild doesn't support framework reference assemblies. <_AndroidNativeLibraryAlwaysJniPreload Include="libmonodroid.so" /> - + From 964df0f8746dbca7098e75b6ba5ebbbd4b7359df Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Tue, 10 Feb 2026 11:05:36 +0100 Subject: [PATCH 04/23] Default config works (CLR only for now) --- .../ApplicationConfigNativeAssemblyGeneratorCLR.cs | 2 +- .../Xamarin.Android.Common.targets | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs index 90165a0d135..2f62ac2a456 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs @@ -791,7 +791,7 @@ internal static bool ShouldIgnoreForJniPreload (TaskLoggingHelper log, ICollecti return false; // We have no idea what it is, so let the caller handle the situation } - return !libsToIgnore.Contains (libFileName); + return libsToIgnore.Contains (libFileName); } internal static ICollection MakeJniPreloadIgnoreCollection (TaskLoggingHelper log, ICollection? alwaysPreload, ICollection? ignorePreload) diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index 94ba0a122f0..c50781c231d 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1695,7 +1695,7 @@ because xbuild doesn't support framework reference assemblies. OutputDirectory="$(_AndroidIntermediateJavaSourceDirectory)mono"> - - <_AndroidNativeLibraryAlwaysJniPreload Include="libmonodroid.so" /> + <_AndroidNativeLibraryNeverJniPreload Include="libmonodroid.so" /> + + + + <_AllNativeLibraries Include="@(AndroidNativeLibrary);@(EmbeddedNativeLibrary);@(FrameworkNativeLibrary)" /> - + Date: Tue, 10 Feb 2026 13:28:31 +0100 Subject: [PATCH 05/23] Beginnings of unit tests --- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 68 +++++++++++++++++++ .../Utilities/EnvironmentHelper.cs | 64 +++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs new file mode 100644 index 00000000000..3d49cfdc5b6 --- /dev/null +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.Build.Framework; +using Mono.Cecil; +using NUnit.Framework; +using Xamarin.Android.Tasks; +using Xamarin.Android.Tools; +using Xamarin.ProjectTools; +using Microsoft.Android.Build.Tasks; + +namespace Xamarin.Android.Build.Tests; + +[Parallelizable (ParallelScope.Children)] +public partial class BuildTest3 : BaseTest +{ + [Test] + public void NativeLibraryJniPreloadDefaultsWork ([Values] AndroidRuntime runtime) + { + const bool isRelease = true; + if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { + return; + } + + if (runtime == AndroidRuntime.NativeAOT) { + Assert.Ignore ("NativeAOT doesn't use JNI preload"); + } + + AndroidTargetArch[] supportedArches = new [] { + AndroidTargetArch.Arm64, + AndroidTargetArch.X86_64, + }; + + var proj = new XamarinAndroidApplicationProject { + IsRelease = isRelease, + }; + proj.SetRuntime (runtime); + proj.SetRuntimeIdentifiers (supportedArches); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + string objDirPath = Path.Combine (Root, builder.ProjectDirectory, proj.IntermediateOutputPath); + List envFiles = EnvironmentHelper.GatherEnvironmentFiles ( + objDirPath, + String.Join (";", supportedArches.Select (arch => MonoAndroidHelper.ArchToAbi (arch))), + true + ); + + EnvironmentHelper.IApplicationConfig app_config = EnvironmentHelper.ReadApplicationConfig (envFiles, runtime); + uint numberOfDsoCacheEntries = runtime switch { + AndroidRuntime.MonoVM => ((EnvironmentHelper.ApplicationConfig_MonoVM)app_config).number_of_dso_cache_entries, + AndroidRuntime.CoreCLR => ((EnvironmentHelper.ApplicationConfig_CoreCLR)app_config).number_of_dso_cache_entries, + _ => throw new NotSupportedException ($"Unsupported runtime '{runtime}'") + }; + + List preloads = EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); + } +} diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index 17c9293cf80..128733659ce 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -98,6 +98,36 @@ public sealed class ApplicationConfig_MonoVM : IApplicationConfig public bool managed_marshal_methods_lookup_enabled; } + // This is shared between MonoVM and CoreCLR hosts, not used by NativeAOT + public sealed class DSOCacheEntry64 + { + // Hardcoded, by design - we want to know if there are any changes in the + // native assembly layout. + public const uint NativeSize = 32; + + public ulong hash; + public ulong real_name_hash; + public bool ignore; + public bool is_jni_library; + public string name; // real structure has an index here, we fetch the string to make it easier + public IntPtr handle; + } + + // This is a synthetic class, not reflecting what's in the generated LLVM IR/assembler source + public sealed class JniPreloadsEntry + { + public uint Index; + public string LibraryName; + } + + // This is a synthetic class, not reflecting what's in the generated LLVM IR/assembler source + public sealed class JniPreloads + { + public uint IndexStride; + public ulong IndexCount; + public List Entries; + } + const uint ApplicationConfigFieldCount_MonoVM = 27; const string ApplicationConfigSymbolName = "application_config"; @@ -107,6 +137,12 @@ public sealed class ApplicationConfig_MonoVM : IApplicationConfig const string AppEnvironmentVariablesNativeAOTSymbolName = "__naot_android_app_environment_variables"; const string AppEnvironmentVariableContentsNativeAOTSymbolName = "__naot_android_app_environment_variable_contents"; + const string DsoJniPreloadsIdxStrideSymbolName = "dso_jni_preloads_idx_stride"; + const string DsoJniPreloadsIdxCountSymbolName = "dso_jni_preloads_idx_count"; + const string DsoJniPreloadsIdxSymbolName = "dso_jni_preloads_idx"; + const string DsoCacheSymbolName = "dso_cache"; + const string DsoNamesDataSymbolName = "dso_names_data"; + static readonly object ndkInitLock = new object (); static readonly char[] readElfFieldSeparator = new [] { ' ', '\t' }; static readonly Regex assemblerLabelRegex = new Regex ("^[_.a-zA-Z0-9]+:", RegexOptions.Compiled); @@ -921,6 +957,34 @@ static void AssertSharedLibraryHasRequiredSymbols (string dsoPath, string readEl } } + public static List ReadJniPreloads (List envFilePaths, uint expectedDsoCacheEntryCount, AndroidRuntime runtime) + { + var ret = new List (); + + foreach (EnvironmentFile envFile in envFilePaths) { + JniPreloads preloads = runtime switch { + AndroidRuntime.CoreCLR => ReadJniPreloads_CLR (envFile), + _ => throw new NotSupportedException ($"Unsupported runtime '{runtime}'") + }; + + ret.Add (preloads); + } + + return ret; + } + + static JniPreloads ReadJniPreloads_CLR (EnvironmentFile envFile) + { + NativeAssemblyParser parser = CreateAssemblyParser (envFile); + NativeAssemblyParser.AssemblerSymbol dsoCache = GetRequiredSymbol (DsoCacheSymbolName, envFile, parser); + NativeAssemblyParser.AssemblerSymbol dsoNamesData = GetRequiredSymbol (DsoNamesDataSymbolName, envFile, parser); + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxStride = GetRequiredSymbol (DsoJniPreloadsIdxStrideSymbolName, envFile, parser); + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxCount = GetRequiredSymbol (DsoJniPreloadsIdxCountSymbolName, envFile, parser); + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdx = GetRequiredSymbol (DsoJniPreloadsIdxSymbolName, envFile, parser); + + throw new NotImplementedException (); + } + static (List stdout, List stderr) RunCommand (string executablePath, string arguments = null) { var psi = new ProcessStartInfo { From d4adec819e9f73ee4f438d7a916b53e217d13082 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Tue, 10 Feb 2026 17:31:41 +0100 Subject: [PATCH 06/23] Environment test helper progress --- .../Utilities/EnvironmentHelper.cs | 251 +++++++++++++++--- 1 file changed, 208 insertions(+), 43 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index 128733659ce..eb3e0ebcbe2 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -158,6 +158,10 @@ public sealed class JniPreloads ".long", }; + static readonly HashSet expectedUInt64Types = new HashSet (StringComparer.Ordinal) { + ".xword", + }; + static readonly string[] requiredSharedLibrarySymbolsMonoVM = { AppEnvironmentVariablesSymbolName, "app_system_properties", @@ -632,21 +636,8 @@ static Dictionary ReadEnvironmentVariables_CoreCLR_NativeAOT (En Assert.IsTrue (appEnvvarsContentsSymbol.Size != 0, $"{envvarsContentsSymbolName} size as specified in the '.size' directive must not be 0"); Assert.IsTrue (appEnvvarsContentsSymbol.Contents.Count == 1, $"{envvarsContentsSymbolName} symbol must have a single value."); - NativeAssemblyParser.AssemblerSymbolItem contentsItem = appEnvvarsContentsSymbol.Contents[0]; - string[] field = GetField (envFile.Path, parser.SourceFilePath, contentsItem.Contents, contentsItem.LineNumber);; - Assert.IsTrue (field[0] == ".asciz", $"{envvarsContentsSymbolName} must be of '.asciz' type"); - - - var sb = new StringBuilder (); - // We need to get rid of the '"' delimiter llc outputs.. - sb.Append (field[1].Trim ('"')); - - // ...and llc outputs NUL as the octal '\000' sequence, we need an actual NUL... - sb.Replace ("\\000", "\0"); - - // ...and since it's an .asciz variable, the string doesn't contain explicit terminating NUL, but we need one - sb.Append ('\0'); - string contents = sb.ToString (); + string[] field; + string contents = ReadStringBlob (envFile, appEnvvarsContentsSymbol, parser); var indexes = new List<(uint nameIdx, uint valueIdx)> (); // Environment variables are pairs of indexes into the contents array @@ -659,11 +650,16 @@ static Dictionary ReadEnvironmentVariables_CoreCLR_NativeAOT (En // Contents array is a collection of strings terminated with the NUL character var ret = new Dictionary (StringComparer.Ordinal); + + const string ContentsAssertionTag = "Environment Variables"; foreach (var envvar in indexes) { Assert.IsTrue (envvar.nameIdx < appEnvvarsContentsSymbol.Size, $"Environment variable name index {envvar.nameIdx} is out of range of the contents array"); Assert.IsTrue (envvar.valueIdx < appEnvvarsContentsSymbol.Size, $"Environment variable value index {envvar.valueIdx} is out of range of the contents array"); - ret.Add (GetFromContents (envvar.nameIdx), GetFromContents (envvar.valueIdx)); + ret.Add ( + GetStringFromBlobContents (ContentsAssertionTag, contents, envvar.nameIdx), + GetStringFromBlobContents (ContentsAssertionTag, contents, envvar.valueIdx) + ); } return ret; @@ -676,23 +672,6 @@ uint GetIndex (NativeAssemblyParser.AssemblerSymbolItem item, string name) Assert.IsTrue (expectedUInt32Types.Contains (field[0]), $"Environment variable {name} index field has invalid type '${field[0]}'"); return UInt32.Parse (field[1], CultureInfo.InvariantCulture); } - - string GetFromContents (uint idx) - { - var sb = new StringBuilder (); - bool foundNull = false; - - for (int i = (int)idx; i < contents.Length; i++) { - if (contents[i] == '\0') { - foundNull = true; - break; - } - sb.Append (contents[i]); - } - - Assert.IsTrue (foundNull, $"Environment variable contents string starting at index {idx} is not NUL-terminated"); - return sb.ToString (); - } } static Dictionary ReadEnvironmentVariables_MonoVM (EnvironmentFile envFile) @@ -963,7 +942,7 @@ public static List ReadJniPreloads (List envFilePa foreach (EnvironmentFile envFile in envFilePaths) { JniPreloads preloads = runtime switch { - AndroidRuntime.CoreCLR => ReadJniPreloads_CLR (envFile), + AndroidRuntime.CoreCLR => ReadJniPreloads_CoreCLR (envFile, expectedDsoCacheEntryCount), _ => throw new NotSupportedException ($"Unsupported runtime '{runtime}'") }; @@ -973,16 +952,134 @@ public static List ReadJniPreloads (List envFilePa return ret; } - static JniPreloads ReadJniPreloads_CLR (EnvironmentFile envFile) + static JniPreloads ReadJniPreloads_CoreCLR (EnvironmentFile envFile, uint expectedDsoCacheEntryCount) { NativeAssemblyParser parser = CreateAssemblyParser (envFile); - NativeAssemblyParser.AssemblerSymbol dsoCache = GetRequiredSymbol (DsoCacheSymbolName, envFile, parser); - NativeAssemblyParser.AssemblerSymbol dsoNamesData = GetRequiredSymbol (DsoNamesDataSymbolName, envFile, parser); - NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxStride = GetRequiredSymbol (DsoJniPreloadsIdxStrideSymbolName, envFile, parser); - NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxCount = GetRequiredSymbol (DsoJniPreloadsIdxCountSymbolName, envFile, parser); - NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdx = GetRequiredSymbol (DsoJniPreloadsIdxSymbolName, envFile, parser); + + NativeAssemblyParser.AssemblerSymbol dsoNamesData = GetNonEmptyRequiredSymbol (DsoNamesDataSymbolName); + Assert.IsTrue (dsoNamesData.Size > 0, "DSO names data must have size larger than zero"); + + NativeAssemblyParser.AssemblerSymbol dsoCache = GetNonEmptyRequiredSymbol (DsoCacheSymbolName); + uint calculatedDsoCacheEntryCount = (uint)(dsoCache.Size / DSOCacheEntry64.NativeSize); + Assert.IsTrue (calculatedDsoCacheEntryCount == expectedDsoCacheEntryCount, $"Calculated DSO cache entry count should be {expectedDsoCacheEntryCount} but was {calculatedDsoCacheEntryCount} instead."); + + uint calculatedDsoCacheEntrySize = (uint)(DSOCacheEntry64.NativeSize * expectedDsoCacheEntryCount); + Assert.IsTrue (calculatedDsoCacheEntrySize == dsoCache.Size, $"Calculated DSO cache size should be {dsoCache.Size} but was {calculatedDsoCacheEntrySize} instead."); + + string dsoNames = ReadStringBlob (envFile, dsoNamesData, parser); + Assert.IsTrue (dsoNames.Length > 0, "DSO names read from source mustn't be empty"); + + List dsoCacheEntries = ReadDsoCache64 (envFile, parser, dsoCache, dsoNames); + Assert.IsTrue ((uint)dsoCacheEntries.Count == expectedDsoCacheEntryCount, $"DSO cache read from the source should have {expectedDsoCacheEntryCount} entries, it had {dsoCacheEntries.Count} instead."); + + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxStride = GetNonEmptyRequiredSymbol (DsoJniPreloadsIdxStrideSymbolName); + uint preloadsStride = GetSymbolValueAsUInt32 (dsoJniPreloadsIdxStride); + Assert.IsTrue (preloadsStride > 0, $"Symbol {dsoJniPreloadsIdxStride.Name} must have value larger than 0."); + + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdxCount = GetNonEmptyRequiredSymbol (DsoJniPreloadsIdxCountSymbolName); + ulong preloadsCount = GetSymbolValueAsUInt64 (dsoJniPreloadsIdxCount); + Assert.IsTrue (preloadsCount > 0, $"Symbol {dsoJniPreloadsIdxStride.Name} must have value larger than 0."); + + NativeAssemblyParser.AssemblerSymbol dsoJniPreloadsIdx = GetNonEmptyRequiredSymbol (DsoJniPreloadsIdxSymbolName); + ulong calculatedPreloadsIdxSize = preloadsCount * 4; // single index field is a 32-bit integer + Assert.IsTrue (dsoJniPreloadsIdx.Size == calculatedPreloadsIdxSize, $"JNI preloads index should have size of {calculatedPreloadsIdxSize} instead of {dsoJniPreloadsIdx.Size}"); throw new NotImplementedException (); + + NativeAssemblyParser.AssemblerSymbol GetNonEmptyRequiredSymbol (string symbolName) + { + var symbol = GetRequiredSymbol (symbolName, envFile, parser); + + Assert.IsTrue (symbol.Size != 0, $"{symbolName} size as specified in the '.size' directive must not be 0"); + return symbol; + } + + uint GetSymbolValueAsUInt32 (NativeAssemblyParser.AssemblerSymbol symbol) + { + NativeAssemblyParser.AssemblerSymbolItem item = symbol.Contents[0]; + string[] field = GetField (envFile.Path, parser.SourceFilePath, item.Contents, item.LineNumber); + Assert.IsTrue (expectedUInt32Types.Contains (field [0]), $"Unexpected 32-bit integer field type for symbol {symbol.Name} in '{envFile.Path}:{item.LineNumber}': {field [0]}"); + return ConvertFieldToUInt32 (DsoJniPreloadsIdxStrideSymbolName, envFile.Path, parser.SourceFilePath, item.LineNumber, field[1]); + } + + ulong GetSymbolValueAsUInt64 (NativeAssemblyParser.AssemblerSymbol symbol) + { + NativeAssemblyParser.AssemblerSymbolItem item = symbol.Contents[0]; + string[] field = GetField (envFile.Path, parser.SourceFilePath, item.Contents, item.LineNumber); + Assert.IsTrue (expectedUInt64Types.Contains (field [0]), $"Unexpected 64-bit integer field type for symbol {symbol.Name} in '{envFile.Path}:{item.LineNumber}': {field [0]}"); + return ConvertFieldToUInt64 (DsoJniPreloadsIdxStrideSymbolName, envFile.Path, parser.SourceFilePath, item.LineNumber, field[1]); + } + } + + static List ReadDsoCache64 (EnvironmentFile envFile, NativeAssemblyParser parser, NativeAssemblyParser.AssemblerSymbol dsoCache, string dsoNamesBlob) + { + var ret = new List (); + + // This follows a VERY strict format, by design. If anything changes in the generated source this is supposed + // to break. + const int itemsPerEntry = 7; // Includes padding entries + for (int i = 0; i < dsoCache.Contents.Count; i += itemsPerEntry) { + ulong lineNumber; + string value; + int index = i; + + // uint64_t hash + (lineNumber, value) = ReadNextFieldValue (index++, ".xword"); + ulong hash = ConvertFieldToUInt64 ("hash", envFile.Path, parser.SourceFilePath, lineNumber, value); + + // uint64_t real_name_hash + (lineNumber, value) = ReadNextFieldValue (index++, ".xword"); + ulong real_name_hash = ConvertFieldToUInt64 ("real_name_hash", envFile.Path, parser.SourceFilePath, lineNumber, value); + + // bool ignore + (lineNumber, value) = ReadNextFieldValue (index++, ".byte"); + bool ignore = ConvertFieldToBool ("ignore", envFile.Path, parser.SourceFilePath, lineNumber, value); + + // bool is_jni_library + (lineNumber, value) = ReadNextFieldValue (index++, ".byte"); + bool is_jni_library = ConvertFieldToBool ("is_jni_library", envFile.Path, parser.SourceFilePath, lineNumber, value); + + // padding, 2 bytes + (lineNumber, value) = ReadNextFieldValue (index, ".zero"); + uint padding1 = ConvertFieldToUInt32 ("padding1", envFile.Path, parser.SourceFilePath, lineNumber, value); + Assert.IsTrue (padding1 == 2, $"Padding field #1 at index {index} of symbol '{dsoCache.Name}' should have had a value of 2, instead it was set to {padding1}"); + index++; + + // uint32_t name_index + (lineNumber, value) = ReadNextFieldValue (index++, ".word"); + uint name_index = ConvertFieldToUInt32 ("name_index", envFile.Path, parser.SourceFilePath, lineNumber, value); + + // void* handle + (lineNumber, value) = ReadNextFieldValue (index, ".xword"); + ulong handle = ConvertFieldToUInt64 ("handle", envFile.Path, parser.SourceFilePath, lineNumber, value); + Assert.IsTrue (handle == 0, $"Handle field at index {index} of symbol '{dsoCache.Name}' should have had a value of 0, instead it was set to {handle}"); + + string name = GetStringFromBlobContents ("DSO JNI preloads", dsoNamesBlob, name_index); + ret.Add ( + new DSOCacheEntry64 { + hash = hash, + real_name_hash = real_name_hash, + ignore = ignore, + is_jni_library = is_jni_library, + name = name, + handle = IntPtr.Zero, + } + ); + } + + return ret; + + (ulong line, string value) ReadNextFieldValue (int index, string expectedType) + { + Assert.IsFalse (index >= dsoCache.Contents.Count, $"Index {index} exceeds the number of items in the {dsoCache.Name} array."); + NativeAssemblyParser.AssemblerSymbolItem item = dsoCache.Contents[index]; + + string[] field = GetField (envFile.Path, parser.SourceFilePath, item.Contents, item.LineNumber); + Assert.IsTrue (field.Length == 2, $"Item {index} of symbol {dsoCache.Name} at {envFile.Path}:{item.LineNumber} has an invalid value."); + Assert.IsTrue (field[0] == expectedType, $"Item {index} of symbol {dsoCache.Name} at {envFile.Path}:{item.LineNumber} should be of type '{expectedType}', but was '{field[0]}' instead."); + + return (item.LineNumber, field[1]); + } } static (List stdout, List stderr) RunCommand (string executablePath, string arguments = null) @@ -1071,7 +1168,17 @@ static uint ConvertFieldToUInt32 (string fieldName, string llvmAssemblerEnvFile, Assert.IsTrue (value.Length > 0, $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint32_t value (not long enough). File generated from '{llvmAssemblerEnvFile}'"); uint fv; - Assert.IsTrue (TryParseInteger (value, out fv), $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint32_t value (not a valid integer). File generated from '{llvmAssemblerEnvFile}'"); + Assert.IsTrue (TryParseInteger (value, out fv), $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint32_t value ('{value}' is not a valid integer). File generated from '{llvmAssemblerEnvFile}'"); + + return fv; + } + + static ulong ConvertFieldToUInt64 (string fieldName, string llvmAssemblerEnvFile, string nativeAssemblerEnvFile, ulong fileLine, string value) + { + Assert.IsTrue (value.Length > 0, $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint64_t value (not long enough). File generated from '{llvmAssemblerEnvFile}'"); + + ulong fv; + Assert.IsTrue (TryParseInteger (value, out fv), $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint64_t value ('{value}' is not a valid integer). File generated from '{llvmAssemblerEnvFile}'"); return fv; } @@ -1081,18 +1188,40 @@ static byte ConvertFieldToByte (string fieldName, string llvmAssemblerEnvFile, s Assert.IsTrue (value.Length > 0, $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint8_t value (not long enough). File generated from '{llvmAssemblerEnvFile}'"); byte fv; - Assert.IsTrue (TryParseInteger (value, out fv), $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint8_t value (not a valid integer). File generated from '{llvmAssemblerEnvFile}'"); + Assert.IsTrue (TryParseInteger (value, out fv), $"Field '{fieldName}' in {nativeAssemblerEnvFile}:{fileLine} is not a valid uint8_t value ('{value}' is not a valid integer). File generated from '{llvmAssemblerEnvFile}'"); return fv; } + // Integers are parsed as signed, since llc will always output signed integers. static bool TryParseInteger (string value, out uint fv) { if (value.StartsWith ("0x", StringComparison.Ordinal)) { return UInt32.TryParse (value.Substring (2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out fv); } - return UInt32.TryParse (value, out fv); + fv = 0; + if (!Int32.TryParse (value, out int signedFV)) { + return false; + } + + fv = (uint)signedFV; + return true; + } + + static bool TryParseInteger (string value, out ulong fv) + { + if (value.StartsWith ("0x", StringComparison.Ordinal)) { + return UInt64.TryParse (value.Substring (2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out fv); + } + + fv = 0; + if (!Int64.TryParse (value, out long signedFV)) { + return false; + } + + fv = (ulong)signedFV; + return true; } static bool TryParseInteger (string value, out byte fv) @@ -1103,5 +1232,41 @@ static bool TryParseInteger (string value, out byte fv) return Byte.TryParse (value, out fv); } + + static string ReadStringBlob (EnvironmentFile envFile, NativeAssemblyParser.AssemblerSymbol contentsSymbol, NativeAssemblyParser parser) + { + NativeAssemblyParser.AssemblerSymbolItem contentsItem = contentsSymbol.Contents[0]; + string[] field = GetField (envFile.Path, parser.SourceFilePath, contentsItem.Contents, contentsItem.LineNumber);; + Assert.IsTrue (field[0] == ".asciz", $"{contentsSymbol.Name} must be of '.asciz' type"); + + var sb = new StringBuilder (); + // We need to get rid of the '"' delimiter llc outputs.. + sb.Append (field[1].Trim ('"')); + + // ...and llc outputs NUL as the octal '\000' sequence, we need an actual NUL... + sb.Replace ("\\000", "\0"); + + // ...and since it's an .asciz variable, the string doesn't contain explicit terminating NUL, but we need one + sb.Append ('\0'); + + return sb.ToString (); + } + + static string GetStringFromBlobContents (string assertionTag, string contents, uint idx) + { + var sb = new StringBuilder (); + bool foundNull = false; + + for (int i = (int)idx; i < contents.Length; i++) { + if (contents[i] == '\0') { + foundNull = true; + break; + } + sb.Append (contents[i]); + } + + Assert.IsTrue (foundNull, $"[{assertionTag} string starting at index {idx} of a string blob is not NUL-terminated"); + return sb.ToString (); + } } } From 531e623ff1d4e07c76d6e52f1cc8d8c17191dc25 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 12:16:17 +0100 Subject: [PATCH 07/23] [clr] first basic test works --- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 31 ++++++++- .../Utilities/EnvironmentHelper.cs | 64 +++++++++++++------ 2 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index 3d49cfdc5b6..4fa3a8630d5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -63,6 +63,35 @@ public void NativeLibraryJniPreloadDefaultsWork ([Values] AndroidRuntime runtime _ => throw new NotSupportedException ($"Unsupported runtime '{runtime}'") }; - List preloads = EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); + const uint ExpectedIndexStride = 4; + const int ExpectedEntryCount = 4; // stride * number_of_libs + + List allPreloads = EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); + foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { + Assert.IsTrue (preloads.IndexStride == ExpectedIndexStride, $"JNI preloads index stride should be {ExpectedIndexStride}, was {preloads.IndexStride} instead. Source file: {preloads.SourceFile}"); + Assert.IsTrue (preloads.Entries.Count == ExpectedEntryCount, $"JNI preloads index entry count should be {ExpectedEntryCount}, was {preloads.Entries.Count} instead. Source file: {preloads.SourceFile}"); + + // DSO cache entries are sorted based on their **mutated name's** 64-bit xxHash, which + // won't change but builds may add/remove libraries and, thus, change the indexes after + // sorting. For that reason we don't verify the index values and use them just for reporting. + // + // Also, all the entries will point to the same library name. Name variations aren't + // stored directly in the DSO cache, just their hashes which are used for lookup at run time. + // + // We use a Dictionary<> here because there might be more libraries to preload in the future. + var expectedLibNames = new Dictionary (StringComparer.Ordinal) { + { "libSystem.Security.Cryptography.Native.Android.so", 0 }, + }; + + for (int i = 0; i < preloads.Entries.Count; i++) { + EnvironmentHelper.JniPreloadsEntry entry = preloads.Entries[i]; + Assert.IsTrue (expectedLibNames.ContainsKey (entry.LibraryName), $"JNI preloads entry at index {i}, referring to library at DSO cache index {entry.Index} has unexpected name '{entry.LibraryName}'; Source file: {preloads.SourceFile}"); + expectedLibNames[entry.LibraryName]++; + } + + foreach (var kvp in expectedLibNames) { + Assert.IsTrue (kvp.Value == ExpectedIndexStride, $"JNI preloads entry '{kvp.Key}' should have {ExpectedIndexStride} instances, it had {kvp.Value} instead. Source file: {preloads.SourceFile}"); + } + } } } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs index eb3e0ebcbe2..ceaf6a04861 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs @@ -124,8 +124,8 @@ public sealed class JniPreloadsEntry public sealed class JniPreloads { public uint IndexStride; - public ulong IndexCount; public List Entries; + public string SourceFile; } const uint ApplicationConfigFieldCount_MonoVM = 27; @@ -160,6 +160,7 @@ public sealed class JniPreloads static readonly HashSet expectedUInt64Types = new HashSet (StringComparer.Ordinal) { ".xword", + ".quad", }; static readonly string[] requiredSharedLibrarySymbolsMonoVM = { @@ -984,7 +985,26 @@ static JniPreloads ReadJniPreloads_CoreCLR (EnvironmentFile envFile, uint expect ulong calculatedPreloadsIdxSize = preloadsCount * 4; // single index field is a 32-bit integer Assert.IsTrue (dsoJniPreloadsIdx.Size == calculatedPreloadsIdxSize, $"JNI preloads index should have size of {calculatedPreloadsIdxSize} instead of {dsoJniPreloadsIdx.Size}"); - throw new NotImplementedException (); + var preloadsIndex = new List (); + for (int i = 0; i < (int)preloadsCount; i++) { + (ulong lineNumber, string value) = ReadNextArrayIndex (envFile, parser, dsoJniPreloadsIdx, i, expectedUInt32Types); + uint index = ConvertFieldToUInt32 ("index", envFile.Path, parser.SourceFilePath, lineNumber, value); + + Assert.True (index < (uint)dsoCacheEntries.Count, $"JNI preload index {index} is larger than the number of items in the DSO cache array ({dsoCacheEntries.Count})"); + preloadsIndex.Add ( + new JniPreloadsEntry { + Index = index, + LibraryName = dsoCacheEntries[(int)index].name, + } + ); + } + Assert.IsTrue (preloadsCount == (uint)preloadsIndex.Count, $"JNI preload index count should be equal to {preloadsCount}, but was {preloadsIndex.Count} instead."); + + return new JniPreloads { + IndexStride = preloadsStride, + Entries = preloadsIndex, + SourceFile = envFile.Path, + }; NativeAssemblyParser.AssemblerSymbol GetNonEmptyRequiredSymbol (string symbolName) { @@ -1024,33 +1044,33 @@ static List ReadDsoCache64 (EnvironmentFile envFile, NativeAsse int index = i; // uint64_t hash - (lineNumber, value) = ReadNextFieldValue (index++, ".xword"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index++, expectedUInt64Types); ulong hash = ConvertFieldToUInt64 ("hash", envFile.Path, parser.SourceFilePath, lineNumber, value); // uint64_t real_name_hash - (lineNumber, value) = ReadNextFieldValue (index++, ".xword"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index++, expectedUInt64Types); ulong real_name_hash = ConvertFieldToUInt64 ("real_name_hash", envFile.Path, parser.SourceFilePath, lineNumber, value); // bool ignore - (lineNumber, value) = ReadNextFieldValue (index++, ".byte"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index++, ".byte"); bool ignore = ConvertFieldToBool ("ignore", envFile.Path, parser.SourceFilePath, lineNumber, value); // bool is_jni_library - (lineNumber, value) = ReadNextFieldValue (index++, ".byte"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index++, ".byte"); bool is_jni_library = ConvertFieldToBool ("is_jni_library", envFile.Path, parser.SourceFilePath, lineNumber, value); // padding, 2 bytes - (lineNumber, value) = ReadNextFieldValue (index, ".zero"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index, ".zero"); uint padding1 = ConvertFieldToUInt32 ("padding1", envFile.Path, parser.SourceFilePath, lineNumber, value); Assert.IsTrue (padding1 == 2, $"Padding field #1 at index {index} of symbol '{dsoCache.Name}' should have had a value of 2, instead it was set to {padding1}"); index++; // uint32_t name_index - (lineNumber, value) = ReadNextFieldValue (index++, ".word"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index++, expectedUInt32Types); uint name_index = ConvertFieldToUInt32 ("name_index", envFile.Path, parser.SourceFilePath, lineNumber, value); // void* handle - (lineNumber, value) = ReadNextFieldValue (index, ".xword"); + (lineNumber, value) = ReadNextArrayIndex (envFile, parser, dsoCache, index, expectedUInt64Types); ulong handle = ConvertFieldToUInt64 ("handle", envFile.Path, parser.SourceFilePath, lineNumber, value); Assert.IsTrue (handle == 0, $"Handle field at index {index} of symbol '{dsoCache.Name}' should have had a value of 0, instead it was set to {handle}"); @@ -1068,18 +1088,26 @@ static List ReadDsoCache64 (EnvironmentFile envFile, NativeAsse } return ret; + } - (ulong line, string value) ReadNextFieldValue (int index, string expectedType) - { - Assert.IsFalse (index >= dsoCache.Contents.Count, $"Index {index} exceeds the number of items in the {dsoCache.Name} array."); - NativeAssemblyParser.AssemblerSymbolItem item = dsoCache.Contents[index]; + static (ulong line, string value) ReadNextArrayIndex (EnvironmentFile envFile, NativeAssemblyParser parser, NativeAssemblyParser.AssemblerSymbol array, int index, string expectedType) + { + return ReadNextArrayIndex (envFile, parser, array, index, new HashSet (StringComparer.Ordinal) { expectedType }); + } - string[] field = GetField (envFile.Path, parser.SourceFilePath, item.Contents, item.LineNumber); - Assert.IsTrue (field.Length == 2, $"Item {index} of symbol {dsoCache.Name} at {envFile.Path}:{item.LineNumber} has an invalid value."); - Assert.IsTrue (field[0] == expectedType, $"Item {index} of symbol {dsoCache.Name} at {envFile.Path}:{item.LineNumber} should be of type '{expectedType}', but was '{field[0]}' instead."); + static (ulong line, string value) ReadNextArrayIndex (EnvironmentFile envFile, NativeAssemblyParser parser, NativeAssemblyParser.AssemblerSymbol array, + int index, HashSet expectedTypes) + { + Assert.IsFalse (index >= array.Contents.Count, $"Index {index} exceeds the number of items in the {array.Name} array."); + NativeAssemblyParser.AssemblerSymbolItem item = array.Contents[index]; - return (item.LineNumber, field[1]); - } + string[] field = GetField (envFile.Path, parser.SourceFilePath, item.Contents, item.LineNumber); + Assert.IsTrue (field.Length == 2, $"Item {index} of symbol {array.Name} at {envFile.Path}:{item.LineNumber} has an invalid value."); + + string expectedTypesList = String.Join (" | ", expectedTypes); + Assert.IsTrue (expectedTypes.Contains (field[0]), $"Item {index} of symbol {array.Name} at {parser.SourceFilePath}:{item.LineNumber} should be of type '{expectedTypesList}', but was '{field[0]}' instead."); + + return (item.LineNumber, field[1]); } static (List stdout, List stderr) RunCommand (string executablePath, string arguments = null) From 8eedb6134b8897d1f3921d33018f1d291e494dd5 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 12:51:00 +0100 Subject: [PATCH 08/23] A tiny refactoring in prep for more tests --- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 85 ++++++++++++------- 1 file changed, 53 insertions(+), 32 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index 4fa3a8630d5..e36bfe17ce9 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -23,12 +23,62 @@ namespace Xamarin.Android.Build.Tests; [Parallelizable (ParallelScope.Children)] public partial class BuildTest3 : BaseTest { + const uint ExpectedJniPreloadIndexStride = 4; + + [Test] + public void NativeLibraryJniPreload_IgnoreAllJniPreload_PreserveRequired ([Values] AndroidRuntime runtime) + { + const int ExpectedEntryCount = 4; // stride * number_of_libs + + List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads (runtime); + if (allPreloads == null) { + return; + } + } + [Test] - public void NativeLibraryJniPreloadDefaultsWork ([Values] AndroidRuntime runtime) + public void NativeLibraryJniPreload_DefaultsWork ([Values] AndroidRuntime runtime) + { + const int ExpectedEntryCount = 4; // stride * number_of_libs + + List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads (runtime); + if (allPreloads == null) { + return; + } + + foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { + Assert.IsTrue (preloads.IndexStride == ExpectedJniPreloadIndexStride, $"JNI preloads index stride should be {ExpectedJniPreloadIndexStride}, was {preloads.IndexStride} instead. Source file: {preloads.SourceFile}"); + Assert.IsTrue (preloads.Entries.Count == ExpectedEntryCount, $"JNI preloads index entry count should be {ExpectedEntryCount}, was {preloads.Entries.Count} instead. Source file: {preloads.SourceFile}"); + + // DSO cache entries are sorted based on their **mutated name's** 64-bit xxHash, which + // won't change but builds may add/remove libraries and, thus, change the indexes after + // sorting. For that reason we don't verify the index values and use them just for reporting. + // + // Also, all the entries will point to the same library name. Name variations aren't + // stored directly in the DSO cache, just their hashes which are used for lookup at run time. + // + // We use a Dictionary<> here because there might be more libraries to preload in the future. + var expectedLibNames = new Dictionary (StringComparer.Ordinal) { + { "libSystem.Security.Cryptography.Native.Android.so", 0 }, + }; + + for (int i = 0; i < preloads.Entries.Count; i++) { + EnvironmentHelper.JniPreloadsEntry entry = preloads.Entries[i]; + Assert.IsTrue (expectedLibNames.ContainsKey (entry.LibraryName), $"JNI preloads entry at index {i}, referring to library at DSO cache index {entry.Index} has unexpected name '{entry.LibraryName}'; Source file: {preloads.SourceFile}"); + expectedLibNames[entry.LibraryName]++; + } + + foreach (var kvp in expectedLibNames) { + Assert.IsTrue (kvp.Value == ExpectedJniPreloadIndexStride, $"JNI preloads entry '{kvp.Key}' should have {ExpectedJniPreloadIndexStride} instances, it had {kvp.Value} instead. Source file: {preloads.SourceFile}"); + } + } + } + + List? NativeLibraryJniPreload_CommonInitAndGetPreloads (AndroidRuntime runtime) { const bool isRelease = true; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { - return; + return null; } if (runtime == AndroidRuntime.NativeAOT) { @@ -63,35 +113,6 @@ public void NativeLibraryJniPreloadDefaultsWork ([Values] AndroidRuntime runtime _ => throw new NotSupportedException ($"Unsupported runtime '{runtime}'") }; - const uint ExpectedIndexStride = 4; - const int ExpectedEntryCount = 4; // stride * number_of_libs - - List allPreloads = EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); - foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { - Assert.IsTrue (preloads.IndexStride == ExpectedIndexStride, $"JNI preloads index stride should be {ExpectedIndexStride}, was {preloads.IndexStride} instead. Source file: {preloads.SourceFile}"); - Assert.IsTrue (preloads.Entries.Count == ExpectedEntryCount, $"JNI preloads index entry count should be {ExpectedEntryCount}, was {preloads.Entries.Count} instead. Source file: {preloads.SourceFile}"); - - // DSO cache entries are sorted based on their **mutated name's** 64-bit xxHash, which - // won't change but builds may add/remove libraries and, thus, change the indexes after - // sorting. For that reason we don't verify the index values and use them just for reporting. - // - // Also, all the entries will point to the same library name. Name variations aren't - // stored directly in the DSO cache, just their hashes which are used for lookup at run time. - // - // We use a Dictionary<> here because there might be more libraries to preload in the future. - var expectedLibNames = new Dictionary (StringComparer.Ordinal) { - { "libSystem.Security.Cryptography.Native.Android.so", 0 }, - }; - - for (int i = 0; i < preloads.Entries.Count; i++) { - EnvironmentHelper.JniPreloadsEntry entry = preloads.Entries[i]; - Assert.IsTrue (expectedLibNames.ContainsKey (entry.LibraryName), $"JNI preloads entry at index {i}, referring to library at DSO cache index {entry.Index} has unexpected name '{entry.LibraryName}'; Source file: {preloads.SourceFile}"); - expectedLibNames[entry.LibraryName]++; - } - - foreach (var kvp in expectedLibNames) { - Assert.IsTrue (kvp.Value == ExpectedIndexStride, $"JNI preloads entry '{kvp.Key}' should have {ExpectedIndexStride} instances, it had {kvp.Value} instead. Source file: {preloads.SourceFile}"); - } - } + return EnvironmentHelper.ReadJniPreloads (envFiles, numberOfDsoCacheEntries, runtime); } } From 1366a7425e7a71dcd1b1c4b734a96edbc1706a1b Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 14:15:14 +0100 Subject: [PATCH 09/23] Fix after rebase --- .../Utilities/MonoAndroidHelper.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs index b5a1d205e88..a7964c63e9d 100644 --- a/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs +++ b/src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs @@ -875,6 +875,18 @@ public static void LogTextStreamContents (TaskLoggingHelper log, string message, log.LogDebugMessage (reader.ReadToEnd ()); } + public static int GetMinimumApiLevel (AndroidTargetArch arch, AndroidRuntime runtime) + { + int minValue = 0; + + Dictionary apiLevels = runtime == AndroidRuntime.MonoVM ? XABuildConfig.ArchToApiLevel : XABuildConfig.ArchToApiLevelNonMono; + if (!apiLevels.TryGetValue (arch, out minValue)) { + throw new InvalidOperationException ($"Unable to determine minimum API level for architecture {arch}"); + } + + return minValue; + } + /// /// Takes `libItem.ItemSpec` and transforms it to a file name of a native library. It will /// remove any paths from `ItemSpec` and will make sure that the file ends with the `.so` From f9cd9b929beee150e403f8e011c9afe7743a8da0 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 14:39:10 +0100 Subject: [PATCH 10/23] New test + small refactoring --- .../Xamarin.Android.Build.Tests/BuildTest3.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index e36bfe17ce9..04e07e00d88 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -26,22 +26,31 @@ public partial class BuildTest3 : BaseTest const uint ExpectedJniPreloadIndexStride = 4; [Test] - public void NativeLibraryJniPreload_IgnoreAllJniPreload_PreserveRequired ([Values] AndroidRuntime runtime) + public void NativeLibraryJniPreload_IgnoreAll_PreservesRequired ([Values] AndroidRuntime runtime) { const int ExpectedEntryCount = 4; // stride * number_of_libs - List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads (runtime); - if (allPreloads == null) { - return; - } + List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads ( + runtime, + (XamarinAndroidApplicationProject proj) => { + proj.SetProperty ("AndroidIgnoreAllJniPreload", "true"); + } + ); + + NativeLibraryJniPreload_VerifyDefaults (allPreloads); } [Test] public void NativeLibraryJniPreload_DefaultsWork ([Values] AndroidRuntime runtime) + { + List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads (runtime); + NativeLibraryJniPreload_VerifyDefaults (allPreloads); + } + + void NativeLibraryJniPreload_VerifyDefaults (List? allPreloads) { const int ExpectedEntryCount = 4; // stride * number_of_libs - List? allPreloads = NativeLibraryJniPreload_CommonInitAndGetPreloads (runtime); if (allPreloads == null) { return; } @@ -74,7 +83,7 @@ public void NativeLibraryJniPreload_DefaultsWork ([Values] AndroidRuntime runtim } } - List? NativeLibraryJniPreload_CommonInitAndGetPreloads (AndroidRuntime runtime) + List? NativeLibraryJniPreload_CommonInitAndGetPreloads (AndroidRuntime runtime, Action? configureProject = null) { const bool isRelease = true; if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { @@ -95,6 +104,7 @@ public void NativeLibraryJniPreload_DefaultsWork ([Values] AndroidRuntime runtim }; proj.SetRuntime (runtime); proj.SetRuntimeIdentifiers (supportedArches); + configureProject?.Invoke (proj); using var builder = CreateApkBuilder (); Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); From 661de64c689101e6f36dad2f67e3388ca6f602e4 Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 14:46:19 +0100 Subject: [PATCH 11/23] Update defaults checks --- .../Tests/Xamarin.Android.Build.Tests/BuildTest3.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs index 04e07e00d88..9526b26e57c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs @@ -37,6 +37,7 @@ public void NativeLibraryJniPreload_IgnoreAll_PreservesRequired ([Values] Androi } ); + // With `$(AndroidIgnoreAllJniPreload)=true` we still must have the defaults in the generated code. NativeLibraryJniPreload_VerifyDefaults (allPreloads); } @@ -73,6 +74,7 @@ void NativeLibraryJniPreload_VerifyDefaults (List for (int i = 0; i < preloads.Entries.Count; i++) { EnvironmentHelper.JniPreloadsEntry entry = preloads.Entries[i]; + Assert.IsFalse (entry.LibraryName == "libmonodroid.so", $"JNI preloads entry at index {i} refers to the .NET for Android native runtime. It must never be preloaded. Source file: {preloads.SourceFile}"); Assert.IsTrue (expectedLibNames.ContainsKey (entry.LibraryName), $"JNI preloads entry at index {i}, referring to library at DSO cache index {entry.Index} has unexpected name '{entry.LibraryName}'; Source file: {preloads.SourceFile}"); expectedLibNames[entry.LibraryName]++; } From ecc47ebc2974e6a2d788fbf767f9415591633d5e Mon Sep 17 00:00:00 2001 From: Marek Habersack Date: Wed, 11 Feb 2026 16:48:47 +0100 Subject: [PATCH 12/23] Add a native library for JNI preload tests, tbc --- src/native/CMakeLists.txt | 12 +++--- .../common/test-jni-library/CMakeLists.txt | 32 +++++++++++++++ src/native/common/test-jni-library/stub.cc | 8 ++++ src/native/native.targets | 41 ++++++++++++++++++- 4 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 src/native/common/test-jni-library/CMakeLists.txt create mode 100644 src/native/common/test-jni-library/stub.cc diff --git a/src/native/CMakeLists.txt b/src/native/CMakeLists.txt index c452759d4dd..dc20e4cdc83 100644 --- a/src/native/CMakeLists.txt +++ b/src/native/CMakeLists.txt @@ -91,8 +91,8 @@ if(ENABLE_CLANG_ASAN AND ENABLE_CLANG_UBSAN) endif() if(ENABLE_CLANG_ASAN OR ENABLE_CLANG_UBSAN) - if(BUILD_ARCHIVE_DSO_STUB) - message(FATAL_ERROR "ASAN/UBSAN builds aren't supported by the archive DSO target") + if(BUILD_ARCHIVE_DSO_STUB OR BUILD_TEST_JNI_LIBRARY) + message(FATAL_ERROR "ASAN/UBSAN builds aren't supported by the archive DSO / test JNI library targets") endif() set(STRIP_DEBUG_DEFAULT OFF) @@ -330,7 +330,7 @@ macro(xa_add_compile_definitions TARGET) endif() endmacro() -if(NOT BUILD_ARCHIVE_DSO_STUB) +if(NOT BUILD_ARCHIVE_DSO_STUB AND NOT BUILD_TEST_JNI_LIBRARY) if(DEBUG_BUILD AND NOT DISABLE_DEBUG) add_compile_definitions(DEBUG) endif() @@ -477,7 +477,7 @@ set(POTENTIAL_COMMON_CXX_COMPILER_ARGS -fno-cxx-exceptions ) -if(NOT BUILD_ARCHIVE_DSO_STUB) +if(NOT BUILD_ARCHIVE_DSO_STUB AND NOT BUILD_TEST_JNI_LIBRARY) list(APPEND POTENTIAL_COMMON_COMPILER_ARGS -g ) @@ -609,7 +609,7 @@ xa_check_c_linker_args(XA_COMMON_C_LINKER_ARGS "${POTENTIAL_XA_COMMON_LINKER_ARG xa_check_c_linker_args(XA_C_DSO_LINKER_ARGS "${POTENTIAL_XA_DSO_LINKER_ARGS}") xa_check_cxx_linker_args(XA_CXX_DSO_LINKER_ARGS "${POTENTIAL_XA_DSO_LINKER_ARGS}") -if(NOT BUILD_ARCHIVE_DSO_STUB) +if(NOT BUILD_ARCHIVE_DSO_STUB AND NOT BUILD_TEST_JNI_LIBRARY) add_compile_options("$<$:${COMMON_CXX_ARGS}>") add_compile_options("$<$:${COMMON_C_ARGS}>") @@ -641,6 +641,8 @@ endmacro() if(BUILD_ARCHIVE_DSO_STUB) add_subdirectory(common/archive-dso-stub) +elseif(BUILD_TEST_JNI_LIBRARY) + add_subdirectory(common/test-jni-library) else() add_subdirectory(common/java-interop) diff --git a/src/native/common/test-jni-library/CMakeLists.txt b/src/native/common/test-jni-library/CMakeLists.txt new file mode 100644 index 00000000000..805d978b411 --- /dev/null +++ b/src/native/common/test-jni-library/CMakeLists.txt @@ -0,0 +1,32 @@ +set(LIB_NAME test-jni-library) + +set(LIB_SOURCES + stub.cc +) + +add_library( + ${LIB_NAME} + SHARED + ${LIB_SOURCES} +) + +target_compile_options( + ${LIB_NAME} + PRIVATE + ${XA_DEFAULT_SYMBOL_VISIBILITY} + ${XA_COMMON_CXX_ARGS} + -nostdlib -fno-exceptions -fno-rtti +) + +target_link_options( + ${LIB_NAME} + PRIVATE + ${XA_COMMON_CXX_LINKER_ARGS} + -nostdlib -fno-exceptions -fno-rtti -s +) + +add_custom_command( + TARGET ${LIB_NAME} + POST_BUILD + COMMAND ${CMAKE_STRIP} "$" +) diff --git a/src/native/common/test-jni-library/stub.cc b/src/native/common/test-jni-library/stub.cc new file mode 100644 index 00000000000..cfc427958be --- /dev/null +++ b/src/native/common/test-jni-library/stub.cc @@ -0,0 +1,8 @@ +#include + +JNIEXPORT jint JNICALL +JNI_OnLoad ([[maybe_unused]] JavaVM *vm, [[maybe_unused]] void *reserved) +{ + // no-op, need just the JNI_OnLoad symbol to be present for JNI preload tests + return JNI_VERSION_1_6; +} diff --git a/src/native/native.targets b/src/native/native.targets index d046fe7d40a..50b48f8d2c4 100644 --- a/src/native/native.targets +++ b/src/native/native.targets @@ -9,7 +9,7 @@ + DependsOnTargets="_PrepareCommonProperties;_ConfigureAndBuildArchiveDSOStub;_ConfigureAndBuildTestJniLibrary;_ConfigureRuntimes;_BuildAndroidRuntimes;_BuildAndroidAnalyzerRuntimes;_CopyToPackDirs">