Technical notes‎ > ‎

Mastering Android NDK Build System - Part 2: Standalone toolchain

This is the part 2 of “Mastering NDK” article. In the previous part (part 1), we have introduced how to use ndk-build to build Android native projects, and we also covered several advanced techniques to manage and customize the build script for bigger projects.

Although ndk-build is probably more than enough for most of the Android native projects, there might still be demand for the standalone toolchain in some cases. For example, if you already have a C/C++ project which might be quite complex and has a complicated makefile. In that case, you may not want to convert everything to Android.mk and Application.mk. Using standalone toolchain makes more sense in this case, and it allows you to keep your original makefile or reuse most of the original makefile. Therefore, in this part, I will briefly cover the usage of the standalone toolchain, and give a few code examples.

The source code of all examples can be found here: https://github.com/robertwgh/mastering-ndk.

Table of Contents

  1. Before we start
  2. The ndk-build Workflow
  3. Use customized toolchain for your projects
  4. Summary
    Comments

1. Before we start

The Android NDK official document contain a chapter “STANDALONE-TOOLCHAIN”, which gives useful information about the standalone toolchain. However, that document is short of details, and there is no example to demonstrate the usage. Therefore, it might be hard to follow the official document. But it is still a good reference to have anyways.

2. The ndk-build Workflow

Actually, when we use ndk-build, if we enable the debug option V=1 as follows:

ndk-build V=1

We will see what is actually done by ndk-build. The following console print comes from the compilation of example 1.

$ ndk-build V=1
rm -f ./libs/arm64-v8a/lib*.so ./libs/armeabi/lib*.so ./libs/armeabi-v7a/lib*.so ./libs/armeabi-v7a-hard/lib*.so ./libs/mips/lib*.so ./libs/mips64/lib*.so ./libs/x86/lib*.so ./libs/x86_64/lib*.so

rm -f ./libs/arm64-v8a/gdbserver ./libs/armeabi/gdbserver ./libs/armeabi-v7a/gdbserver ./libs/armeabi-v7a-hard/gdbserver ./libs/mips/gdbserver ./libs/mips64/gdbserver ./libs/x86/gdbserver ./libs/x86_64/gdbserver

rm -f ./libs/arm64-v8a/gdb.setup ./libs/armeabi/gdb.setup ./libs/armeabi-v7a/gdb.setup ./libs/armeabi-v7a-hard/gdb.setup ./libs/mips/gdb.setup ./libs/mips64/gdb.setup ./libs/x86/gdb.setup ./libs/x86_64/gdb.setup

[armeabi-v7a] Compile++ thumb: hello <= hello.cpp
/cygdrive/d/development/android-ndk-r10d/toolchains/arm-linux-androideabi-4.8/prebuilt/windows-x86_64/bin/arm-linux-androideabi-g++ -MMD -MP -MF ./obj/local/armeabi-v7a/objs-debug/hello/hello.o.d.org -fpic -ffunction-sections -funwind-tables -fstack-protector -no-canonical-prefixes -march=armv7-a -mfpu=vfpv3-d16 -mfloat-abi=softfp -fno-exceptions -fno-rtti -mthumb -Os -g -DNDEBUG -fomit-frame-pointer -fno-strict-aliasing -finline-limit=64 -O0 -UNDEBUG -marm -fno-omit-frame-pointer -ID:/development/android-ndk-r10d/sources/cxx-stl/stlport/stlport -ID:/development/android-ndk-r10d/sources/cxx-stl//gabi++/include -Ijni -DANDROID  -Wa,--noexecstack -Wformat -Werror=format-security -fPIE  -frtti   -frtti -fexceptions  -ID:/development/android-ndk-r10d/platforms/android-19/arch-arm/usr/include -c  jni/hello.cpp -o ./obj/local/armeabi-v7a/objs-debug/hello/hello.o && ./obj/convert-dependencies.sh ./obj/local/armeabi-v7a/objs-debug/hello/hello.o.d

[armeabi-v7a] Executable     : hello
/cygdrive/d/development/android-ndk-r10d/toolchains/arm-linux-androideabi-4.8/prebuilt/windows-x86_64/bin/arm-linux-androideabi-g++ -Wl,--gc-sections -Wl,-z,nocopyreloc --sysroot=D:/development/android-ndk-r10d/platforms/android-19/arch-arm -Wl,-rpath-link=D:/development/android-ndk-r10d/platforms/android-19/arch-arm/usr/lib -Wl,-rpath-link=./obj/local/armeabi-v7a ./obj/local/armeabi-v7a/objs-debug/hello/hello.o D:/development/android-ndk-r10d/sources/cxx-stl/stlport/libs/armeabi-v7a/thumb/libstlport_static.a -lgcc -no-canonical-prefixes -march=armv7-a -Wl,--fix-cortex-a8  -Wl,--no-undefined -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now -fPIE -pie   -lc -lm -o ./obj/local/armeabi-v7a/hello

[armeabi-v7a] Install        : hello => libs/armeabi-v7a/hello
install -p ./obj/local/armeabi-v7a/hello ./libs/armeabi-v7a/hello
/cygdrive/d/development/android-ndk-r10d/toolchains/arm-linux-androideabi-4.8/prebuilt/windows-x86_64/bin/arm-linux-androideabi-strip --strip-unneeded ./libs/armeabi-v7a/hello

As we can see, ndk-build actually did the following things:

  1. Remove previously built files in the output folder libs.
  2. Build hello.o from the source code.
  3. Build executable from hello.o.
  4. Copy executable file to subfolders libs folder according to the ABI.

The above building information shows how the ndk-build invokes the toolchain to build the project. Basically, /cygdrive/d/development/android-ndk-r10d/toolchains/arm-linux-androideabi-4.8/prebuilt/windows-x86_64/bin/arm-linux-androideabi-g++ is the standalone toolchain coming with the NDK installation. And if we read the whole command, we find that the command just invokes the cross-compilation version g++, with building parameters such as include path, library path and so on. The most straightforward way to use standalone toolchain is to mimic what ndk-build does, and directly invoke the right compilers based on your target architecture and platforms.

Under the NDK_ROOT/toolchains directory, we can find different toolchains for ARM, X86, X86_64, MIPS and so on. There are also versions with different compilers such as g++ and clang. Therefore, we can basically choose whatever is suitable for our projects.

3. Use customized toolchain for your projects

Apparently, the above method works, but it is very verbose and not suitable for larger projects. What we can do is to create a “customized” toolchain for a specific platform and ABI, with the help of a tool $NDK/build/tools/make-standalone-toolchain.sh provided with NDK installation.

Let’s look at an example. Assume we have a project with the following configurations:

  • Support android-19 platform.
  • Host system is Windows 7 64bit.
  • Target architecture is ARM.

We can create a script generate_standalone_toolchain.sh to help us export the toolchain we need:

generate_standalone_toolchain.sh

NDK=/cygdrive/d/development/android-ndk-r10d
SYSROOT=$NDK/platforms/android-19/arch-arm/
mkdir -p /cygdrive/d/development/standalone_toolchain/
$NDK/build/tools/make-standalone-toolchain.sh --arch=arm --platform=android-19 --system=windows-x86_64 --install-dir=/cygdrive/d/development/standalone_toolchain/
chmod -R 755 /cygdrive/d/development/standalone_toolchain

The helper script $NDK/build/tools/make-standalone-toolchain.sh creates a temporary directory under /tmp, copies files to that directory, and finally copies the files to the specified folder. Please make sure you have the enough permission, otherwise you will meet “permission denied” error when accessing the /tmp directory.

We should see the following console information if the path is configured correctly:

$ ./generate_standalone_toolchain.sh
Auto-config: --toolchain=arm-linux-androideabi-4.8
Copying prebuilt binaries...
Copying sysroot headers and libraries...
Copying c++ runtime headers and libraries...
Copying files to: /cygdrive/d/development/standalone_toolchain/
Cleaning up...
Done.

Once this is done, we can see the whole toolchain is copied from NDK_ROOT/toolchains to /cygdrive/d/development/standalone_toolchain/.

Example: helloworld

To test the customized toolchain, we create an example project helloworld. The project structure is very simple:

+-- helloworld
|   +-- hello.cpp
|   +-- Makefile

hello.cpp

#include <iostream>
int main()
{
    std::cout << "Hello World!" << std::endl;
}

Makefile

STANDALONE_TOOLCHAIN=/cygdrive/d/development/standalone_toolchain/bin/
CC=$(STANDALONE_TOOLCHAIN)/arm-linux-androideabi-gcc
CXX=$(STANDALONE_TOOLCHAIN)/arm-linux-androideabi-g++
CFLAGS=-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -c -Wall
LDFLAGS=-march=armv7-a -Wl,--fix-cortex-a8
SOURCES=hello.cpp
OBJECTS=$(SOURCES:.cpp=.o)
EXECUTABLE=hello

all: $(SOURCES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS) 
    $(CXX) $(LDFLAGS) $(OBJECTS) -o $@
.cpp.o:
    $(CXX) $(CFLAGS) $< -o $@

clean: 
    rm *.o hello;

We can simply compile the code as follows, and we will get executable files:

$ cd helloworld
$ make
/cygdrive/d/development/standalone_toolchain/bin//arm-linux-androideabi-g++ -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -c -Wall hello.cpp -o hello.o
/cygdrive/d/development/standalone_toolchain/bin//arm-linux-androideabi-g++ -march=armv7-a -Wl,--fix-cortex-a8 hello.o -o hello

Another example: clcompute

This is a more complicated example, which does parallel vector addition using OpenCL. Assume we have CL include and library files available:

+-- D:\opencl_lib
|   +-- include
|       +-- CL
|   +-- libs
|       +-- libOpenCL.so

The CL include header files can be downloaded from Khronos Group website. And the libOpenCL.so library file can be retrieved from an OpenCL-capable phone. You can use adb pull to pull it from your phone (or tablet). You can refer to the table in the part 1 of this article for the detailed position of the libOpenCL.so for different SoC chipsets.

The project structure:

+-- clcompute
|   +-- clcompute.cpp
|   +-- Makefile

Makefile

STANDALONE_TOOLCHAIN=/cygdrive/d/development/standalone_toolchain/bin/
OPENCL=/cygdrive/d/opencl_lib/
CC=$(STANDALONE_TOOLCHAIN)/arm-linux-androideabi-gcc
CXX=$(STANDALONE_TOOLCHAIN)/arm-linux-androideabi-g++
CFLAGS=-march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -c -I $(OPENCL)/inc/
LDFLAGS=-march=armv7-a -Wl,--fix-cortex-a8 -L$(OPENCL)/libs/ -lOpenCL

SOURCES=clcompute.cpp
OBJECTS=$(SOURCES:.cpp=.o)
EXECUTABLE=clcompute

all: $(SOURCES) $(EXECUTABLE)
$(EXECUTABLE): $(OBJECTS) 
    $(CXX) $(LDFLAGS) $(OBJECTS) -o $@
.cpp.o:
    $(CXX) $(CFLAGS) $< -o $@

clean: 
    rm *.o $(EXECUTABLE);

As you can see, once you have exported the standalone toolchain, the makefile is basically the same as a normal makefile, once you have specified the path to your toolchain. We can imagine that by using the standalone toolchain, we can build some complicated projects which may require significant amount of effort is ndk-build was used.

4. Summary

In this technical note (part 1 and part 2), we have covered the usage and techniques related to the compilation of the Android native projects. Hope this article is useful, and if you have some better ideas or any comments, please feel free to leave your word below.

Comments

Disqus comments