Friday, August 17, 2012

Using Native Code (JNI) on Android Apps

Android is written in Java and period. You must be able to do everything you need in Java.  But some applications like games using OpenGL will need some extra performance, and therefore they will go native.  As someone who had C++ as the lingua franca  at college, I took special interest on how the Java Native Interface (JNI) works on Android. Thus, I made a small Android app that requests the native layer to sum two integers. In its turn, the native layer displays the result on a TextView.


What do you need:

Preparing the field

First we create a simple activity that has a method with the native qualifier. That tells the JVM  that that method is not implemented in Java but by the native layer on C or C++. With Eclipse and ADT 20 that is pretty easy.


package com.example.testjni;

import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.widget.TextView;

public class MainActivity extends Activity {

    static {
        System.loadLibrary("teste");
    }
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        sum(3,5);
        
    }

    public void setTextView(String text) {
        TextView tv = (TextView)findViewById(R.id.text);
        tv.setText(text);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }
    
    public native int sum(int a, int b);
}

The code above creates a simple activity with a TextView. Line 11 instructs the Dalvik VM to load the shared library teste.so. You will see later that this the name of the library that will be generated to hold the native implementation of the method sum(int,int).  This is done inside a static block so it is executed when the class MainActivity is loaded and therefore before any instance of it is created; so Dalvik can execute the native method when requested.

My intention is that the activity after creating the views will call the native method to calculate the sum of 3 and 5, and then set the TextView with the result.


Generating the Header file

I created a jni folder in the project's directory  to hold all the native code. Now we need to implement the native method. When the native method is called, the JVM will look for C function with a  particular signature. The easiest way to know which signature you should use for your implementation is to use the javah tool from the JDK.


#javah -classpath ./bin/classes:~/Downloads/android-sdk-macosx/platforms/android-16/android.jar -d ./jni com.example.testjni.MainActivity

I ran the command above in the project's folder as above. javah needs the definition of all classes in your program. Thus I set the classpath  to the folder where the class files for my project are put (bin/classes) and I also added the path to the android.jar.  In my case I am using the API level 16, so I picked the right android.jar for it.  the -d parameter tells to output the header file to folder jni.  You need to pass the full qualified name ( i.e., including package name) of the class with the native method.  That command generated the follow header:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_testjni_MainActivity */

#ifndef _Included_com_example_testjni_MainActivity
#define _Included_com_example_testjni_MainActivity
#ifdef __cplusplus
extern "C" {
#endif

/*
 * Class:     com_example_testjni_MainActivity
 * Method:    sum
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_testjni_MainActivity_sum
  (JNIEnv *, jobject, jint, jint);

#ifdef __cplusplus
}
#endif
#endif


Note that even if the code gets compile on C++, the C-linkage is forced for the function, so the JVM can find the method without had to deal with the C++ mangling . C++ supports overloading function and mangles function signatures (embeds  parameters type, class and namespace scopes in the name) in order to support it.


Implementing the native method

I will show the implementation in C and C++.  The reason behind that is  that although the two languages are very similar ( you can compile C code with a C++ compiler),  the JNI  for C++ offer some inline methods to make it more object oriented.

The C code

#include <stdio.h>
#include stdlib.h>
#include <jni.h>
#include <android/log.h>
#include "com_example_testjni_MainActivity.h"


#define LOG_TAG "TesteNativeC"
#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))

static char* buildString(jint a, jint b, jint sum){
    const char* fmt_string = "The sum of %d and %d is %d";
    const int buffer_size = snprintf(NULL,0,fmt_string,a,b,sum);
    LOGI("Buffer size is %d",buffer_size);
    char* buffer = (char*) malloc((buffer_size+1) * sizeof(char));
    if( buffer != NULL){
        snprintf(buffer,(buffer_size+1),fmt_string,a,b,sum);
    }
        return buffer;
}


JNIEXPORT jint JNICALL Java_com_example_testjni_MainActivity_sum
  (JNIEnv * env, jobject obj, jint a, jint b){
    jint result =  a+b;
    jclass cls = (*env)->GetObjectClass(env, obj);
    jmethodID mid = (*env)->GetMethodID(env, cls, "setTextView", "(Ljava/lang/String;)V");
    if(mid == 0) return 0;
    char* text = buildString(a,b,result);
    if(text != NULL){
        LOGI("At this point string is <<%s>>",text);
        jstring textToDisplay = (*env)->NewStringUTF(env,text);
        (*env)->CallVoidMethod(env, obj, mid, textToDisplay); 
        free(text);
    }
    
    return result;
  }

Function buildString is declared static because it is not intended to be exported, used outside of the library. Its objective is to build the string "The sum of 3 and 5 is 8".  For that, we call function snprintf from the standard C library. Note that I am using snprintf , the "safe-version" of sprintf. The reason for that is to prevent buffer overrun,  a common technique used by attackers to inject malicious .  In our case we don't need to do that because input does not come from external source. However, best practices should always be enforced. snprintf is used twice: first with a NULL parameter  to calculate the size of the necessary buffer to hold the string, and a second time to build the string.

The implementation of the native method follow conventional JNI idioms and it is very similar to using reflection in Java. So I just calculated the sum, retrieved the reference to the setTextView(String) method of the activity, build the string and called the method.  And, this is very important, I  released the allocated char array. NewStringUTF does a copy of the passed char array, so you need to free the buffer to not get a memory leak.


The C++ code

#include <jni.h>
#include <sstream>
#include <string>
#include <android/log.h>
#include "com_example_testjni_MainActivity.h"

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__))

using std::stringstream;
using std::string;

const char* LOG_TAG = "TesteNativeCpp";

static string buildString(jint a, jint b, jint sum){
    stringstream buffer;
    buffer<<"The sum of "<<a<<" and "<<b<<" is "<<sum;
    return buffer.str();
}

JNIEXPORT jint JNICALL Java_com_example_testjni_MainActivity_sum
  (JNIEnv * env, jobject obj, jint a, jint b){
    jint result =  a+b;
    jclass cls = env->GetObjectClass(obj);
    
    jmethodID mid = env->GetMethodID(cls, "setTextView", "(Ljava/lang/String;)V");
    if(mid == 0) return 0;
    
    string text = buildString(a,b,result);
    LOGI("At this point string is <<%s>>",text.c_str());
    jstring textToDisplay = env->NewStringUTF(text.c_str());
    env->CallVoidMethod(obj, mid,textToDisplay); 

    return result;
  }
  

The C++ code is not much different. I could have used the same code  as of the C version for the function buildString, but I decided to do a code that is closer to latest C++ standards ( Not C++11 yet :-) ). Besides this uses STL containers. Using STL requires extra build config to compile. So I have the chance to show you how to setup the build for that case.

Note that for C++, env works more like an object : if in C we have (*env)->function(env,parameters), in C++ we have env->method(parameters).


Building


Now we need a Makefile for the job. I created the file Android.mk under the jni folder

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := teste
LOCAL_SRC_FILES := teste.cpp
LOCAL_LDLIBS    := -llog

include $(BUILD_SHARED_LIBRARY)



This makefile is based on the one use for the "hello word" sample of the NDK. LOCAL_MODULE will tell to generate the shared object teste.so. LOCAL_SRC_FILES lists the files to be compiled. cpp extension is required for C++ files. LOCAL_LDLIDS tells to link against the library that allows our native code to output log to logcat.
Remember I said we need extra config to use STL containers. This means to create an Application.mk file besides the Android.mk in the jni folder

APP_STL := stlport_static

This single line sets the  STL library to be linked static.

Now we can build . You just  need to invoke <ndk_folder>/ndk-build on you project folder. This will generate the shared library that will be packaged on you APK. After you have built the library, you just need to build your APK as you would normally do (Eclipse or ant).

And that is the final result :




No comments:

Post a Comment