Android NDK Advanced Tutorial

Using 3rd party C++ libraries with Prebuilts and standalone toolchain.


Copyright 2013 Piotr Morgwai KotarbiƄski
Licensed under the Apache License, Version 2.0


Intro

This documents explains some advanced use-cases of Android NDK including: During this tutorial we will build a simple application that uses a simple native lib that in turn uses some 3rd party C++ lib. We will use Crypto++ 5.6.1 as an example.
3rd party sources don't change as often as our working project so building them once per their release and using Prebuilts feature saves us unnecessary builds.
As providing Android.mk replacement for each Makefile in 3rd party sources requires deep understanding of given lib's structure and may be sometimes quite challenging, it's usually easier to just replace standard GNU-GCC based toolchain with the one from Android NDK.

Prerequisites


Building Crypto++ with NDK toolchain

Preparing NDK toolchain

Reference docs: docs/STANDALONE-TOOLCHAIN.html

First we need to prepare a toolchain for the right architecture (mips. arm, x86) and Android version. It is probably the best idea to choose the lowest version of Andy you want to support (ie the same version you put as minSdkVersion in your AndroidManifest.xml file). If you intend to support multiple devices with different archs then you will have to build Cryptopp separately for each arch. In this tutorial we will build for armv5te (referred to as 'armeabi' in Andy docs). Code for this arch can be also run on armv7-a CPUs (referred to as 'armeabi-v7a') so it covers most of Android devices.
To the point: set and export NDK shell variable to point to the location of your NDK installation and then in your working dir issue the following command:
$NDK/build/tools/make-standalone-toolchain.sh --platform=android-8 --install-dir=./ndk-toolchain
Replace android-8 with the desired version for your app. Such prepared toolchain targets arm arch (both armv5te and armv7-a). If you want to prepare toolchain for another arch add --arch=mips or --arch=x86 accordingly.
Now add ndk-toolchain folder to your path and export CXX shell var to g++ from ./ndk-toolchain/bin folder (it will be named something-something-g++).

Supplying general compiler flags

In case of a perfect Makefile all you should need now to cross-compile the sources of a 3rd party lib is setting the target to the desired arch, optionally setting CXXFLAGS shell vars and it should all work just by typing 'make'.
Makefile of Crypto++ assumes that the host system is also a target, which is not our case as we are using cross-compiler and our target platform (Andy) has considerably different structure than standard Linux distro. Hence we need to change compiler options for our native host system (Linux on x86) to be those for our target system (Andy on armv5te in this example). This boils down to 2 changes in the GNUmakefile file: After applying the above changes typing make should successfully build static version of Crypto++ lib for Andy. To build a shared version type make libcryptopp.so

Choosing STL version

Reference docs: docs/STANDALONE-TOOLCHAIN.html, docs/CPLUSPLUS-SUPPORT.html

By default NDK toolchain will link your C++ shared libs against a static version of GNU STL lib. However if you are using several shared libs it is not acceptable to link against the static version of STL as each of your shared lib will have its own copy of STL. This will result in several copies of global vars defined in STL and may lead to memory leak or corruption. To use the shared version of STL you just need to add -lgnustl_shared linker option:
LDLIBS += -lgnustl_shared
Also neither LDFLAGS nor LDLIBS var is originally used in the command that builds shared library in the GNUmakefile file so you need to append it at its end like this:
libcryptopp.so: $(LIBOBJS)
	$(CXX) -shared -o $@ $(LIBOBJS) $(LDFLAGS) $(LDLIBS)

GNU STL is distributed under GPLv3 license which is not acceptable for some people. NDK provides also STLport and it is possible to use it instead, but it is a bit more complicated as standalone toolchain does not include it. In order to make it work you need to point the compiler to the right header files and linker to the location of the STLport lib: in the GNUmakefile define the LDFLAGS, LDLIBS and CXXFLAGS vars like this:
CXXFLAGS += -nostdinc++ -I$(NDK)/sources/cxx-stl/stlport/stlport
LDFLAGS += -nodefaultlibs -L$(NDK)/sources/cxx-stl/stlport/libs/armeabi
LDLIBS += -lstlport_shared -lc -lm -ldl -lgcc
-nostdinc++ tells the compiler not to include stdlib++ header files which belong to gnustl in our case. The -I flag points the compiler to the header files for STLport.
-nodefaultlibs tells the linker not to link against standard C and C++ libs that may belog to gnustl: it will link only against the platform minimal C runtime (crt* stuff) and the libraries specified by -l flags that we specify in LDLIBS: STLport, libc and the low level compiler stuff. Finally -L flag points the linker to the location of these libs. If you are building for different arch than armeabi you need to change the path in this flag accordingly.

The last problem is that Crypto++ seems to rely on implicit inclusion of header file containing definition of fd_set. This is not the case when using STLport so you need to add the following preprocessor command somewhere in wait.h file:
#include <sys/select.h>

Patch for Crypto++ sources

Below is the patch for Crypto++ sources with all the changes described above.

DISCLAIMER:
Described changes to the Crypto++'s GNUmakefile are UGLY HACKS just to make it work ASAP and are DEFINITELY NOT A GOOD EXAMPLE HOW TO WRITE MAKEFILES in general. For example you should generally introduce a notion of a target platform rather than putting options for it into the host platform.
diff -r -u cryptopp-orig/GNUmakefile cryptopp-andy.armeabi.stlport/GNUmakefile
--- cryptopp-orig/GNUmakefile	2010-08-09 14:22:42.000000000 +0200
+++ cryptopp-andy.armeabi.stlport/GNUmakefile	2013-03-09 19:17:12.934327979 +0100
@@ -37,7 +37,7 @@
 ifeq ($(UNAME),Darwin)
 CXXFLAGS += -arch x86_64 -arch i386
 else
-CXXFLAGS += -march=native
+CXXFLAGS += -march=armv5te
 endif
 endif
 
@@ -78,7 +78,12 @@
 endif
 
 ifeq ($(UNAME),Linux)
-LDFLAGS += -pthread
+## uncomment the below line to use GNU STL
+#LDLIBS += -lgnustl_shared
+## uncomment the below 3 lines to use STLport
+LDFLAGS += -nodefaultlibs -L$(NDK)/sources/cxx-stl/stlport/libs/armeabi -Wl,--no-undefined
+LDLIBS += -lstlport_shared -lc -lm -ldl -lgcc
+CXXFLAGS += -nostdinc++ -I$(NDK)/sources/cxx-stl/stlport/stlport
 ifneq ($(shell uname -i | $(EGREP) -c "(_64|d64)"),0)
 M32OR64 = -m64
 endif
@@ -151,7 +156,7 @@
 	$(RANLIB) $@
 
 libcryptopp.so: $(LIBOBJS)
-	$(CXX) -shared -o $@ $(LIBOBJS)
+	$(CXX) -shared -o $@ $(LIBOBJS) $(LDFLAGS) $(LDLIBS)
 
 cryptest.exe: libcryptopp.a $(TESTOBJS)
 	$(CXX) -o $@ $(CXXFLAGS) $(TESTOBJS) -L. -lcryptopp $(LDFLAGS) $(LDLIBS)
diff -r -u cryptopp-orig/wait.h cryptopp-andy.armeabi.stlport/wait.h
--- cryptopp-orig/wait.h	2010-08-06 18:44:32.000000000 +0200
+++ cryptopp-andy.armeabi.stlport/wait.h	2013-03-08 15:50:01.758533453 +0100
@@ -13,6 +13,7 @@
 #include <winsock2.h>
 #else
 #include <sys/types.h>
+#include <sys/select.h>
 #endif
 
 #include "hrtimer.h"

Creating android app project and writing Java sources

Reference docs: docs/CPLUSPLUS-SUPPORT.html

Let's create a standard android app project and our main activity will just display a return value from a native method. Create a class named Native that will be responsible for loading native libs and will contain the native method. Libs have to be loaded in the order of their dependencies, ie if liba.so depends on libb.so you first need to load libb.so and then liba.so. Thus you need to start from loading the STL lib you decided to use, then load Crypto++, and finally our small lib that uses it (let's call it libcrypt_user.so)
package pl.morgwai.ndktutorial;

public class Native {

	static {
		System.loadLibrary("stlport_shared");
		//System.loadLibrary("gnustl_shared");
		System.loadLibrary("cryptopp");
		System.loadLibrary("crypt_user");
	}

	public native long fun(int i);
}

Writing C++ sources

To avoid mistakes in JNI function names that result in mysterious 'Unsatisfied link' exception you can generate them automatically with javah tool: go to the bin/classes subfolder of your app project folder and run javah with the fully qualified name of your Native class as an argument:
javah pl.morgwai.ndktutorial.Native
This tool generates 'classic' JNI headers. On the other hand Android NDK seems not use macros JNIEXPORT and JNICALL: most of the sample apps from NDK don't use them and Eclipse ADT marks them as errors. To strip them you can use the below sed command:
sed -e 's#JNIEXPORT##' < pl_morgwai_ndktutorial_Native.h | sed -e 's#JNICALL##' >native.h
Now you can include native.h file in your sources or just copy the relevant part from it.
If don't want to use javah tool remember to put your function or its header in extern "C" braces.
Now let's create the implementation of the function in file named crypt_user.cpp. Inside the implementation just use some global var from Crypto++:
#include <jni.h>
#include <cryptlib.h>

extern "C" {
jlong Java_pl_morgwai_ndktutorial_Native_fun
    (JNIEnv* env, jobject o, jint i) {
  long long t = CryptoPP::INFINITE_TIME / i;
  return t;
}
}

Wiring the JNI stuff

Hint: if you encounter problems during this stage do clean your project manually by deleting folders bin and obj and all arch specific result subfolders from lib folder. Few times I myself have lost several hours because of stale objects lying there around.

Global Android.mk

Reference docs: docs/OVERVIEW.html

Each shared lib will have its own subfolder and Android.mk file so the main Android.mk file from the jni folder just needs to include them:
include $(call all-subdir-makefiles)

Application.mk

Reference docs: docs/APPLICATION-MK.html

We also need to specify some global stuff in Application.mk file in the jni folder:
APP_ABI := armeabi
APP_CPPFLAGS += -fexceptions -frtti
APP_STL := stlport_shared
#APP_STL := gnustl_shared
APP_ABI specifies the arch(s) which our libs should be built for. If you want to build apk for multiple archs enumerate them all separated by space. For example APP_ABI := armeabi armeabi-v7a x86 mips
APP_CPPFLAGS specifies CXXFLAGS to be used by NDK when building your shared libs. By default NDK compiles CPP sources without exception nor RTTI support. As we are using STL version that uses them we need to turn them on here. (Contrary to this, the standalone toolchain has exception and RTTI support turned on by default)
APP_STL specifies which version of STL to include in your apk.

Wiring modules

Reference docs: docs/PREBUILTS.html, docs/ANDROID-MK.html

Now create a subfolder cryptopp in the jni folder. Inside it create subfolders named after each arch you want to build your shared libs for. Place your prebuilt libcryptopp.so files in them accordingly. Next to them (ie inside cryptopp folder) create a subfolder named include and place there all header files that you may need to include in the sources of your libs that use this prebuilt lib. In case of Crypto++ just copy all header files (*.h) from its source folder.
Finally create Android.mk file inside the cryptopp folder:
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := cryptopp
LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libcryptopp.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include

include $(PREBUILT_SHARED_LIBRARY)
The last line says that this module is a prebuilt shared library and the LOCAL_SRC_FILES points to the location of its binary. The TARGET_ARCH_ABI var will be substituted accordingly during the build process for the given arch.
LOCAL_EXPORT_C_INCLUDES specifies the location of header files for other libs that use this one.


The last step is to create subfolder named crypt_user, placing there the source file of our small cpp lib (crypt_user.cpp) we created previously and creating Android.mk file for it:
LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := crypt_user
LOCAL_SRC_FILES := crypt_user.cpp
LOCAL_SHARED_LIBRARIES := cryptopp

include $(BUILD_SHARED_LIBRARY)
LOCAL_SHARED_LIBRARIES specifies other shared lib modules this module depends on so NDK knows where to find include files and what to link against.

Checkpoint

Your app should work fine now. In case of problems you can try to see a reference sample app for this tutorial at github.

Externalizing and importing modules

Reference docs: docs/IMPORT-MODULE.html

If you intend to use your prebuilt lib (or any other module) in several project it makes sense to define it as an external module and reuse it in any project that needs it.
Define env var NDK_MODULE_PATH to contain a path somewhere outside of your app project tree: you will store your external modules there. Next move cryptopp subfolder of jni folder there. In the main Android.mk file from jni folder append the following line at the end:
$(call import-module,cryptopp)
The second argument cryptopp must correspond to the subfolder name of your external module in NDK_MODULE_PATH folder. Inside its Android.mk file you can actually define several modules that you can later use as LOCAL_SHARED_LIBRARIES or LOCAL_STATIC_LIBRARIES in your projects. None of them has to be named after the subfolder name.

Since Crypto++ relies on RTTI and exception support it's a good idea to export appropriate flags to all users of this module: add the following line before PREBUILT_SHARED_LIBRARY in cryptopp module's Android.mk file:
LOCAL_EXPORT_CPPFLAGS := -fexceptions -frtti
As our only 'local' module crypt_user does not depend on these flags directly anymore, you can safely remove APPAPP_CPPFLAGS line from your Application.mk file. crypt_user needs to be compiled with these flags as well to be properly linked with cryptopp, but they will be imported for him thanks to the declared dependency in LOCAL_SHARED_LIBRARIES in its Android.mk file.

You can see it working in import-module branch of the reference app git repo.

Supporting multiple STL versions

Different projects may need to use different STL versions. That's why when you provide prebuilt lib as an external module it's a good idea to provide binaries for each STL version that may be needed.
Inside your cryptopp folder create subfolders named after STL versions, for example stlport_shared and gnustl_shared and inside them create subfolders for supported archs and place prebuilt binaries there. Now change the definition of LOCAL_SRC_FILES in Android.mk file to look like this:
LOCAL_SRC_FILES := $(APP_STL)/$(TARGET_ARCH_ABI)/libcryptopp.so
Sometimes even a single app may need to have different builts with different STL versions. There's a small problem here that in such case you don't know which lib to load in your java code. The solution is simply to try to load all of them and catch the error when the wrong ones fail. Our Native class would look like this:
package pl.morgwai.ndktutorial;

import android.util.Log;

public class Native {

	public static final String LOG_TAG = Native.class.getSimpleName();

	static {
		try {
			System.loadLibrary("stlport_shared");
		} catch (UnsatisfiedLinkError e) {
			Log.i(LOG_TAG, "stlport not found");
		}
		try {
			System.loadLibrary("gnustl_shared");
		} catch (UnsatisfiedLinkError e) {
			Log.i(LOG_TAG, "gnustl not found");
		}
		System.loadLibrary("cryptopp");
		System.loadLibrary("crypt_user");
	}

	public native long fun(int i);
}
Now it will all work just by changing APP_STL var in the Application.mk file.

In case of problems you can refer to multiple-stl branch of the reference app.

The End!

That's it. Hope this tutorial was useful for you :)
Feedback is welcome of course.