Integrate a C++ library with your Android build system

Purpose & Scope

This article is intended for a software engineer audience developing Android applications.
We will explain how to integrate a CMake based C++ library with your Android build system.
That includes compiling and debugging your C++ library from Android Studio along side your application.
This solution was implemented in October 2017 on Android Studio 2.3 and proved to work just fine with Android Studio 3.0 too.

Disclaimer

I started coding aged 12 and been working as a software engineer since 1997 however do not take my words for granted as I still have very limited experience on the Android platform.
Considering the flexibility of Gradle there is most certainly various ways to accomplish our goals nevertheless the solution presented in this document worked well for us.

Background

Our team of three software engineers inherited quite a large project with almost 400K source code lines in Java and nearly 3M in C++.
Parts of that code base has been under development for over a decade.
The Continuous Integration setup from the former team was using two build machines running separate instances of Jenkins:
  • Linux build machine doing the C++ library builds using a CMake custom tool chain.
  • Windows build machine doing the Android builds after downloading the native library binaries.
We use Android Studio and Visual Studio on our Windows developer workstations.
To build that C++ library on Windows developer's machines the former team gave us a Linux Virtual Machine image that was put together years ago.
That setup worked well enough but had the following inconvenience:
  • Two build machines to administrate.
  • Linux build machine setup arcane. None of us had the knowledge to redeploy such a build machine if it failed.
  • Convoluted C++ build process, slowing down developers.
  • No debugger support for C++ code, slowing down maintenance and new feature development.
Therefore we decided to build that C++ library with Gradle together with other Java modules and our application.

Proof of concept

Native build from application

Luckily for us, nowadays the Android Gradle Plugin does support inclusion of CMake projects through that ExternalNativeBuild.cmake property.
Therefore, in theory, it should be as simple as referencing our top level CMakeLists.txt from our application build.gradle.
It appears this is the approach taken by every code sample I came across such as this hello-gl2 sample.
That worked well enough for our proof of concept and within a couple of days we could build our C++ library.

Not variant friendly

However this results in building your library once per-variant of your application which was not the intended behaviour and caused inflated build times. In fact, the documentation linked above mentions that the cmake block "Encapsulates per-variant configurations for your external ndk-build project".
As a consequence this approach did not scale well with our application's 17 variants. In fact it would take Android Studio about 45 minutes just to synchronise after touching our build.gradle.
For the purpose of our proof of concept we therefore commented out most variants.

Specify your ABIs

To further speed up your build you will also need to tell Gradle which ABI you want to build as by default it seems to be generating all supported ones.
This is done using that abiFilters property, see samples below.

Debugging

Our C++ build was now working, albeit without variants, but still debugging was not.
It turns out that, by default, Gradle for Android strips out debug information from your binaries before packaging them in your APK.
To tell Gradle to keep debug information you will need something like that:
Code:
android {
    // ...
    packagingOptions {
        // By default .so libraries are striped from debug information when creating APK
        // To prevent this make sure your .so files are matching that doNotStrip pattern
        // Our patterns matches only debug variant of our libraries as named by our CMake configuration
        doNotStrip "**D.so"
    }
    // ...
}

Final solution

Embolden by our proof of concept success we were then looking for a solution we could actually use in our production environment.
That meant fixing our variant scaling issue.
In the following samples we will be assuming our C++ library is called Engine.

Native build from library

The way forward was to move our native build from our application to an Android library which would be included by our application as a dependency.
Where an android application build produces an APK the packaged output of an Android library will be an AAR zip archive. If like myself you are new to the world of Android or even Java libraries I strongly recommend you read through this excellent Android Library documentation.
We would then now have a directory structure similar to that one:
Code:
project
├ build.gradle
├ settings.gradle
├ app
│ └ build.gradle
└ lib
  └ build
    ├ AndroidManifest.xml
    ├ build.gradle
    └ CMakeLists.txt
Below you can find samples of some of the key files in that directory structure.

project/settings.gradle

Code:
// Here we define our Engine module making sure it is named properly and not just using the parent folder of the Gradle file.
include 'Engine'
project(':Engine').projectDir = file('lib/build')

project/app/build.gradle

Code:
apply plugin: 'com.android.application'

android {

    packagingOptions {
        // By default .so libraries are striped from debug information when creating APK
        // To prevent this make sure your .so files are matching that doNotStrip pattern
        // See Gradle pattern documentation for details:
        // https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/util/PatternFilterable.html
        // Needed in both build.gradle file, library and application
        doNotStrip "**D.so"
    }

    buildTypes {
        debug {
            debuggable true
            jniDebuggable true
            renderscriptDebuggable true
            minifyEnabled false
        }

        release {
            debuggable false
            jniDebuggable false
            renderscriptDebuggable false
            minifyEnabled true
        }
    }
}

// Here we define our dependency on our Engine library
dependencies {
    // Select Engine Release or Debug build.
    // See: https://developer.android.com/studio/projects/android-library.html#publish_multiple_variants
    // To debug Engine uncomment the following line and comment out the next one.
    //debugCompile project(path: ':Engine', configuration: 'debug')
    debugCompile project(path: ':Engine', configuration: 'release')
    releaseCompile project(path: ':Engine', configuration: 'release')
}
Note that by default we still use the release variant of our library even for application's debug builds as Engine debug builds do run significantly slower than the release ones.
Developers must then locally enable debug build to be able to use C++ breakpoints.

project/lib/build/build.gradle

Code:
apply plugin: 'com.android.library'

android {
    compileSdkVersion 24
    buildToolsVersion '25.0.0'
    // Make sure we publish debug variant too
    // See: https://developer.android.com/studio/projects/android-library.html#publish_multiple_variants
    publishNonDefault true

    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            // If your native library comes with some JNI you'll need to compile them too
            // We want to compile the following Java files
            java.srcDirs '../java'
        }
    }

    packagingOptions {
        // By default .so libraries are striped from debug information when creating APK
        // To prevent this make sure your .so files are matching that doNotStrip pattern
        // See Gradle pattern documentation for details:
        // https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/util/PatternFilterable.html
        // Needed in both build.gradle file, library and application
        doNotStrip "**D.so"
    }

    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 25
 
        multiDexEnabled true
        dimension "default"

        externalNativeBuild {
            cmake {   
                // Build only the following targets
                abiFilters 'armeabi'   
                // Android options
                arguments '-DANDROID_PLATFORM=android-14',
                        '-DANDROID_TOOLCHAIN=gcc',
                        '-DANDROID_STL=gnustl_static',
                        // CMAKE options
                        "-DCMAKE_INSTALL_PREFIX=.externalNativeBuild/engine_install",
                        // Engine options
                        "-DENGINE_DUMMY_BUILD_OPTION:STRING=ON",
            }
        }
    }

    externalNativeBuild {
        cmake {
            path 'CMakeLists.txt'
        }
    }
}

Conclusions

We've seen how native libraries should be integrated in their own Android library module rather than directly from an Android application module.
To further optimise your native libraries' build time you should specify whatever ABI you really need.
To be able to debug native code you want to make sure your binaries are not striped from their debug information before packaging.

References

 
Last edited:
Back
Top