Mastering Android NDK Build System - Part 1: Techniques with ndk-build

MASTERING_ANDROID_NDK 掌握 Android NDK构建系统 - 第1部分:使用ndk-build的技术 本文不是“Hello world!” - NDK的类型教程。虽然我仍然会快速介绍ndk-build的基本知识,但它不是本文的重点。相反,我将总结一些非常有用的NDK技术和我在项目中使用的技巧。希望这些提示对于想要构建一些实用项目而不是玩具项目来学习NDK的人来说非常有用。因此,目标读者是中型或高级 Android开发人员。本文包含两部分: 第1部分:ndk-build 在这一部分中,我们将讨论如何灵活地使用ndk-build来构建项目,以及如何组织项目的文件结构。 第2部分:独立工具链 在第2 部分中,将讨论独立工具链的设置和使用。 所有示例的源代码可以在这里找到: https://github.com/robertwgh/mastering-ndk。 立即下载

This article is not a “Hello world!”-type tutorial for NDK. Although I will still provide a quick walk-through of the very basic knowledge of ndk-build, but it is not the focus of this article. Instead, I will summarize some very useful NDK techniques and tips I have been using in my projects. Hope those tips can be very useful for anyone who wants to build some practical projects rather than a toy project to learn NDK. So, the target readers are medium or advanced Android developers. The article contains two parts:

  • Part 1: ndk-build
    In this part, we will discuss how to flexibly use ndk-build to build your projects, and how to organize the file structure of your projects.
  • Part 2: standalone toolchain
    In part 2, the set up and usage of standalone toolchain will be discussed.

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

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Basics of ndk-build
  4. Building Native Executable Using NDK
  5. Useful Techniques
    5.1 How to compile source code not in jni directory
    5.2 Get rid of jni folder
    5.3 Use custom names for makefiles
    5.4 Using include to embed .mk files
    5.5 About LOCAL_PATH and CLEAR_VARS
    5.6 How to debug .mk makefiles
    5.7 Build for multiple target architectures
  6. Summary
    Comments

1. Introduction

Android NDK (Native Development Kit) is a power tool for Android application developers who want efficient and high performance native code, or who has to deal with low-level hardware details (such as OpenGL, OpenCL and so on).

The Android NDK official documents (an online version) are kind of OK, if you have been working with NDK for a while. However, it is really not designed for someone just starting with Android NDK development. The problem with the official documents is that there is no emphasis, so that important information might be overlooked easily.

There are also many online tutorials and articles showing the basics of NDK and the usage of NDK building tools. However, the information are distributed everywhere. There is no single place discussing these topics and techniques in depth. Hopefully, this article will cover some of them.

2. Prerequisites

In this article, I make the following assumptions:

  • you know what NDK is;
  • you know C/C++;
  • you have already installed Android NDK on your computer. In my setting, I install NDK under D:\development\android-ndk-r10d. In later part of this article, I will call that path NDK_ROOT.
  • to avoid long path name when using ndk-build, I added NDK_ROOT to system PATH environment variable.

3. Basics of ndk-build

Of course, the first step of using the Android NDK is downloading the NDK installation package from Android Developer network. After installing the NDK package, these are what you got:

  • NDK_ROOT\ndk-build.cmd script;
  • documents under NDK_ROOT\docs;
  • toolchains and compilers;
  • source code for some native libraries;
  • some sample codes.

The sample codes could be very useful if you want to learn the basic set up and the syntax of the makefiles for NDK. Going through the sample code provided by NDK, you will find that most of the code samples assume that you are working on an Android application project, and will use NDK to build the JNI part of the Android application. That’s why you notice that all project put C/C++ source code and makefiles under a jni folder.

The following is a typical file structure of NDK samples:

+-- project_root
|   +-- jni
|       +-- Android.mk
|       +-- Application.mk
|       +-- main.c
|   +-- obj
|   +-- libs

As you can see, the jni directory is the heart of the whole NDK project, which contains C/C++ source code, two makefiles Android.mk and Application.mk. As will be discussed later, you will see that the C/C++ source code is not necessary to sit inside jni folder. Moreover, you don’t need to have exactly the same name for makefiles. But as a starting point, using Android.mk and Application.mk will be easiest way to go and can save you a lot of effort, unless you really don’t like the current names of the makefiles. By default, the ndk-build will try to locate

The other two folders obj and libs are generated by the NDK building system, and contains the intermediate files and final binary code, respectively.

The Android.mk and Application.mk are the most important makefiles for a NDK project. The Android.mk is more like a traditional makefile, defining source code, path to include header files, path for the linker to locate the libraries, module name, build type, and so on. The Application.mk defines Android application related properties, such as the Android SDK version, debug or release mode, target platform ABI (architecture binary interface), standard C/C++ library, and so on.

A typical Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE    := <module_name>  # name your module here.
LOCAL_SRC_FILES := main.c
include $(BUILD_SHARED_LIBRARY)

A minimal Application.mk

APP_ABI := all

To build such project, we can go to the project_root, and type ndk-build (assuming you have the NDK_ROOT in the system PATH, the NDK building script will automatically find the native code under jni folder.

$ cd project_root
$ ndk-build

If there is no bug in the code, the compiled shared library lib<module_name>.so will be generated under libs/<abi>/ directory. Once you get this shared library file, your Android application building system will pack it into the final APK installer file. You will be able to call the native functions using JNI in your JAVA code.

In this article, we focus more on how to build executable binaries using NDK, since we will be easier to test our results immediately in that way. But remember that all the techniques talked here will be the same and can be directly applied to a shared library project without any change.

4. Building Native Executable Using NDK

Let’s first build a “Hello World!” test. The project structure will be like this:

+-- ex1_helloworld
|   +-- jni
|       +-- Android.mk
|       +-- Application.mk
|       +-- hello.c

Hello.cpp

#include <iostream>

int main()
{
    std::cout << "Hello World!" << std::endl;
    return 0;
}

Android.mk
Please notice that a comment starts with "#" in .mk files.

LOCAL_PATH:= $(call my-dir) # Get the local path of the project.
include $(CLEAR_VARS) # Clear all the variables with a prefix "LOCAL_"

LOCAL_SRC_FILES:=hello.cpp # Indicate the source code.
LOCAL_MODULE:= hello # The name of the binary.
include $(BUILD_EXECUTABLE) # Tell ndk-build that we want to build a native executable.

Application.mk

APP_OPTIM := debug    # Build the target in debug mode. 
APP_ABI := armeabi-v7a # Define the target architecture to be ARM.
APP_STL := stlport_static # We use stlport as the standard C/C++ library.
APP_CPPFLAGS := -frtti -fexceptions    # This is the place you enable exception.
APP_PLATFORM := android-19 # Define the target Android version of the native application.

You may already found out that, the major difference between a shared library project and a native project is only one line in the Android.mk.

For a shared library, we use:

include $(BUILD_SHARED_LIBRARY)

For an executable binary, we use:

include $(BUILD_EXECUTABLE)

To this point, we can build our “Hello World!” NDK project:

$ cd project_root
$ ndk-build
[armeabi-v7a] Cygwin         : Generating dependency file converter script
[armeabi-v7a] Compile++ thumb: hello <= hello.cpp
[armeabi-v7a] Executable     : hello
[armeabi-v7a] Install        : hello => libs/armeabi-v7a/hello

Then, we can push the hello native program to an Android device and run it (to achieve this, we need Android ADB tool. Please install Android SDK, and set the ANDROID_SDK_ROOT/platform-tools to system PATH. Of course, you may also find ADB install package from internet if you don’t want to bother to install Android SDK.)

$ adb root
$ adb shell "mkdir -p /data/mastering_ndk && chmod 777 /data/mastering_ndk"
$ adb push ./libs/armeabi-v7a/hello /data/mastering_ndk
$ adb shell "cd /data/mastering_ndk && chmod 777 ./hello && ./hello"
$ Hello World!

Tip: The above command only works for rooted devices. If you have a stock version device without root permission, you can utilize the Android Native Program Launcher tool to launch the native executable on any Android device.

We have recalled the basics of ndk-build. From the next section, we will show some techniques to better utilize the NDK build system for some larger projects.

5. Useful Techniques

5.1 How to compile source code not in jni directory

Assume you have a big project, presumably a cross-platform project, so chances are high that you cannot easily move all your source code to under the jni folder. This is actually not to do with some small modification to the existing Android.mk makefile. The following example will show how to achieve that. You can find the complete project in example 2: ex2_src_not_in_jni_folder.

Project structure:

+-- ex2_src_not_in_jni_folder
|   +-- jni
|       +-- Android.mk
|       +-- Application.mk
|   +-- src
|       +-- hello.c

Android.mk

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

LOCAL_SRC_FILES:= ../src/hello.cpp
LOCAL_MODULE:= hello
include $(BUILD_EXECUTABLE)

Application.mk remains the same. We can build the project and execute the binary using the same way as in example 1.

5.2 Get rid of jni folder

The jni folder makes more sense for a JNI native project in an Android application project. If we want something more meaningful to our project, we can get rid of that specific folder which is used by ndk-build by default. To achieve that, a few variables in the makefiles should be set appropriately. The following steps will achieve that goal. The complete example project can be found in example 3.

Project structure:

+-- ex3_get_rid_of_jni_folder
|   +-- Android.mk
|   +-- Application.mk
|   +-- src
|       +-- hello.c

As you can see, we removed jni folder and moved the Android.mk and Application.mk up to the project_root folder.

The new Android.mk

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

LOCAL_SRC_FILES:= src/hello.cpp # This has changed!
LOCAL_MODULE:= hello
include $(BUILD_EXECUTABLE)

The new Application.mk

APP_OPTIM := debug
APP_ABI := armeabi-v7a
APP_STL := stlport_static 
APP_CPPFLAGS := -frtti -fexceptions
APP_PLATFORM := android-19
APP_BUILD_SCRIPT := Android.mk  # this line is new!

Please notice that the APP_BUILD_SCRIPT indicates the major makefile entry for the whole application. In our case, it is Android.mk. Everything looks good so far, then, we use the following command to build the project, please notice that we add NDK_APPLICATION_MK variable to the ndk-build command to tell the ndk-build where to find the Application.mk. In this example, we use the following command:

$ ndk-build NDK_APPLICATION_MK=./Application.mk

However, we will have the following error:

$ ndk-build NDK_APPLICATION_MK=./Application.mk
Android NDK: Could not find application project directory !
Android NDK: Please define the NDK_PROJECT_PATH variable to point to it.
/cygdrive/d/development/android-ndk-r10d/build/core/build-local.mk:148: *** Android NDK: Aborting    .  Stop.

The NDK_PROJECT_PATH is a system environmental variable. Let’s define it to where the Application.mk is located and re-build.

$ export NDK_PROJECT_PATH=.
$ ndk-build NDK_APPLICATION_MK=./Application.mk
[armeabi-v7a] Cygwin         : Generating dependency file converter script
[armeabi-v7a] Compile++ thumb: hello <= hello.cpp
[armeabi-v7a] Executable     : hello
[armeabi-v7a] Install        : hello => libs/armeabi-v7a/hello

There is another way to fix the above build error. The solution is to create an empty AndroidManifest.xml file in the same folder with Application.mk.

After adding this dummy AndroidManifest.xml file, we can build the project without defining NDK_PROJECT_PATH variable. The final project structure is:

+-- ex3_get_rid_of_jni_folder
|   +-- Android.mk
|   +-- AndroidManifest.xml
|   +-- Application.mk
|   +-- src
|       +-- hello.c

5.3 Use custom names for makefiles

We can push the previous technique even further to define our own makefiles. The following example can be found in example 4.

In the following example, we rename the application makefile to MyApplication.mk, and the module makefile to MyAndroid.mk.

Project structure:

+-- ex4_custom_make_files
|   +-- MyAndroid.mk
|   +-- AndroidManifest.xml
|   +-- MyApplication.mk
|   +-- src
|       +-- hello.c

MyAndroid.mk is the same as the previous Android.mk.
The new MyApplication.mk

APP_OPTIM := debug
APP_ABI := armeabi-v7a
APP_STL := stlport_static 
APP_CPPFLAGS := -frtti -fexceptions
APP_PLATFORM := android-19
APP_BUILD_SCRIPT := MyAndroid.mk

And the build command becomes:

$ ndk-build NDK_APPLICATION_MK=./MyApplication.mk
[armeabi-v7a] Compile++ thumb: hello <= hello.cpp
[armeabi-v7a] Executable     : hello
[armeabi-v7a] Install        : hello => libs/armeabi-v7a/hello

Everything builds well and we successful got the binary hello.

5.4 Using include to embed .mk files

To better handle large projects containing multiple submodules, in the form of static libraries, shared libraries, or pre-built files, the NDK build system allows a makefile to include another makefiles. The following is the syntax:

include PATH_TO_MK_FILE/Android.mk

This will include Android.mk file under PATH_TO_MK_FILE directory to the current makefile. This “include” feature provides us tremendous flexibility to create some very creative way to utilize the building system.

Let’s look at a simple example (example 5) and see how it works. Please notice that this example contains a very simple makefiles. With the power of “include”, I am confident to say that you can create much more complicated building scripts, which can almost do anything for any project, no matter how complex that project is.

In the following example project, we have a main() function inside source file compute.cpp which calls add() and mul() functions to perform addition and multiplication on the input numbers. We define add() and mul() functions in two submobules, and compile them into two static libraries. Finally, when build the executable, the linker will link everything together and generate the final executable binary.

So, to better handle the submodules and separate every submobules, in this project, we create a Android.mk for each module. As you will see soon, by organizing the makefiles this way, the project now has a very scalable structure. To be more specific, if you want to add one more submobule to the same project, you just simply add another submodule folder (whatever it is called, let’s say, divide), and create a new Android.mk for that new submodule “divide”. Then, you just need to change one line in the makefile of the main module. All the existing submodules remain untouched. The project is very easy to maintain and extend to support more functions.

Top level

First, let’s look at the project structure and have a overall picture:

+-- ex5_using_include_to_embed_make_files
|   +-- makefiles
|       +-- Android.mk
|       +-- Application.mk
|   +-- src
|       +-- main
|           +-- compute.cpp
|           +-- Android.mk
|       +-- submodules
|           +-- add
|               +-- add.cpp
|               +-- Android.mk
|           +-- mul
|               +-- mul.cpp
|               +-- Android.mk   
|           +-- Android.mk          
|   +-- AndroidManifest.xml

makefiles/Application.mk

APP_OPTIM := debug
APP_ABI := armeabi-v7a
APP_STL := stlport_static 
APP_CPPFLAGS := -frtti -fexceptions
APP_PLATFORM := android-19
APP_BUILD_SCRIPT := makefiles/Android.mk

makefiles/Android.mk

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

include $(TOP_LOCAL_PATH)/../src/submodules/Android.mk
include $(TOP_LOCAL_PATH)/../src/main/Android.mk

Here, this Android.mk serves as the top level makefile and includes another two Android.mk, one for the submodules, and the other for the main module.

Main module : compute

We first look at the main module.

src/main/compute.cpp

#include <iostream>

int add(int a, int b);
int mul(int a, int b);

int main()
{
    int a = 2;
    int b = 3;
    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;
    std::cout << "add(a, b) = " << add(a, b) << std::endl;
    std::cout << "mul(a, b) = " << mul(a, b) << std::endl;

    return 0;
}

src/main/Android.mk

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= compute.cpp
LOCAL_MODULE:= compute
LOCAL_STATIC_LIBRARIES:= add mul
include $(BUILD_EXECUTABLE)

We build the main module as an executable by defining include $(BUILD_EXECUTABLE). And we also define the LOCAL_STATIC_LIBRARIES to add mul, which means this main module depends on two static libraries with the module names add and mul, respectively. But where are these two modules are defined, let’s continue to look at the submobules.

Sub-modules: add and mul

src/submodules/Android.mk

include $(call all-subdir-makefiles)

Here, in the submodules folder, the makefiles only contains one line, which calls a function in the NDK build system. This command include $(call all-subdir-makefiles) is basically equivalent to including all the Android.mk files in all the sub-directories manually. In our case, this will help us include src/submodules/add/Android.mk and src/submodules/mul/Android.mk.

Let’s take a look at submodule add.

src/submodules/add/add.cpp

int add(int a, int b)
{
    return a + b;
}

src/submodules/add/Android.mk

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= ./add.cpp
LOCAL_MODULE:= add
include $(BUILD_STATIC_LIBRARY)

We define module add to be a static library.

Similarly, we have the submodule mul, which almost has the same makefile as add module.

src/submodules/mul/mul.cpp

int mul(int a, int b)
{
    return a * b;
}

src/submodules/mul/Android.mk

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= ./mul.cpp
LOCAL_MODULE:= mul
include $(BUILD_STATIC_LIBRARY)

So far, we have listed all the files in the projects. And the build flow is clearly shown as follows:

  1. Build static library add using add.cpp;
  2. Build static library mul using mul.cpp;
  3. Build main module compute using compute.cpp;
  4. Link compute to static libraries libadd.a and libmul.a, generate executable compute.
Build and Execute

We use the same command from the previous examples to build the project.

$ ndk-build NDK_APPLICATION_MK=./makefiles/Application.mk
[armeabi-v7a] Cygwin         : Generating dependency file converter script
[armeabi-v7a] Compile++ thumb: compute <= compute.cpp
[armeabi-v7a] Compile++ thumb: add <= add.cpp
[armeabi-v7a] StaticLibrary  : libadd.a
[armeabi-v7a] Compile++ thumb: mul <= mul.cpp
[armeabi-v7a] StaticLibrary  : libmul.a
[armeabi-v7a] Executable     : compute
[armeabi-v7a] Install        : compute => libs/armeabi-v7a/compute

We execute the binary.

$ adb shell "mkdir -p /data/mastering_ndk && chmod 777 /data/mastering_ndk"
$ adb push ./libs/armeabi-v7a/compute /data/mastering_ndk
$ adb shell "cd /data/mastering_ndk && chmod 777 ./compute && ./compute"
a = 2
b = 3
add(a, b) = 5
mul(a, b) = 6

We can see that the results are exactly what we expected. Clearly, by using “include”, the project becomes more structured. All the modules are built separately, but have the ability to share variables in the makefiles. One can imagine that in a big project, there must be much more settings, such as LOCAL_C_INCLUDES, LOCAL_CFLAGS, LOCAL_LDFLAGS, LOCAL_LDLIBS and so on. Many submobules may have the same values for these settings. In those cases, we can extract the common part and put the common ones in a common makefile, and then include it in makefiles for each submodule. Doing that will save a lot of coding effort, and will make the makefiles easier to modify and maintain. When adding new submodules, the effort of writing new makefiles will be minimal.

5.5 About LOCAL_PATH and CLEAR_VARS

The following two NDK built-in functions are quite important:

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

The first one (LOCAL_PATH:= $(call my-dir)) retrieves the current local path of the Android.mk file, so that all the variables in the same file can generate absolute path based on this local path. The command LOCAL_PATH:= $(call my-dir) clears all the NDK built-in variables starting with LOCAL_, such as LOCAL_SRC_FILES, LOCAL_C_INCLUDES, LOCAL_CFLAGS, LOCAL_LDFLAGS, LOCAL_LDLIBS and so on, except for the LOCAL_PATH.

This works perfectly fine when you have just a single Android.mk in the project. However, if you use “include” to put multiple makefiles together, you need to be careful about the above two commands.

The reason is that the LOCAL_PATH variable can be overwritten by the subsequent call to the command LOCAL_PATH:= $(call my-dir). For example, in the following case:

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

include subfolder/Android.mk
LOCAL_SRC_FILES:= $(LOCAL_PATH)/test.cpp

The problem with the above makefiles is that after the “include”, in the subfolder/Android.mk, the LOCAL_PATH may be modified by the included Android.mk. Then, when you try to locate the test.cpp, the ndk-build will fail, since the path to the file is wrong now.

If you pay enough attention to example 5, you will see a small trick has been used there. Let’s take a look at the top level makefile of example 5.

ex5_using_include_to_embed_make_files/makefiles/Android.mk

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

include $(TOP_LOCAL_PATH)/../src/submodules/Android.mk
include $(TOP_LOCAL_PATH)/../src/main/Android.mk

To avoid using the wrong LOCAL_PATH, I defined TOP_LOCAL_PATH to guarantee the two include use the same local path.

Some people use another trick, that is in the include Android.mk, the first thing is to back up the LOCAL_PATH variable to a temporary variable, and before exiting, restore the LOCAL_PATH. For example:

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

Similarly, if your modules share some common settings, be careful to use the include $(CLEAR_VARS), only use it when you know it is really safe and necessary.

5.6 How to debug .mk makefiles

For a small project, it might be easier to find out issues in the makefiles. However, as your project gets bigger, and especially when you include multiple modules including shared library, static library and executables, things get more complicated. You may face issues like “cannot find the rule to build foo“ or “cannot link the library bar“. Sometimes, it may be just due to a typo in some corner, but it may cause you several hours to find it, since the information provided by ndk-build is indeed very limited, and often times inadequate.

Fortunately, there are some extremely useful tools:

$(error error message)
$(warning warning message)
$(info information message)

These commands can be inserted into your Android.mk or Application.mk.

There are some differences among the above three commands. The $(info) command simply prints out some information, like printf() in C. The $(warning) command not only prints information, but also inserts the line number indicating where this warning is generated. The $(error)command will print information and also stop the subsequent building process. Normally, $(info) is enough for us to show information we are interested, for example, if we want to check the current LOCAL_PATH variable, we can do the following:

$(info LOCAL_PATH=$(LOCAL_PATH))

Or, if we defined a variable in our Android.mk file, we can check the value of it too:

BUILD_MODE:=NATIVE_MODE
DEVICE_NAME:=NEXUS-5
$(info BUILD_MODE is $(BUILD_MODE) for device $(BUILD_MODE))
Another approach

Another useful approach to help debug the makefiles is use the V=1 option for ndk-build. For example, in our example 5, if we use ndk-build V=1, the following is what we will see:

$ ndk-build NDK_APPLICATION_MK=./MyApplication.mk V=1
[Robert: Here I have skipped some print information...]
[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/src/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

The most useful part is the line starting with /cygdrive/d/development/android-ndk-r10d/toolchains/arm-linux-androideabi-4.8/prebuilt/windows-x86_64/bin/arm-linux-androideabi-g++. This basically shows what command line is exactly used to build the target output. You can see all the compilation and linking information here. For a complicated projects, especially the ones with embedded makefiles, this would allow us to check if the path for the libraries, path to the header files, and project dependencies are correctly set.

5.7 Build for multiple target architectures

Finally, we will discuss the method to build binaries for multiple target ABIs (ABI=Architecture Binary Interface). This will be useful when you want to release your native program for multiple platforms, such as ARM-v5, ARM-v7, x86 and so on. This will be extremely useful when building a native JNI shared library for an Android application, since in that case, you need to seriously consider how you publish and release your final applications. And the method you build your native libraries and release them will affect the compatibility of your applications.

For a typical project without linking external/3rd-party shared libraries, this could be achieved easily by setting the APP_ABI variable in the Application.mk. This has been discussed in details in the official NDK documents: Android Native CPU ABI Management. Basically, we can choose the target ABI in just one line in Application.mk:

 APP_ABI := armeabi armeabi-v7a mips x86

You can simply set APP_ABI := all to build native code for all the above target ABIs.

This method works perfectly for the projects in which you do not rely on any external libraries. Often times, we need to link some existing libs to our projects. That means there will chances that the external libs are provided in binary format. This will make the problem a little tricky. In these cases, we need to detect the current target APP_ABI for the current build, and then perform the right operations according to that architecture.

I will use a very practical problem I have met as an example to show why this is nontrivial. The problem I met was during my development of an Android application with OpenCL-accelerated native code. (For those who don’t know OpenCL: OpenCL is a open spec for heterogeneous computing maintained by Khronos Group; most of the modern desktop CPUs, GPUs and the latest generation of mobile GPUs, support OpenCL, therefore, you can utilize OpenCL to harness the power of GPU’s parallel architecture to accelerate your algorithms.). The way the OpenCL works is that the SoC chip vendors implement the OpenCL software stack including drivers, compilers in shared libraries. The problem here is that for different devices with application SoC chipsets provided by different vendors, the OpenCL support is enabled by the driver library, which is typically a shared library residing in the directories such as /system/vendor/lib, or /system/lib, or some other directories. Therefore, for different chipsets used in the mobile devices (smartphones or tablets), the shared library will be very different. A problem of building OepnCL program is that you need to dynamically link to these OpenCL driver library, such that the OpenCL API functions called by your application can be resolved during linking time.

The following table shows the OpenCL libraries for mobile GPU from a few major mobile SoC chip vendors.

SoC ChipsetGPUArchOpenCL library
Samsung Exynos (5420 or 5433)ARM Mali T628 or T760ARM/system/vendor/lib/egl/libGLES_mali.so
Qualcomm Snapdragon 800, 801, 805, 810Adreno A330, A420, A430ARM/system/vendor/lib/libOpenCL.so
Intel Atom Z3560PowerVR G6430x86/system/vendor/lib/libPVROCL.so

As we can easily see that the shared libraries are located in different places in the Android system, with different names, and even with different architectures. If you want to build an application support all the above platforms, build the native part with correct dynamic linking with be crucial.

We can achieve this goal by using the TARGET_ARCH variable or TARGET_ARCH_ABI variables, which indicate the current target architecture or target architecture ABIs.

The TARGET_ARCH and TARGET_ARCH_ABI is slightly different: TARGET_ARCH reports the architecture name; while TARGET_ARCH_ABI reports architecture along with the instruction set version. For example, if the current APP_ABI is “armeabi-v7a”, the TARGET_ARCH will only show “arm”, while the TARGET_ARCH_ABI will be “armeabi-v7a”. Keep this in mind, so that you can take full advantage of these two variables.

The following is a example in which we build a native program (or shared library used by JNI) targeting at the support of multiple architectures:

ifeq ($(TARGET_ARCH),x86)
    GPU_FAMILY=powervr
    OPENCL_LIB := PVROCL
    OPENCL_INC_DIR    := $(OPENCL_COMMON)/include/CL12/
    OPENCL_LIB_DIR    := $(OPENCL_COMMON)/libs/$(GPU_FAMILY)_$(TARGET_ARCH)/
    LOCAL_LDLIBS    := -llog  -L$(OPENCL_LIB_DIR) -l$(OPENCL_LIB) 
    MYMODULE_NAME:=mymodule_$(GPU_FAMILY)
    include OTHER_MK_FILES.mk
endif

ifeq ($(TARGET_ARCH),arm)
    #Adreno
    GPU_FAMILY:=adreno
    OPENCL_LIB := OpenCL
    OPENCL_INC_DIR    := $(OPENCL_COMMON)/include/CL12/
    OPENCL_LIB_DIR    := $(OPENCL_COMMON)/libs/$(GPU_FAMILY)_$(TARGET_ARCH)/
    LOCAL_LDLIBS    := -llog  -L$(OPENCL_LIB_DIR) -l$(OPENCL_LIB) 
    MYMODULE_NAME:=mymodule_$(GPU_FAMILY)
    include OTHER_MK_FILES.mk

    #Mali
    GPU_FAMILY:=mali
    OPENCL_LIB := GLES_mali
    OPENCL_INC_DIR    := $(OPENCL_COMMON)/include/CL11/
    OPENCL_LIB_DIR    := $(OPENCL_COMMON)/libs/$(GPU_FAMILY)_$(TARGET_ARCH)/
    LOCAL_LDLIBS    := -llog  -L$(OPENCL_LIB_DIR) -l$(OPENCL_LIB) 
    MYMODULE_NAME:=mymodule_$(GPU_FAMILY)
    include OTHER_MK_FILES.mk
endif

This is part of the complete makefile, which only shows the section of using TARGET_ARCH variable. After this section, we will include a few makefiles to perform the regular build process. Please notice that the $(OPENCL_COMMON) folder contains the shared libraries for different target architectures (we can adb pull them from the actual devices).

By doing this, we basically achieve a few different versions of the native code. Assume the module name for the native program is “mymodule”. Then the final generated binary files will have the following structure.

+-- libs
|   +-- armeabi-v7a
|       +-- libmymodule_adreno.so
|       +-- libmymodule_mali.so
|   +-- x86
|       +-- libmymodule_powervr.so

These libraries will be packed into the Android APK installer. During installation, the correct architecture version will be installed based on the primary ABI property (please read here for details) of the given device.

In our example, if we install the APK on an device with ARM CPU, the first two .so files will be installed. In your program, you can easily detect the device chip vendor, and load the corresponding shared library. If the APK is installed on a x86 device, then the only x86 library will installed and will be loaded automatically when the application executes.

Of course, you need to call System.loadLibrary() function in your JAVA code or link the shared library dynamically using dlopen() and dlsym() functios in the native code, but once we get the above libraries, the effort of loading the correct libraries becomes trivial.

6. Summary

In this article, we have covered the basics of ndk-build, followed by the discussion of some semi-hidden features or techniques in the ndk-build system, which I personally think are quite useful. I hope this article is useful for Android NDK developers.

Although every single techniques might be easy and straightforward, when you combine them all together, I am sure that you will be able to create some not only powerful but also efficient build script. Please download the examples from github and give them a try, what you need is only the Android NDK, and an Android device (or Virtual devices such as Genymotion).

If you think the article is useful, or if you think there is room I could improve. Please comment.
Or if you have some even better techniques I do not cover, please also leave a comment.


http://web.guohuiwang.com/technical-notes/androidndk1

Mastering Android NDK(pdf+epub+mobi+code_files).zip 立即下载

相关推荐

Mastering Android NDK-Packt Publishing 2015.epub

Android ndk 经典好书

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 ...

omnispace的博客 474

YOLO26算法车道线检测+训练好的模型+360张数据集+pyqt可视化界面.zip

附训练好的 YOLO 权重与 PyQt 可视化检测界面,页面底部含可视化效果预览,毕设/课设开箱即用。 【数据集概况】 · 检测类别(中文):[未获取()] 【训练曲线与评估图】 【模型训练配置】 参数 | 值 模型 | yolo26n 训练轮数 | 100 epochs 输入尺寸 | 640x640 批次大小 | 24 优化器 | auto 初始学习率 | 0.01 训练设备 【关键指标汇总】 训练了 55 个 epoch,最终轮指标: 指标 | 数值 mAP50 | **0.9512** mAP50-95 | 0.6746 Precision | 0.8796 Recall | 0.8825 train/box_loss | 0.8608 train/cls_loss | 0.7232 val/box_loss | 0.9955 val/cls_loss | 0.5511 【训练过程分析】 这轮训练我经历了从初始到稳定的曲折过程。前 20 个epoch,loss 曲线呈现了陡峭的下降趋势,mAP50 指标从 0.8 开始逐步攀升,仿佛在寻找一个合适的平衡点。然而,从第 21 个epoch 开始,mAP50 的提升速率显著放缓,整体曲线显得有些曲折,有时甚至停滞不前,让我一度以为训练出现了问题。直到第 35 个epoch,loss 开始逐渐趋于平稳,这标志着模型已经找到了一个较好的收敛区间。到了最后的 20 个 epoch,尽管 mAP50 依旧在稳步上升,但提升幅度已经不大,这意味着模型已经接近其性能天花板。 在这个过程中,我重点关注了 box_loss 和 cls_loss 的变化。初期阶段,box_loss 快速下降,从 0.9 逐渐降至 0.86,而 cls_loss 则从 0.75 逐步下降至... 使用场景:毕业设计、课程设计、深度学习入门、工业场景落地验证。

Android NDK 开发方面的书籍

Java数字图像处理:编程技巧与应用实践(这本先看(主要为图像算法), 下面那本OpenCV Android后看(主要算法的在android的 应用))。 包邮OpenCV Android开发实战 贾志刚 智能系统与技术丛书 NDK 核心模块 图像处理书 这一本书介绍基于NDK的人脸美颜开发的;(下面两本没有了) 推荐网上的两篇tutorial,英文的,但是讲的比较全面。(下面这两本网上...

疾风剑豪 2250

IEC 61851-23:2023 电动汽车导电充电系统 - 第23部分:直流电动汽车供电设备 - 完整英文电子版(388页)

IEC 61851-23:2023适用于用于在供电网络与电动汽车(EV)之间实现能量传输的电动车供电设备,A侧额定最大电压可达1000伏交流电或最高1500伏直流电,B侧额定最大电压最高可达1500伏直流电。 本文件规定了系统A、系统B和系统C的电动车供应设备,定义见附件AA、附件BB和附件CC。

关于AI开发方法论-以文档为索引的高质量协作开发:让一次现象描述精准命中几个代码块

对AI的编码规范进行约束

ndk-build出错,错误以及解决办法如下

用NDK编译jni目录下的文件了,我的系统是Ubuntu10.04,NDK版本是android-ndk-r7b。 切换到工程的jni目录下执行:ndk-build(ndk-build的路径已经添加到系统路径了,如果没有添加,需要指定完整路径),直接报一下错误: [html]view plaincopy Android...

weixin_33869377的博客 1245

syslog_test.py

syslog_test.py

【友云音】【问题排查记录-9】master启动异常

问题现象 uap水平集群增加 问题分析

Z.Virgil的博客 2169

Simulink “双碳”背景下汽车减少碳排放建模与仿真

内容概要:本文围绕“双碳”战略背景,系统阐述了基于Simulink的汽车碳排放建模与仿真技术,重点引入模糊PID控制优化动力系统性能,以实现节能减排目标。文档整合了多个Matlab/Simulink科研仿真案例,涵盖无刷直流电机(BLDC)控制、三相PWM整流器、电池管理系统、路径规划、信号处理、智能优化算法及图像处理等领域,全面展示Matlab在智能控制与绿色能源系统中的应用价值。同时强调科研应兼具严谨逻辑、创新思维与工具赋能,并提供完整的仿真资源链接与学习路径支持实践落地。; 适合人群:具备Matlab/Simulink基础,从事自动化、电气工程、车辆工程、控制科学、电力电子及智能制造等相关领域的科研人员或高年级本科生、研究生;适用于开展节能减排、新能源系统优化、智能控制算法研究等课题的研究者。; 使用场景及目标:①学习在Simulink中构建汽车能耗与排放仿真模型并应用模糊PID进行动态优化;②掌握BLDC、PWM整流器等关键部件的建模与控制策略设计;③借鉴丰富案例提升科研效率,拓展在智能控制、路径规划、信号处理等方向的应用能力;④通过提供的网盘资源获取完整代码与资料,支撑论文写作与项目开发。; 阅读建议:建议按照资源目录顺序系统学习,结合百度网盘提供的Matlab/Simulink代码动手实践,重点关注控制策略的设计、仿真结果的分析与优化过程,同时注重培养科研思维与工程实际问题的结合能力。

西部牛仔_1794033330851.apk

西部牛仔_1794033330851.apk

Android NDK Gradle3.0 以上最新生成.so之旅

写这篇文章前,自己踩过很多坑,希望能助大家一步跨坑。 1、环境搭建:通过SDKManager-SDK Tools下载NDK插件,下载后到SDK Location里面检查里面的NDK路径: 检查local.properties文件里面有没有NDK路径: 2、使用native关键字编写JNI接口: 编写之后,make project,再到工程目录E:\work\MyApplication\stu

xiaozhu0922的博客 1万+

状态估计变分贝叶斯近似的递归噪声自适应卡尔曼滤波(Matlab代码实现)

内容概要:本文详细介绍了一种基于变分贝叶斯近似的递归噪声自适应卡尔曼滤波方法,并配套提供了完整的Matlab代码实现。该方法旨在解决传统卡尔曼滤波在面对过程噪声和观测噪声统计特性未知或时变情况下性能退化的问题,通过引入变分贝叶斯推断技术,实现对噪声协方差的在线联合估计与动态更新,从而提升滤波器在非平稳环境中的状态估计精度与鲁棒性。文中系统阐述了算法的理论基础、数学推导流程、递归计算结构及关键实现步骤,涵盖了概率图模型构建、似然函数近似、变分迭代优化等核心技术环节,适用于复杂动态系统中的自适应信号处理与状态估计任务。; 适合人群:具备良好线性系统理论、概率统计与贝叶斯估计基础,并熟悉Matlab编程的研究生、科研人员及从事导航、控制、传感融合等相关领域的工程技术人员; 使用场景及目标:①应对实际工程中因噪声先验信息缺失或环境变化导致的标准卡尔曼滤波失效问题;②应用于无人机导航、智能驾驶、工业状态监测、传感器融合等高精度自适应估计场景,提升系统在不确定环境下的稳定性和准确性; 阅读建议:建议读者结合所提供的Matlab代码逐模块研读,重点理解变分贝叶斯迭代过程中噪声协方差矩阵的后验更新机制,并通过仿真实验对比所提方法与标准KF/EKF/UKF的估计性能差异,深入掌握噪声自适应滤波的设计思想与实现技巧。

ANDROID_SDK_HOME设置错误(ANDROID_SDK_ROOT)

打开AndroidStudio后,错误提示如下: SDK Validation ANDROID_SDK_HOME is set to the root of your SDK: C:\Users\romulus\AppData\Local\Android\Sdk This is the path of the preference folder expected by the Android ...

xiaoxiaosunzhao的专栏 1万+

PUMA:DOA估计模式的改进实现(Matlab代码实现)

内容概要:本文围绕PUMA(Propagator Method-based DOA Estimation)算法在波达方向(DOA)估计中的改进实现展开,重点在于通过Matlab编程对算法进行仿真与性能验证。作为阵列信号处理领域的高分辨率DOA估计算法,PUMA在传统方法基础上提升了估计精度与鲁棒性,尤其在低信噪比、小样本等恶劣条件下仍能保持优异性能。文中系统阐述了PUMA算法的理论基础、数学模型及其相较于MUSIC、ESPRIT等经典算法的优势,并通过仿真实验进行对比分析,展示其在分辨力、稳定性和计算效率方面的综合优势,适用于实际雷达、无线通信与电子侦察等应用场景。; 适合人群:具备信号处理、通信工程、电子信息或雷达系统等相关专业背景的研究生、科研人员及工程技术人员,熟悉阵列信号处理理论与Matlab编程环境者尤佳; 使用场景及目标:①用于学术研究中DOA估计算法的性能优化与新方法对比验证;②应用于雷达系统、5G/6G通信、智能天线、电子对抗等领域的高精度空间谱估计任务;③作为Matlab仿真实践资源,支持课程设计、学位论文复现与科研项目开发; 阅读建议:建议结合阵列信号处理基础知识体系学习本资源,重点关注PUMA算法的改进机制与核心代码实现逻辑,建议动手运行并调整仿真参数以深入理解不同因素(如阵元数、信噪比、快拍数)对估计性能的影响。

Datalog语言详解

Datalog是一种声明式逻辑编程语言,起源于数据库理论和逻辑编程领域。作为Prolog的子集,它专注于递归查询和推理,通过定义事实和规则来描述数据关系而非计算步骤。Datalog的关键特性包括规则安全、递归、分层否定和扩展聚合功能,保证了程序的终止性和高效性。相较于SQL,Datalog更擅长处理递归查询;相比Prolog,它更注重数据库友好性。Datalog在程序分析、知识图谱、网络安全等领域有广泛应用,其声明式特性使代码简洁易维护,但同时也面临生态系统小、性能调试难等挑战。现代工具如Soufflé、D

bulita great world 1838

kettle 启动Spoon.bat时,错误提示Could not create the java virtual machine

错误提示:Could not create the java virtual machine SpoonDebug.txt错误信息: DEBUG: Using JAVA_HOME DEBUG: _PENTAHO_JAVA_HOME=C:\Program Files (x86)\Java\jdk1.6.0_02 DEBUG: _PENTAHO_JAVA=C:\Program File

小克里 3万+

STK12与Python联合仿真(一):环境搭建

STK仿真python接口,安装与测试

qq_42635142的博客 1万+

设置android home环境变量,[错误:找不到“ANDROID_HOME”环境变量失败.尝试设置手动设置...

我在Linux中使用Cordova薄荷17我安装了android sdk,cordova,android studio,nodejs 4问题在这里当我想在codova中构建应用程序并在终端上运行它时:sudo cordova构建android表明Running command: /home/tnt/hello12/platforms/android/cordova/build[Error: Fai...

weixin_35997246的博客 721

矢量量化的基本原理:高效数据压缩与信息检索的关键矢量量化(Vector Quantization,简称VQ)是一种数据压缩和信

此外,在矢量量化过程中,可能会引入一定的信息损失,对一些精细特征的表示可能存在一定的误差。它通过将高维数据映射到低维码本空间中,并将相似的数据映射到相同的码本向量中,实现对数据的高效表示和存储。最重要的是,矢量量化可以用于数据的检索和分析,提供了高效的数据查询和处理手段。通过使用码本索引,我们可以高效地存储和检索大量的数据。在图像处理中,矢量量化可以用于图像压缩,将复杂的图像数据转化为较少的码字,实现图像的存储和传输。矢量量化通过将连续的高维数据分割并映射到有限的码本集合中,实现数据的离散表示和压缩。

m0_70886513的博客 1371

注册电气工程师详细教材

注册电气工程师详细教材

qq_40844444的博客 4242
上一篇: Building an Android NDK with recent GCC and binutils (2011)
下一篇: Mastering Android NDK Build System - Part 2: Standalone toolchain
Omni-Space
博客等级 码龄11年 2158粉丝 23原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值