Building clean RPN part two – Calling native code using the NDK

This may be a surprise to some people who know me, but I am not particularly excited by the idea of writing my own arbitrary precision RPN parsing engine.  Thus when I started to build the Clean RPN calculator app I decided to reuse the C Mapm library, which already includes an example RPN engine capable of performing arbitrary precision calculations using a wide variety of mathematical functions.

Android has the ability to compile and call native C code via the android NDK, which makes use of the java’s JNI for the actual bridge.  There are many examples available on the internet of how to use the NDK to call into C code.  Unfortunately most of them are of the ‘hello world’ nature and only show how to call through to one C file, which is included in the android project and contains only a few functions.  The Mapm library is much more complex, and includes many header and source files along with its own set of build scripts based on make.  I decided to use the following approach:

  1. Modify the Mapm build to use the NDK toolcain and produce a version of libmam.a suitable for embeding in android apps
  2. Include the mapm header file and RPN calculator engine inside my android project
  3. Write a custom bridge using JNI to call through to the functions I need from mapm

Step 1 – Modify Mapm build

The first step is to modify the mapm build to use the android NDK toolchain.  The NDK contains a complete C/C++ toolchain suitable for runing on MacOS, inside the android-ndk-r8b/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86/ folder

The mapm library uses make and comes with several handmade build files, which are relatively simple to modify.  I created a copy of makefile.osx which is provided the mapm zip, and renamed it to makefile.android.  At the top of the makefile a variable is defined with the location of the c compiler.  I change this to point to the absolute path of the NKD’s c compiler and set the –sysroot parameter.

CC = /Applications/android-sdk-mac_86/android-ndk-r8b/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86/bin/arm-linux-androideabi-gcc --sysroot=/Applications/android-sdk-mac_86/android-ndk-r8b/platforms/android-8/arch-arm

Later in the make file comes the step which builds the actual mapm lib. This also needs modifying to use the NDK’s toolchain:

libmapm.a: $(OBJS)
	rm -f libmapm.a
	/Applications/android-sdk-mac_86/android-ndk-r8b/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86/bin/arm-linux-androideabi-ar rc libmapm.a $(OBJS)
	/Applications/android-sdk-mac_86/android-ndk-r8b/toolchains/arm-linux-androideabi-4.6/prebuilt/darwin-x86/bin/arm-linux-androideabi-ranlib libmapm.a

Running:

make -f makefile.android

at the command line will now produce a libmamp.a which is suitable for use in an Android project.

If mapm had made use of autotools it may have been simpler to use a tool such as Androgenizer to bridge the gap between autotools and the android NDK build process.

 

Step 2 – Include mapm headers and RPN calculator

To make use of C code and libraries in an android project the relevant code needs to be placed in a /jni folder in your project.  In this case there were 3 things that I copied across:

  1. The libmamp.a produced by the modified build.
  2. The mamp header file – m_apm.h
  3. The a modified version of calc.c from mamp which implements the RPN parser.

 

Step 3 – JNI bridge

So far there has been no JNI code written, which means there is no way to call into the mamp library from java.  To keep the JNI specific code out of the mapm sources a separate file called native_interface.c was created.  This file is maped to a ‘native’ method in a java class using JNI.  native_inferface uses JNI to convert an array of java strings into a  c array of c string, passes this into the modified version of calc.c from mapm to perform the actual calculation then converts the result from a c string back to a java string:

#include 
#include 
#include <android/log.h>
#include 
#include "m_apm.h"

/*  prototypes from calc_modified  */
char* runCalc(int argc, char *argv[]);

JNIEXPORT jstring JNICALL Java_au_com_lukesleeman_rpn_MainActivity_doMath(JNIEnv * env, jobject this, jobjectArray stringArray)
{

	// Convert our java array of strings into a c array of cstrings
	int stringCount = (*env)->GetArrayLength(env, stringArray);
    char *args[stringCount];
    args[0] = "calc";

	int i;
    for (i=0; i<stringCount; i++) {         jstring string = (jstring) (*env)->GetObjectArrayElement(env, stringArray, i);
        const char *rawString = (*env)->GetStringUTFChars(env, string, 0);
        args[i+1] = rawString;
        // Don't forget to call `ReleaseStringUTFChars` when you're done.
    }

    // Run the calculations
    char * out = runCalc(stringCount + 1, args);

	// Copy result into a java string
	jstring result = (*env)->NewStringUTF(env, out);

    // cleanup
    free(out);

    for (i=0; i<stringCount; i++) {         jstring string = (jstring) (*env)->GetObjectArrayElement(env, stringArray, i);

        (*env)->ReleaseStringUTFChars(env, string, args[i+1]);
    }

    return result;

}

On the java side, we hava a class called au.com.lukesleeman.rpn.MainActivity with a single  native method:

private native String doMath(String [] maths);

An android.mk file needs to be written to tell the NKD how to compile the code:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_SRC_FILES := calc_modified.c , native_interface.c 

LOCAL_LDLIBS += libmapm.a -llog 
LOCAL_MODULE := mapm

include $(BUILD_SHARED_LIBRARY)

The last step is to run the NDK build tool from within eclipse following the process described here.  This will compile the JNI code and link it to the existing libmamp.a.

Conclusion

By splitting out the native C code from JNI specific C code we were able to use the existing mapm build system to compile libmamp.  It was simple to modify the build system to use the NDK toolchain to compile the library.  This allowed us to quickly integrate an existing C RPN calculation engine into the clean RPN app.