NativeBridge - Manage Java Native Interface Functionality of Alternative Architecture on Android
点击查看目录
Deprecated: I had been planning to finished this article before I left Intel at 2017. However, due to an unexpected tricky bug which took nearly all my time to handle, I could never have enough time to get this work done. What a pity…
Abstract
Android, the most popular mobile operating system, hosts applications composed by Java code and native code. Java Virtual Machine of Android provides Java Native Interface functionality as the bridge of Java world and native world. As native world is platform dependent, it requires significant effort for application vendors to enable ARM applications, which are of the majority in Android ecosystem, on Non-ARM devices. Thus, Android is bind to ARM platform though it’s a modern operating system. To address this issue, Android introduced NativeBridge to manage the Java Native Interface functionality of alternative architecture, such that a platform can support non-native applications on it. In this way, Android applications can run on any Android platforms regardless of the architecture.
Introduction
Android is the most popular mobile operating system. As Google is bringing Android to wearables, televisions, automotive and etc., the hardware platforms that Android deployed on are of tremendous technology including different application binary interfaces (ABI). Android software stack is as Figure below.
Most Android application logic is composed by Java and native (C/C++ mostly) code. Java was designed to be a platform-independent programming language while native code is not. A specific application with native code therefore cannot run all platforms, and this is bad for the ecosystem. To enable Android different platforms, Android system and native development kid (NDK) support 7 ABIs, such as armeabi
, x86
and mips
. With the toolchain, developers can build their code for different platforms.
However, applications always equip third-party software development kit (SDK) to enhance their functionality. In practice, not all SDK support every ABI. On the other hand, some cooperations may need to reuse legacy native code on Android while may not have a chance to recompile. For example, most legacy desktop softwares are x86 based while the mobile is dominated by ARM. In addition, it requires significant effort for a company to maintain consistent experience on different ABIs.
The problem in Android ecosystem is actually the problem that binary translation has been trying to address. Consider a Android platform, if the system can translate the ABI of a specific application to its native ABI, newly emerging platforms (e.g. x86 and mips) don’t need to worry about the application ecosystem.
Since Lollipop, Android introduces NativeBridge to address such issues. In this article, we firstly describe the Java Native Interface, which connects Java and native world, of Android. Secondly, we introduce the design of NativeBridge interface and its integration of Android system. After that, the additional functionality that to manage a alternative architecture native world is addressed.
The Java Native Interface on Android
As Android supports both Java and native, Java Native Interface (JNI) is one of the core of Android system. The Java Native Interface (JNI) is a powerful feature of the Java platform. Applications that use the JNI can incorporate native code written in programming languages such as C and C++, as well as code written in the Java programming language. We are not going to discuss every detail of the JNI implementation of Android, because there are a large amount of excellent books already, but to introduce the basics and NativeBridge related part.
Basic of Java Native Interface
This part basically comes from The Java™ Native Interface - Programmer’s Guide and Specification.
JNI design ensures that it offers binary compatibility among different Java virtual machine (JVM) implementations on a given ABI, and exposes enough JVM functionality to enable native methods and applications to accomplish useful tasks.
Before calling from Java into native, JNI firstly needs to load native libraries that are located by class loaders. Class loaders provide the namespace separation needed to run multiple components inside an instance of the same virtual machine. Each class or interface type is associated with its defining loader, the loader that initially reads the class file and defines the class or interface object.
After library loading, JNI links the native method which will be called. The linking procedures involves the following steps:
- Determining the class loader of the class that defines the native method.
- Searching the set of native libraries associated with this class loader to locate the native function that implements the native method.
- Setting up the internal data structures so that all future calls to the native method will jump directly to the native function.
The calling convention determines how a native function receives arguments and returns results when incorporating with Java. The JNI requires the native methods to be written in a specified standard calling convention on a given ABI, and JVM handles the convention between Java and native.
JNI defines the mapping of Java types to native, and each type is binded to a signature that is one char, a subset is as table below. The combination of the signature of native method arguments is named as shorty.
Java Type | Native Type | Signatures | Description |
---|---|---|---|
boolean | jboolean | Z | unsigned 8 bits |
byte | jbyte | B | signed 8 bits |
char | jchar | C | unsigned 16 bits |
short | jshort | S | signed 16 bits |
int | jint | I | signed 32 bits |
long | jlong | L | signed 64 bits |
float | jfloat | F | 32 bits |
double | jdouble | D | 64 bits |
Besides calling convention, JVM exposes JNIEnv
with which native function can leverage (reference, create, and delete) Java objects. These Java objects are identified by “ID” that managed by JVM. Whenever operates a Java object, native function must provides the ID of that Java object.
The JNI functionality on Android is slightly different from the specification, you can refer to the official resource. Next two sections reveal the implementation of library loading and method linking of Android.
The Library Loading on Android
The library loading is invoked by System.loadLibrary(libname)
(code, doc) in Java code of an application. JVM analyses the Java stack to get the class loader of the class that loads the library, and searches the native library path of that class loader and converts a library name (abc
for example) to a path (data/app/com.example.app/lib/armeabi/libabc.so
for example). After that, JVM switch from Java world to native world by calling Runtime_nativeLoad
which is linked when JVM bootstrapping (Runtime::InitNativeMethod
).
JVM then walks the dedicated loaded library list it maintains for each class loader. If not loaded, calls into NativeLoader which maps class loader to namespace of dynamic linker. NativeLoader then calls interface 3 of Figure 2, dlopen()
as it is, into dynamic linker which loads the library into a specific namespace finally. Namespace is further discussed in Namespace based Dynamic Linking - Isolating Native Library of Application and System in Android.
After a native library is loaded, the native method needs to be linked before be called. Yet JVM has no knowledge of which library should a native method belows to. It’s developer’s responsibility to explicitly load the dedicated library before invoking any native method of it.
The Method Linking on Android
To achieve binary compatibility across platforms, JVM needs to generate code that translate arguments and return value for native method accordingly, which is the concrete meaning of method linking.
To generate the convention code, JVM calls into GetQuickGenericJniStub()
which falls to artQuickGenericJniTrampoline
where BuildGenericJniFrameVisitor
generates the code from shorty on stack. In the JNI design of Android, the final element on the stack is a pointer to the native code of which the address is obtained via artFindNativeMethod()
. JavaVMExt::FindCodeForNativeMethod
then walks all loaded library of the class loader so search the symbol with name that translated from shorty. The symbol searching is through interface 4 of Figure 2 - dlsym()
.
Once JVM gets the address of the native method, it caches the address internally through RegisterNativeMethod
such that future calling of this method doesn’t need to search symbol. At this point, we can say the native method has been linked.
The NativeBridge to Handle JNI Functionality
In last section, we have seen the JNI implementation which enables the calling from Java to native on Android. NativeBridge, aiming to support alternative architecture on a given platform, needs to introduce similar stack. In this section, we discuss the design of NativeBridge to handle JNI compatibility of alternative architecture.
The NativeBridge Architecture
NativeBridge is designed to be a generic one which divides into two parts - the NativeBridge Interface (NBItf) and the NativeBridge Implementation (NBImpl). NBItf is the generic interface to bridge the Android system with a NBImpl which handles the compatibility of a dedicated ABI. NBItf is part of Android Open Source Project (AOSP) while NBImpl is provided by a device vendor. One example of NBImpl is Intel Houdini.
For most NativeBridge functionality, NBItf simply calls into NBImpl. These functionalities are described in NativeBridgeCallbacks
, and as table below. We will refer to these functions in later detailed discussion.
The Callback | Description | Peer in Linker | Active Version | Interface in Figure 2 |
---|---|---|---|---|
version |
Version of the interface. | N/A | Since L | 5 |
initialize |
Initialize NativeBridge Implementation. | N/A | Since L | 5 |
loadLibrary |
Load a shared library. | dlopen |
L to N | 1 |
getTrampoline |
Get a native bridge trampoline for specified native method. | dlsym |
Since L | 1 |
isSupported |
Check whether native library is supported by the NativeBridge Implementation. | N/A | L to N | 1 |
getAppEnv |
Provide environment values required by the app according to the ISA. | N/A | Since L | 5 |
isCompatibleWith |
Check whether the bridge is compatible with the given version. | N/A | Since M | 5 |
getSignalHandler |
Retrieve a signal handler of NativeBridge Implementation for a specified signal. | N/A | Since M | 1 |
unloadLibrary |
Decrements the reference count on the dynamic library handler. | dlclose |
Since O | 2 |
getError |
Dump failure message of loading library or searching symbol. | dlerror |
Since O | 2 |
isPathSupported |
Check whether library paths are supported by NativeBridge Implementation. | N/A | Since O | 2 |
initAnonymousNamespace |
Initializes anonymous namespace at native bridge side. | android_init_anonymous_namespace |
Since O | 2 |
createNamespace |
Create new namespace in which native libraries will be loaded. | android_create_namespace |
Since O | 2 |
linkNamespaces |
Creates a link which shares some libraries from one namespace to another. | android_link_namespaces |
Since O | 2 |
loadLibraryExt |
Load a shared library within a namespace. | android_dlopen_ext |
Since O | 2 |
getVendorNamespace |
Get vendor namespace that is used to load vendor public libraries. | android_get_exported_namespace |
Since O | 2 |
Similar with handling applications of native architecture, NativeBridge needs to support library loading and method linking of alternative architecture.
Library Loading in NativeBridge
As the dynamic linker is only capable of loading library developed for native platform, NativeBridge is responsible for loading library of alternative architecture. This is trivial since any binary translation system, such as FX!32: A Profile-Directed Binary Translator, needs to provide the capability. The difference is that NativeBridge is handling library loading in a JNI scheme.
Concretely, Android Runtime maintains the state needs_native_bridge
which indicates whether an application needs the help of NativeBridge - equivalent as whether an application is of alternative architecture. The state is obtained via isPathSupported()
which calls into implementation in NBImpl per namespace.
When to load a library, the logic path of two architectures are the same when called into NativeLoader. NativeLoader dispatches the library loading task to NativeBridge or dynamic linker by considering needs_native_bridge
respectively.
Since Oreo, the underlying library loading functionality of alternative architecture is provided through loadLibraryExt
.
Method Linking in NativeBridge
Besides library loading, NativeBridge also handles method linking which includes manage calling convention of the alternative architecture as well as symbol searching since the Android Runtime only takes care of the calling convention of the native architecture.
In particular, when Android Runtime down to walk library list to search for a symbol in FindSymbol
, NativeBridge is called (interface 1 in Figure 2) through getTrampoline
which eventually falls (interface 5 in Figure 2) into NBImpl. NBImpl is responsible for searching for the symbol, and build the calling convention.
TODO
remaining part includes:
-
JNI section
- the routine of Java->native and native->Java
- the native activity
-
Signal section
- fault manager of ART
- sigchain
Opps! cannot remember all stuffs…. Someone can continue to work….