Suppose you have an existing C/C++ code library or a full game you’d like to port to Android, but don’t want to translate them to Java. One solution is to keep as much code as possible in C/C++ and then use the Android NDK and the Java Native Interface to communicate between the two languages.
Overview
In order to run C/C++ code on Android, it must first be compiled into a shared library (generally a *.so file). Then, after loading the library, the Java code can begin calling native functions and viceversa.
To build a shared library, you can use the ndk-build shell script included with the Android NDK. It needs to run in a Unix-like environment so, if you’re on Windows, you will require Cygwin. Lately you can also try “ndk-build.cmd”, an experimental Windows-compatible script included in the “r7″ release of the NDK.
Create a “jni” folder and put your C/C++ files in there, as well as an Android.mk makefile:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := myjni LOCAL_SRC_FILES := main.cpp include $(BUILD_SHARED_LIBRARY)
I’m naming my shared library “myjni” and have only one file to compile, main.cpp. To build, just navigate to the folder that is one level below the “jni” folder, and execute the “ndk-build” script. For example on Windows, if my source files were in C:\jnitest\jni\ and my NDK installation was in C:\android-ndk then I would do the following in Cygwin:
$ cd /cygdrive/c/jnitest [ENTER] $ /cygdrive/c/android-ndk/ndk-build [ENTER]
Calling a native function from Java
In order to call a C/C++ function, you must first declare it in Java. To make things easier, we will have an abstract class in Java that can load our native library and declares a native function:
public abstract class MyJNILibrary { public static void Load() { System.loadLibrary("myjni"); } public static native int ComputeSum(int a, int b); }
First we have the Load() function which loads the native library and allows Java to use it. Then we have a static ComputeSum() function we’ve marked as “native”. This means Java will assume its implementation is inside the C/C++ shared library. Notice I said “assume”. You can declare a native function and never implement it. But if you call the function, a MethodNotImplementedException is thrown. Our function, ComputeSum, will take two integers, add them together, and return the result.
Now let’s look at the C/C++ code. The way to implement ComputeSum() is:
#ifdef __cplusplus extern "C" { #endif JNIEXPORT jint JNICALL Java_com_adbad_jnitest_MyJNILibrary_ComputeSum(JNIEnv * env, jobject obj, jint a, jint b) { return (a + b); } #ifdef __cplusplus } #endif
Compilers will usually build .c files as C code, and .cpp files as C++ code. If your file is being compiled as C++ code, then you need to add “extern C{…}” for Java to be able to find the functions. Read why here.
Looking back on ComputeSum, notice the difference between the Java signature and the C/C++ signature. JNIEXPORT and JNICALL are special macros used by JNI, which I’ll talk about in a future post (but feel free to look them up). The “jint” datatype is used for receiving and sending Java ints. Function names are longer because Java uses a special naming convention to find them:
Java_[PACKAGE_NAME]_[CLASS_NAME]_[FUNCTION_NAME]
The package name in Java is “com.adbad.jnitest”, and appears as “com_adbad_jnitest” in C++. As for the “env” and “obj” function arguments, we’ll discuss them later. Now that the function has been declared in Java and implemented in C/C++, we can start calling it. You can do the following in your Java code:
int result = MyJNILibrary.ComputeSum(5, 3);
…and the result should be “8″ as expected.
Calling a Java function from C/C++ code
Going the other way around is a bit harder because usually, we’ll want to call a Java member function on a specific object instance. To achieve this, we’ll need a handle to the member function and the instance of the object. We’ll create a class called TestClass and give it a ComputeMult function that multiplies two floating point values, casts the result to an integer, and returns the integer.
public class TestClass { public TestClass() { } public int ComputeMult(float a, float b) { return (int)(a * b); } }
And here is the C/C++ code that calls the function:
jclass testClassHandle = env->FindClass("com/adbad/jnitest/TestClass"); jmethodID computeMultHandle = env->GetMethodID(testClassHandle, "ComputeMult", "(FF)I");
Here is where that JNIEnv function argument is handy. It is a pointer that contains the interface to the Java Virtual Machine. It can be stored, but is only valid in the current thread. There’s also a C/C++ difference in the way it’s accessed. On C, you use “(*env)->SOME_FUNCTION(env, …”, and on C++ you use “env->SOME_FUNCTION(…)”. I’ve used the C++ version in this post.
We use the JNIEnv pointer to get the handles we need. The FindClass function returns a handle to the passed in class. The fully qualified name is used, and in this case, the package name is separated by forward slash. The GetMethodID receives a class handle, a member function name, and a signature. The signature follows a special format:
- The data types for function arguments go inside the parenthesis. Our ComputeMult function receives two floats, so we write that as “(FF)”.
- Following the parenthesis is the return type for the function. ComputeMult returns an integer, so we write that as “I”.
- Custom types are also supported and are written using the fully-qualified name, a prefix of “L”, and a suffix of “;”. For example, a signature of “(Ljava/util/ArrayList;)Z” describes a function that takes a java.util.ArrayList instance and returns a boolean (which is what the “Z” stands for).
In C/C++, the jobject data type is used for non-primitive types. Let’s assume we already have the method handle plus a jobject named “testClassInstance”. Now, let’s call the ComputeMult function:
jfloat a = 4.0f; jfloat b = 0.5f; jint result = env->CallIntMethod(testClassInstance, computeMultHandle, a, b);
The “result” integer variable should now hold a value of 2.
Creating an instance of a Java class in C/C++
In the previous section we called a function on an existing Java object. But what if we want to create our own instance? Let’s create a TestClass object:
jclass testClassHandle = env->FindClass("com/adbad/jnitest/TestClass"); jmethodID testClassCtor = env->GetMethodID(testClassHandle, "<init>", "()V"); jobject testClassInstance = env->NewObject(arrayListClass, arrayListCtor);
As you can see, getting a handle to the constructor is the same as getting any other member function. The only difference is that “<init>” is used as the function name. To create the instance, we use the JNIEnv’s “NewObject” method and pass in the class and constructor handles.
Helpful Links
http://www.cygwin.com
http://answers.yahoo.com/question/index?qid=20080326074018AAarBJ9
http://java.sun.com/docs/books/jni/html/types.html
http://en.wikipedia.org/wiki/Java_Native_Interface
http://java.sun.com/docs/books/jni/html/fldmeth.html
http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html