JNI Examples for Android
Introduction
Java interface
We start by defining a Java class JNIExampleInterface, which will provide the interface to calling the native functions, defined in a native(C++) library. The native functions corresponding to Java functions will need to have matching call signatures (i.e. the count and types of the arguments, aswell as return type). The easiest way to get the correct function signatures in the native library is to first write downtheir Java prototypes, and then use the javah tool togenerate the native JNI header with native function prototypes. These can be cut and pasted into the C++ file for implementation.
The Java functions which are backed by the corresponding native functions are declared in a usual way, adding a native qualifier.We also want to demonstrate how we could do the callbacks, i.e. calling theJava code from native code. That leads to the following high-level view of ourinterface class:
package org.wooyd.android.JNIExample;
import android.os.Handler;
import android.os.Bundle;
import android.os.Message;
import org.wooyd.android.JNIExample.Data;
public class JNIExampleInterface {
static Handler h;
}
public JNIExampleInterface() {}
publicJNIExampleInterface(Handler h) {
this.h = h;
}
<Example native functions>=(<-U)
public static native void callVoid();
public static native Data getNewData(int i, String s);
public static native String getDataString(Data d);
public static void callBack(String s) {
Bundle b = new Bundle();
b.putString("callback_string", s);
Message m =Message.obtain();
m.setData(b);
m.setTarget(h);
m.sendToTarget();
}
package org.wooyd.android.JNIExample;
public class Data {
public int i;
public String s;
public Data() {}
public Data(int i, String s) {
this.i = i;
this.s = s;
}
}
After the sourcefiles Data.java and JNIExampleInterface.java are compiled, we can generate the JNIheader file, containing the prototypes of the native functions, correspondingto their Java counterparts:
$ javac -classpath /path/to/sdk/android.jar \
org/wooyd/android/JNIExample/*.java
$ javah -classpath . org.wooyd.android.JNIExample.JNIExampleInterface
Native library implementation
At a high level,the Java library (consisting, in this case, of a single source file JNIExample.cpp) will look like that:
#ifdef __cplusplus
extern "C" {
#endif
<getDataString implementation>
<initClassHelper implementation>
#ifdef __cplusplus
}
#endif
The followingincludes define the functions provided by Android's version of JNI, as well assome useful helpers:
#include <jni.h>
#include <JNIHelp.h>
#include <android_runtime/AndroidRuntime.h>
Various otherthings which will come in handy:
<Miscellaneous includes>=(<-U)
#include <string.h>
#include <unistd.h>
#include <pthread.h>
It is useful tohave some global variables to cache things which we know will not change duringthe lifetime of our program, and can be safely used across multiple threads.One of such things is the JVM handle. We can retrieve it every time it's needed(for example, using android::AndroidRuntime::getJavaVM() function),but as it does not change, it's better to cache it.
We can alsouse global variables to cache the references to required classes. As described below, it is not always easy to doclass resolution in native code, especially when it is done from native threads(see "Calling Java functions" section [->] fordetails). Here we are just providing the global variables to hold instancesof Data andJNIExampleInterface class objects, as well as defining someconstant strings which will come in handy:
static JavaVM *gJavaVM;
static jobjectgInterfaceObject, gDataObject;
const char *kInterfacePath= "org/wooyd/android/JNIExample/JNIExampleInterface";
const char *kDataPath= "org/wooyd/android/JNIExample/Data";
Defines gJavaVM, jobject, kDataPath, kInterfacePath (links are toindex).
[*]Calling Java functions from Java and native threads
The callVoid() function is the simplest one, as it does not take any arguments, andreturns nothing. We will use it to illustrate how the data can be passed backto Java through the callback mechanism, by calling the Java callBack() function.
At thispoint it is important to recognize that there are two distinct possibilities here: the Java functionmay be called either from a thread which originated in Java or from a nativethread, which has been started in the native code, and of which JVM has noknowledge of. In the former case the call may be performed directly, in the latterwe must first attach the native thread to the JVM. That requires an additionallayer, a native callback handler, which will do the right thing in either case.We will also need a function to create the native thread, so structurally theimplementation will look like this:
<callVoid implementation>=(<-U)
Nativecallback handler gets the JNI environment (attaching the native thread if necessary), uses a cachedreference to the gInterfaceObject to getto JNIExampleInterface class,obtains callBack() method reference,and calls it:
static void callback_handler(char*s) {
int status;
JNIEnv *env;
bool isAttached = false;
status = gJavaVM->GetEnv((void**) &env, JNI_VERSION_1_4);
if(status < 0) {
LOGE("callback_handler:failed to get JNI environment, "
"assuming nativethread");
status = gJavaVM->AttachCurrentThread(&env,NULL);
if(status < 0) {
LOGE("callback_handler:failed to attach "
"currentthread");
return;
}
isAttached = true;
}
/* Construct a Java string */
jstring js =env->NewStringUTF(s);
jclass interfaceClass =env->GetObjectClass(gInterfaceObject);
if(!interfaceClass) {
LOGE("callback_handler:failed to get class reference");
if(isAttached) gJavaVM->DetachCurrentThread();
return;
}
/* Find the callBack method ID*/
jmethodID method =env->GetStaticMethodID(
interfaceClass,"callBack", "(Ljava/lang/String;)V");
if(!method) {
LOGE("callback_handler:failed to get method ID");
if(isAttached) gJavaVM->DetachCurrentThread();
return;
}
env->CallStaticVoidMethod(interfaceClass, method, js);
if(isAttached) gJavaVM->DetachCurrentThread();
}
Defines callback_handler (links areto index).
A few commentsare in order:
- The JNI environment, returned by the JNI GetEnv() function is unique for each thread, so must be retrieved every time we enter the function. The JavaVM pointer, on the other hand, is per-program, so can be cached (you will see it done in the JNI_OnLoad() function), and safely used across threads.
- When we attach a native thread, the associated Java environment comes with a bootstrap class loader. That means that even if we would try to get a class reference in the function (the normal way to do it would be to use FindClass() JNI function), it would trigger an exception. Because of that we use a cached copy of JNIExampleInterface object to get a class reference (amusingly, we cannot cache the reference to the class itself, as any attempt to use it triggers an exception from JVM, who thinks that such reference should not be visible to native code). This caching is also done in JNI_OnLoad(), which might be the only function called by Android Java implementation with a functional class loader.
- In order to retrieve the method ID of the callBack() method, we need to specify its name and JNI signature. In this case the signature indicates that the function takes ajava.lang.String object as an argument, and returns nothing (i.e. has return type void). Consult JNI documentation for more information on function signatures, one useful tip is that you can use javap utility to look up the function signatures of non-native functions (for native functions the signature information is already included as comments into the header, generated by javah).
- Someone more paranoid than me could use locking to avoid race conditions associated with setting and checking of the isAttached variable.
In order to testcalling from native threads, we will also need a function which is started in aseparate thread. Its only role is to call the callback handler:
void *native_thread_start(void*arg) {
sleep(1);
callback_handler((char*) "Called from native thread");
}
Defines native_thread_start (linksare to index).
We now haveall necessary pieces to implement the native counterpart of the callVoid() function:
/*
* Class: org_wooyd_android_JNIExample_JNIExampleInterface
* Method: callVoid
* Signature: ()V
*/
JNIEXPORT void JNICALLJava_org_wooyd_android_JNIExample_JNIExampleInterface_callVoid
(JNIEnv *env, jclass cls) {
pthread_t native_thread;
callback_handler((char*) "Called from Java thread");
if(pthread_create(&native_thread, NULL,native_thread_start,NULL)) {
LOGE("callVoid: failedto create a native thread");
}
}
Defines JNICALL (links are to index).
Implementationof other native functions
The getNewData() function illustrates creation of a new Java object in the nativelibrary, which is then returned to the caller. Again, we use a cached Data object reference in order to obtain the class and create a newinstance.
<getNewData implementation>=(<-U)
/*
* Class: org_wooyd_android_JNIExample_JNIExampleInterface
* Method: getNewData
* Signature:(ILjava/lang/String;)Lorg/wooyd/android/JNIExample/Data;
*/
JNIEXPORT jobjectJNICALLJava_org_wooyd_android_JNIExample_JNIExampleInterface_getNewData
(JNIEnv *env, jclass cls, jint i, jstring s) {
jclass dataClass =env->GetObjectClass(gDataObject);
if(!dataClass) {
LOGE("getNewData:failed to get class reference");
return NULL;
}
jmethodID dataConstructor =env->GetMethodID(
dataClass,"<init>", "(ILjava/lang/String;)V");
if(!dataConstructor) {
LOGE("getNewData:failed to get method ID");
return NULL;
}
jobject dataObject= env->NewObject(dataClass, dataConstructor, i, s);
if(!dataObject) {
LOGE("getNewData:failed to create an object");
return NULL;
}
return dataObject;
}
Defines jobject (links are to index).
The getDataString() functionillustrates how a value stored in an object's attribute can be retrieved in anative function.
<getDataString implementation>=(<-U)
/*
* Class: org_wooyd_android_JNIExample_JNIExampleInterface
* Method: getDataString
* Signature:(Lorg/wooyd/android/JNIExample/Data;)Ljava/lang/String;
*/
JNIEXPORT jstringJNICALLJava_org_wooyd_android_JNIExample_JNIExampleInterface_getDataString
(JNIEnv *env, jclass cls, jobject dataObject){
jclass dataClass =env->GetObjectClass(gDataObject);
if(!dataClass) {
LOGE("getDataString:failed to get class reference");
return NULL;
}
jfieldID dataStringField =env->GetFieldID(
dataClass, "s","Ljava/lang/String;");
if(!dataStringField) {
LOGE("getDataString:failed to get field ID");
return NULL;
}
jstringdataStringValue = (jstring)env->GetObjectField(
dataObject,dataStringField);
return dataStringValue;
}
Defines jstring (links are to index).
The JNI_OnLoad() function implementation
The JNI_OnLoad() functionmust be provided by the native library in order for the JNI to work withAndroid JVM. It will be called immediately after the native library is loadedinto the JVM. We already mentioned a couple of tasks which should be performedin this function: caching of the global JavaVM pointer and caching of the object instances to enable us to callinto Java. In addition, any native methods which we want to call from Java mustbe registered, otherwise Android JVM will not be able to resolve them. Theoverall structure of the function thus can be written down as follows:
<JNIOnLoad implementation>=(<-U)
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv *env;
gJavaVM = vm;
LOGI("JNI_OnLoadcalled");
if (vm->GetEnv((void**) &env,JNI_VERSION_1_4) != JNI_OK) {
LOGE("Failed to get theenvironment using GetEnv()");
return -1;
}
<Native function registration>
return JNI_VERSION_1_4;
}
We need some wayto cache a reference to a class, because native threads do not have access to a functional classloader. As explained above, wecan't cache the class references themselves, as it makes JVMunhappy. Instead we cache instances of these classes, so thatwe can later retrieve class references using GetObjectClass() JNI function. One thing to remember isthat these objects must be protected from garbage-collecting using NewGlobalRef(), as that guarantees that they will remain available to different threadsduring JVM lifetime. Creating the instances and storing them in the globalvariables is the job for the initClassHelper() function:
<initClassHelper implementation>=(<-U)
void initClassHelper(JNIEnv*env, const char *path, jobject *objptr) {
jclass cls = env->FindClass(path);
if(!cls) {
LOGE("initClassHelper:failed to get %s class reference", path);
return;
}
jmethodID constr =env->GetMethodID(cls, "<init>", "()V");
if(!constr) {
LOGE("initClassHelper:failed to get %s constructor", path);
return;
}
jobject obj =env->NewObject(cls, constr);
if(!obj) {
LOGE("initClassHelper:failed to create a %s object", path);
return;
}
(*objptr) =env->NewGlobalRef(obj);
}
Defines initClassHelper (links are toindex).
With thisfunction defined, class instance caching is trivial:
<Class instance caching>=(<-U)
initClassHelper(env,kInterfacePath,&gInterfaceObject);
initClassHelper(env,kDataPath,&gDataObject);
In order toregister the native functions, we create an array of JNINativeMethod structures, which contain function names, signatures (they can besimply copied from the comments, generated by javah), and pointers to the implementing functions. This array is then passedto Android's registerNativeMethods() function:
<Native function registration>=(<-U)
JNINativeMethod methods[] = {
{
"callVoid",
"()V",
(void *)Java_org_wooyd_android_JNIExample_JNIExampleInterface_callVoid
},
{
"getNewData",
"(ILjava/lang/String;)Lorg/wooyd/android/JNIExample/Data;",
(void *)Java_org_wooyd_android_JNIExample_JNIExampleInterface_getNewData
},
{
"getDataString",
"(Lorg/wooyd/android/JNIExample/Data;)Ljava/lang/String;",
(void *)Java_org_wooyd_android_JNIExample_JNIExampleInterface_getDataString
}
};
if(android::AndroidRuntime::registerNativeMethods(
env, kInterfacePath,methods, NELEM(methods)) != JNI_OK) {
LOGE("Failed toregister native methods");
return -1;
}
Building the native library
In order to buildthe native library, you need to include Android's native headers and linkagainst native libraries. The only way I know to get those is to check out andbuild the entire Android source code, and then build it. Procedure is describedin detail at Android GetSource page. Make sure that you use the branch tag matching your SDKversion, for example code in the release-1.0 branchmatches Android 1.1 SDK.
For an exampleof CXXFLAGS and LDFLAGS you need to use to create a shared library with Android toolchain,check out the Makefile, included in the exampleproject tarball. They are derived from build/core/combo/linux-arm.mk in Android source.
You will probablywant to build the entire example project, so you will need a copy of the SDK aswell. This code has been tested to build with Android's 1.1 SDK and run on thecurrently released version of the phone. Once you downloaded the SDK and the exampletarball and unpacked them, you can build the project using the command
ANDROID_DIR=/path/to/android/source SDK_DIR=/path/to/sdk make
Using nativefunctions in Java code
We will nowcreate a simple activity, taking advantage of the JNI functions. One non-trivialtask we will have to do in onCreate() method ofthe activity is to load the native JNI library, to make the functions definedthere accessible to Java. Overall structure:
package org.wooyd.android.JNIExample;
public class JNIExample extends Activity
{
TextView callVoidText,getNewDataText, getDataStringText;
Button callVoidButton,getNewDataButton, getDataStringButton;
Handler callbackHandler;
JNIExampleInterfacejniInterface;
@Override
public void onCreate(BundlesavedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
}
Importsneeded to draw the UI and display it to the user:
<Imports>=(<-U) [D->]
import android.app.Activity;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
Importsneeded to enable communication between the Java callback and the UI thread:
<Imports>+=(<-U) [<-D->]
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
Imports formanipulation with the native library:
<Imports>+=(<-U) [<-D->]
import java.util.zip.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.File;
We will alsoneed access to our JNI interface class and toy Data class:
<Imports>+=(<-U) [<-D->]
importorg.wooyd.android.JNIExample.JNIExampleInterface;
importorg.wooyd.android.JNIExample.Data;
Loggingutilities will also come in handy:
<Imports>+=(<-U) [<-D]
import android.util.Log;
At this time theonly officialy supported way to create an Android application is by using theJava API. That means, that no facilities are provided to easily build andpackage shared libraries, and automatically load them onapplication startup. One possible way to include the library into the applicationpackage (file with extension .apk) is to place itinto the assets subdirectory of theAndroid project, created with activitycreator. During thepackage build it will be automatically included into the APK package, howeverwe still will have to load it by hand when our application starts up. Luckily,the location where APK is installed is known, and APK is simply a ZIP archive,so we can extract the library file from Java and copy it into the applicationdirectory, allowing us to load it:
try {
String cls ="org.wooyd.android.JNIExample";
String lib ="libjniexample.so";
String apkLocation ="/data/app/" + cls + ".apk";
String libLocation ="/data/data/" + cls + "/" + lib;
ZipFile zip = newZipFile(apkLocation);
ZipEntry zipen =zip.getEntry("assets/" + lib);
InputStream is =zip.getInputStream(zipen);
OutputStream os = newFileOutputStream(libLocation);
byte[] buf = new byte[8092];
int n;
while ((n = is.read(buf))> 0) os.write(buf, 0, n);
os.close();
is.close();
System.load(libLocation);
} catch (Exception ex) {
Log.e("JNIExample", "failed to install native library:" + ex);
}
The restsimply demonstrates the functionality, provided by the native library, by calling the nativefunctions and displaying the results. For the callVoid() demo we need to initialize a handler first, and pass it to the JNIinterface class, to enable us to receive callback messages:
<callVoid demo>=(<-U) [D->]
callVoidText = (TextView)findViewById(R.id.callVoid_text);
callbackHandler = new Handler(){
public voidhandleMessage(Message msg) {
Bundle b =msg.getData();
callVoidText.setText(b.getString("callback_string"));
}
};
jniInterface = newJNIExampleInterface(callbackHandler);
We also setup a button which will call callVoid() from the native library when pressed:
<callVoid demo>+=(<-U) [<-D]
callVoidButton = (Button)findViewById(R.id.callVoid_button);
callVoidButton.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v){
jniInterface.callVoid();
}
});
For getNewData() we pass theparameters to the native function and expect to get the Data object back:
getNewDataText = (TextView)findViewById(R.id.getNewData_text);
getNewDataButton = (Button)findViewById(R.id.getNewData_button);
getNewDataButton.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v){
Data d = jniInterface.getNewData(42,"foo");
getNewDataText.setText(
"getNewData(42,\"foo\") == Data(" + d.i + ", \"" + d.s +"\")");
}
});
And prettymuch the same for getDataString():
getDataStringText = (TextView)findViewById(R.id.getDataString_text);
getDataStringButton = (Button)findViewById(R.id.getDataString_button);
getDataStringButton.setOnClickListener(new Button.OnClickListener() {
public void onClick(View v){
Data d = new Data(43,"bar");
String s =jniInterface.getDataString(d);
getDataStringText.setText(
"getDataString(Data(43, \"bar\")) == \"" + s +"\"");
}
});
Try pushing thebuttons and see whether it actually works!
Unresolved issues and bugs
Even though theexample is fully functional, there are a couple unresolved issues remaining,which I was not able to figure out so far. Problems appear when you start theactivity, then press the Back button to hide it, and then start it again. In myexperience, calls to native functions in such restarted activity will fail spectacularly.callVoid() simply crashes witha segmentation fault, while calls to getNewData() and getDataString() cause JVM to abortwith an error, because it is no longer happy with the globally cached objectreference. It appears that activity restart somehow invalidates our cachedobject references, even though they are protected with NewGlobalRef(), and the activity is running within the original JVM (activity restartdoes not mean that JVM itself is restarted). I don't have a good explanation onwhy that happens, so if you have any ideas, please let me know.
- <Callback handler>: U1, D2
- <callVoid demo>: U1, D2, D3
- <callVoid function>: U1, D2
- <callVoid implementation>: U1, D2
- <Class instance caching>: U1, D2
- <Data.java>: D1
- <Example callback>: U1, D2
- <Example constructors>: U1, D2
- <Example native functions>: U1, D2
- <getDataString demo>: U1, D2
- <getDataString implementation>: U1, D2
- <getNewData demo>: U1, D2
- <getNewData implementation>: U1, D2
- <Global variables>: U1, D2
- <Imports>: U1, D2, D3, D4, D5, D6
- <initClassHelper implementation>: U1, D2
- <JNI includes>: U1, D2
- <JNIExample.cpp>: D1
- <JNIExample.java>: D1
- <JNIExampleInterface.java>: D1
- <JNIOnLoad implementation>: U1, D2
- <Load JNI library>: U1, D2
- <Miscellaneous includes>: U1, D2
- <Native function registration>: U1, D2
- <Thread start function>: U1, D2
- callback_handler: D1, U2, U3
- gJavaVM: D1, U2, U3
- initClassHelper: D1, U2
- JNICALL: D1, U2, U3
- jobject: D1, D2, U3, U4
- jstring: U1, U2, D3
- kDataPath: D1, U2
- kInterfacePath: D1, U2, U3
- native_thread_start: D1, U2