You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Use Android shared library loader for JNI libraries (#10376)
Fixes: #10324Fixes: #7616
Context: https://docs.oracle.com/en/java/javase/17/docs/specs/jni/invocation.html#library-and-version-management
Context: #7616 (comment)
Java language/virtual machine supports implementing portions
of the API in a language (like C, C++ or Rust) which compiles
into native binary code instead of being JIT-ed or interpreted
at run time. Such implementations are contained in native shared
libraries which have to conform to a set of rules laid out in
the JNI (Java Native Interface) documentation.
Part of the specification describes a function (`JNI_OnLoad`) which
may be present in the shared library and, if it's there, it will
be called by the JavaVM when the library is loaded. For this to
happen, however, the load must be initiated by calling the
`System.loadLibrary(string)` Java method. This method will find
the named shared library, load it using an OS-specific mechanism and
then call all the exported functions described in the JNI specification,
if they are present.
Until this PR, .NET for Android (and Xamarin.Android before it) were
loading all the shared libraries in the same way, via `dlopen(2)` instead
of by using `System.loadLibrary(string)` which resulted in some of those
libraries not being initialized properly. This PR fixes the issue by
identifying shared libraries which contain the `JNI_OnLoad` function and
loading them at run time by calling `System.loadLibrary(string)` instead
of just `dlopen(2)`. This makes it certain that the libraries are properly
initialized.
However, Android environment is quite complex and not everything in the PR
is implement the way it was intended to. The problem lies in the ability of
`System.loadLibrary(string)` to find the actual shared library file. The file
can be found in a number of locations, among them two application-specific ones:
* The APK archive's `lib/{ABI}/` directory, when shared libraries are not
extracted from the archive on installation.
* The application-specific library location on the file system, when shared
libraries are extracted from the archive on installation.
In either case, the location is not known beforehand as each time the application
is installed, it will get a different path where both its archive and extracted
files are located. This requires the Java runtime to provide that information to
the application in some way. The way ART (the Java runtime on Android) does it is
via class loaders, which are special classes that know how to find and load Java
components as well as the native libraries. `System.loadLibrary(string)` uses that
information to locate the .so files with JavaVM extensions.
The mechanism described above works well as long as the `System.loadLibrary` call
is made from a thread that's fully attached to the Java VM, which is to say that
the VM environment sets up the class loaders correctly, so that they contain information
about the application-specific shared library locations. With the correctly configured
class loaders, we can see a similar message when loading the shared library with
`System.loadLibrary`:
```
08-13 12:06:48.269 11989 11989 D nativeloader: Load /data/app/~~Xy-UIVle34c_VksRd2_LEg==/com.xamarin.XAPerfTest.net10-GbhwYcau77FAjV_FW1uZwg==/split_config.arm64_v8a.apk!/lib/arm64-v8a/libSystem.Security.Cryptography.Native.Android.so using class loader ns clns-9 (caller=/data/app/~~Xy-UIVle34c_VksRd2_LEg==/com.xamarin.XAPerfTest.net10-GbhwYcau77FAjV_FW1uZwg==/base.apk): ok
```
The bits to note above are the `class loader ns clns-9` information - it's a dynamically
configured loader that is fully informed on application-specific shared library locations
and the name of the caller (cryptic-looking path ending with `base.apk`).
This loader is used during, for instance, our native runtime configuration - when it is
being intialized from our (Java) package manager at application startup.
However, the problem is that the above class loader is no longer around when we call
`System.loadLibrary` on a thread that's not fully attached to the Java VM:
```
08-13 12:16:10.659 12472 12472 D nativeloader: Load libSystem.Security.Cryptography.Native.Android.so using system ns (caller=/system/framework/framework.jar!classes3.dex): dlopen failed: library "libSystem.Security.Cryptography.Native.Android.so" not found
```
In this case note that both the class loader (named here just `system ns`) and the
caller are generic, they have no knowledge of the application-specific shared library
locations.
This observation lead to the idea of using the native looper (`ALooper`) interface
to post the shared library load request to the main thread from native code, and then
call `System.loadLibrary` on it. This assumed that the main thread, which originally had
the application-specific class loader, would still be around and able to handle the
load properly. Unfortunately, this doesn't appear to be the case. Despite us attaching
to the Java thread with JNI API (`AttachCurrentThread`), the application-specific
class loader isn't there. This was discovered a few years ago already (see the link to
issue #7616 comment) but we haven't been able to find a way to fully attach the thread
to the Java VM so that the class loaders are correctly set up.
This, unfortunately, leads us to our only remaining option - preloading of the JNI libraries
at application started, while we're still in the properly configured main thread.
This PR implements just that, but it also implements and uses code to load JNI shared libraries
on-demand from any thread by posting the request to the main thread, as it may just happen
that a request to load a shared library will happen on a separate managed thread during
startup and we might get lucky to run the loading code on a still-attached main thread.
In the future more work is required (much more) to investigate the internals of the ART
runtime in order to try to find a way to fully attach managed threads so that class loaders
are set up properly.
0 commit comments