We will now attempt to go through these steps. Assuming you already have environment ready to start Android and NDK development.
Create Android project
Create an Android project called “MyFirstJNI“, with package “com.example.myfirstjni“.
To create new project, right click on File ->New -> Android Application Project. Give project name and select an API. Here in this case I have, Application Name as “My First JNI”, Project name as “MYFirstJNI” and package name “com.example.myfirstjni”. |
Create java source
For this Paper, we will use simple c program – Palindrome Checker – that accepts string from user and returns boolean, True if string is Palindrome and False if string is not Palindrome.
We will start by defining C function prototype as Java methods(can be called as native methods wrapper). This class will be used to load library and expose native methods.
In this example we will create Java class JNIWrapper. This class will expose isPalindrome native method.
package com.example.myfirstjni; public class JNIWrapper { //Declare native method private static native boolean isPalindrome(String inString); // Provide additional functionality and call native method public static boolean checkPalindrome(String inString){ return isPalindrome(inString); } // Load library static { System.loadLibrary("palindromeChecker"); } } |
Make JNI folder
Create a folder named “jni” in the Eclipse project’s root directory. (Right-click on the project -> New -> Folder). Create a subfolder “include” under “jni” for storing the header files.
Create C/C++ Header file using “javah”
Now, we will create header file using “javah” utility.
Open terminal(cmd in windows). Navigate to include folder that we created in last step.
$ javah -classpath ../../bin/classes/ -o Palindrome.h com.example.myfirstjni.JNIWrapper |
Here,
- -classpath: In our case, we need JNIWrapper.class which is under “../../bin/classes/”.
- -o: to set file output name, in our case it’s Palindrome.h
You need to use the fully-qualified class name (including package) and not the (.class) extension. |
This will create header called “Palindrome.h” under include folder. Refresh your eclipse project.
Header file contains prototype function,
JNIEXPORT jboolean JNICALL Java_com_example_myfirstjni_JNIWrapper_isPalindrome (JNIEnv *, jclass, jstring); |
The native method “isPalindrome(String inString)” mapped in the above header in the native code.
Write C code
Now, this is the time to implement functionality in C source.
Right-click on jni folder -> New -> File. Give name “Palindrome.c” to the file. This will create C source file called “Palindrome.c”.
#include <jni.h> #include "include/Palindrome.h" JNIEXPORT jboolean JNICALL Java_com_example_myfirstjni_JNIWrapper_isPalindrome (JNIEnv *env, jclass this , jstring string){ const char *text = (*env)->GetStringUTFChars(env, string, 0); int begin, middle, end, length = 0; jboolean isPalindrome = JNI_FALSE; while ( text[length] != '\0' ) length++; end = length - 1; middle = length/2; for ( begin = 0 ; begin < middle ; begin++ ) { if ( text[begin] != text[end] ) { isPalindrome = JNI_FALSE; break ; } end--; } if ( begin == middle ){ isPalindrome = JNI_TRUE; } return isPalindrome; } |
This native code gets string and returns true if string is palindrome and false if string is not palindrome.
Create Android.mk
Create an Android makefile called “Android.mk” under “jni” directory. Right-click on “jni” folder -> New -> File and give name “Android.mk”. This file is used for Android build tools.
For our Paper, we will have following in our makefile.
# Defines the root to all other relative paths # The macro function my-dir, provided by the build system, # specifies the path of the current directory (i.e. the # directory containing the Android.mk file itself) LOCAL_PATH := $(call my-dir) # Clear all LOCAL_XXX variables with the exception of # LOCAL_PATH ( this is needed because all variables are global) include $(CLEAR_VARS) # List all of our C files to be compiled (header file # dependencies are automatically computed) LOCAL_SRC_FILES := Palindrome.c # The name of our shared module ( this name will be prepended by lib and postfixed by .so) LOCAL_MODULE := palindromechecker # Collects all LOCAL_XXX variables since "include $(CLEAR_VARS)" # anddetermineswhattobuild(in this case a shared library) include $(BUILD_SHARED_LIBRARY) |
There are number of sample Android.mk files in the samples/ directory of the NDK. It’s easiest to copy the “Android.mk” file from another (sample) project. |
Build NDK
Start terminal(CMD in windows), change directory to project root directory. Run “ndk-build” command.
The command “ndk-build” comes from the NDK’s installation directory. So easiest way is to add this directory in PATH. |
$ ndk-build Compile thumb : palindromechecker <= Palindrome.c SharedLibrary : libpalindromechecker.so Install : libpalindromechecker.so => libs/armeabi/libpalindromechecker.so |
Note: To remove generated libraries, run
$ ndk-build clean Clean: palindromechecker [armeabi] Clean: stdc++ [armeabi] |
Run the android app
Refresh eclipse project and you will see library “libPalindromeChecker.so” is generated under libs->armeabi.
We are almost done with the native side of the code. Now, call “checkPalindrome(String inString)” method anywhere from your Android project. And it will call native method to return whether string is palindrome or not.
Check logcat to confirm that the shared library “libpalindromechecker.so” is loaded. |
Complexities integrating NDK
- Environment set up is not trivial. You will need C/C++ setup as well. Also, integration with eclipse is confusing too. ie. Once you make changes in the C files and generate new libraries, the eclipse project will have to be manually refreshed.
- JNI is not for the faint of heart. So if you’re not familiar with JNI, it can take some time to get things right.
- Adding JNI to project increases the complexity in an application.
- Memory-management in NDK can be painful. Is is important to incorporate elegant memory management techniques.
- Debugging native code is complex and could be time consuming.
Conclusion
NDK integration can be painless if you follow all the steps carefully. However, using NDK as a solution is not always ideal. Before going down the path of using NDK, try to optimize the Java code to achieve the desired results. While using NDK, you need to weigh the benefits achieved against the complexities. By performing tests on your application, you can ascertain the gain that will be achieved by using NDK.
Some rules of thumb:
NDK should be used for:
- Developing components with complicated calculations (game physics, AI, pattern recognitions, data encryption and compression, images/video/audio processing);
- Making “a clone” of a C++ desktop application for Android.
NDK should not be used for:
- Creating user interfaces;
- Working with static data in bulk.