Header Ads

[Android NDK ] Basic Procedure

The steps that you have to take to develop JNI libraries for Android phones are simple in principle. They are outlined below.

  • Install the JNI/NDK package from Google
  • Create your Android project
  • Make a JNI folder in your Android project root directory (called 'jni')
  • Put your JNI sources in the 'jni' folder
  • Create an 'Android.mk' file, and place it in the 'jni' folder
  • Optionally create an 'Application.mk' file, and place it in the 'jni' folder
  • Open a command line terminal and navigate to the root directory of your Android project.
  • Execute 'ndk-build', (if it's in your PATH variable) or execute '/path/to/command/ndk-build'
  • The 'ndk-build' command creates the binary for your library and puts it in the proper folder.
  • Switch to Eclipse, Refresh the 'Project Explorer View' (F5)
  • Rebuild the project
  • Run your project testing your JNI library.
The tutorial will now attempt to go through these steps. We will skip the 'Install the JNI/NDK' step as it's already been covered. top

Create Android Project

This step entails using Eclipse to create a new Android project. This project will have a folder in the Eclipse 'workspace' directory. The location of the 'workspace' directory and the project directory inside it should be known to you. The 'project' directory name is the name of your project to eclipse.
On a linux system the 'workspace' directory is commonly located in the user's home directory. The project is inside it. When this or other documentation refers to the project's root, for a project named 'projectname', they would be referring to the following path.
/home/myname/workspace/projectname/
This is important for steps where you create the 'jni' folder and where you call the 'ndk-build' script. top

Make JNI Folder

A folder named 'jni' (lowercase) must be created by you in the Eclipse project's root directory. In a linux system where the user is called 'myname' and the project is called 'projectname', this folder would be located in the 'projectname' folder, as referred to by the following path.
/home/myname/workspace/projectname/jni/
This would place a folder at the same level as the Eclipse/Java 'bin', 'src', and 'res' folders. It would also be at the same level as the 'AndroidManifest.xml' file. Inside this folder is where the source c or c++ documents need to be placed.
If you use Eclipse to create this folder, then you are done with this step. If you create the folder from the command line, don't forget to type F5 or click on 'Refresh' to let Eclipse register that there has been a change to the folder structure. top

JNI Sources

Here you work on the actual source files in c or c++. You will probably return to this step many times as your JNI library evolves. A large part of this how-to is focused on showing how c can be used to create a simple JNI library like the one used in the Awesomeguy program.
This document will explain the creation of a JNI library consisting of a single source file. Choose a name for your file and place it in the 'jni' folder. For this document the library will be called 'example' so the file will be called 'example.c'. On a linux system the path to the 'example.c' file will look something like the path shown below.
/home/myname/workspace/projectname/jni/example.c
After placing the file 'example.c' in the 'jni' folder you should refresh Eclipse so that it recognizes the new file. The file 'example.c' should be a simple text file. To edit the file you need a regular text editor. On linux systems, when viewing the project from Eclipse, if you double click on the file 'example.c' in the Project Explorer view, the system's text editor is automatically opened with 'example.c' loaded. You can then make changes and save and re-compile the source code. top

Create Android mk File

Before the NDK build tools will compile your code they need a 'Makefile' file. That's the purpose of the file 'Android.mk'. Because our project only uses a single source file for the JNI library, our 'Android.mk' file will be simple to write. Information on writing the 'Android.mk' file can be found in the 'docs' folder inside the 'android-ndk-r4b' folder. The most relevant document is the 'ANDROID-MK.TXT', and this document does a thorough job of explaining the parts of the file that are required.
An example 'Android.mk' file is shown below. This file is taken from the Awesomeguy project.
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := awesomeguy
LOCAL_CFLAGS    := -Werror
LOCAL_SRC_FILES := awesomeguy.c
LOCAL_LDLIBS    := -llog 

include $(BUILD_SHARED_LIBRARY)
For the purposes of this tutorial we'll leave most of the file as it is.
  • LOCAL_PATH - this line should be left as it is since your source file ('example.c') is in the same directory as the 'Android.mk' file.
  • include $(CLEAR_VARS) - this line should be left as it is. It is required.
  • LOCAL_MODULE - this line should be changed to match your module name. For this tutorial we'll change it to 'example'. This name should not have any spaces in it as it will be made into the actual library's name ('libexample.so' for us).
  • LOCAL_CFLAGS - This line can be left as it is. It is for compiler flags.
  • LOCAL_SRC_FILES - this line should be changed to 'example.c' since that's our source file.
  • LOCAL_LDLIBS - leave this the same.
  • include $(BUILD_SHARED_LIBRARY) - leave this the same.
So our new 'Android.mk' file should look like this.
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := example
LOCAL_CFLAGS    := -Werror
LOCAL_SRC_FILES := example.c
LOCAL_LDLIBS    := -llog 

include $(BUILD_SHARED_LIBRARY)
The NDK build tools will compile the code, and then if there are no errors it will name the object code 'libexample.so' and place it in the Android project in a new folder that it creates especially for this purpose. The name of the folder is 'libs/armeabi/'. The build tools also manage the creation of some other files that are necessary.
The name, 'libexample.so' comes from the 'LOCAL_MODULE' line in the 'Android.mk' file, and it is the library name, but when referring to the module in the java code you refer to 'example' not 'libexample.so'. top

Go To Project Root

The project root is the base folder for the Eclipse Android project. It's found in the 'workspace' directory. It is not the 'jni' folder, but instead its parent. Using the conventions we've been following for linux systems, the path to the project root directory would be the following.
/home/myname/workspace/projectname/
You want to 'cd' into this directory from the command line. top

Execute Build Command

The command to start the build scripts is called 'ndk-build'. The script for 'ndk-build' is located in the Android NDK package of software that you already downloaded. If you followed the tutorial so far, you would have placed the 'android-ndk-r4b' folder in your 'bin' directory, and then you would have added it to your path. If this is the case, simply type 'ndk-build' at the command line.
If you didn't put the 'android-ndk-r4b' folder in your PATH variable in the '.bashrc' file, then you can explicitly specify the path to the 'ndk-build' script when you invoke it. The command for this is shown below. top
/home/myname/bin/android-ndk-r4b/ndk-build

What Build Command Does

The ndk-build script compiles the source code for the Android project and places it in a directory where it can be included by the Eclipse build function in the final apk file.
The folder where the resulting binary library goes is the 'libs/armeabi/' folder. Two other files are placed in that folder along with the JNI library that we are creating. They are created automatically, so we're not going to worry about them. One is the 'gdb.setup' file, and one is the 'gdbserver' file. top

Refresh Project Explorer View

The 'Project Explorer' is one of the Eclipse windows that is visible when doing standard editing on the default Java perspective. It is commonly on the left side of the Eclipse screen. It shows all the files and folders in a project. You can refresh the view by selecting a project and then pressing the <F5> key, or by right clicking on the project name and selecting 'Refresh' from the menu that is displayed. top

Rebuild Project

Like refreshing the project, you can rebuild the project by right clicking on the project name in the Project Explorer and then selecting 'Build Project' from the menu that's displayed. Some activities, like the 'Run' activity, automatically rebuild the project when the Eclipse IDE sees that the project has changed. top

Test Project

Test your project thoroughly. This step goes without saying.

Writing JNI Sources

For our purposes here we want to build a JNI library from a single c source file. It is possible to use c++ and several source files, but we're not going to do that.
We need to be able to write methods that can be called in java code. We need to be able to pass variables to the library and pass variables from the library to java.
We will assume that the JNI library that we're using is called 'libexample.so', and that the module name, as defined in the 'Android.mk' file is 'example'. We'll also assume that the package name for the Android project we're working on is something simple. We'll use this.
org.testjni.android
This means you have a line in your program files that looks like this.
package org.testjni.android;
It also means that there is a file folder structure in your Eclipse project under 'src' that looks like this.
src/org/testjni/android/
We'll assume that the Java file that we want to call the JNI from is called 'Game.java'.
In your JNI library source file you must '#include' the library 'jni.h'. Including the library would look like this. Including this library ensures that the source file you are working on will compile.
#include <jni.h>
top

Naming Functions

Example One

The naming conventions for these methods are fairly complex. The average JNI native function name is unwieldy and looks something like this:
JNIEXPORT void JNICALL Java_org_testjni_android_Game_someFunction(JNIEnv * env, jobject  obj)
{}
In this example the method body is empty. The java native method 'someFunction()' would call the c function above. The name for this c function can be broken down into parts. The first part is the information before the method name. This is listed below.
  1. the macro JNIEXPORT
  2. the return type for the method ('void' in this example)
  3. the macro JNICALL
After that comes the actual name of the method. The name can be broken down into parts. They are listed below. The parts are separated by underscores.
  1. the text 'Java_'
  2. the text for the package name of the Android app, with the periods replaced by underscores
  3. the name of the class that the function will be part of
  4. the name of the function
This is followed by the two mandatory parameters 'JNIEnv * env' and 'jobject obj'. Other parameters for the function could have followed the mandatory ones listed above. top

Example Two

JNIEXPORT int JNICALL Java_org_testjni_android_Game_someFunction(JNIEnv * env, jobject  obj)
{
return 3;
}
In this example the method body is the 'return' statement that returns the number 3. The java native method 'someFunction()' would call the c function above. Note that the return type is 'int' not 'void'. This is as you would expect from a c function. top

Example Three

JNIEXPORT void JNICALL Java_org_testjni_android_Game_someFunction(JNIEnv * env, jobject  obj, jint x)
{
int xx = x;
return;
}
In this example the method body is not empty, but performs only a trivial task. The java native method 'someFunction(int x)' would call the c function above. In all examples the first two parameters to the c function are mandatory. The variables 'JNIEnv * env' and 'jobject obj' are included in all functions that are going to be called from the java code.
NOTE: You must '#include' the library 'jni.h' at the beginning of your source file for these functions to compile right. That would look like this. Do not forget this step.
#include <jni.h>
top

Java Declaration

There are two parts to the java declaration for the example code that we are using for this explanation. One part loads the library, and another part makes reference to the method so that the other java code in the app can call it. The declaration looks like the code below.
static {
        System.loadLibrary("example");
}
This code has to be included in the Java class where the JNI is to be used. The second part of the java listing would look as follows.
public native void someFunction();
This line of code is like a java method declaration without the body. This function is also 'public' so it can be called by methods of other classes if they have an instantiated version of the class 'Game'.
You should note that the c method declaration is different from the java declaration, but you need both, and the c version must include the package name and the java class name. top

Passing Arrays

One thing that was essential in the awesomeguy.c file was the passing of arrays between the JNI code and the Java code. In awesomeguy the bitmaps for the main character and the other graphic elements were converted to arrays in the java code and then passed to the JNI library during the class instantiation phase. When it was time to draw the actual screen, the JNI code assembled a large array that was passed back to the java code. In java the array was converted to a bitmap and displayed on the screen. top

Arrays as Parameter

Here is an example of code actually used in the awesomeguy.c JNI file. It shows how a 'jintArray', which is an array of integers, can be passed to a JNI library. It also shows the processing that's needed to access the data in the manner of a c int array.
Note that the first two parameters to the JNI method are always the same. They are 'JNIEnv * env, and jobject obj'. After that the method can be customized in different ways.
JNIEXPORT void JNICALLJava_org_davidliebman_android_awesomeguy_Panel_setTileMapData(JNIEnv * env, jobject  obj,
jintArray a_bitmap, jintArray b_bitmap, jintArray c_bitmap, jintArray d_bitmap)
{
  //jsize a_len = (*env)->GetArrayLength(env, a_bitmap);
  jint *a = (*env)->GetIntArrayElements(env, a_bitmap, 0);
  //jsize b_len = (*env)->GetArrayLength(env, b_bitmap);
  jint *b = (*env)->GetIntArrayElements(env, b_bitmap, 0);
  //jsize c_len = (*env)->GetArrayLength(env, c_bitmap);
  jint *c = (*env)->GetIntArrayElements(env, c_bitmap, 0);
  //jsize d_len = (*env)->GetArrayLength(env, d_bitmap);
  jint *d = (*env)->GetIntArrayElements(env, d_bitmap, 0);
  setTileMapData(a, b, c, d );
}
There are lines in the above code that are purposely commented out. They demonstrate how to determine the size of the array that has been passed to the JNI library. In our program, we know the size of the arrays. We rely on the fact that the arrays are a given size representing their visual size when displayed on the screen.
There are three important aspects of the method above. They're listed below. For this explanation we'll focus on 'a_bitmap'.
  1. The parameters in the function signature call for 'jintArray' elements to be passed to the method.
  2. The size of the 'jintArray' is determined by the line: 'jsize a_len = (*env)->GetArrayLength(env, a_bitmap);'
  3. The actual array pointer to be used in c style code is found like this: 'jint *a = (*env)->GetIntArrayElements(env, a_bitmap, 0);'
The Java code that allows us to use this method is:
public native void setTileMapData( int [] a, int [] b, int [] c, int [] d);
top

Arrays as Return Type

Arrays can be returned to the Java code from the JNI library. The process is involved. Below is a block of code from the Awesomeguy JNI library where we pass an integer to the method and return an entire array from the method.
JNIEXPORT jintArray JNICALLJava_org_davidliebman_android_awesomeguy_Panel_drawLevel(JNIEnv * env, jobject  obj,
jint animate)
{
        int j,k;
        jint size = SCREEN_WIDTH * SCREEN_HEIGHT;
        jint fill[size]; 
        jintArray graphic;
        drawLevel(animate);
        graphic = (*env)->NewIntArray(env, size);
        if(graphic == NULL) {
                LOGE("ARRAY NOT CREATED");
                return NULL;
        }
        for (j = 0; j < SCREEN_HEIGHT; j++) {
                for (k = 0; k < SCREEN_WIDTH ; k ++ ) {
                        fill[ (j * SCREEN_WIDTH) + k ] = (jint) screen[j][k];
                }
        }
        
        
        (*env)->SetIntArrayRegion(env, graphic,0, size, fill);
        return graphic;
}
We'll try to go over the code step by step.
  1. The first thing to notice is the 'jintArray' return type in the method signature.
  2. On the next line we pass a jint called 'animate' as a paramter.
  3. We declare two ints, 'j' and 'k' to be used as for() loop index variables.
  4. We declare a jint called 'size' that is the size of our array.
  5. We declare a c style array called fill[] that will hold our c data.
  6. We declare a special 'jintArray' called 'graphic'. This object still needs to be initialized.
  7. We call the method that will populate the 2D 'screen' array. This method calls all the necessary drawing functions of our library.
  8. We initialize the jintArray with a special JNI method that uses both the 'env' object and the 'size' jint that we defined previously.
  9. We test if the 'graphic' object was properly created. If not we return null.
  10. Using 'j' and 'k' we populate the 'fill' array. The 'fill' array is 1 dimensional and the 'screen' array is 2 dimensional.
  11. Using the special 'SetIntArrayRegion()' method we place the 'fill' data in the 'graphic' object. The variables 'size' and 'env' are also used in this method call.
  12. We return the 'graphic' object.
That covers the process we used to convert a c style array into a java array and pass it back to the java code. We only do this once in the entire library. Below is the code that must be included in the java to allow the JNI method to be called.
public native int[] drawLevel(int num);
top

No comments:

Powered by Blogger.