If you are working with native (C or C++) code in your Android project, you are probably using CMake or ndk-build for building and compiling this into a native library that gets eventually packaged into your APK. This entire process is controlled by yet another build tool called Gradle. Although this may seem unnecessary complicated, it should not be forgotten that there is actually a lot going on under the hood before your code gets assembled onto an Android device.

While adding dependencies in Android Gradle is trivial simple (it only takes one line added in the dependencies section of your build.gradle project file), things don’t look that easy for C or C++ projects at first glance. However, if done right, adding or updating a native dependency is not much more complicated than with Gradle. The first step is to link Gradle to your CMake-based project in the build.gradle file:

android { ... defaultConfig { ... externalNativeBuild { cmake { arguments "-DANDROID_TOOLCHAIN=clang" cppFlags "-std=c++14 -DANDROID" } } } ... // Use this to link Gradle to your CMake-based project externalNativeBuild { cmake { path "CMakeLists.txt" } } ...

Afterwards we can use the full feature set of CMake to manage the native side of our project dependencies. When defining a build configuration, a good advice is to always think in terms of sources, build targets and dependencies between those targets exclusively. That is, we have some source files (either locally stored or coming from an external resource as we will see later), which are used to build a logical target and dependencies between those targets. This approach is commonly called Modern CMake and scales nicely with an increasing number of build targets, without the need to fiddle with the implementation details of any given dependency on a build system level.

Wrapping native libraries into logical build targets

As the Android NDK comes with several native libraries already, it is good practice to wrap them into logical build targets in CMake that we can link against. A typical C or C++ based library has one or more include directories, a shared or static library file and optionally some defines or options that need to be set when linking against this library. For instance, if we want to use Android’s logging features from within a native library, we need to link against the native log library. However, we would like to define all details of this library in a central location once and continue using it as a logical build target in CMake. For this, we need to write a Find-module for this library:

# app/cmake/Modules/FindAndroidLog.cmake include ( FindPackageHandleStandardArgs ) if ( NOT AndroidLog_LIBRARIES AND NOT Android::Log ) find_path ( AndroidLog_INCLUDE_DIR log.h HINTS ${ ANDROID_NDK } /sysroot/usr/include/android ) find_library ( AndroidLog_LIBRARY NAMES log ) find_package_handle_standard_args ( AndroidLog DEFAULT_MSG AndroidLog_LIBRARY AndroidLog_INCLUDE_DIR ) if ( AndroidLog_FOUND ) set ( AndroidLog_LIBRARIES ${ AndroidLog_LIBRARY } ) set ( AndroidLog_INCLUDE_DIRS ${ AndroidLog_INCLUDE_DIR } ) add_library ( Android::Log SHARED IMPORTED ) set_target_properties ( Android::Log PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${ AndroidLog_INCLUDE_DIR } IMPORTED_LOCATION ${ AndroidLog_LIBRARY } ) else () if ( AndroidLog_FIND_REQUIRED ) message ( FATAL_ERROR "Could not find Android logging library." ) endif () endif () endif ()

In this case, the environment variable ANDROID_NDK is set from Gradle. Hence, we can use it to point CMake to the correct version of the Android log library and set its include directories, library name and path accordingly. We can continue declaring all remaining Android libraries needed for our project and include them just like any other external library in CMake using find_package() and finally link against them in our own library:

find_package ( AndroidLog REQUIRED ) find_package ( AndroidCPUFeatures REQUIRED ) ... add_library ( my_native_cpp_lib ${ SOURCES } ) target_link_libraries ( my_native_cpp_lib PRIVATE Android::Log Android::CPUFeatures )

Adding remote, thirdparty libraries using CMake

The interesting part is now the integration of remote, thirdparty libraries into our Android project. Those libraries may not even use CMake as their build system, use a different version control system or are only available as compressed zip or tar packages. For this task, we can use CMake’s ExternalProject_Add() functionality. In its core ExternalProject_Add() is a custom CMake function that executes several steps to incorporate a given library or framework into the underlying software project. It has a sophisticated mechanism that enables to adjust every part of the build process.

As Android builds every native library in different ABIs, using different toolchains, we need to set some default arguments that get passed to each external project. ExternalProject_Add() per default doesn’t inherit any variables from the parent project. Thus, we set all the variables that are as well listed in the official documentation:

set ( MyAndroidProject_INSTALL_PREFIX ${ CMAKE_CURRENT_BINARY_DIR } /install ) set ( MyAndroidProject_DEFAULT_ARGS -DANDROID_ABI:STRING= ${ ANDROID_ABI } -DANDROID_NATIVE_API_LEVEL:STRING= ${ ANDROID_NATIVE_API_LEVEL } -DANDROID_NDK:STRING= ${ ANDROID_NDK } -DANDROID_PLATFORM:STRING= ${ ANDROID_PLATFORM } -DANDROID_STL:STRING= ${ ANDROID_STL } -DANDROID_TOOLCHAIN:STRING= ${ ANDROID_TOOLCHAIN } -DBUILD_SHARED_LIBS:BOOL= ${ BUILD_SHARED_LIBS } -DCMAKE_BUILD_TYPE:STRING= ${ CMAKE_BUILD_TYPE } -DCMAKE_C_COMPILER:STRING= ${ CMAKE_C_COMPILER } -DCMAKE_CXX_COMPILER:STRING= ${ CMAKE_CXX_COMPILER } -DCMAKE_GENERATOR:STRING= ${ CMAKE_GENERATOR } -DCMAKE_MAKE_PROGRAM:FILEPATH= ${ CMAKE_MAKE_PROGRAM } -DCMAKE_TOOLCHAIN_FILE:FILEPATH= ${ CMAKE_TOOLCHAIN_FILE } )

Now for adding an external dependency, we need to pass its URL as well as the steps needed to build it. For instance, let’s assume we wanted to incorporate the FFTW library into our project. In this case, we’d call ExternalProject_Add() as follows:

ExternalProject_Add ( fftw URL ${ fftw_url } URL_MD5 ${ fftw_md5 } INSTALL_DIR ${ MyAndroidProject_INSTALL_PREFIX } /fftw CMAKE_ARGS -Wno-dev ${ MyAndroidProject_DEFAULT_ARGS } -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR> -DBUILD_TESTS=OFF -DDISABLE_FORTRAN=ON ) ExternalProject_Add_Step ( fftw CopyLibToJniFolder COMMAND ${ CMAKE_COMMAND } -E make_directory ${ MyAndroidProject_SOURCE_DIR } /src/main/jniLibs/ ${ ANDROID_ABI } COMMAND ${ CMAKE_COMMAND } -E copy ${ MyAndroidProject_INSTALL_PREFIX } /fftw/lib/fftw3.so ${ MyAndroidProject_SOURCE_DIR } /src/main/jniLibs/ ${ ANDROID_ABI } / DEPENDEES install ) ExternalProject_Get_Property ( fftw install_dir ) set ( FFTW_ROOT ${ install_dir } CACHE INTERNAL "" ) file ( MAKE_DIRECTORY ${ FFTW_ROOT } /include ) set ( FFTW_INCLUDE_DIR ${ FFTW_ROOT } /include ) set ( FFTW_LIBRARY ${ FFTW_ROOT } /lib/fftw3.so ) add_library ( FFTW::FFTW SHARED IMPORTED ) add_dependencies ( FFTW::FFTW fftw ) set_target_properties ( FFTW::FFTW PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${ FFTW_INCLUDE_DIR } IMPORTED_LOCATION ${ FFTW_LIBRARY } )

In the case of FFTW, this library already uses CMake, which makes the integration a little bit easier. After fetching and building this library locally, we copy its build artifacts to a directory where Android expects any additional, native library. Finally, we again wrap this dependency into a logical build target and set all necessary properties in order to just link against it from our own native library. This way, we don’t need to deal with any usage requirements as they are now encapsulated within FFTW::FFTW. The call to add_dependencies() ensures that this library will be built and ready as soon as some other target refers to FFTW::FFTW.

The (remote) URL together with the MD5 hash of the FFTW package is coming from a central Projects.cmake file, which can now be used to adjust the version of the library just like any external Java or Kotlin dependency from Gradle as promised at the beginning of this article:

# FFTW list ( APPEND projects fftw ) set ( fftw_version "3.3.7" ) set ( fftw_url "http://www.fftw.org/fftw- ${ fftw_version } .tar.gz" ) set ( fftw_md5 "0d5915d7d39b3253c1cc05030d79ac47" ) # x265 list ( APPEND projects x265 ) set ( x265_version "2.6" ) set ( x265_url "https://bitbucket.org/multicoreware/x265/downloads/x265_ ${ x265_version } .tar.gz" ) # GTest list ( APPEND projects gtest ) set ( gtest_version "1.8.0" ) set ( gtest_url "https://github.com/google/googletest/archive/release- ${ gtest_version } .tar.gz" ) set ( gtest_md5 "16877098823401d1bf2ed7891d7dce36" )

Granted, the integration of remote, thirdparty libraries is not as straightforward as its Gradle counterpart. However, using the approach I’ve described in this article, we are now able to update any native library by changing one line of code. For example, we could now create another job on our CI system that builds this project continuously using the latest version of each thirdparty library, running all the unit or performance tests and therefore reveal any bugs or conflicts that may arise, in case we want to upgrade to a newer version.

I’m available for software consultancy, training and mentoring. Please contact me, if you are interested in my services.