[release/10.0.1xx] Support for exempting native libraries from JNI preload#10879
[release/10.0.1xx] Support for exempting native libraries from JNI preload#10879jonathanpeppers wants to merge 4 commits intorelease/10.0.1xxfrom
Conversation
Fixes: #10617 Context: cba39dc cba39dc introduced support for preloading of JNI native libraries at application startup. However, it appears that in some scenarios this behavior isn't desired. This PR introduces a mechanism which allows exempting some or all (with exception of the BCL libraries) libraries from the preload mechanism. In order to not preload any JNI libraries it's now possible to set the `$(AndroidIgnoreAllJniPreload)` MSBuild property to `true`. It is also possible to exempt individual libraries from preload by adding their name to the `AndroidNativeLibraryNoJniPreload` MSBuild item group, for instance: <ItemGroup> <AndroidNativeLibraryNoJniPreload Include="libMyLibrary.so" /> </ItemGroup>
There was a problem hiding this comment.
Pull request overview
Adds an opt-out mechanism for the JNI native-library preload behavior introduced for .NET Android startup, allowing projects to exempt specific native libraries (or all of them) from being preloaded while still preserving required framework defaults.
Changes:
- Introduces
$(AndroidIgnoreAllJniPreload)and@(AndroidNativeLibraryNoJniPreload)to control JNI preload behavior from MSBuild. - Extends native application config generation to honor “always preload” and “never preload” library lists.
- Adds a small native “test JNI library” plus new build/test infrastructure and NUnit tests validating preload behavior.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/native/native.targets | Builds a new test JNI library as part of native runtime build orchestration. |
| src/native/common/test-jni-library/stub.cc | Adds a minimal JNI_OnLoad implementation for preload testing. |
| src/native/common/test-jni-library/CMakeLists.txt | CMake rules to build and place the test JNI library in test output. |
| src/native/CMakePresets.json.in | Adds XA_TEST_OUTPUT_DIR to CMake cache variables. |
| src/native/CMakeLists.txt | Requires XA_TEST_OUTPUT_DIR and wires the test JNI library subdirectory. |
| src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets | Adds MSBuild property + item groups and passes new inputs to config generation. |
| src/Xamarin.Android.Build.Tasks/Utilities/MonoAndroidHelper.cs | Adds helpers for minimum API lookup and normalizing native library names. |
| src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGeneratorCLR.cs | Implements “ignore/always preload” decision logic for CoreCLR config generation. |
| src/Xamarin.Android.Build.Tasks/Utilities/ApplicationConfigNativeAssemblyGenerator.cs | Implements the same “ignore/always preload” decision logic for MonoVM config generation. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidItem.cs | Adds a ProjectTools item wrapper for AndroidNativeLibraryNoJniPreload. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Android/AndroidBuildActions.cs | Adds build action constant for AndroidNativeLibraryNoJniPreload. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Utilities/EnvironmentHelper.cs | Adds parsing/validation helpers for JNI preload index data from generated native sources. |
| src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest3.cs | New NUnit coverage for JNI preload inclusion/exclusion scenarios. |
| src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs | Wires MSBuild inputs into the native config generator. |
| build-tools/xaprepare/xaprepare/xaprepare.targets | Adds placeholder replacement for @TestOutputDirectory@. |
| build-tools/xaprepare/xaprepare/Steps/Step_GenerateFiles.cs | Plumbs XA_TEST_OUTPUT_DIR placeholder into generated CMake presets. |
| build-tools/xaprepare/xaprepare/Application/Properties.Defaults.cs.in | Adds KnownProperties.TestOutputDirectory default wiring. |
| build-tools/xaprepare/xaprepare/Application/KnownProperties.cs | Adds TestOutputDirectory to known xaprepare properties. |
| Documentation/docs-mobile/building-apps/build-properties.md | Documents AndroidIgnoreAllJniPreload. |
| Documentation/docs-mobile/building-apps/build-items.md | Documents AndroidNativeLibraryNoJniPreload. |
| int ExpectedEntryCount = ExpectedJniPreloadIndexStride * numberOfLibs; | ||
| foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { | ||
| Assert.IsTrue (preloads.IndexStride == (uint)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}"); |
There was a problem hiding this comment.
Local variable ExpectedEntryCount uses PascalCase, but locals in this test suite use camelCase (e.g., shouldMarshalMethodsBeEnabled in BuildTest2). Rename to expectedEntryCount for consistency.
| int ExpectedEntryCount = ExpectedJniPreloadIndexStride * numberOfLibs; | |
| foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { | |
| Assert.IsTrue (preloads.IndexStride == (uint)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}"); | |
| int expectedEntryCount = ExpectedJniPreloadIndexStride * numberOfLibs; | |
| foreach (EnvironmentHelper.JniPreloads preloads in allPreloads) { | |
| Assert.IsTrue (preloads.IndexStride == (uint)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}"); |
| public PackageNamingPolicy PackageNamingPolicy { get; set; } | ||
| public List<ITaskItem> NativeLibraries { get; set; } = []; | ||
| public ICollection<ITaskItem>? NativeLibrariesNoJniPreload { get; set; } | ||
| public ICollection<ITaskItem>? NativeLibrarysAlwaysJniPreload { get; set; } |
There was a problem hiding this comment.
Property name NativeLibrarysAlwaysJniPreload is misspelled ("Librarys"). This propagates to callers and makes the API harder to read/search. Rename to NativeLibrariesAlwaysJniPreload (and update all assignments/usages accordingly).
| public ICollection<ITaskItem>? NativeLibrarysAlwaysJniPreload { get; set; } | |
| public ICollection<ITaskItem>? NativeLibrariesAlwaysJniPreload { get; set; } |
| public PackageNamingPolicy PackageNamingPolicy { get; set; } | ||
| public List<ITaskItem> NativeLibraries { get; set; } = []; | ||
| public ICollection<ITaskItem>? NativeLibrariesNoJniPreload { get; set; } | ||
| public ICollection<ITaskItem>? NativeLibrarysAlwaysJniPreload { get; set; } |
There was a problem hiding this comment.
Property name NativeLibrarysAlwaysJniPreload is misspelled ("Librarys"). Rename to NativeLibrariesAlwaysJniPreload to keep naming consistent and avoid spreading the typo to other components.
| public ICollection<ITaskItem>? NativeLibrarysAlwaysJniPreload { get; set; } | |
| public ICollection<ITaskItem>? NativeLibrariesAlwaysJniPreload { get; set; } |
| BundledAssemblyNameWidth = assemblyNameWidth, | ||
| NativeLibraries = uniqueNativeLibraries, | ||
| NativeLibrariesNoJniPreload = NativeLibrariesNoJniPreload, | ||
| NativeLibrarysAlwaysJniPreload = NativeLibrariesAlwaysJniPreload, |
There was a problem hiding this comment.
Object initializer uses NativeLibrarysAlwaysJniPreload (typo: "Librarys"). Rename the corresponding generator property to NativeLibrariesAlwaysJniPreload and update these assignments.
| NativeLibrarysAlwaysJniPreload = NativeLibrariesAlwaysJniPreload, | |
| NativeLibrariesAlwaysJniPreload = NativeLibrariesAlwaysJniPreload, |
| var nameMutations = new List<string> (); | ||
| var dsoNamesBlob = new LlvmIrStringBlob (); | ||
| int nameMutationsCount = -1; | ||
| ICollection<string> ignorePreload = MakeJniPreloadIgnoreCollection (Log, NativeLibrarysAlwaysJniPreload, NativeLibrariesNoJniPreload); |
There was a problem hiding this comment.
DsoCacheJniPreloadIgnore is no longer consulted after switching to MakeJniPreloadIgnoreCollection/ShouldIgnoreForJniPreload (it’s now dead code). Either remove it, or seed the ignore set with it so the hardcoded safety list remains effective even if MSBuild doesn’t pass NativeLibrariesNoJniPreload.
| ICollection<string> ignorePreload = MakeJniPreloadIgnoreCollection (Log, NativeLibrarysAlwaysJniPreload, NativeLibrariesNoJniPreload); | |
| ICollection<string> ignorePreload = MakeJniPreloadIgnoreCollection (Log, NativeLibrarysAlwaysJniPreload, NativeLibrariesNoJniPreload); | |
| if (DsoCacheJniPreloadIgnore != null) { | |
| foreach (string name in DsoCacheJniPreloadIgnore) { | |
| ignorePreload.Add (name); | |
| } | |
| } |
| 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]); | ||
| } |
There was a problem hiding this comment.
In GetSymbolValueAsUInt64, the field name passed to ConvertFieldToUInt64 is DsoJniPreloadsIdxStrideSymbolName, but this helper is used for the count symbol too. This makes assertion messages misleading; pass the appropriate symbol name (e.g., symbol.Name or DsoJniPreloadsIdxCountSymbolName).
| from being preloaded at application startup. By default, all such libraries | ||
| will be loaded by the runtime early during application startup in order to | ||
| assure their proper initialization. However, in some cases it might not be the | ||
| desired behavior and this property allows to effectively disable this behavior. |
There was a problem hiding this comment.
Grammar: “this property allows to effectively disable this behavior” reads awkwardly. Consider rephrasing to “allows effectively disabling this behavior” (or similar) for clarity.
| desired behavior and this property allows to effectively disable this behavior. | |
| desired behavior and this property allows you to effectively disable it. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…elNonMono don't exist on .NET 10
- Fix grammar in build-items.md: "on individual basis" → "on an individual basis" - Fix grammar in build-properties.md: "allows to effectively disable this behavior" → "allows you to effectively disable it" - Fix typo NativeLibrarysAlwaysJniPreload → NativeLibrariesAlwaysJniPreload Co-authored-by: jonathanpeppers <840039+jonathanpeppers@users.noreply.github.com>
Backport of: #10787
Fixes: #10617
Context: cba39dc
cba39dc introduced support for preloading of JNI native libraries at application startup. However, it appears that in some scenarios this behavior isn't desired.
This PR introduces a mechanism which allows exempting some or all (with exception of the BCL libraries) libraries from the preload mechanism.
In order to not preload any JNI libraries it's now possible to set the
$(AndroidIgnoreAllJniPreload)MSBuild property totrue.It is also possible to exempt individual libraries from preload by adding their name to the
AndroidNativeLibraryNoJniPreloadMSBuild item group, for instance: