黎明灰烬 +

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.

Figure 1: Android Software Stack

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:

  1. Determining the class loader of the class that defines the native method.
  2. Searching the set of native libraries associated with this class loader to locate the native function that implements the native method.
  3. 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.

Figure 2: Software Stack of Android in Perspective of JNI

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:

黎明灰烬 博客

Creative Commons License 信箱:i(at)jackwish.net

计算机

清 谈