Android NDK: Using C/C++ Native Libraries to Write Android Apps

JetRuby Agency
JetRuby Agency
Published in
12 min readMar 14, 2018

--

What first comes to mind when you hear «Android app»? Most certainly, it’s Java or Kotlin. However, apart from Android SDK, Google also has NDK — Native Development Kit, which makes it possible to write apps using C/C++ code.

In this post, we’ll show you how to mix existing C/C++ native libraries into an Android Java project. We will guide you through the process of building a small Android Studio project, in which we will include a C++ library called “SuperpoweredSDK” and use some of its API in the Java layer.

By the end of this article, you’ll learn how to create a simple app, which will be able to load an .mp3 track from the raw folder, toggle playback, and apply tempo and pitch shifting in real time.

But before we dive in, please check out our newly released landing page about our mobile app development expertise:

And now - let’s go!

What is Android NDK?

Android NDK is a companion tool of Android SDK that allows you to build performance-critical parts of your app using native code (with the help of such languages as C and C++).

It provides headers and libraries that allow you to build activities, handle user input, use hardware sensors, access application resources, and more, when programming in C or C++. If you write native code, your applications are still packaged into an .apk file and they still run inside of a virtual machine on the device. The fundamental Android application model doesn’t change.

Advantages of Android NDK

In a nutshell, it allows you to:

  • increase performance (sorting big data, complex algorithms, CPU-intensive tasks)
  • utilize the power of existing libraries written in C/C++ or your own native code that you want to reuse
  • create cross-platforming solutions (Android & iOS)
  • leverage direct low-level programming (i.e hardware operation)

Here is the official documentation of Android NDK: https://developer.android.com/ndk/guides/index.html

What is Java Native Interface?

Java Native Interface defines the way managed code (written in Java) interacts with native code (written in C/C++). It’s vendor-neutral and supports loading code from dynamic shared libraries. JNI allows you to call native methods from Java and vice versa.

Let’s elaborate on this one a bit. What does «native» code mean at all? «Native» means that the code is native to the processor that your code is compiled for. C/C++ are compiled directly into CPU-understandable instructions without any intermediate mechanisms like JVM. For more details, you can refer to these guides:

Superpowered SDK

Superpowered Audio Engine is a cross-platform library that focuses on mobile low-latency realtime audio processing. The authors claim it to be more effective and deliver more performance than OpenSLES and Apple Core Audio.

Basically this SDK provides you with a set of cool features like audio filters, effects, audio decoding capabilities, support for different audio formats and much more. Imagine playing a track, applying flanger or some reverb and time-stretching/pitch-shifting it simultaneously — all of it in real time, blazingly fast and without worrying about the infamous 10ms problem.

You can find everything you need to know about Superpowered SDK here:

Prerequisites

Here is a list of things you need first:

1) Install Android Studio 3.0 Stable https://developer.android.com/studio/index.html

2) Download Superpowered SDK library and extract it’s zip content into any directory you wish: https://github.com/superpoweredSDK/Low-Latency-Android-Audio-iOS-Audio-Engine

3) Download and install Android NDK tools set, CMake and LLDB from the Android SDK manager in Android Studio https://developer.android.com/ndk/guides/index.html#download-ndk

4) Android app development basics: creating activity, XML layout, using standard widgets, editing gradle files, importing drawables, be acquainted with application lifecycle.

5) Some basic knowledge of Android Data Binding. I’m using Data Binding in the project to get rid of a boilerplate findViewById code when inflating activity and working with it’s views.

Creating a new Android Studio project with a support for C/C++

The process of creating a new AS project with a support of native code is the same as creating a standard Android project apart from a few additional steps:

  1. In the Configure your new project section of the wizard, check the Include C++ Support checkbox. Click Next.
  2. Complete all other fields and the next few sections of the wizard as normal.
  3. When prompted, choose create an Empty Activity
  4. In the Customize C++ Support section of the wizard, you can customize your project with the following options:
  • C++ Standard: Use the drop-down list to select which standardization of C++ you want to use. Selecting Toolchain Default uses the default CMake setting. The other two options are C++11 and C++14.
  • Exceptions Support. With this option enabled, Android studio adds the -fexceptions flag to cppFlags in your module-level build.gradle file, which Gradle passes to CMake. The default value is -fno-exceptions. This may be used to ensure compatibility with versions of NDK earlier than NDKr5, which did not have support for exceptions handling. To read about it in more details here is the link: https://gcc.gnu.org/onlinedocs/libstdc++/manual/using_exceptions.html
  • Runtime Type Information Support: with this option, you will be able to use code reflection features in your C++ code during the runtime. Check this box if you want support for RTTI. If enabled, Android Studio adds the -frtti flag to cppFlags in your module-level build.gradle file, which Gradle passes to CMake.

5. Click Finish.

After the project is set up you can always manually turn all of these features on and off by just going to module build.gradle file and adding/removing the flags.

Let’s have a quick look at some of the key files Android Studio has created for us:

CmakeLists.txt under the SuperpoweredSample/app directory:

Android Studio’s default build tool for native libraries is CMake. Configuring CMake is necessary to build your native source code into a library. The CMakeLists file is also needed if you are importing and linking against prebuilt or platform libraries (like we are doing right now with Superpowered SDK).

To include your native library project as a Gradle build dependency, you need to provide Gradle with the path to your CMake or ndk-build script file. When you build your app, Gradle runs CMake or ndk-build, and packages shared libraries with your APK. Gradle also uses the build script to know which files to pull into your Android Studio project, so you can access them from the Project window. If you don’t have a build script for your native sources, you need to create a CMake build script before you proceed.

Once you link Gradle to a native project, Android Studio updates the Project pane to show your source files and native libraries in the cpp group, and your external build scripts in the External Build Files group.

build.gradle in app module:

As you can see, the settings that have been tweaked when creating this project are reflected here, namely:

  • support for STL exceptions and RTTI feature

And this is how Gradle is linked with CMake project build script. This way it is possible to add native library project as Gradle dependency. You’re basically providing the path for the CMakeLists file inside the project.

In this case, CmakeLists.txt is in the same directory as the module build.gradle file so by providing just the name of the build script you’re specifying a relative path to the file. In the event that your CmakeLists.txt lies in some other directory you’ll need to provide a full path to the file

Once you’ve linked Gradle to theCMake project, you can change the way CMake builds native libraries by configuring certain NDK-specific variables. To do so, you need to pass some arguments to CMake file from the module-level build.gradle file. Let’s add some arguments:

PATH_TO_SUPERPOWERED — here you’re passing the path to the superpowered library that you’ve recently got from the local.properties file into a CMake build script which will be used there

PANDROID_TOOLCHAIN=clang — Specifies the compiler toolchain CMake should use. Possible options are “clang” and “gcc”. Since Gcc is deprecated in Android NDK, you’ll be using clang toolchain.

ANDROID_ARM_NEON — Specifies whether CMake should build the native library with NEON support. NEON is an Andvanced SIMD extension used for the ARM CPUs architecure. This is basically an extended instructions set for ARM processors. NEON can be used to dramatically speed up certain mathematical operations and is particularly useful in DSP and image processing tasks. Since SuperpoweredSDK is an Audio Processing engine aimed at high performance it needs this option enabled.

For the full list of the available CMake variables you may refer to: https://developer.android.com/ndk/guides/cmake.html#variables

Then you need to specify some flags for the C/C++ compiler:

‘-O3’ — this option controls various sorts of optimizations that compiler does to the native code. To learn more about various levels of optimizations, check this guide: https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html#Optimize-Options

-fsigned-char — Allows the type char in the native libraries to be signed, like signed char. Each kind of machine has a default for what char should be. It is either like unsigned charby default or like signed char by default. By default on Android NDK char type is unsigned, but char is treated as signed on x86. This is an option imposed by Superpowered SDK authors, so we’ll include this as well.

We’ll modify the CMake build script to compile Superpowered library for the Android project.

Include Superpowered SDK library into the project

If you haven’t downloaded Superpowered SDK yet, it’s about time you did: https://github.com/superpoweredSDK/Low-Latency-Android-Audio-iOS-Audio-Engine

Let’s have a quick overview of key directories of zip contents:

  • Low-Latency-Android-Audio-iOS-Audio-Engine-master folder which includes several sample projects demonstrating some of the features provided by the library.
  • Low-Latency-Android-Audio-iOS-Audio-Engine-master/Superpowered. This is the library itself. It contains two types:
  • .a files — these are static libraries of the Superpowered engine
  • .h files — these are basically interfaces to the functionality implemented in .a static libraries.

Here is what you need to do next:

  1. Go to local.properties file and specify a location you extracted Superpowered library into. Your local.properties file should look like this:

2. Go to the module-level build.gradle file and do some modifications:

  • Create a Properties object to store your local.properties and then extract a variable for superpowerd.dir path:
  • Add some abiFilters that will tell NDK which CPUs the application will be compiled for. ABI stands for Application Binary Interface. The ABI defines how an application’s machine code is supposed to interact with the system at runtime. You must specify an ABI for each CPU architecture you want your app to work with. Different processors support different instructions set. By adding this method you ensure that your app’s native code can run on ~99% of all Android devices.
  • Enable Android Data Binding and Java 8 features:

This is how the build.gradle file finally looks like:

3. Now let’s go to CMakeLists and make some necessary additions to tell Android Studio to include a precompiled library:

Let’s break this down. Here you’ve added some key instructions to add and link native libraries:

  • file(GLOB CPP_FILES «*.cpp») — this one is used to collect a list of all source files that will be used in the library. Than the list is assigned to a variable CPP_FILES that will be used further.
  • add_library() — adds a library target to be built from the specified source files.

You’re passing several arguments here:

SuperpoweredExample — the name the library will have. The library will appear in the project view with this specified name

SHARED means the library will be linked dynamically and loaded at runtime

src/main/cpp/SuperpoweredExample.cpp — path to a .cpp file that the library will consist of. Currently it will consist only of a single file, so you specify it here.

It is also possible to specify several sources for a library, which you did in the second add_library() call. You’re going to use a piece of code from one of the Superpowered sample apps — CrossExample.

It can be found on the library Github project: https://github.com/superpoweredSDK/Low-Latency-Android-Audio-iOS-Audio-Engine/tree/master/Examples_Android

  • PATH_TO_SUPERPOWERED — this one is actually a path to the library you’ve set earlier in the build.gradle file with «-DPATH_TO_SUPERPOWERED:STRING=${superpowered_sdk_path}» line. Here, you’re including sources of the SDK itself.

After you’ve passed a variable to CMake from Gradle, you need to set it like this:

  • set(<variable> <value>… CACHE <type> <docstring> [FORCE]) — sets a given cache variable (entry). Cache entries are meant to provide user-settable values:

Now you’re able to read a path to SDK from it.

  • target_link_libraries(<target> … <item>… …) — specifies libraries or flags to use when linking a given target and/or its dependents:

You need to link SuperpoweredExample to SuperpoweredAndroidAudioIO, as the former depends on the latter and is using some of the methods provided there. After that, you link SuperpoweredAndroidAudioIO to the rest of the required libraries like OpenSLES, log and android. We also specified the path to the compiled Superpowered sources.

To delve more into the details visit this link for CMake documentation. There you will find descriptions for all of the CMake commands that you might find useful https://cmake.org/cmake/help/v3.4/index.html

Now you should be able to build the project and see the native library in the Project View.

Implementing UI and adding C++ code

Now let’s get to implementing a layout for the app. The app consists of just a single activity with some basic controls for audio playback and some handles for audio effects. Here is the full XML file:

Then you can start writing some code. The whole functionality is divided between two files: java/MainActivity.java and cpp/SuperpoweredExample.cpp. Let’s take a look at the SuperpoweredExample header file:

You need to include other header files that provide the functionality you want to use along with defining a couple of macros and constants used in the SuperpoweredExample.cpp:

  • Constructor and destructor. You need to do the initial engine setup, loading the track from the raw folder, setting up audio buffers and take care of resources deallocation when you’re done. You also need to initiate a player object that will take care of audio playback and will receive effects changes you apply with controls in MainActivity:
  • Implementing methods responsible for audio playback and effects processing:

Changing tempo:

Pitch shifting:

Seeking to a position. This will be called when tapping playback seekbar:

Here, you’re applying effects directly on the player instance.

This method will finally execute the actual work. By calling player->process() you’re processing audio input.

process() method will get called periodically by the audio engine itself via audioProcessing static method defined here in the same file:

The following are the actual JNI methods. They are linked to the Java layer, and you call these methods from MainActivity. Also, let the extern «C» labels not confuse you. They are use to tell C++ compiler not to do name mangling for these methods during compilation. This is a standard practice of using C code with C++ compilers. Always remember to use «extern «C»» with your native methods used in Java layer.

The naming convention also looks strange, and truly so. A native method name is concatenated from the following components:

  • the prefix Java_
  • a mangled fully-qualified class name
  • an underscore (_) separator
  • a mangled method name
  • for overloaded native methods, two underscores (__) followed by the mangled argument signature

JNIEXPORT is defined in NDK_ROOT/platforms/android-9/arch-arm/usr/include/jni.h. JNIEXPORT is used to make native functions appear in the dynamic table of the built binary (*.so file). If these functions aren’t in the dynamic table, JNI will not be able to find the functions to call them so the RegisterNatives call will fail at runtime.

The JNIEnv type is a pointer to a structure storing all JNI function pointers. It is defined as follows:

typedef const struct JNINativeInterface *JNIEnv;

For more details about referring to JNI-specific types and structs you may browse Oracle documentation:

https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/functions.html

Next, in MainActivity, you need to statically load the libraries:

Then you need to declare methods corresponding to defined JNIEXPORT methods in SuperpoweredExample.cpp:

Notice that the “native” keyword is used to declare a native method.

Luckily, in the Java layer, there is no need for JNIEXPORT naming convention. The method name should just include the actual method name without any prefixes. You will be calling these methods in MainActivity, and those calls will be addressed to the previously defined native JNIEXPORT methods in SuperpoweredExample.cpp.

When the activity starts, you need to get the sample rate and buffer size recommended for the particular device this app is running on:

These attributes are used in the native libraries when allocating input and output buffers.

This piece of code is responsible for toggling tempo changes. radioGroup listens to user interactions and calls the native method to change the track’s tempo:

You’ve also got a method to toggle playback start and pause:

Here is how MainActivity should look like after that:

In conclusion

Woo-hoo! If you’re reading this, it means you’ve successfully built your own Android application using Android NDK.

By the way, the full project is available on Github: https://github.com/artem-leushin/superpowered-ndk-example.

If you have any related questions or simply want to share you experience on the subject, feel free to drop us a line at questions@jetruby.com

P.S. If you liked the article, please support it with claps!

Thanks for scrolling! =)

--

--

JetRuby is a Digital Agency that doesn’t stop moving. We expound on subjects as varied as developing a mobile app and through to disruptive technologies.