Using the NDK and JNI to build android applications

25 Aug

When writing applications for android in java, there are often parts that you expected to perform better. It’s not always the algorithm that is to blame. Java has its limits. Even with a JIT compiler it will have more runtime overhead, and a garbage collector will run at times you don’t want it to. We can solve this by moving demanding pieces of our code to C, C++ or assembler. 

The good

  • Speed. Games run at 60fps again. We (almost) no longer have to worry about data conversions, GC stalls or unoptimized code.
  • Cross platform code. For me this is one of the most important benefits. I like writing a game only once and having it run on both iOS and android without changes.
  • Standard API’s like OpenGL ES, OpenSL ES.

The bad

  • Lack of native API’s. GUI’s, Bitmap loading, in-app purchasing and many other features and third party libraries (facebook for instance) don’t have native API’s. There is some support to access a Bitmap’s pixels, but creation has to happen in java or JNI code.
  • Lack of access to system libraries. What is worse is that while android is built on libpng, libjpeg, freetype, etc, these libraries are not accessible from the NDK. This means you have to compile your own versions, increasing the size of your application. Shared libraries are especially useful in case of mobile and embedded devices with their limited storage, so it’s a pity we can’t make use of it here.
  • Native debugging doesn’t seem to work as advertised, but this can be alleviated by debugging the code in Xcode or visual studio instead.

The ugly

  • JNI. While it is easy to write JNI code as an intermediate between java and C++, it certainly isn’t pretty.

JNI: calling C++ from java and vice versa

Writing JNI code isn’t so different from writing bindings for Python, Ruby or Lua. Since we start from a java Activity, we will start by calling a C++ function from java. (Note that there is also the NativeActivity, though due to the lack of native C++ API’s, it’s easier to work with a default Activity).

In java we declare the C++ function to call as a method in our Activity and tell java to load the compiled code.

private native float setActivity(Activity activity);
private native float calculate(float[] input);

static
{
    System.loadLibrary("native");
}

In C++ we do a forward declaration in C, as java won’t find C++ decorated functions. Each function has the environment and the object instance whose method was called as first and second parameter respectively.

extern "C" {
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved);
JNIEXPORT void JNICALL Java_com_example_MainActivity_setActivity(JNIEnv * env, jobject obj, jobject activity);
JNIEXPORT void JNICALL Java_com_example_MainActivity_calculate(JNIEnv * env, jobject obj, jfloatArray coefficients, jfloat value);
};

Then we declare our function. Note that we need to include the java class in the name, as more than one class may define the same method.

JNIEXPORT jfloat JNICALL Java_com_example_MainActivity_calculate(JNIEnv * env, jobject obj, jfloatArray coefficients, jfloat value) {
    jboolean isCopy;
    int size = env->GetArrayLength(coefficients);
    jfloat *ptr = env->GetFloatArrayElements(coefficients, &isCopy);
    float result = 0;
    float t = 1;
    for (int i=0; i
        result += ptr[i] * t;
        t *= (float)value;
    }
    env->ReleaseFloatArrayElements(values, ptr, JNI_ABORT);
    return result;
} 

If all we wanted to do was some native calculations, we are done apart from some marshaling. However at some point we may want to call a java method from C++ . To do this we need two things, the java virtual machine and an object instance whose method we want to call.

Saving the VM pointer directly is ok, but to keep the instance we need to ask for a long term reference, otherwise it might get deleted or the memory might move inside the environment and the temporary reference becomes invalid.

*gJavaVm = NULL;
jobject gActivity;

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVm, void *reserved) {
    gJavaVm = javaVm;
    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL Java_com_example_MainActivity_setActivity(JNIEnv * env, jobject obj, jobject activity) {
    jobject gActivity = env->NewGlobalRef(activity);
}

When calling a java method, we first attach the current thread to the VM and get a pointer to the environment. Then we need the method id before we can call it. To get this  method id, we need to give the name, the return value, as well as the parameters as there might be multiple methods with the same name. Since this lookup can be slow, we cache the returned id. Finally we can make the call. Note that the callXmethod depends on the return value.

std::string httpGet(const std::string &url) {
    JNIEnv *env;
    javaVm->AttachCurrentThread(&env, NULL);
    static jmethodID midHttpGet = NULL;
    if (!midHttpGet)
        midHttpGet = env->GetMethodID(env->GetObjectClass(gActivity),"httpGet", "(Ljava/lang/String;)Ljava/lang/String;");
    jstring result = env->CallObjectMethod(gActivity, midHttpGet, env->NewStringUTF(url.c_str()));
    jboolean isCopy;
    const char *ptr = env->GetStringUTFChars(result, &isCopy);
    std::string data = ptr;
    env->ReleaseStringUTFChars(result, ptr);
    return data;
}

Marshaling

One of the most time consuming tasks is to figure out how we are going to transport our data. As already shown in some of the code above, aside from primitive values like ints or floats, we have access to higher level objects like java arrays and strings. While the former can be casted, the latter have lock and unlock functions to access data. Then there is the generic object which we can use as a map. While maps sound promising to emulate structs, they aren’t that handy. To access a field, we first need to know the field id, we can’t just index using a string. To get the index, we first need to get the class. So we need FindClass followed by GetFieldID to eventually call GetXField, and preferably cache the id’s for later use.

Doing all this is good when you want to make a nice interface to access API’s, but if you just want to write cross platform games, it is better to forget structs altogether and do as few as possible in java. If you use JNI to do a JSON call for example, it might be best to just pass the url as a string and get the JSON data back as a string. Parsing the JSON data in java and trying to mold it into the primitives which you can transport through JNI can be tedious, and you want your compatibility layer as small as possible to reduce platform specific issues.

The following table shows the C++ type in case there is a cast available, the JNI type as well as the string used to define the parameter in a method definition

C++ type JNI type Parameter string
bool jboolean Z
unsigned char jbyte B
char jchar C
short jshort S
long jlong J
float jfloat F
double jdouble D
jstring Ljava/lang/String;
jobject Lfully-qualified-class;
jXArray [X

In case you want to pass pointers (callback function pointers or pointers to data or objects which are used later in a C/C++ call from java) you need to cast it to a jlong. So in JNI you would write:

void callAsynchronousMethodWithCallback(void *onFinished) {
    JNIEnv *env;
    javaVm->AttachCurrentThread(&env, NULL);
    static jmethodID midHttpGet = NULL;
    if (!midAsynchronousMethod)
        midAsynchronousMethod = env->GetMethodID(env->GetObjectClass(gActivity),"asynchronousMethod", "(J)V");
    env->CallVoidMethod(gActivity, midAsynchronousMethod, (jlong)onFinished);
}

In java you just pass the callback back to C/C++ using a JNI call and call it. Depending on the parameters you might need to marshal these.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: