Modern Android NDK Tutorial
This short guide outlines the basics of utilizing the Android NDK to use C or C++ code in your Android App when using Android Studio 2.2+.
I had a bit of confusion when I started using the NDK and I’ve pieced together information I’ve learnt from various sources (a list of all sources is available at the bottom). This guide leans more towards C++ than C and covers:
- Getting Started (Setting up the NDK)
- Creating a Native method that can be invoked by Java
- Passing Java objects between Java and Native Code
- Passing an array of Java objects between Java and Native Code
Android Studio 2.2+ uses a CMake buildscript instead of ndk-build as was used previously. If you ever get stuck, then I recommend studing Google’s NDK sample code.
Getting Started
Read Google’s “Getting Started with the NDK”
https://developer.android.com/ndk/guides/index.html
Their guide will provide you with a core set of information you need to use the Android NDK.
Add your C or C++ source files to your project
https://developer.android.com/studio/projects/add-native-code.html#create-sources
Create a CMake Build Script
https://developer.android.com/studio/projects/add-native-code.html#create-cmake-script
Here is an example of a CMakeLists file. I am interested in having my Java code communicate with a C++ file named Myfile.cpp
.
# Sets the minimum version of CMake required to build your native library.
# This ensures that a certain set of CMake features is available to
# your build.
cmake_minimum_required(VERSION 3.4.1)
# Specifies a library name, specifies whether the library is STATIC or
# SHARED, and provides relative paths to the source code. You can
# define multiple libraries by adding multiple add.library() commands,
# and CMake builds them for you. When you build your app, Gradle
# automatically packages shared libraries with your APK.
add_library( # Specifies the name of the library.
Myfile
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/Myfile.cpp )
# Add the directories where the Cpp header files are to let CMake find them
# during compile time
include_directories(src/main/cpp/)
Myfile.cpp
has OpenGL Mathematics as a dependency. I downloaded the glm C++ files I needed (I don’t believe you can use Gradle to manage C or C++ dependencies the same way you can Java) and stashed them at src/main/cpp/glm
, thus include_directories(src/main/cpp/)
is needed to include this dependency.
Link Gradle to your Native Library
You can specify optional arguments and flags for your CMake file. For example, MyFile.cpp
requires C++ 11 and must allow exceptions.
externalNativeBuild {
cmake {
// Required by MyFile.cpp
cppFlags "-std=c++11" //Enable C++ 11
cppFlags "-fexceptions" //Allow exceptions
}
Communicating between Java and Native Code
Now that you’ve set up the NDK, imported your Native (C/C++) code, and linked it to your Android project, next you need to enable your Java and Native Code to communicate with each other.
Java Side
I recommend creating a Java class to statically load your Native Library and declare your native methods that the Java Code that invoke.
// Wrapper for native library
public class NativeLib {
static {
// Replace "Myfile" with the name of your Native File
System.loadLibrary("Myfile");
}
// Declare your native methods here
public static native String string();
}
Use System.loadLibrary("Myfile");
to load Myfile
(or whatever the Native file you want to interface with is called).
Native Side
- Create a Native Method that can be invoked by Java
You can’t just invoke any of your Native methods via your Java code, you need to implement Native methods that can be called by your Java code in a very specific way:
//Myfile.Cpp
//You need this to allow methods that can be invoked by Java
#include <jni.h>
//This syntax is needed if you are invoking a C++ method. C methods are slightly different
//Since the String method returns a String object, specify jstring. Replace this with whatever return object your method has.
extern "C" JNIEXPORT jstring JNICALL
//Replace the crazy long method name with the path to the Java file that is invoking it (com/example/NativeLib in this case).
//The Native Methods must always have JNIEnv *env, jobject object as the first two parameters
Java_com_example_NativeLib_string(JNIEnv *env, jobject object) {
//This is how you return a jstring via C++ code
return env->NewStringUTF("Hello from JNI LIBS!");
}
C Syntax vs. C++ Syntax
Original Post by Philippe Leefsma
// C syntax: my package name is com.autodesk.and.jnitester
// and my activity invoking the native method is MainActivity,
// hence the name of my method is
//Java_com_autodesk_and_jnitester_MainActivity_MethodName
JNIEXPORT
jstring
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_getMessageFromNative(
JNIEnv *env,
jobject callingObject)
{
return (*env)->NewStringUTF(env, "Native code rules!");
}
// C++ syntax: Required to declare as extern "C" to prevent c++ compiler
// to mangle function names
extern "C"
{
JNIEXPORT
jstring
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_getMessageFromNative(
JNIEnv *env,
jobject callingObject)
{
return env->NewStringUTF("Native code rules!");
}
};
- Passing Objects between Java and Native Code
Original Post by Philippe Leefsma
First, define a simple POJO on the Java Side:
public class MeshData
{
private int _facetCount;
public float[] VertexCoords;
public MeshData(int facetCount)
{
_facetCount = facetCount;
VertexCoords = new float[facetCount];
// fills up coords with dummy values
for(int i=0; i<facetCount; ++i) {
VertexCoords[i] = 10.0f * i;
}
}
public int getFacetCount()
{
return _facetCount;
}
}
Java method to pass your object to your Native Code:
public native float getMemberFieldFromNative(MeshData obj);
Reading a passed Java Object’s fields in Native Code:
JNIEXPORT
jfloat
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_getMemberFieldFromNative(
JNIEnv *env,
jobject callingObject,
jobject obj) //obj is the MeshData java object passed
{
float result = 0.0f;
//Get the passed object's class
jclass cls = env->GetObjectClass(obj);
// get field [F = Array of floats
jfieldID fieldId = env->GetFieldID(cls, "VertexCoords", "[F");
// Get the object field, returns JObject (because it’s an Array)
jobject objArray = env->GetObjectField (obj, fieldId);
// Cast it to a jfloatarray
jfloatArray* fArray = reinterpret_cast<jfloatArray*>(&objArray);
jsize len = env->GetArrayLength(*fArray);
// Get the elements
float* data = env->GetFloatArrayElements(*fArray, 0);
for(int i=0; i<len; ++i) {
result += data[i];
}
// Don't forget to release it
env->ReleaseFloatArrayElements(*fArray, data, 0);
return result;
}
Returning a value via a Native Function:
JNIEXPORT
jint
JNICALL Java_com_autodesk_adn_jnitester_MainActivity_invokeMemberFuncFromNative(
JNIEnv *env,
jobject callingObject,
jobject obj)
{
jclass cls = env->GetObjectClass(obj);
jmethodID methodId = env->GetMethodID(cls, "getFacetCount", "()I");
int result = env->CallIntMethod(obj, methodId);
//Return the facet count (an int)
return result;
}
Instantiating a Java Object in Native Code:
JNIEXPORT
jobject
JNICALLJava_com_autodesk_adn_jnitester_MainActivity_createObjectFromNative(
JNIEnv *env,
jobject callingObject,
jint param)
{
jclass cls = env->FindClass("com/autodesk/adn/jnitester/MeshData");
jmethodID methodId = env->GetMethodID(cls, "<init>", "(I)V");
jobject obj = env->NewObject(cls, methodId, param);
return obj;
}
- Passing an Array of Objects between Java and Native Code
Java method to pass your array of objects to your Native Code:
You can’t pass a List or objects to your Native code, it needs to be an array.
public native int processObjectArrayFromNative(MeshData[] objArray);
Reading an array of Java Objects (a jobjectArray) in Native Code:
JNIEXPORT
jint
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_processObjectArrayFromNative(
JNIEnv *env,
jobject callingObject,
jobjectArray objArray)
{
int resultSum = 0;
int len = env->GetArrayLength(objArray);
//Get all the objects in the array
for(int i=0; i<len; ++i) {
jobject obj = (jobject) env->GetObjectArrayElement(objArray, i);
resultSum += getFacetCount(env, obj);
}
return resultSum;
}
Return an array of objects using Native Code:
JNIEXPORT
jobjectArray
JNICALL
Java_ProcessInformation_getAllProcessPid(
JNIEnv*env,
jobject obj)
{
//Create a vector (an array) of Strings and add items to it vector<string>vec;
vec.push_back("Ranjan.B.M");
vec.push_back("Mithun.V");
vec.push_back("Preetham.S.N");
vec.push_back("Karthik.S.G");
cout<<vec[0];
cout<<vec[0];
//Instantiate your object Array and return it!
jclass clazz = (env)->FindClass("java/lang/String");
jobjectArray objarray = (env)->NewObjectArray(vec.size() ,clazz ,0);
for(int i = 0; i < vec.size(); i++) {
string s = vec[i];
cout<<vec[i]<<endl;
jstring js = (env)->NewStringUTF(s.c_str());
(env)->SetObjectArrayElement(objarray , i , js);
}
return objarray;
}
Sources:
http://adndevblog.typepad.com/cloud_and_mobile/2013/08/android-ndk-passing-complex-data-to-jni.html
http://www.developer.com/java/data/manipulating-java-objects-in-native-code.html
https://developer.android.com/training/articles/perf-jni.html
https://developer.android.com/studio/projects/add-native-code.html