One of Java's greatest advantages is that its design allows for cross-platform capability. This feature, however, is also a bug with regard to other aspects of programming. It is constrained in its interaction with the local machine, and thus the local machine instructions cannot be utilized to achieve the full performance potential of the machine. To ameliorate this weakness, there is the Java Native Interface, a Java platform that interacts with the machine on the local level. It can be employed to allow the use of legacy code and more interaction with the hardware for efficient performance. This article explores the JNI workflow, provides code examples of how Java calls in both C and C++, and introduces the Android Native Development Kit (NDK), which compiles the C/C++ code into applications that can run on an Android device.
JNI and NDK
This article focuses on the capabilities of the Java Native Interface, which overcomes the limitations of Java by allowing Java code and native code software collaborate and share resources. We introduce the basic workflow of JNI, and then provide instructions and code examples for the general framework of a C/C++ function call via a JNI and Java program, noting the slight variations that exist between C and C++. The Android NDK is also introduced and explained. By the end of the article, a developer should understand how to utilize both JNI and NDK on Android devices.
JNI Introduction
We know that Java applications do not run directly on the hardware, but actually run in a virtual machine. The source code of an application is not compiled to get the hardware instructions, but is instead compiled to get the interpretation of a virtual machine to execute code. For example, Android applications run in the Dalvik virtual machine; its compiled code is executable code for the Dalvik virtual machine in DEX format. This feature means that Java runs on the virtual machine and actually ensures its cross-platform capability: that is, its "compile once, run anywhere" feature. This cross-platform capability of Java causes it to be less connected to and limits its interaction with the local machine's various internal components, making it difficult to use the local machine instructions to utilize the performance potential of the machine. It is difficult to take advantage of locally based instructions to run a huge existing software library, and thus functionality and performance are limited.
Is there a way to make Java code and native code software collaborate and share resources? The answer is yes: the Java Native Interface (JNI), which is an implementation method of a Java local operation. JNI is a Java platform defined as the Java standard to interact with the code on the local platform (It is generally known as the host platform. But this article is about the mobile platform, and in order to distinguish it from the mobile cross-development host, we have renamed it the local platform). The so-called "interface" includes two directions, one is Java code to call native functions (methods), and the other is local application calls to the Java code. Relatively speaking, the former method is used more in Android application development. So we will put our emphasis on the approach in which Java code calls native functions.
The way Java calls native functions through JNI is to have the local method stored in the form of library files. For example, on a Windows platform the files are in .dll file format, in the Unix/Linux machine the files are in .so file format. By an internal method of calling the local library file, it enables Java to establish close contact with the local machine and is called the system-level approach for various interfaces.
JNI usually has two usage scenarios: first, to be able to use legacy code (for example C/C++, Delphi, and other development tools); second, to more directly interact with the hardware for better performance. We will see some of this as we go through the article.
JNI general workflow is as follows: Java initiates calls so that the local function's side code (such as a function written in C/C++) runs. This time the object is passed over from the Java side, and run at a local function completion. After finishing running a local function, the value of the result is returned to the Java code. Here JNI is an adapter, completing mapping between the variables and functions (Java methods) between the Java language and native compiled languages (such as C/C++). We know that Java and C/C++ are very different in function prototype definitions and variable types. In order to make the two match, JNI provides a jni.h file to complete the mapping between the two. This process is shown in Figure 1.
Figure 1: JNI general workflow.
The general framework of a C/C++ function call via a JNI and Java program (especially Android application) is as follows:
- The way of compiling native is declared in the Java class (C/C++ function).
- The .java source code file containing the native method is compiled (Build project in Android).
- The
javah
command generates an .h file, which corresponds to the native method according to the .class files. - C/C++ methods are used to achieve the local method.
- The recommended method for this step is first to copy the function prototypes into the .h file and then modify the function prototypes and add the function body. In this process, the following points should be noted:
- The JNI function call must use the C function. If it is the C++ function, do not forget to add the extern "C" keyword;
- The format of the method name should follow the following template:
Java_pacakege_class_method
, namely theJava_package
name class name and function method name. - The C or C++ file is compiled into a dynamic library (under Windows this is a .dll file, under Unix/Linux a .so file).
Use the System.loadLibrary()
or System.load()
method in the Java class to load the dynamic library generated.
These two functions are slightly different:
System.loadLibrary()
: Loads the default directory (for Windows, for example, this is\System32
,jre\bin
, and so on) under the local link library;System.load()
: Depending on the local directory to add the cross-link library, you must use an absolute path.
In the first step, Java calls the native C/C++ function; the format is not the same for both C and C++. For example, for Java methods such as non-passing parameters and returning a String
class, C and C++ code for the function differ in the following ways:
C code:
Call function:(*env) -> <jni function> (env, <parameters>) Return jstring:return (*env)->NewStringUTF(env, "XXX");
C++ code:
Call function:env -> <jni function> (<parameters>) Return jstring:return env->NewStringUTF("XXX");
in which NewStringUTF
are the Java String
object's functions generated in C/C++ provided by the JNI.
Java Methods and their Corresponding Relationship with the C Function Prototype Java
Earlier we said that in the code framework for Java programs to call a C/C++ function, you can use the javah
command, and this will generate the corresponding .h file for native methods according to the .class files. The .h file is generated in accordance with certain rules, so as to make the correct Java code find the corresponding C function to execute.
For example, for the following Java code for Android:
public class HelloJni extends Activity { public void onCreate(Bundle savedInstanceState) { TextView tv.setText(stringFromJNI() ); // Use C function Code } public native String stringFromJNI(); }
For the C function stringFromJNI()
used in the fifth row, the function prototype in the .h file generated by javah
is:
JNIEXPORT jstring JNICALL Java_com_example_hellojni_HelloJni_stringFromJNI (JNIEnv *, jobject);
In this regard, C source code files for the definition of the function code are roughly:
/* …… Signature: ()Ljava/lang/String; */ jstring Java_com_example_hellojni_HelloJni_stringFromJNI (JNIEnv* env, jobject this ) { …… return (*env)->NewStringUTF(env, “……”); }
From the above code, we can see that the function name is quite long, but still very regular, in full accordance with the naming convention: java_package_class_method
. That is: the stringFromJNI()
method in Hello.java
corresponds to the Java_com_example_hellojni_HelloJni_stringFromJNI()
method in C/C++.
Notice the comment for Signature: ()Ljava/lang/String;
. Here the "()
" of "()Ljava/lang/String;
" indicates the function parameter is empty, which means that besides the two parameters JNIEnv *
and jobject
, there are no other parameters. JNIEnv *
and jobject
are two parameters that all JNI functions must have, respectively, for the JNI environment and corresponding Java class (or object) itself. "Ljava/lang/String;
" indicates the function's return value is a Java String
object.