A JNI Guide on Android
2014-11-27 by terryoy, in guides
1. Begin a JNI Project
To create a JNI project in ADT, first is to setup NDK support for the workspace. Open “Preferences” -> “Android” -> “NDK”, set the NDK Locaiton to your ndk root.
After you create a new Android project, native support is not yet enabled. Right click on the project, and select “Android Tools” -> “Add Native Support…”. It prompts a “Library Name” input for you to type in the native library name you want to use. On succeed, it will generate a “jni” folders containing a “.cpp” file and a “Android.mk” makefile.
***a small hint on creating project: if you created an project without an initial activity (which it's possible when your only interest is in building an native app), you need to specify your MainActivity in the AndroidManifest.xml. On the “Application” sheet in Manifest editor, first add a “Activity” node, which connects to your Activity subclass; then create an “Intent Filter” node, and then an “Action” node and a “Category” node under the IntentFilter node. The Action node selects “android.intent.action.MAIN”, and the Category node selects “android.intent.category.LAUNCHER”.
2. Information on Android.mk
The syntax of Android.mk is based on GNU Make. This file is only a snippet of the whole make process. The whole process also includes an Application.mk and a Android.mk provided by the NDK build system.
# my-dir is a macro defined by Android, which provides the path for where the make file is.
LOCAL_PATH := $(call my-dir)
# CLEAR_VARS clears all the "LOCAL_*" variables except "LOCAL_PATH"
include $(CLEAR_VARS)
LOCAL_MODULE := jnidemo # provide the lib name (e.g. libjnidemo.so), and also the name to load in Java Class
LOCAL_SRC_FILES := jnidemo.cpp # a source file list to compile and build into the library
#LOCAL_SHARE_LIBRARIES := avilib # (optional 1): if you depends on other libraries, you can load it here
# build the library
include $(BUILD_SHARED_LIBRARY)
#$(call import-module,transcode/avilib) # (optional 2): put the 3rd party library outside project folder, under NDK_MODULE_PATH and import it using this line
* If you have more than one library to build, just duplicate the part from “include $(CLEAR_VARS)” to “include $(BUILD_SHARED_LIBRARY)”.
* NDK build also supports executable build, only by replacing “include $(BUILD_SHARED_LIBRARY)” with “include $(BUILD_EXECUTABLE)”, the output will also be in libs/
_* If you need to trigger build under command line, go to the project root folder, and type ndk-build
to build.
3. Import JNI library in Java Class
Use the code below to load a library in the Java class.
static {
System.loadLibrary("jnidemo");
// System.load("c:/path/to/library.so"); // this is not recommended since it would be platform dependent
}
4. Working With Native Functions
There are two sides of the JNI interface: Java calling C/C++, and C/C++ calling Java. In Java, you can call a native funciton implmeneted by a C/C++ shared library; and in C/C++, you might also need to trigger Java methods inside C/C++ code. So let's talk about both respectively.
4.1 Java Calling C/C++
In a Java class, you can define a native method which will be implmemented by C/C++ code. In this case, you can call native C/C++ functions from Java.
Class A {
public native String stringFromJNI(); // native method
static { // load the shared library that contains the native method
System.loadLibrary("jnidemo");
}
}
You can use javah -classpath bin/classes com.teatime.jnidemo.A
to generate the C/C++ header for the java native methods. It will create a “com_teatime_jnidemo_A.h” file as below:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_teatime_jnidemo_A */
#ifndef _Included_com_teatime_jnidemo_A
#define _Included_com_teatime_jnidemo_A
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_teatime_jnidemo_A
* Method: stringFromJNI
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_teatime_jnidemo_A_stringFromJNI
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Here you can see the Java method signature is converted to something have similar meaning: “jstring” means the Java String class, “jobject” is for the object instance which calls this method.
However, you don't really want to type this command to generate every file you make, so a more convenient way is to setup a “Run -> External Tools -> External Tools Configurations” to trigger it inside Eclipse IDE. The configuration will be like below: (note that the path segment is joined by “:” on mac/linux and “;” on windows)
Name: Generate C/C++ Header File
Location: ${system_path:javah}
Working Directory: ${project_loc}/jni
Arguments: -classpath "${project_classpath}:${env_var:ANDROID_SDK_HOME}/platforms/android-15/android.jar" ${java_type_name}
Refresh -> Refresh Resources upon completions; The project containing the selected resource
Common -> Display in favourites menu -> External Tools
There are type mappings betwine Java and C as below. For more information you could check out the JNI document here.
4.2 C/C++ Calling Java
In C/C++, you need to include jni.h
and use JNIEnv
to work with the Java methods in JVM. The JNIEnv object is to keep everything consistent inside JVM.
typedef const struct JNINativeInterface *JNIEnv;
There's a list of functions for JNIEnv object you can check out here
For primitive types, you can use directly convert to the c types directly. For Here are some examples:
// primitive types
int cvalue = 100;
jint value = cvalue;
// create Java string
jstring javaString = (*env)->NewStringUTF(env, "Hello World!");
// convert Java string to C string
const jbyte *str;
jboolean isCopy;
str = (*env)->GetStringUTFChars(env, javaString, &isCopy); // isCopy tells the function to get a copy or return the original string
// release string after GetStringChars/GetStringUTFChars
(*env)->ReleaseStringUTFChars(env, javaString, str);
// operating array
jintArray javaArray = (*env)->NewIntArray(env, 10);
if (0 != javaArray) { // this only creates a java array
// approach 1: you can ask for a C array pointer to operate on it
jint* nativeDirectArray = (*env)->GetIntArrayElements(env, javaArray, &isCopy); // isCopy tells the function to get a copy or the original
// ... do something
// release the pointer ref, otherwise will cause memory leak
(*env)->ReleaseIntArrayElements(env, javaArray, nativeDirectArray, 0); // last param can be: 0, JNI_COMMIT, JNI_ABORT
// 0: apply the content and release the native array
// JNI_COMMIT: apply the content, don't release the native array
// JNI_ABORT: don't apply the content, but release the array
// aproach 2: work with a local array, and submit changes when needed
jint nativeArray[10];
(*env)->GetIntArrayRegion(env, javaArray, 0, 10, nativeArray); // copy the content to nativeArray
(*env)->setIntArrayRegion(env, javaArray, 0, 10, nativeArray); // apply the changes back to the original array
}
// calling a method
jmethodID isntanceMethodId = (*env)->GetMethodID(env, clazz, "instanceMethod", "()Ljava/lang/String;");
jstring stringValue = (*env)->CallStringMethod(env, instance, instanceMethodId);
Often you will need to checkout the method signature of the Java class, so that you can lookup the method in JNI interface. Here is a small trick to print the method signature in JNI style.
$ cd bin/classes
$ javap -s com.jnidemo.MyJNIClass
5. References
Best Practices for using Java Native Interface: http://www.ibm.com/developerworks/library/j-jni/
JNI Documentation: http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html