Header Ads

Android NDK: Passing complex data between Java and JNI methods

Here is the definition of the NDK from the Android SDK documentation:

The NDK (Native Development Kit) is a toolset that allows you to implement parts of your app using native-code languages such as C and C++. For certain types of apps, this can be helpful so you can reuse existing code libraries written in these languages…
This post is NOT an introduction to using the NDK, so if this is the first time you are dealing with it, I suggest you take a look at a getting started tutorial. Here are some good ones:
Once you set up your NDK project, it is time to invoke native methods written in C/C++ from your Android Java code. I choose to use the C++ syntax because I like it better, here is our first method that will simply return a string from native code. I illustrate here the difference between C and C++ syntaxes:
// 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!");
     }
};
In the Android application I am working on at the moment, I use a 3d engine library written in C++, so I will need to pass to my native methods some more complex data type from to to Java.
Here is the simplified data structure I will use:
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;
    }
}
The second jni method illustrates how to access the VertexCoords member field of a MeshData object created from Java:
JNIEXPORT
jfloat
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_getMemberFieldFromNative(
                JNIEnv *env,
                jobject callingObject,
                jobject obj)
{
           float result = 0.0f;

           jclass cls = env->GetObjectClass(obj);

           // get field [F = Array of float
           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;
}
-
The next jni method invokes a member function on the Java object and returns the result:

// utility method
int getFacetCount(JNIEnv *env, jobject obj)
{
           jclass cls = env->GetObjectClass(obj);
           jmethodID methodId = env->GetMethodID(cls, "getFacetCount""()I");
           int result = env->CallIntMethod(obj, methodId);

           return result;
}

JNIEXPORT
jint
JNICALL    Java_com_autodesk_adn_jnitester_MainActivity_invokeMemberFuncFromNative(
                     JNIEnv *env,
                     jobject callingObject,
                     jobject obj)
{
           int facetCount = getFacetCount(env, obj);

           return facetCount;
}
-
Android You can even instantiate a Java object from the native method and return it to the caller:

JNIEXPORT
jobject
JNICALL
Java_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;
}

Android Our last jni method illustrates how to process an array of Java objects as argument:
JNIEXPORT
jint
JNICALL
Java_com_autodesk_adn_jnitester_MainActivity_processObjectArrayFromNative(
                JNIEnv *env,
                jobject callingObject,
                jobjectArray objArray)
{
           int resultSum = 0;

           int len = env->GetArrayLength(objArray);

           for(int i=0; i<len; ++i)
           {
                jobject obj = (jobject) env->GetObjectArrayElement(objArray, i);

                resultSum += getFacetCount(env, obj);
           }

           return resultSum;
}

Here is the Android Activity code that will invoke those jni methods:

public class MainActivity extends Activity
{
     static
     {
           // "adnjni.dll" in Windows, "libadnjni.so" in Unixes
           System.loadLibrary("adnjni");
     }

     public native String getMessageFromNative();
    
     public native float getMemberFieldFromNative(MeshData obj);
    
     public native int invokeMemberFuncFromNative(MeshData obj);
    
    public native MeshData createObjectFromNative(int param);
   
    public native int processObjectArrayFromNative(MeshData[] objArray);

     @Override
     public void onCreate(Bundle savedInstanceState)
     {
           super.onCreate(savedInstanceState);
    
           // get simple string from native
           String msg = getMessageFromNative();

           // access class member in native code and return result to caller
           MeshData obj = new MeshData(3);
           msg += "\n\nResult getMemberFieldFromNative: " +
               getMemberFieldFromNative(obj);
          
        msg += "\nResult invokeMemberFuncFromNative: " +
            invokeMemberFuncFromNative(obj);

        // create object in native method and return it to caller
        MeshData obj2 = createObjectFromNative(18);
        msg += "\n\nResult createObjectFromNative: " + obj2.getFacetCount();
       
        // process object array in native code and return result to caller
        MeshData[] objArray = new MeshData[]
           {
                new MeshData(10),
                new MeshData(20)
           };
       
        int arrayRes = processObjectArrayFromNative(objArray);
        msg += "\n\nResult processObjectArrayFromNative: " + arrayRes;
       
        TextView textView = new TextView(this);
           textView.setText(msg);
           setContentView(textView);
     }
}
-
The last thing I am adding to my project is the ability to run under an x86 emulator because it is much faster for testing. if you don’t know what I am talking about, take a look here: Fast debugging of Android applications

In order to do that, I simply add a file named “Application.mk” to my jni project folder:

Application.mk:
APP_ABI := all

This will compile the jni library for every platform. You can see the difference when you look at the command line output hen building the jni libraries with ndk-build:

Screen Shot 2013-08-30 at 12.42.52 AM

Finally here is the output of my JniTester app running in my x86 emulator. I let you do the math to verify the output from native methods is valid Winking smile

Screen Shot 2013-08-30 at 12.36.20 AM

As always, full project is available for download here:

No comments:

Powered by Blogger.