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/ folder, but will not be packed into an .apk file.

_* 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.

Primitive Types Reference Types

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


Tags: cppandroid