Three ways to compile for Android Native

http://justmobiledev.com/wp-content/uploads/2018/03/Native-Android-NDK-.jpgThree ways to compile for Android Native

Scrambled, Benedict, or sunny-side up – if you want to find out how your like your native builds compiled, this post is for you. In this post I would like to provide a practical guide for compiling Android native C/C++ code different waysfor the different Android architectures.

What are the different ways to compile for native Android?

CMAKE versus ndk-build

Android currently provides two build systems for native: CMAKE and ndk-build.

CMAKE

CMAKE is a cross-platform, open-source build system that was introduced in Android Studio in version 2.2. CMAKE is currently the default build system, ndk-build is still supported for legacy reasons. The way CMAKE builds your ndk builds can be controlled in your module-level gradle file with a variety of variables, e.g. which Android API level to target or whether to use C++ exceptions as described here. You can read more about CMAKE on the project website.

To define how your native C/C++ files are compiled, you use one or multiple CMakeLists.txt files, in which you can use CMAKE language commands. For a full reference of the CMAKE language, please refer to the documentation.

Android Studio versus Command Line

Android Studio

For building native code you have the choice to build inside or outside the Android Studio IDE. The advantages of using Android Studio are the fact that you have a build system integration with gradle and the rest of your modules. So you can build the native library and all modules consuming it at once and run it on a device in one step. The building of the library itself will be slower in Android Studio. Naturally, Android Studio also comes with syntax high-lighting for the gradle system, your CMakeLists.txt and your native source code. Android Studio is also more useful when developing the JNI layer or the integration between Java and native layer or when debugging these layers.

Command-line

If you are most of a ‘hard-boiled egg’ kinda guy, you don’t really give a crap about fancy IDEs, you enjoy fast compiles and are really just focused on developing your C/C++ code, then command-line builds are for you.

Which architecture do you need to target?

As with so many other technical questions, the answer is: It depends. If you are developing an Android app for a specific manufacturer and device model, you can find out which CPU architecture it uses by researching the processor it uses and sifting through the processor hardware specs. If you have the actual device, you can install an app like ‘Droid Hardware Info‘ . More about finding out about your device hardware in this article here.

Most likely you will be developing for the whole device market, so you want to target the most common, or even all CPU architectures on devices on the market. To find out which architectures are currently supported by the Android OS, you can check the handy Android documentation here.

Here are the instruction sets currently supported by Android:

  • armeabi
  • armeabi-v7a
  • arm64-v8a
  • x86
  • x86_64

As new processors are being developed – and new devices are hitting the market almost daily – this list is subject to change, so keep an eye on it. And these are the architectures we are going to compile for in this post. So get your Android Studio and your C-compiler ready – we’re going to get this show on the road.

Android Studio and CMAKE

First, go ahead and launch Android Studio. Next, open the download manager and install CMAKE, LLDB and NDK from the SDK Tools tab. LLDB is the debugger Android Studio uses to debug native code. The NKD is some 850 MB zipped and well over a Gig unzipped, so make sure you have sufficient disk space on your machine.

Now, let’s go ahead and create a new project in Android Studio.

Compile and run this baby and you’re done.

Say what??? But… but… (lower lip quivering) – I haven’t wrote a single line of native code, let alone configure the build system, etc.

True. So let’s take a look at what just happened.

By selecting the ‘Include C++ Support’ flag when you created the new project, Android did a couple of things for us, specifically:
1. It selected ‘cmake’ as the default build system
2. It added ‘cmake’ in your module gradle file in the defaultConfig section:

1
2
3
4
5
6
7
8
9
android{
    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
}

3. It create a build file for cmake called ‘CMakeLists.txt’ in the PROJECT_ROOT/app directory. In order to see the file in the IDE, you have to switch to the Android Studio ‘Project’ view.

4. It added a reference to the CMakeLists.txt file in the module build gradle ‘android’ node:

1
2
3
4
5
6
7
android{
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

5. It created a C++ file called ‘native-lib.cpp’ in the PROJECT_ROOT/app/src/main/cpp directory with a single JNI/C++ function that returns ‘Hello from C++’ to the Java layer.

6. In the MainActivity, it added a static directive to load the native library ‘native-lib’:

1
2
3
    static {
        System.loadLibrary("native-lib");
    }

7. At the bottom of the MainActivity, it declares the JNI method stringFromJNI() so that it can be called from the MainActivity Java code.

1
public native String stringFromJNI();

8. Finally, in the MainActivity, onCreate() method, it calls the native stringFromJNI() function and sets the returned string in a TextView:

1
2
3
        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());

Let’s take a closer look at the CMakeLists.txt, which contains the build directives for cmake. The magic, that brings all the pieces together happens in the following section:

1
2
3
4
5
6
7
8
add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

Here we are specifying the C/C++ files that need to be compiled, the type of library and the name of the library.

I know it’s a lot of stuff but it’s really not too hard to understand once you read it a second time.

Gradle command-line builds

You can use the same gradle build system that used in Android Studio and execute it from the command line using the ‘gradlew’ wrapper.

You can use gradle wrapper with either cmake or ndk-build, depending on how you linked your C++ project with gradle. You can control which build system to use within Android Studio by right-clicking on your project, selecting the ‘Link C++ project with gradle’ option and selecting your build system. Additional configuration in the module gradle file is necessary. Please refer to the Android NDK guides for details.

For the purpose of this blog, we’ll stick with CMAKE since is was already configured by Android Studio when we created the project.

The most important gradlew commands are:

1
2
3
gradlew clean
gradlew build
gradlew clean build

Once you executed ‘gradlew build’ you should have your .so shared libs build with command-line gradle.

Android Studio and command-line ndk-build

Now we are going to take the project you just created and switch the build system to a command-line ndk-build. There are multiple ways to set this up, for example using Cygwin, a linux environment for Windows. I’ve found the easiest way is to set up an Ubuntu sub-system on Windows 10. Instructions on how to set this up can be found here.

Once you successfully installed Ubuntu from the MS app store, go ahead and download the linux-x86_64 distribution of the ndk to your local machine from the Android Developer website here. Make sure you have sufficient disk space on your machine since the unzipped file may take up more than a Gig.

Go ahead and create a new file called ‘Application.mk’ in the PROJECT_ROOT/app/jni directory. The Application.mk file is a tiny GNU Makefile fragment that defines several variables for compilation, e.g. which minimum API version to use, which modules to compile, or which architectures to target.

Application.mk

This is how the Application.mk could look like:

1
2
3
APP_BUILD_SCRIPT := Android.mk
APP_ABI := all
APP_PLATFORM := android-23

So we are specifying where the Android.mk is located, that we are targeting all instruction sets, and that the minimum API version is 23.
For more information, the Application.mk documentation is here.

Android.mk

The Android.mk file resides in a subdirectory of your project’s jni/ directory, and describes your sources and shared libraries to the build system. It is really a tiny GNU makefile fragment that the build system parses once or more.
Let’s go ahead and create a new Android.mk file and place it in the PROJECT_ROOT/jni directory.
Add the following content:

1
2
3
4
5
LOCAL_PATH := $(call cpp)
include $(CLEAR_VARS)
LOCAL_MODULE := fourwaystocompilenative
LOCAL_SRC_FILES := native-lib.cpp
include $(BUILD_SHARED_LIBRARY)

The variable ‘LOCAL_PATH’ indicates the location of the source files in the development tree. Here, the macro function my-dir, provided by the build system, returns the path of the current directory (the directory containing the Android.mk file itself).

The CLEAR_VARS variable points to a special GNU Makefile that clears many LOCAL_XXX variables for you, such as LOCAL_MODULE, LOCAL_SRC_FILES, and LOCAL_STATIC_LIBRARIES. Note that it does not clear LOCAL_PATH.

Next, the LOCAL_MODULE variable stores the name of the module that you wish to build. The build system, when it generates the final shared-library file, automatically adds the proper prefix and suffix to the name that you assign to LOCAL_MODULE. For example, the example that appears above results in generation of a library called libfourwaystocompilenative.so./

The ‘LOAD_SRC_FILES’ variable enumerates the source files, with spaces delimiting multiple files

You can use a variety of variables and macros in the Android.mk file, which are explained in detail in the documentation.

Your project should now have the following directory structure:

1
2
3
4
5
6
7
8
9
  PROJECT_ROOT
    app
      jni
        Application.mk
        Android.mk
      src
        main
          cpp
            native-lib.cpp

Next, you want to open up an Ubuntu prompt and add your NDK directory to your Path variable so you can use ndk-build:

1
2
3
export PATH=$PATH:<path to your NDK>
for example:
export PATH=$PATH:/mnt/d/Android/NDK/android-ndk-r10b

Next, cd to your PROJECT_ROOT/app directory and now you should be able to use your ndk build commands:

1
2
ndk-build clean
ndk-build

That’s it. You now should have your .so shared libs build with ndk-build in the PROJECT_ROOT/libs directory.

Hope you enjoyed the post. Happy programming!