300 likes | 409 Vues
Writing Native Code for Android Systems. Why ndk (native developers kit). There exist large c/ c++ code libraries E.g., Audio and video compression, e.g., Ogg Vorbis , The LAME Project (mp3), .. OpenGL O penSL ES Low level audio Advanced CPU features
E N D
Why ndk (native developers kit) • There exist large c/c++ code libraries • E.g., Audio and video compression, e.g., OggVorbis, The LAME Project (mp3), .. • OpenGL • OpenSL ES • Low level audio • Advanced CPU features • E.g., some ARM cpus support the NEON instruction set for signal and video processing
Apps that use the NDK are typically mixed java and c/c++ • Pure c/c++ apps are possible • The java app is like a regular app. The java app is started by os. • The c/c++ program is placed in a shared library • Shared libraries have names like libMyProgram.so • The application package (.apk) will include the java app and the shared library • jni (Java Native Interface) must be used to move data between java and c++
Outline of Steps • Gotohttp://developer.android.com/sdk/ndk/index.html and get NDK • Note: the current version of the ndk works well in windows. Previous versions needed cygwin • Make regular java project, e.g., MyProject • Make subdirectory, MyProject/jni • Write c++ code in MyProject/jni • Describe project sources in MyProject/jni/Android.mk • Like a make file, but much easier • Linux and MAC • Build project by running the command ../android-ndk-r7/ndk-build from your MyProject/jni directory • Windows • Build project by running the command c:\android\android-ndk-r7b\ndk-build from your MyProject\jni directory • ndk-build is like make • ndk-build • Builds • ndk-build clean • Cleans everything • Generates shared lib (libXX.so file) and places it in correct directory so the java program can get it • Make .apk file by building the app in eclipse • Important: whenever you make a change in the c++ program, of course, you need to run ndk-build. But, you also must rerun the java compile. To do this, make a trivial change in your java code and resave. Or run clean project from eclipse
HelloJni • Make new app called • Package: edu.udel.eleg454.HelloJni • Activity Name: HelloJni • Project: HelloJni • Make new subdirectory in project call jni • i.e., HelloJni/jni • In jni directory make new file called • MyHelloJni.cpp • In this file, put • #include <string.h> • #include <jni.h> • extern "C" { • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_HelloJni_HelloJniActivity_stringFromJNI( JNIEnv* env, • jobjectthiz ) • { • return env->NewStringUTF("Hello from JNI!"); • } • } • Save file • Important: function names must be exactly correct • Java_packageNameWithDotReplacedByUnderScore_JavaClassNameThatWillCallThisFunction_functionName
Android.mk • In HelloJni/jni make new file called Android.mk • Put the following in Android.mk • LOCAL_PATH := $(call my-dir) • include $(CLEAR_VARS) • LOCAL_MODULE := HelloJni • LOCAL_SRC_FILES := MyHelloJni.cpp • include $(BUILD_SHARED_LIBRARY) • Note that LOCAL_MODULE definesthe module name • Build library • Open terminal. • Cd dir to <workspace>/HelloJni/jni • Run build • <android-ndk-r7b>/ndk-build • Check that libHelloJni.so is created
In java HelloJni • After public class HelloJniActivity extends Activity { • public native String stringFromJNI(); // the c++ function name • static { • System.loadLibrary("HelloJni"); // shared lib is called libHelloJni.so. • // this name is from the LOCAL_MODULE part of the Android.mk file • } • In onCreate, after setContentView(R.layout.main); put • Log.e("debug","callingjni"); • Log.e("debug",stringFromJNI()); // last part of name of c++ function • Log.e("Debug","done"); • Run and check log • Note: public native … allows any function to be defined. But when this function is called, the shared library must have already been loaded (via System.loadLibrary)
play • Change c++ function to be make string • Hello from JNI 2 • Instead of • Hello from JNI! • Rebuild and run from eclipse • Log does not show anything. Not even an error • In eclipse make trivial change (delete and add ;) • Run, and everything is ok
C++ Function name • Change c++ function name. recompile and see error in LogCat • “no implementation found for native …” • Make a new class called TestJni • Move jni stuff into TestJni • Run and see error • Change function name from • Java_edu_udel_eleg454_HelloJNI_HelloJNIActivity_stringFromJNI • To • Java_edu_udel_eleg454_helloJNI_TestJni_stringFromJNI • And runs ok
Logging from c++ • In cpp file, add • #include <android/log.h> • In Android.mk, after include $(CLEAR_VARS) add, • LOCAL_LDLIBS := -llog • In function add • __android_log_print(ANDROID_LOG_INFO, "DEBUG", "Here we are");
Passing strings from java to c++ with JNI • In java code, make function arg include a string • Change • public native String stringFromJNI(); • To • public native String stringFromJNI(String name); • And change • Log.e("debug",stringFromJNI()); • To • Log.e("debug",stringFromJNI("string para")); • In c++ code • Change • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz) • To • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz, jstringjavaString ) • And add • const char *str = env->GetStringUTFChars(javaString, 0); // convert java string to c++str • __android_log_print(ANDROID_LOG_INFO, "DEBUG", str); // do something • env->ReleaseStringUTFChars(javaString, str); // release str • Build, compile, run • Note: after release, str is no longer valid
Passing int, floats, etc to c++ • In java • Change • public native String stringFromJNI(); • To • public native String stringFromJNI(intval); • And change • Log.e("debug",stringFromJNI()); • To • inti = 100; • Log.e("debug",stringFromJNI(i)); • In c++ • Change • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz) • To • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz, jintji ) • And comment out • const char *str = env->GetStringUTFChars(javaString, 0); • env->ReleaseStringUTFChars(javaString, str); • Add • char str[80]; • sprintf(str,"data is %d",ji); // be sure to add #include <stdio.h> • __android_log_print(ANDROID_LOG_INFO, "DEBUG", str); • Build, compile, run
Jni Data types • C++ type = jave type • unsigned char = jboolean • signed char = jbyte • unsigned short = jchar • Short = jshort • Long = jlong • Long long = jlong • __int64 = jlong • float = jfloat • double = jdouble
Passing arrays of ints to c++ • In java • Define function to take int array as argument • Replace • public native String stringFromJNI(); • With • public native String stringFromJNI(int[] val); • In onCreate • Make array • int[] ints = new int[]{1,1,2,3,5,8,13}; • Call function with ints as augment • Log.e("debug",stringFromJNI(ints));
Passing arrays of ints to c++ • In c++ • Define function to take array as argument • JNIEXPORT jstring JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz, jintArrayjiArray ) • Get size of array • jsizearrayLength = env->GetArrayLength(jiArray); • Get pointer to array • jint *data = env->GetIntArrayElements(jiArray, 0); • Do something with data • char str[80]; • for (inti=0; i<arrayLength; i++) { • sprintf(str,"val %d is %d",i,data[i]); • __android_log_print(ANDROID_LOG_INFO, "DEBUG", str); • data[i] = i; • } • Release pointer • env->ReleaseIntArrayElements(jiArray, data, 0); • Build, compile, run
More passing arrays to c++ • env->ReleaseIntArrayElements(jiArray, data, 0); • Last argument is 0 => data is copied back to java and java can delete data array • Last argument is JNI_COMMIT => data is copied back, but java should not delete the array • Last argument is JNI_ABORT => data is not copied back and java can delete • Check if the data was changed in c++ • In java, after Log.e("debug",stringFromJNI(ints)); add • for (int i=0; i<ints.length; i++) { • Log.e("DEBUG","ret val["+i+"] = "+ints[i]); • } • run, and see that ints is updated
Returning data • Java • define function to return int • public native intstringFromJNI(int[] val); • Call function and print return value • Log.e("debug","results = "+stringFromJNI(ints)); • C++ • Change function prototype to return jint • JNIEXPORT jint JNICALL • Java_edu_udel_eleg454_helloJni_HelloJni_stringFromJNI( JNIEnv* env, • jobjectthiz, jintArrayjiArray ) • return int • return 12; • Build, compile, run
Return arrays • Same as returning a string but use NewIntArray
SWIG • SWIG automatically builds an interface between java and c/c++ • SWIG constructs an interface that is composed of java and c/c++ code. The result is that your java code and your c/c++ code is simpler • you don’t need to mess with JNI • You don’t need to change the basic c++ code to support java. Just use swig to make an interface • Swig also for interfaces between c/c++ and many other scripting languages • More info • on swig and java: http://www.swig.org/Doc1.3/Java.html • Section 5 of http://www.swig.org/Doc2.0/SWIGDocumentation.pdf • Swig and android: http://swig.svn.sourceforge.net/viewvc/swig/trunk/Doc/Manual/Android.html • Swig is a bit time consuming to get set up, but if you are working with a complicated interface between java and c/c++, it is worth it. • I have found jni is difficult to work with. We already saw some problems with how difficult the naming is. And there are other complications • Windows set up • http://www.swig.org/download.html • Download full version of SWIG (not the windows executable version) • Decompress into D:\SWIG • Download windows executable version • Decompress and get swig.exe and paste swig.exe into D:\SWIG • Linux • apt-get install swig
Swig 1 • New app, TestSwig • Make jni directory • You can make this in eclipse package explorer • right click on TestSwig • New->Folder: Folder name: jni • Check that jni appears between bin and res • Otherwise, make it in file explorer. Then, in eclipse package explorer, right click TestSwig and select Refresh • Either way, Jni must appear in list of directories, otherwise this process will fail • Make testSwig.c • Could be TestSwig.cpp, but there is a minor difference later • #include <string.h> • #include <jni.h> • #include <android/log.h> • #include <stdio.h> • inttestThis(double val) { • char str[128]; • sprintf(str,"Here we are: %f",val); • __android_log_print(ANDROID_LOG_INFO, "TestSwig", str); • return 12; • } • Make TestSwig.h • A .h file is needed to the interface. Any function that is aprt of the interfaces (i.e., can be called from java) must be in a .h file • inttestThis(double val); • Make interface file called TestSwig.i and save in jni directory. This defines the interface between java and c/c++ • %module testSwig • %{ • #include "TestSwig.h" • %} • extern inttestThis(double val); • Get ready to run swig • Make directory • TestSwig\src\edu\udel\eleg454\Swig • Run swig • Open command prompt • Change to TestSwig\jni directory • execute • D:\swig\swig.exe –java –package edu.udel.eleg454.Swig –outdir ../src/edu/udel/eleg454/Swig testSwig.i • -package will make this interface a package • -outdir defines the directory. This directory must be made before running swig • Check • Check that java files are made in
Import to eclipse • Import • The first import is a bit odd. After the first time, everything runs smoothly • Open file explorer and browse to TestSwig\src\edu\udel\eleg454 • Cut Swig directory • i.e., the directory with all the java files we just made • Paste to d:\temp\testSwig\edu\udel\eleg454\Swig • you need to make this directory • Make sure that you cut, i.e., that Swig directory is deleted from TestSwig\src\edu\udel\eleg454 • Go Back to eclipse • In project explorer, right click on TestSwig/src (the project) • Select import • Under General is File System , select File System. Next • From Directory: Browse to d:\temp and select testSwig. OK • Select the check box next to edu • Set Into folder to TestSwig/src • Click finish • Check that under /src is a new package edu.udel.eleg454.Swig • Now, the next time you run swig.exe …. The files will be updated. • The reason we had to move them to \temp is that eclipse does not let you import to the location where the files are. • I don’t understand why sometime when you import the eclipse package explorer shows you edu/udel/eleg454/Swig and other times, edu.udel.eleg454.Swig. We must have edu.udel.eleg454.Swig • Advanced/optional: • You can make swig.exe run automatically • In package explores, right click on project, TestSwig -> properties->Builders-> new-> program click ok • Name: swig • Location: d:\swig\swig.exe • Work directory, ${project_loc}/jni • Augments: • –java –package edu.udel.eleg454.Swig –outdiredu/udel/eleg454/Swig testSwig.i
NDK • Android.mk • LOCAL_PATH := $(call my-dir) • include $(CLEAR_VARS) • LOCAL_LDLIBS := -llog • LOCAL_MODULE := TestSwig • LOCAL_SRC_FILES := testSwig.ctestSwig_wrap.c • include $(BUILD_SHARED_LIBRARY) • testSwig.c is our c code • testSwig_wrap.c is generated by swig • Don’t attempt to read testSwig_wrap.c. It is not meant to be read or understood
java • Now we can use this nice interface in our java code • In TestSwigActivity, after public class TestSwigActivity extends Activity {, add • static { • System.loadLibrary("TestSwig"); • } • It is too bad that you need to add this manually. I think swig should add it to the java files automatically. But it does not, so you need to add it here • In onCreate, after setContentView(), add • Log.e("testSwig","test: "+testSwig.testThis(2)); • testSwig is the class, and testThis is the c/c++ function
Structures • In TestSwig.h add • structTestStruct { • int a; • double b; • }; • In TestSwig.i, after extern inttestThis(double val); add • structTestStruct { • int a; • double b; • }; • Note that the struct must be defined in both places. • It is defined in .i so swig makes an interface • It is defined in .h so that TestSwig_wrap.c has a .h where the struct is defined • Run swig as before • Swig will generate class TestStruct and TestStruct.java with operators • getA(), getB(), setA(int a), setB(double b) • To use TestStruct in java, TestStructtestStruct = new TestStruct(); • The extra java files can be added to the project as before • Cut them from the directory to some other directory. Then drag the file from file explorer to eclipse package explorer
classes • Make TestSwigClass.h • class TestSwigClass { • public: • int a; • double b; • void set_b(double _b) { b = _b; }; • intget_a() {return a;} • TestSwigClass() {}; • void process() { • // nothing yet • } • }; • At the top of TestSwig.i, add TestSwigClass.h to module section, so it looks like • %module testSwig • %{ • #include "testSwig.h" • #include "TestSwigClass.h" • %} • At the end of TestSwig.i, add the contents of TestSwigClass.h • Running swig for c++ code is slightly different than running for c code • D:\swig\swig.exe –java –c++ –package edu.udel.eleg454.Swig –outdir ../src/edu/udel/eleg454/Swig –o testSwig_wrap.cpp testSwig.i • This will generate a testSwig_wrap.cxx file, not testSwig_wrap.c • Android ndk does not accept .cxx files. So you need to add –o testSwig_wrap.cpp to make the correct output file • Note: mixing .c and .cpp is a bad idea. So rename TestSwig.c to TestSwig.cpp and update Android.mk • This will generate even more java files that need to be added to your project
Extending structures/classes: motivation • A key goal of SWIG is that the c/c++ code does not need to be changed to support the java interface • SWIG generates a interface code in c/c++ and several in java • However, sometimes simply “porting” the c/c++ interface to java is insuffucient • E.g., some c/c++ functions return data in the arguments, which might not be compatible with java • E.g., java makes functions that c/c++ does not, such as toString • E.g., the c/c++ interface does not fit well into java, so a new interface should be made
Extending classes/structures • A new class can be defined in the .i file and these or any class can be extended • The result of an extension is that the java interface appears as it would if the c/c++ interface had these changes. • However, the c/c++ code is not changed
Extending classes/structures • Consider the TestSwigClassmade before. • Goal: extend to include add fucntion • In swig.i, add • %extend TestSwigClass{ • void add(class TestSwigClass*other) { • self->a = self->a + other->a; • self->b = self->b + other->b; • } • }; • Note that self is like this. When this function is generate, it is not a member function of TestSwigClass (that would require changing TestSwigClass). • Instead, the following is generated • SWIGINTERN void TestSwigClass_add(TestSwigClass *self, TestSwigClass*other){ • self->a = self->a + other->a; • self->b = self->b + other->b; • } • Note that self is automatically added as a parameter, this you can use the same way you use this. • The java interface is • public void add(TestSwigClass other) { …} • One drawback of extending classes is that your extension code might have bugs and then debugging requires examining testSwig_wrap.cxx
Making a new interface class/structure • Instead of only extending an existing class/structure, you might want to make an entire new class • Again, this class should only be implemented in the interface code generated by swig • E.g., make class MyInterfaceClass • In testSwig.i, • At the top, in the %module testSwig section, add • class MyInterfaceClass { • public: • inta; • double b; • }; • Also, after the %module testSwigsection, add (yes, you must add the same stuff in two places) • class MyInterfaceClass { • public: • inta; • double b; • }; • After class MyInterfaceClass, add • %extend MyInterfaceClass{ • MyInterfaceClass() { • MyInterfaceClass*u = new MyInterfaceClass(); • u->a = 0; • u->b= 0; • return u; • } • }; • Clearly, if you need to make more than a small number of interface classes, you should just make them in c/c++ yourself and not rely on swig.
stl::vector, stl::string, … • Much of stl can be accommodated • See section 8.4 in the swig documentation • However, my install was missing stl_list.i, so I could not get list to work. Vector and string did work