Android NDK passing complex data not so scary anymore

VORM
Thinking with VORM
Published in
5 min readFeb 3, 2017

--

Originally published at vorm.io by Dominik Jura

To configure Your environment I strongly recommend using Experimental Gradle Build.

You can find more info about it under these links:
http://tools.android.com/tech-docs/new-build-system/gradle-experimental/

and also a handy tutorial to build UR first project:
https://codelabs.developers.google.com/codelabs/android-studio-jni/

Passing data by arguments and return statement
Let’s start with a simple example how to pass String from C to Java:

Java code:

...    private void callJniMethod(){
String msg = getMsgFromJni();
Log.d(TAG, msg);
}
static {
System.loadLibrary("foo");
}
public native String getMsgFromJni();
} //FooActivity

C code:

... JNIEXPORT jstring JNICALL Java_com_foo_sample_FooActivity_getMsgFromJni(JNIEnv *env, jobject obj) 
{
return (*env)->NewStringUTF(env, "Hello World NDK");
}

To pass primitive types we just need to know the equivalent from the Native side and use it properly. Below You can find a table with those equivalents.

Primitive Types and Native Equivalents
Reference types that correspond to different kinds of Java objects.

The following table summarizes the encoding for the Java type signatures:

It’s important to remember when passing arrays objects such as jdoubleArray, jstring and so on. Firstly we need to obtain the array element pointers.

In general, the garbage collector may move Java arrays. However, the Java Virtual Machine guarantees that the result of Get<type>ArrayElements points to a nonmovable array of integers. The JNI will either “pin” down the array or it will make a copy of the array into nonmovable memory. Thanks to this, the native code must call Release<type>ArrayElements when it has finished using the array, as follows:

Release<type>ArrayElements enables the JNI to copy back and free the memory referenced by the body parameter if it is a copy of the original Java array. The “copy back” action enables the calling program to obtain the new values of array elements that the native method may have modified. Release<type>ArrayElements will “unpin” the Java array if it has already been pinned in memory.

Java code:

...   private void callJniMethod(){
double [] tmpArrayLeft = {1, 2, 3};
double [] tmpArrayRight = {4, 5, 6};
int tmpIntValue = 1;
float tmpFloatValue = 2.3f;
passingDataToJni(tmpArrayLeft, tmpArrayRight, tmpIntValue, tmpFloatValue);
}
static {
System.loadLibrary("foo");
}
public native void passingDataToJni(double[] doubleLeftArray, double[] doubleRightArray,
int intValue, String stringValue);
} //FooActivity

Code C:

...JNIEXPORT void JNICALL Java_com_foo_sample_FooActivity_passingDataToJni(JNIEnv *env, jobject instance, 
jdoubleArray doubleLeftArray_,
jdoubleArray doubleRightArray_,
jint intValue, jstring stringValue_) {
jdouble *doubleLeftArray = (*env)->GetDoubleArrayElements(env, doubleLeftArray_, NULL);
jdouble *doubleRightArray = (*env)->GetDoubleArrayElements(env, doubleRightArray_, NULL);
const jchar *stringValue = (*env)->GetStringChars(env, stringValue_, 0);
const char *stringValueUTF = (*env)->GetStringUTFChars(env, stringValue_, 0);
// TODO
(*env)->ReleaseDoubleArrayElements(env, doubleLeftArray_, doubleLeftArray, 0);
(*env)->ReleaseDoubleArrayElements(env, doubleRightArray_, doubleRightArray, 0);
(*env)->ReleaseStringUTFChars(env, stringValue_, stringValueUTF);
(*env)->ReleaseStringChars(env, stringValue_, stringValue);
}

Note that the Get<type>ArrayElements function might potentially copy the entire array. You may want to limit the number of elements that are copied, especially if your array is large. If you are only interested in a small number of elements in a (potentially) large array, you should instead use the Get/Set<type>ArrayRegion functions. These functions allow you to access, via copying, a small set of elements in an array.

Passing and returning complex data like C structures, Java objects

The NDK provides functions that native methods use to get and set Java member variables. You can get and set both instance and class member variables. Similar to accessing methods, you use one set of NDK functions to access instance member variables and another set of NDK functions to access class member variables.

public class FooModel {
float fooFloat;
long fooLong;
int fooInt;
String fooString;
...
//getters and setters
}
... private void callJniMethod(){
FooModel fooModel = new FooModel();
passingJavaObject(fooModel);
Log.d(TAG, fooModel.getFooString());
}
static {
System.loadLibrary("foo");
}
public native void passingJavaObject(FooModel fooModel); } //FooActivity...JNIEXPORT void JNICALL Java_com_foo_sample_FooActivity_passingJavaObject(JNIEnv *env, jobject instance, jobject fooModel_) { jclass fooModel = (*env)->GetObjectClass(env, fooModel_);
jfieldID fooFloat = (*env)->GetFieldID(env, fooModel, "fooFloat", "D");
jfieldID fooLong = (*env)->GetFieldID(env, fooModel, "fooLong", "J");
jfieldID fooInt = (*env)->GetFieldID(env, fooModel, "fooInt", "I");
jfieldID fooString = (*env)->GetFieldID(env, fooModel, "fooString", "Ljava/lang/String;");
(*env)->SetFloatField(env, fooModel_ , fooFloat, 1.1f); (*env)->SetLongField(env, fooModel_ , fooLong, 1);
(*env)->SetIntField(env, fooModel_ , fooInt, 1);
(*env)->SetObjectField(env, fooModel_ , fooString,
(*env)->NewStringUTF(env, "foo"));
}

Another way is to create a Java object directly in C code, setup his fields and then return it. Where in FindClass function we pass as second argument the path to class. In GetMehodID second argument is a class instance for the method, the third one is “<init>” which it means we will be calling the constructor, and the fourth argument is what parameters will take constructor. In our example constructor is not taking any arguments so we pass an empty bracket.

...JNIEXPORT jobject JNICALL Java_com_foo_sample_FooActivity_passingJavaObject(JNIEnv *env, jobject instance) {    jclass fooModelClass = (*env)->FindClass(env, "com/foo/sample/FooModel");
jmethodID methodId = (*env)->GetMethodID(env, fooModelClass, "<init>", "()V");
jobject fooModel = (*env)->NewObject(env, cls, methodId);
//getters and setters return fooModel;
}

Calling Java method from C/C++ and passing data as they arguments.

This section illustrates how to call Java methods from native language methods. The GetMethodID takes as second parameter class where the method will be found, as third parameter name of the method and the fourth parameter is what kinds of arguments does a method take and what it will return.

C code:

...JNIEXPORT void JNICALL Java_com_foo_sample_FooActivity_passingJavaObject(JNIEnv *env, jobject instance) {    jmethodID callbackJava = (*env)->GetMethodID(env, instance, "callback", "([D[DILjava/lang/String;)V"); 
if (NULL == callbackJava) return;
// Your code withe 2 double arrays, int and String to pass (*env)->CallVoidMethod(env, instance, callbackJava, doubleArrayLeft, doubleArrayRight, intValue, stringValue); }

Java code:

...    private void callback(double[] left, double[] right, int intValue, String stringValue) {
Log.d(TAG, Arrays.toString(left));
Log.d(TAG, Arrays.toString(right));
Log.d(TAG, String.valueOf(intValue));
Log.d(TAG, stringValue);
}
static {
System.loadLibrary(“foo”);
}
public native void callJavaMethod(); } //FooActivity

Personally, I found it much easier to pass primitive types and arrays of primitive typed from NDK to Java. Object creation/access is messy and hard to debug but sometimes it’s the only way for a much cleaner and transparent code.

http://vorm.io

Originally published at vorm.io on February 3, 2017.

--

--