镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

[CoreCLR] A library in AndroidNativeLibraryNoJniPreload is loaded with System.loadLibrary from the main thread on its first p/invoke, which fails 4 times in the system namespace before the call binds #13263

Open

Description

@pawegio

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;
}
  1. 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

  1. jonathanpeppers commented on Oct 27, 2026

    @jonathanpeppers
    Member

    🤖 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.loadLibrary selects 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 calls java.lang.System.loadLibrary directly 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 calls System.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:

    Observation Explanation
    Four failed loads The P/Invoke handler tries a normalized name and the original name. Each attempt can make two more attempts in 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.
    Loading the library from the activity does not prevent the failures The RTLD_NOLOAD handle lookup is after the Java load. A Java exception returns early, before the loader checks for an existing native handle.
    The P/Invoke eventually succeeds The Android P/Invoke handler returns null. CoreCLR then uses its normal native-library resolution. This order is visible in dotnet/runtime's dllimport.cpp.
    Each new imported function can repeat the failures CoreCLR calls the Android handler before its normal library-cache lookup. The unsuccessful Android path has not recorded a library handle, even if CoreCLR's fallback previously found the library.

    History and affected runtimes

    This is not a new .NET 11-only path, and the loader defect is not specific to CoreCLR.

    1. Use Android shared library loader for JNI libraries #10376, merged August 19, 2025, introduced Java loading for libraries exporting JNI_OnLoad, in place of loading them only with dlopen. It added the shared loader and main-thread callback used by both Mono and CoreCLR.
    2. The description of that change already documents the same limitation: loading through the main-thread callback can still select the system namespace and fail to find an app library. Startup preloading was added to avoid this failure in the usual case.
    3. Support for exempting native libraries from JNI preload #10787 added the preload exclusions. [release/10.0.1xx] Support for exempting native libraries from JNI preload #10879 backported them to .NET 10 on March 5, 2026.
    4. I checked release/10.0.1xx at commit 49516ae50dffb0421b7faf5f60f9df9cc220743d. 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 AndroidNativeLibraryNoJniPreload for 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 into OnCreate. 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

    AndroidJavaSource is the correct build-action spelling. An explicit Java wrapper can keep the first P/Invoke inside an app Java call:

    NativeLibraryWorkaround.java:

    package com.example.workaround;
    
    public final class NativeLibraryWorkaround {
        public static void initialize(String libraryName, Runnable firstCall) {
            System.loadLibrary(libraryName);
            firstCall.run();
        }
    }

    Java files inside the project are included and bound automatically. To set the metadata explicitly, use Update rather than adding a duplicate Include:

    <ItemGroup>
      <AndroidJavaSource Update="NativeLibraryWorkaround.java" Bind="true" />
      <AndroidNativeLibraryNoJniPreload Include="libreprolib.so" />
    </ItemGroup>

    Call the generated binding synchronously from OnCreate on the main thread:

    using var firstCall = new Java.Lang.Runnable (() => {
        var sum = repro_add (2, 3);
        Log.Info ("Repro", $"First P/Invoke returned {sum}");
    });
    
    Com.Example.Workaround.NativeLibraryWorkaround.Initialize (
        "reprolib", firstCall);

    Replace the library name and first call as appropriate. Do not start a thread or use await inside the callback: the first P/Invoke must occur while the Java callback is active on the main thread. A helper that only calls System.loadLibrary and 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 loaded warning, 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 private Runtime.loadLibrary0 APIs. Main-thread dispatch is not required merely to select the class loader. Handle lookup and caching must also cover app-loaded libraries while preserving required JNI_OnLoad initialization.

  2. jonathanpeppers commented on Oct 27, 2026

    @jonathanpeppers
    Member

    @pawegio does the suggested workaround help?

    We can investigate the "Product fix direction", if that solves your issue.

  3. pawegio commented on Oct 27, 2026

    @pawegio
    Author

    Thanks! Yes, it helps. With the first P/Invoke made synchronously in OnCreate, there are no failed loads.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: App RuntimeIssues in `libmonodroid.so`.needs-triageIssues that need to be assigned.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions