ikerhurtado.com
You're in
Iker Hurtado's pro blog
Developer | Entrepreneur | Investor
Software engineer (entrepreneur and investor at times). These days doing performant frontend and graphics on the web platform at Barcelona Supercomputing Center

Introduction to the Android NDK build system

22 Jan 2015   |   iker hurtado  
Share on Twitter Share on Google+ Share on Facebook
In this entry I gather some basic and relevant info about the Android NDK build system. From NDK revision 10d, the official documentation is quite complete but is not on-line (it goes bundled in NDK package)

The Android NDK provides its own process (ndk-build) to build a native libraries by means of Android-specific makefiles.

I usually work with Eclipse. This IDE calls ndk-build command when you press the build button.

Android.mk

The main configuration file is Android.mk, a Makefile fragment which can be found in the jni/ folder.

The file syntax is designed to allow to group the sources into modules. A module can be a static library, a shared library or a standalone executable. One or more modules can be defined in each Android.mk file and the same source file can be used in multiple modules too.

A very basic version of that file would look like this:

LOCAL_PATH := $(call my-dir) 

include $(CLEAR_VARS) 

LOCAL_MODULE:= nativedemo 
LOCAL_SRC_FILES := NativeDemo.cpp 
LOCAL_LDLIBS := -llog -landroid 

include $(BUILD_SHARED_LIBRARY) 

I explain the previous lines:

LOCAL_PATH := $(call my-dir): It sets the local build path for the makefile which allows it to find other files relative to its own path.

include $(CLEAR_VARS): It then calls an external command which clears the previously set build variables.

LOCAL_MODULE:= nativedemo: This variable must be defined to identify each module.

LOCAL_SRC_FILES := NativeDemo.cpp: This variable must contain a list of C and/or C++ source files that will be built and assembled into a module. Headers and included files should not be listed here: the build system will compute dependencies automatically.

LOCAL_LDLIBS := -llog -landroid: Android’s existing libraries linked in dynamically.

include $(BUILD_SHARED_LIBRARY): The BUILD_SHARED_LIBRARY is a variable provided by the build system that points to a Makefile script that is in charge of collecting all the information defined in LOCAL_XXX variables since the latest 'include ' and determine what to build, and how to do it exactly.

In the official NDK documentation there is a very detailed explanation about all Android.mk options and variables. The LOCAL_C_INCLUDES var seems useful to know:

LOCAL_C_INCLUDES: An optional list of paths, relative to the NDK root directory, which will be appended to the include search path when compiling all sources (C, C++ and Assembly). For example:
LOCAL_C_INCLUDES := sources/foo 

Application.mk

The Application.mk is an optional and application-level build file that is also placed in the jni/ directory.

Its purpose is to describe which modules are needed by the application; it also defines the variables that are common for all modules.

I extract some important variables descriptions for Application.mk from NDK documentation:

  • APP_MODULES: If this variable is defined, it tells ndk-build to only list the corresponding modules and those that they depend on. It must be a space-separated list of module names as they appear in the LOCAL_MODULE definition of Android.mk files.
    It the variable is undefined, ndk-build looks for the list of all installable (shared library or executable, which will generate a file in libs/$ABI/) top-level modules, i.e. those listed by your Android.mk and any file it includes directly. Imported modules are not top-level though.
  • APP_OPTIM: This optional variable can be defined to either 'release' or 'debug'.
    A 'release' mode is the default, and will generate highly optimized binaries. The 'debug' mode will generate un-optimized binaries which are much easier to debug.
    Note that it is possible to debug both 'release' and 'debug' binaries, but the 'release' builds tend to provide less information during debugging sessions: some variables are optimized out and can't be inspected, code re-ordering can make stepping through the code difficult, stack traces may not be reliable, etc...
  • APP_ABI: By default, the NDK build system will generate machine code for the 'armeabi' ABI. This corresponds to an ARMv5TE based CPU with software floating point operations. You can use APP_ABI to select a different ABI.
  • APP_PLATFORM: Name the target Android platform. For example, 'android-3' correspond to Android 1.5 system images. Each platform corresponds to a set of headers files, and a shared library file that contains the corresponding implementation, and which must be linked against by your native code. The headers corresponding to a given API level are now located under $NDK/platforms/android-/arch-arm/usr/include
  • APP_STL: By default, the NDK build system provides C++ headers for the minimal C++ runtime library (/system/lib/libstdc++.so) provided by the Android system.
    However, the NDK comes with alternative C++ implementations that you can use or link to in your own applications. Define APP_STL to select one of them.
In the official NDK documentation there is a very detailed explanation about all Application.mk variables.

Shared and static libraries

It is important to understand the differences between shared and static libraries and how the Android build system works with them.

The build system only copies shared libraries into the application package. At Android NDK, static libraries are only used to build the shared libraries, because only these are packaged into the apk file.

Shared libraries are pieces of executable loaded on demand to memory as a whole. Only shared libraries can be loaded directly from Java code. A built shared library is located under the libs/abi_dir folder and will be copied to the application's apk file.

A static library is an group of object files compiled from the source code. They are built as files ending with .a suffix. Later, part of its binary code is copied (without regards to code duplication) into a targeted executable or library at build time by a compiler or linker.

In contrast with shared libraries, static libraries can be stripped, which means that unnecessary symbols (like a function which is never called from the embedding library) are removed from the final binary.

Static versus shared

It's important be aware of advantages and drawbacks of choosing shared or static libraries for packing our code.

Good reasons for using a shared library will be:

  • the library is used in several other libraries
  • almost all pieces of code are required to run
  • the library needs to be selected dynamically at runtime (what will avoid memory duplication)

On the other hand, the reasons for using a static library:

  • it is used in one or only a few places
  • only part of its code is necessary to run
  • if loading it at the beginning of your application is not a concern

Use example of static library: when integrating third party code into a project, instead of including the source code directly, the code can be compiled as a static library and then combined into the shared library.

Other use case of static library: When you use a big library but only use a small part of it, import this library as static is the best option. The Android NDK build system resolves the dependencies at build time and only copy the parts that are used in the final shared library. This means a smaller library size and smaller apk file size.

The inclusion of code from static library to several shared libraries can cause some problems, because some global variables used by the C++ runtime library are duplicated.
When we need to force the entire static library to be built into the final shared library (for example, there are circular dependencies among several static libraries). We can use the LOCAL_WHOLE_STATIC_LIBRARIES variable at Android.mk or the "--whole-archive" linker flag.

Sharing modules between several projects

Shared or static common modules can be shared between modules, however, these modules must be part of the same NDK project which may be a drawback sometimes. Luckily, Android NDK allows sharing and reusing modules between NDK projects.

For more information consult the book Pro Android C++ with the NDK or the official NDK documentation.

Prebuilt libraries

A NDK project can use the prebuilt libraries the same way as the ordinary shared libraries.

There are two common reasons for using prebuilt libraries: when you want to use a third-party library and only the library binary is provided and when you need use prebuilt version of your shared modules to speed up the builds.

Although they are already compiled, prebuild modules still require an Android.mk build file: each prebuilt library must be declared as a single independent module to the build system.

Must take into account the prebuilt library declaration does not carry any information about the machine architecture that it is built for.

For more information consult the book Pro Android C++ with the NDK or the official NDK documentation.

Building Standalone Executable

In order to facilitate testing and quick prototyping, Android NDK also provides support for building a standalone executable. The standalone executables are regular Linux applications that can be copied to the Android device without being packaged into an APK file, and they can get executed directly without being loaded through a Java application.

For more information consult the book Pro Android C++ with the NDK.

POST A COMMENT: