Comments 0

Document transcript

JNI Examples for AndroidJurij Smakovjurij@wooyd.orgApril 25,2009Contents1 Licence 12 Introduction 13 Java interface 24 Native library implementation 44.1 Headers and global variables.....................54.2 Calling Java functions from Java and native threads.......64.3 Implementation of other native functions..............94.4 The JNIOnLoad() function implementation............115 Building the native library 146 Using native functions in Java code 147 Unresolved issues and bugs 181April 25,2009 JNIExample.nw 11 LicenceThis document and the code generated from it are subject to the followinglicence:Copyright (C) 2009 Jurij Smakov <jurij@wooyd.org>Permission is hereby granted,free of charge,to any person obtaining a copyof this software and associated documentation files (the"Software"),to dealin the Software without restriction,including without limitation the rightsto use,copy,modify,merge,publish,distribute,sublicense,and/or sellcopies of the Software,and to permit persons to whom the Software isfurnished to do so,subject to the following conditions:The above copyright notice and this permission notice shall be included inall copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED"AS IS",WITHOUT WARRANTY OF ANY KIND,EXPRESS ORIMPLIED,INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,DAMAGES OR OTHERLIABILITY,WHETHER IN AN ACTION OF CONTRACT,TORT OR OTHERWISE,ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS INTHE SOFTWARE.2 IntroductionWhile JNI is a pretty exciting way to greatly extend Android functionality andport existing software to it,to date there is not a lot of detailed documentationon how to create the native libraries and interface with themfromthe Android'sJava Virtual Machine (JVM).This document aims at lling this gap,by pro-viding a comprehensive example of creating a native JNI library,and using itfrom Java.This document has been generated from source using noweb,a literate pro-gramming tool.The JNIExample.nw is the source in noweb format.It canbe used to generate the document output in a variety of formats (for example,PDF),as well as generate the JNI example source code.The complete Android project,including the source code generated fromJNIExample.nw is available for download.So,if you are impatient,just grabit and check out the"Building the native library"section 5,which describesprerequisites for the build and the build procedure itself.This document is not a replacement for other general JNI documentation.If you are not familiar with JNI,you may want to have a look at the followingresources: Sun's Java Native Interface guide Java Native Interface:Programmer's Guide and SpecicationAlso,there are a couple of blog entries,which contain some bits of useful infor-mation:April 25,2009 JNIExample.nw 2 JNI in Android How to add a new module to AndroidIf you notice any errors or omissions (there are a couple of known bugs andunresolved issues 7),or have a suggestion on how to improve this document,feel free to contact me using the email address mentioned above.3 Java interfaceWe start by dening a Java class JNIExampleInterface,which will provide theinterface to calling the native functions,dened in a native (C++) library.Thenative functions corresponding to Java functions will need to have matching callsignatures (i.e.the count and types of the arguments,as well as return type).The easiest way to get the correct function signatures in the native library is torst write down their Java prototypes,and then use the javah tool to generatethe native JNI header with native function prototypes.These can be cut andpasted into the C++ le for implementation.The Java functions which are backed by the corresponding native functionsare declared in a usual way,adding a native qualier.We also want to demon-strate how we could do the callbacks,i.e.calling the Java code from nativecode.That leads to the following high-level view of our interface class:2 hJNIExampleInterface.java 2i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;hExample constructors 3aihExample native functions 3bihExample callback 3ci}This code is written to le JNIExampleInterface.java.April 25,2009 JNIExample.nw 3One valid question about this denition is why we need a Handler class at-tribute.It turns out that it will come in handy in situations,when the nativelibrary wants to pass some information to the Java process through a callback.If the callback will be called by a native thread (for extended discussion see"Calling Java functions"section 4.2),and then will try to modify the applica-tion's user interface (UI) in any way,an exception will be thrown,as Androidonly allows the thread which created the UI (the UI thread) to modify it.Toovercome this problem we are going to use the message-passing interface pro-vided by Handler to dispatch the data received by a callback to the UI thread,and allow it to do the UI modications.In order for this to work,we are goingto accept a Handler instance as an argument for non-trivial constructor (rea-sons for keeping trivial one will become apparent later),and save it in a classattribute,and that's pretty much the only task for the constructor:3a hExample constructors 3ai (2)public JNIExampleInterface() {}public JNIExampleInterface(Handler h) {this.h = h;}To illustrate various argument-passing techniques,we dene three nativefunctions: callVoid():takes no arguments and returns nothing; getNewData():takes two arguments and constructs a new class instanceusing them; getDataString():extracts a value from an object,which is passed as anargument.3b hExample native functions 3bi (2)public static native void callVoid();public static native Data getNewData(int i,String s);public static native String getDataString(Data d);The callback will receive a string as an argument,and dispatch it to theHandler instance recorded in the constructor,after wrapping it in a Bundle:3c hExample callback 3ci (2)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();}April 25,2009 JNIExample.nw 4We also need a denition of a dummy Data class,used purely for illustrativepurposes:4a hData.java 4ai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;}}This code is written to le Data.java.After the source les Data.java and JNIExampleInterface.java are com-piled,we can generate the JNI header le,containing the prototypes of thenative functions,corresponding to their Java counterparts:$ javac -classpath/path/to/sdk/android.jar\org/wooyd/android/JNIExample/*.java$ javah -classpath.org.wooyd.android.JNIExample.JNIExampleInterface4 Native library implementationAt a high level,the Java library (consisting,in this case,of a single source leJNIExample.cpp) will look like that:4b hJNIExample.cpp 4bihJNI includes 5aihMiscellaneous includes 5bihGlobal variables 5ci#ifdef __cplusplusextern"C"{#endifhcallVoid implementation 6ihgetNewData implementation 9bihgetDataString implementation 10ihinitClassHelper implementation 12aihJNIOnLoad implementation 11i#ifdef __cplusplus}#endifThis code is written to le JNIExample.cpp.April 25,2009 JNIExample.nw 54.1 Headers and global variablesThe following includes dene the functions provided by Android's version ofJNI,as well as some useful helpers:5a hJNI includes 5ai (4b)#include <jni.h>#include <JNIHelp.h>#include <android_runtime/AndroidRuntime.h>Various other things which will come in handy:5b hMiscellaneous includes 5bi (4b)#include <string.h>#include <unistd.h>#include <pthread.h>It is useful to have some global variables to cache things which we know willnot change during the lifetime of our program,and can be safely used acrossmultiple threads.One of such things is the JVMhandle.We can retrieve it everytime it's needed (for example,using android::AndroidRuntime::getJavaVM()function),but as it does not change,it's better to cache it.We can also use global variables to cache the references to required classes.As described below,it is not always easy to do class resolution in native code,especially when it is done from native threads (see"Calling Java functions"section 4.2 for details).Here we are just providing the global variables to holdinstances of Data and JNIExampleInterface class objects,as well as deningsome constant strings which will come in handy:5c hGlobal variables 5ci (4b)static JavaVM *gJavaVM;static jobject gInterfaceObject,gDataObject;const char *kInterfacePath ="org/wooyd/android/JNIExample/JNIExampleInterface";const char *kDataPath ="org/wooyd/android/JNIExample/Data";April 25,2009 JNIExample.nw 64.2 Calling Java functions from Java and native threadsThe callVoid() function is the simplest one,as it does not take any arguments,and returns nothing.We will use it to illustrate how the data can be passedback to Java through the callback mechanism,by calling the Java callBack()function.At this point it is important to recognize that there are two distinct possibili-ties here:the Java function may be called either from a thread which originatedin Java or from a native thread,which has been started in the native code,and of which JVM has no knowledge of.In the former case the call may beperformed directly,in the latter we must rst attach the native thread to theJVM.That requires an additional layer,a native callback handler,which will dothe right thing in either case.We will also need a function to create the nativethread,so structurally the implementation will look like this:6 hcallVoid implementation 6i (4b)hCallback handler 7ihThread start function 8ihcallVoid function 9aiApril 25,2009 JNIExample.nw 7Native callback handler gets the JNI environment (attaching the nativethread if necessary),uses a cached reference to the gInterfaceObject to get toJNIExampleInterface class,obtains callBack() method reference,and callsit:7 hCallback handler 7i (6)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 native thread");status = gJavaVM->AttachCurrentThread(&env,NULL);if(status < 0) {LOGE("callback_handler:failed to attach""current thread");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();}April 25,2009 JNIExample.nw 8A few comments are in order: The JNI environment,returned by the JNI GetEnv() function is uniquefor 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 JNIOnLoad() function),and safely used acrossthreads. When we attach a native thread,the associated Java environment comeswith a bootstrap class loader.That means that even if we would try toget a class reference in the function (the normal way to do it would be touse FindClass() JNI function),it would trigger an exception.Because ofthat we use a cached copy of JNIExampleInterface object to get a classreference (amusingly,we cannot cache the reference to the class itself,asany attempt to use it triggers an exception from JVM,who thinks thatsuch reference should not be visible to native code).This caching is alsodone in JNIOnLoad(),which might be the only function called by AndroidJava implementation with a functional class loader. In order to retrieve the method ID of the callBack() method,we needto specify its name and JNI signature.In this case the signature indicatesthat the function takes a java.lang.String object as an argument,andreturns nothing (i.e.has return type void).Consult JNI documentationfor more information on function signatures,one useful tip is that you canuse javap utility to look up the function signatures of non-native func-tions (for native functions the signature information is already includedas comments into the header,generated by javah). Someone more paranoid than me could use locking to avoid race conditionsassociated with setting and checking of the isAttached variable.In order to test calling fromnative threads,we will also need a function whichis started in a separate thread.Its only role is to call the callback handler:8 hThread start function 8i (6)void *native_thread_start(void *arg) {sleep(1);callback_handler((char *)"Called from native thread");}April 25,2009 JNIExample.nw 9We now have all necessary pieces to implement the native counterpart of thecallVoid() function:9a hcallVoid function 9ai (6)/** Class:org_wooyd_android_JNIExample_JNIExampleInterface* Method:callVoid* Signature:()V*/JNIEXPORT void JNICALL Java_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:failed to create a native thread");}}4.3 Implementation of other native functionsThe getNewData() function illustrates creation of a new Java object in thenative library,which is then returned to the caller.Again,we use a cachedData object reference in order to obtain the class and create a new instance.9b hgetNewData implementation 9bi (4b)/** Class:org_wooyd_android_JNIExample_JNIExampleInterface* Method:getNewData* Signature:(ILjava/lang/String;)Lorg/wooyd/android/JNIExample/Data;*/JNIEXPORT jobject JNICALL Java_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;}April 25,2009 JNIExample.nw 10The getDataString() function illustrates how a value stored in an object'sattribute can be retrieved in a native function.10 hgetDataString implementation 10i (4b)/** Class:org_wooyd_android_JNIExample_JNIExampleInterface* Method:getDataString* Signature:(Lorg/wooyd/android/JNIExample/Data;)Ljava/lang/String;*/JNIEXPORT jstring JNICALL Java_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;}jstring dataStringValue = (jstring) env->GetObjectField(dataObject,dataStringField);return dataStringValue;}April 25,2009 JNIExample.nw 114.4 The JNIOnLoad() function implementationThe JNIOnLoad() function must be provided by the native library in order forthe JNI to work with Android JVM.It will be called immediately after the nativelibrary is loaded into the JVM.We already mentioned a couple of tasks whichshould be performed in this function:caching of the global JavaVM pointer andcaching of the object instances to enable us to call into Java.In addition,anynative methods which we want to call from Java must be registered,otherwiseAndroid JVM will not be able to resolve them.The overall structure of thefunction thus can be written down as follows:11 hJNIOnLoad implementation 11i (4b)jint JNI_OnLoad(JavaVM* vm,void* reserved){JNIEnv *env;gJavaVM = vm;LOGI("JNI_OnLoad called");if (vm->GetEnv((void**) &env,JNI_VERSION_1_4)!= JNI_OK) {LOGE("Failed to get the environment using GetEnv()");return -1;}hClass instance caching 12bihNative function registration 13ireturn JNI_VERSION_1_4;}April 25,2009 JNIExample.nw 12We need some way to cache a reference to a class,because native threads donot have access to a functional classloader.As explained above,we can't cachethe class references themselves,as it makes JVM unhappy.Instead we cacheinstances of these classes,so that we can later retrieve class references usingGetObjectClass() JNI function.One thing to remember is that these objectsmust be protected fromgarbage-collecting using NewGlobalRef(),as that guar-antees that they will remain available to dierent threads during JVM lifetime.Creating the instances and storing them in the global variables is the job forthe initClassHelper() function:12a hinitClassHelper implementation 12ai (4b)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);}With this function dened,class instance caching is trivial:12b hClass instance caching 12bi (11)initClassHelper(env,kInterfacePath,&gInterfaceObject);initClassHelper(env,kDataPath,&gDataObject);April 25,2009 JNIExample.nw 13In order to register the native functions,we create an array of JNINativeMethodstructures,which contain function names,signatures (they can be simply copiedfrom the comments,generated by javah),and pointers to the implementingfunctions.This array is then passed to Android's registerNativeMethods()function:13 hNative function registration 13i (11)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 to register native methods");return -1;}April 25,2009 JNIExample.nw 145 Building the native libraryIn order to build the native library,you need to include Android's native headersand link against native libraries.The only way I know to get those is to checkout and build the entire Android source code,and then build it.Procedure isdescribed in detail at Android Get Source page.Make sure that you use thebranch tag matching your SDK version,for example code in the release-1.0branch matches Android 1.1 SDK.For an example of CXXFLAGS and LDFLAGS you need to use to create a sharedlibrary with Android toolchain,check out the Makefile,included in the exampleproject tarball.They are derived from build/core/combo/linux-arm.mk inAndroid source.You will probably want to build the entire example project,so you will needa copy of the SDK as well.This code has been tested to build with Android's1.1 SDK and run on the currently released version of the phone.Once youdownloaded the SDK and the example tarball and unpacked them,you canbuild the project using the commandANDROID_DIR=/path/to/android/source SDK_DIR=/path/to/sdk make6 Using native functions in Java codeWe will now create a simple activity,taking advantage of the JNI functions.One non-trivial task we will have to do in onCreate() method of the activityis to load the native JNI library,to make the functions dened there accessibleto Java.Overall structure:14 hJNIExample.java 14ipackage org.wooyd.android.JNIExample;hImports 15aipublic class JNIExample extends Activity{TextView callVoidText,getNewDataText,getDataStringText;Button callVoidButton,getNewDataButton,getDataStringButton;Handler callbackHandler;JNIExampleInterface jniInterface;@Overridepublic void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.main);hLoad JNI library 16aihcallVoid demo 16bihgetNewData demo 17bihgetDataString demo 17ci}}April 25,2009 JNIExample.nw 15This code is written to le JNIExample.java.Imports needed to draw the UI and display it to the user:15a hImports 15ai (14) 15b.import android.app.Activity;import android.view.View;import android.widget.Button;import android.widget.TextView;Imports needed to enable communication between the Java callback and the UIthread:15b hImports 15ai+ (14)/15a 15c.import android.os.Bundle;import android.os.Handler;import android.os.Message;Imports for manipulation with the native library:15c hImports 15ai+ (14)/15b 15d.import java.util.zip.*;import java.io.InputStream;import java.io.OutputStream;import java.io.FileOutputStream;import java.io.File;We will also need access to our JNI interface class and toy Data class:15d hImports 15ai+ (14)/15c 15e.import org.wooyd.android.JNIExample.JNIExampleInterface;import org.wooyd.android.JNIExample.Data;Logging utilities will also come in handy:15e hImports 15ai+ (14)/15dimport android.util.Log;April 25,2009 JNIExample.nw 16At this time the only ocialy supported way to create an Android applicationis by using the Java API.That means,that no facilities are provided to easilybuild and package shared libraries,and automatically load them on applicationstartup.One possible way to include the library into the application package(le with extension.apk) is to place it into the assets subdirectory of theAndroid project,created with activitycreator.During the package build itwill be automatically included into the APK package,however we still will haveto load it by hand when our application starts up.Luckily,the location whereAPK is installed is known,and APK is simply a ZIP archive,so we can extractthe library le from Java and copy it into the application directory,allowing usto load it:16a hLoad JNI library 16ai (14)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 = new ZipFile(apkLocation);ZipEntry zipen = zip.getEntry("assets/"+ lib);InputStream is = zip.getInputStream(zipen);OutputStream os = new FileOutputStream(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 rest simply demonstrates the functionality,provided by the native library,by calling the native functions and displaying the results.For the callVoid()demo we need to initialize a handler rst,and pass it to the JNI interface class,to enable us to receive callback messages:16b hcallVoid demo 16bi (14) 17a.callVoidText = (TextView) findViewById(R.id.callVoid_text);callbackHandler = new Handler() {public void handleMessage(Message msg) {Bundle b = msg.getData();callVoidText.setText(b.getString("callback_string"));}};jniInterface = new JNIExampleInterface(callbackHandler);April 25,2009 JNIExample.nw 17We also set up a button which will call callVoid() from the native librarywhen pressed:17a hcallVoid demo 16bi+ (14)/16bcallVoidButton = (Button) findViewById(R.id.callVoid_button);callVoidButton.setOnClickListener(new Button.OnClickListener() {public void onClick(View v) {jniInterface.callVoid();}});For getNewData() we pass the parameters to the native function and expectto get the Data object back:17b hgetNewData demo 17bi (14)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 pretty much the same for getDataString():17c hgetDataString demo 17ci (14)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 +"\"");}});April 25,2009 JNIExample.nw 18Try pushing the buttons and see whether it actually works!7 Unresolved issues and bugsEven though the example is fully functional,there are a couple unresolved issuesremaining,which I was not able to gure out so far.Problems appear whenyou start the activity,then press the Back button to hide it,and then start itagain.In my experience,calls to native functions in such restarted activity willfail spectacularly.callVoid() simply crashes with a segmentation fault,whilecalls to getNewData() and getDataString() cause JVM to abort with an er-ror,because it is no longer happy with the globally cached object reference.Itappears that activity restart somehow invalidates our cached object references,even though they are protected with NewGlobalRef(),and the activity is run-ning within the original JVM (activity restart does not mean that JVM itselfis restarted).I don't have a good explanation on why that happens,so if youhave any ideas,please let me know.