Repository navigation
Description
Android framework version
net11.0-android (Preview)
Affected platform version
.NET SDK 11.0.100-rc.2.26504.105, Android workload 37.2.0-rc.2.84, CoreCLR (UseMonoRuntime=false), Release, android-arm64; Android 17 (API 37) emulator
Description
On CoreCLR, an app's own native library that is listed in AndroidNativeLibraryNoJniPreload is not found by the first [DllImport] into it. Before the call binds, the runtime posts a load of the library to the main thread, which calls java.lang.System.loadLibrary. There the caller is the framework (caller=/system/framework/framework.jar), so the library is looked for in the system linker namespace, not the app's class-loader namespace, and it fails 4 times. Each failure is logged with its stack. Then monodroid-assembly warns "Shared library '...' not loaded, p/invoke '...' may fail", and the call is bound some other way and works.
This happens whether or not the app has already loaded the library itself with System.loadLibrary from its activity, which is what such an app does.
Why we list a library there: Firebase's C++ library (libFirebaseCppApp) reads the activity in its JNI_OnLoad, so it must not be preloaded as the process starts. The app loads it from its activity instead. With its ~37 p/invokes, an app gets ~150 UnsatisfiedLinkError stacks (half its logcat) and the main thread is held at every first call.
Cost in the repro below: the first call takes 528-896 ms, against 0.9 ms when the library is preloaded.
Expected
The first repro_add call binds to the app's lib/arm64/libreprolib.so (already loaded in the java_first true case) with no failed loads, as when the library is preloaded (-p:ReproPreload=true: 0 failures, first call 0.9 ms).
Notes
From the runtime's own strings (libnet-android.release.so: MainThreadDsoLoader, "Looper CB called on thread %d. Will attempt to load DSO", SystemLoadLibraryWrapper::load), the lookup goes through a load posted to the main thread's looper. Called from a looper callback, System.loadLibrary has no app frame on the stack, so the library is resolved in the system namespace, where an app's library is never found. Calling Runtime.loadLibrary0 with the app's class loader (or dlopen of the library's path in the app's native library directory) would find it. The lookup also doesn't notice that the app has already loaded the library. Because the warning path returns no handle, every first call of each import pays again.
Steps to Reproduce
#include <jni.h>
#include <android/log.h>
int repro_add(int a, int b) { return a + b; }
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
__android_log_print(ANDROID_LOG_INFO, "Repro", "libreprolib JNI_OnLoad");
return JNI_VERSION_1_6;
}dotnet new android, with this project file:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net11.0-android</TargetFramework>
<SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationId>com.example.nojnipreloadrepro</ApplicationId>
<ApplicationVersion>1</ApplicationVersion>
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<UseMonoRuntime>false</UseMonoRuntime>
<RuntimeIdentifiers>android-arm64</RuntimeIdentifiers>
</PropertyGroup>
<ItemGroup>
<AndroidNativeLibrary Include="../native/arm64-v8a/libreprolib.so" Abi="arm64-v8a" />
<AndroidNativeLibraryNoJniPreload Include="libreprolib.so" Condition="'$(ReproPreload)' != 'true'" />
</ItemGroup>
</Project>and this activity:
using System.Runtime.InteropServices;
using Android.Util;
namespace NoJniPreloadRepro;
[Activity(Label = "@string/app_name", MainLauncher = true)]
public class MainActivity : Activity
{
[DllImport("reprolib")]
private static extern int repro_add(int a, int b);
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
SetContentView(Resource.Layout.activity_main);
// --ez java_first true: the app loads the library itself first, as one whose JNI_OnLoad needs the activity does.
if (Intent?.GetBooleanExtra("java_first", false) == true)
{
Java.Lang.JavaSystem.LoadLibrary("reprolib");
Log.Info("Repro", "loaded libreprolib through Java (System.loadLibrary)");
}
// The first p/invoke, from a thread of the app's own.
new Thread(() =>
{
var watch = System.Diagnostics.Stopwatch.StartNew();
var sum = repro_add(2, 3);
Log.Info("Repro", $"repro_add(2, 3) = {sum}, first call took {watch.Elapsed.TotalMilliseconds:0.0} ms");
}).Start();
}
}Did you find any workaround?
No response
Relevant log output
D nativeloader: Load libreprolib.so using system ns (caller=/system/framework/framework.jar!classes3.dex): dlopen failed: library "libreprolib.so" not found
W System.err: java.lang.UnsatisfiedLinkError: dlopen failed: library "libreprolib.so" not found
W System.err: at java.lang.Runtime.loadLibrary0(Runtime.java:1122)
W System.err: at java.lang.Runtime.loadLibrary0(Runtime.java:1033)
W System.err: at java.lang.System.loadLibrary(System.java:1765)
W System.err: at android.os.MessageQueue.nativePollOnce(Native Method)
W System.err: at android.os.MessageQueue.next(MessageQueue.java:773)
W System.err: at android.os.Looper.loop(Looper.java:397)
W System.err: at android.app.ActivityThread.main(ActivityThread.java:9523)
... (4 times, on the main thread)
W monodroid-assembly: Shared library 'reprolib' not loaded, p/invoke 'repro_add' may fail (on the calling thread)
I Repro : repro_add(2, 3) = 5, first call took 895.6 ms
Activity
jonathanpeppers commentedon Oct 27, 2026
🤖 AI-generated investigation: This comment was written by GitHub Copilot from a source-code and Git-history investigation. The reported failure matches the code paths described below. I have not reproduced the timings on a device, and the suggested workaround is not yet device-verified.
Findings
The main fault is in the shared JNI library loader, not in the meaning of
AndroidNativeLibraryNoJniPreload.Android's
System.loadLibraryselects a class loader from the calling Java class. Calling it from the main thread does not, by itself, provide an app Java caller. Our native looper callback callsjava.lang.System.loadLibrarydirectly through JNI. The framework caller and system namespace shown in this report are consistent with that code path.Relevant source, at the revision inspected:
CoreClrBootstrapBuilder.cs: the exclusion removes the library from startup preloading but retains its JNI classification. That classification is necessary; excluding a library from early loading does not make it a non-JNI library.DsoLoader: a later load of a JNI library still callsSystem.loadLibrary, with a main-thread dispatch when needed.SystemLoadLibraryWrapper: calls the framework method directly and prints and clears the Java exception on failure.The other symptoms follow from this:
MonodroidDl, using the stored name and requested name. With one app library directory, this gives four attempts. The exact count can differ with packaging and runtime.RTLD_NOLOADhandle lookup is after the Java load. A Java exception returns early, before the loader checks for an existing native handle.dotnet/runtime'sdllimport.cpp.History and affected runtimes
This is not a new .NET 11-only path, and the loader defect is not specific to CoreCLR.
JNI_OnLoad, in place of loading them only withdlopen. It added the shared loader and main-thread callback used by both Mono and CoreCLR.release/10.0.1xxat commit49516ae50dffb0421b7faf5f60f9df9cc220743d. Its Mono loader and CoreCLR loader both use the same shared JNI-loading path.Thus, source inspection indicates a pre-existing shared-loader defect exposed by skipping startup preloading. It does not establish that Mono has the same retry count, fallback outcome, or timing; those need a device comparison.
Suggested workaround to try
Keep
AndroidNativeLibraryNoJniPreloadfor a library whose initialization must wait for the activity. Removing it could restore the original initialization-order problem.First try making one safe P/Invoke synchronously from
MainActivity.OnCreate, after the activity state required by the library is ready, and before starting the worker thread. This keeps an app Java caller on the stack and avoids the native looper callback. If it succeeds through our loader, the stored library handle should allow later imports using the same library name to work from background threads without repeating the Java load.For the supplied repro, move the initial
repro_add(2, 3)call intoOnCreate. For Firebase, choose a native call that is valid at that initialization point; do not make an arbitrary call solely to force loading.Optional explicit Java callback
AndroidJavaSourceis the correct build-action spelling. An explicit Java wrapper can keep the first P/Invoke inside an app Java call:NativeLibraryWorkaround.java:Java files inside the project are included and bound automatically. To set the metadata explicitly, use
Updaterather than adding a duplicateInclude:Call the generated binding synchronously from
OnCreateon the main thread:Replace the library name and first call as appropriate. Do not start a thread or use
awaitinside the callback: the first P/Invoke must occur while the Java callback is active on the main thread. A helper that only callsSystem.loadLibraryand returns before a later background P/Invoke is not enough, as the report already shows.To check the workaround, confirm that the first call has no failed Java loads or
Shared library ... not loadedwarning, then call additional imports from a worker thread and confirm that the failures do not return.Product fix direction
Use an app-loaded Java helper as the caller of
System.loadLibrary, instead of calling the framework method directly through JNI. This supplies the correct class loader without privateRuntime.loadLibrary0APIs. Main-thread dispatch is not required merely to select the class loader. Handle lookup and caching must also cover app-loaded libraries while preserving requiredJNI_OnLoadinitialization.jonathanpeppers commentedon Oct 27, 2026
@pawegio does the suggested workaround help?
We can investigate the "Product fix direction", if that solves your issue.
pawegio commentedon Oct 27, 2026
Thanks! Yes, it helps. With the first
P/Invokemade synchronously inOnCreate, there are no failed loads.