http://morgwai.pl/ndkTutorial/
Using
3rd party C++ libraries with Prebuilts and standalone
toolchain.
Intro
This
documents explains some advanced use-cases of Android NDK
including:
Using standalone NDK toolchain
Using Prebuilts feature of NDK
Dealing with multiple shared libs
Dealing with C++ STL issues
Importing external modules
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
useCrypto++
5.6.1as
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
Android
SDK r21.x or higher and basic understanding of
Android platform and Android app structure
NDK r8d or
higher and basic understanding of NDK and JNI
(here is a
good tutorial to learn them)
Eclipse
IDE with ADT
installed is recommended, but it is also
possible to use just
GNU make and basic understanding of Makefiles
Basic knowledge of shell commands and concepts
This tutorial was created while working on Linux on x86, but it
should be easy to "port" it to other platforms on which Android SDK
is available
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 exportNDKshell
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
Replaceandroid-8with
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=mipsor--arch=x86accordingly.
Now addndk-toolchainfolder
to your path and exportCXXshell
var to g++ from./ndk-toolchain/binfolder
(it will be namedsomething-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 settingCXXFLAGSshell
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
theGNUmakefilefile:
switch the target architecture (-march option)
from native to armv5te
remove linker option to use glibc pthreads (LDFLAGS +=
-pthread option)
After
applying the above changes
typingmakeshould
successfully build static version of Crypto++ lib for Andy. To
build a shared version typemake
libcryptopp.so
Choosing right 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_sharedlinker
option (ie changeLDFLAGS
+= -pthreadtoLDFLAGS
+= -lgnustl_sharedinstead
of just removing it). AlsoLDFLAGSvar
is not originally used in the command that builds shared library in
theGNUmakefilefile
so you need to append it at its end.
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. Thus you need to point linker to its
location: define theLDFLAGSvar
like this:
LDFLAGS += -L$(NDK)/sources/cxx-stl/stlport/libs/armeabi -lstlport_shared
If
you are building for different arch than armeabi you need to change
the path accordingly of course.
Patch for Crypto++
GNUmakefile
Below
is the patch forGNUmakefilefile
with all the changes described above.
DISCLAIMER:
Described changes to the Crypto++'s GNUmakefile
areUGLY
HACKSjust
to make it work ASAP and areDEFINITELY
NOT A GOOD EXAMPLE HOW TO WRITE MAKEFILESin
general. For example you should not
put-llinker
options intoLDFLAGSbut
intoLDLIBSand
generally introduce a notion of target platform rather than putting
options for it into host platform.
--- cryptopp-orig/GNUmakefile 2010-08-09 14:22:42.000000000 +0700
+++ cryptopp-andy.armeabi/GNUmakefile 2013-01-25 01:50:12.200354620 +0700
@@ -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,8 @@
endif
ifeq ($(UNAME),Linux)
-LDFLAGS += -pthread
+#LDFLAGS += -lgnustl_shared
+LDFLAGS += -L$(NDK)/sources/cxx-stl/stlport/libs/armeabi -lstlport_shared
ifneq ($(shell uname -i | $(EGREP) -c "(_64|d64)"),0)
M32OR64 = -m64
endif
@@ -151,7 +152,7 @@
$(RANLIB) $@
libcryptopp.so: $(LIBOBJS)
- $(CXX) -shared -o $@ $(LIBOBJS)
+ $(CXX) -shared -o $@ $(LIBOBJS) $(LDFLAGS)
cryptest.exe: libcryptopp.a $(TESTOBJS)
$(CXX) -o $@ $(CXXFLAGS) $(TESTOBJS) -L. -lcryptopp $(LDFLAGS) $(LDLIBS)
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 namedNativethat
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
itlibcrypt_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
withjavahtool:
go to thebin/classessubfolder
of your app project folder and
runjavahwith
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
macrosJNIEXPORTandJNICALL:
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
belowsedcommand:
sed -e 's#JNIEXPORT##' < pl_morgwai_ndktutorial_Native.h | sed -e 's#JNICALL##' >native.h
Now
you can includenative.hfile
in your sources or just copy the relevant part from
it.
If don't want to usejavahtool
remember to put your function or its header
inextern
"C"braces.
Now let's create the implementation of the function in file
namedcrypt_user.cpp.
Inside the implementation just use some global var from
Crypto++:
#include
#include
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 foldersbinandobjand
all arch specific result subfolders
fromlibfolder.
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
andAndroid.mkfile
so the mainAndroid.mkfile
from thejnifolder
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
inApplication.mkfile
in thejnifolder:
APP_ABI := armeabi
APP_CPPFLAGS += -fexceptions -frtti
APP_STL := stlport_shared
#APP_STL := gnustl_shared
APP_ABIspecifies
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 exampleAPP_ABI
:= armeabi armeabi-v7a x86 mips
APP_CPPFLAGSspecifies
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_STLspecifies
which version of STL to include in your apk.
Wiring modules
Reference
docs:docs/PREBUILTS.html,docs/ANDROID-MK.html
Now create a subfoldercryptoppin
thejnifolder.
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 insidecryptoppfolder)
create a subfolder namedincludeand
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 createAndroid.mkfile
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
theLOCAL_SRC_FILESpoints
to the location of its binary.
TheTARGET_ARCH_ABIvar
will be substituted accordingly during the build process for the
given arch.
LOCAL_EXPORT_C_INCLUDESspecifies
the location of header files for other libs that use this
one.
The last step is to create subfolder
namedcrypt_user,
placing there the source file of our small cpp lib
(crypt_user.cpp)
wecreated
previouslyand
creatingAndroid.mkfile
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_LIBRARIESspecifies
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
areference
sample appfor
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 varNDK_MODULE_PATHto
contain a path somewhere outside of your app project tree: you will
store your external modules there. Next
movecryptoppsubfolder
ofjnifolder
there. In the mainAndroid.mkfile
fromjnifolder
append the following line at the end:
$(call import-module,cryptopp)
The
second argumentcryptoppmust
correspond to the subfolder name of your external module
inNDK_MODULE_PATHfolder.
Inside itsAndroid.mkfile
you can actually define several modules that you can later use
asLOCAL_SHARED_LIBRARIESorLOCAL_STATIC_LIBRARIESin
your projects. None of them has to 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 beforePREBUILT_SHARED_LIBRARYincryptoppmodule'sAndroid.mkfile:
LOCAL_EXPORT_CPPFLAGS := -fexceptions -frtti
As
our only 'local' modulecrypt_userdoes
not depend on these flags directly anymore, you can safely
removeAPPAPP_CPPFLAGSline
from yourApplication.mkfile.crypt_userneeds
to be compiled with these flags as well to be properly linked
withcryptopp,
but they will be imported for him thanks to the declared dependency
inLOCAL_SHARED_LIBRARIESin
itsAndroid.mkfile.
You can see it working inimport-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.
Insied yourcryptoppcreate
subfolders named after STL versions, for
examplestlport_sharedandgnustl_sharedand
inside them create subfolders for supported archs and place
prebuilt binaries there. Now change the definition
ofLOCAL_SRC_FILESinAndroid.mkfile
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. OurNativeclass
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
changingAPP_STLvar
in theApplication.mkfile.
In case of problems you can refer
tomultiple-stl
branch of the reference app.
The End!
That's
it. Hope this tutorial was useful for you :)
Feedback is welcome of course.