Posted on

Cross compiling C/C++ libraries for Android (updated)

There are a few forms a C/C++ library that you want to cross compile to Android might come in.

For instance:

  • An Autotools project
  • A CMake project

To get started let’s make a standalone Android toolchain (a toolchain being compilers, libraries and headers for cross compiling our source code to a specific target architecture and platform ABI) .

NB. This guide describes how to build C/C++ libraries as prebuilt libraries (shared objects) on Android. This is particularly useful if the any of the following apply:

  • You want to distribute your libraries to third-party NDK developers without distributing your sources
  • Using a prebuilt version of your own libraries to speed up your build.

If none of the above applies then you should build your libraries as static libraries (and ignore this guide :-)).

Making a standalone toolchain

Firstly you’ll need to download and install the Android NDK. The Android NDK comes bundled with a script that will generate a standalone toolchain for cross compilation (see here). By default, specifying –arch=arm will default clang to target the armeabi-v7a ABI. By setting platform to android-L the latest api will be selected (the available ABIs can be found here).

NB. This recipe was tested against Android NDK r18, r19 has broken CMake’s Android support at the time of writing.

# set ANDROID_NDK_HOME
export ANDROID_NDK_HOME="/Users/benmorris/Downloads/android-ndk-r18b"

# make the toolchain (armeabi-v7a nb. for x86 set --arch=x86)
${ANDROID_NDK_HOME}/build/tools/make-standalone-toolchain.sh --arch=armeabi-v7a --platform=android-L --install-dir=android-toolchain --stl=libc++

# add the toolchain to our path
export PATH="`pwd -P`/android-toolchain/bin:$PATH"

Cross compiling an autotools based project to Android

Having made our standalone toolchain above, let’s compile zlib for Android using the toolchain (Zlib is precompiled and available in the standalone toolchain’s sysroot but lets recompile it here for demonstration purposes). In the same shell session that we used to create our toolchain we first need to set some variables (used by configure) to ensure our toolchain’s compiler (and supporting utilities) are detected:

# nb. for x86 set CHOST="i686-linux-android"
export CHOST="arm-linux-androideabi"
export CC="${CHOST}-clang"
export CXX="${CHOST}-clang++"
export RANLIB="${CHOST}-ranlib"
export LD="${CHOST}-ld"
export AR="${CHOST}-ar"
export ARFLAGS="cr"
export CHOST="${CHOST}"

Now we are ready to go ahead and build the library.

# download and extract zlib
wget http://zlib.net/zlib-1.2.11.tar.gz
tar zxf zlib-1.2.11.tar.gz

# configure and make it
cd zlib-1.2.11
./configure
make

We can confirm that the generated library’s ELF file format is as expected by inspecting it using the toolchain supplied nm tool:

../android-toolchain/bin/arm-linux-androideabi-nm libz.a

You should be able to see the library’s data, symbols, exported and imported function names.

Or:

file libz.so

You should see mention of the ARM abi you compiled against:

libz.so: ELF 32-bit LSB pie executable ARM, EABI5 version 1 (SYSV), dynamically linked, BuildID[sha1]=ce2abedd344601985bf13860bcb2fbd86c1b8b23, with debug_info, not stripped

CMake

In theory you can compile CMake based projects either via a standalone toolchain, an NDK provided toolchain or via Android studio however the reconciling of CMake / NDK’s treatment of building libraries for Android is a work in progress.

Some useful Links:

CMake with a standalone toolchain (NDK <= r18)

NB. This approach uses the prebuilt standalone toolchain we created above and so targets a single architecture. For a multi-architecture friendly approach that doesn’t require you create a standalone toolchain as above for each architecture see the section below.

For a CMake based project we’ll need to tell CMake how to find our standalone toolchain (that we created above). Let’s do this for the popular 3D model asset import library “assimp” below:

wget https://sourceforge.net/projects/assimp/files/assimp-3.1/assimp-3.1.1_no_test_models.zip
unzip assimp-3.1.1_no_test_models.zip
cd assimp-3.1.1

cmake ./ \
  -DCMAKE_SYSTEM_NAME=Android                                     \
  -DCMAKE_ANDROID_STANDALONE_TOOLCHAIN=/path/to/android-toolchain \ 
  -DCMAKE_BUILD_TYPE=Release                                      \ 
  -DCMAKE_INSTALL_PREFIX=`pwd -P`/build                           \ 
  -DASSIMP_BUILD_STATIC_LIB="On"                                  \ 
  -DBUILD_SHARED_LIBS="Off" 
cmake --build . --config Release

CMake with the NDK provided android.toolchain.cmake (NDK <= r18)

This is currently my preferred approach. If we use this approach then we can easily compile for multiple architectures by wrapping the code below in a loop invoking the build for each architecture.

cmake ./ \ 
  -DCMAKE_SYSTEM_NAME=Android                                                  \
  -DCMAKE_SYSTEM_VERSION=26                                                    \
  -DCMAKE_ANDROID_NDK=/path/to/android/ndk                                     \
  -DCMAKE_ANDROID_ARCH_ABI=x86                                                 \
  -DCMAKE_ANDROID_NDK_TOOLCHAIN_VERSION=clang                                  \
  -DCMAKE_ANDROID_STL_TYPE=c++_shared                                          \
  -DCMAKE_BUILD_TYPE=Release                                                   \ 
  -DCMAKE_INSTALL_PREFIX=`pwd -P`/build                                        \ 
  -DASSIMP_BUILD_STATIC_LIB="On"                                               \ 
  -DBUILD_SHARED_LIBS="Off" 
cmake --build . --config Release

To perform the build then for multiple architectures you’ll do something along the lines of (taken from a project I recently ported):

#!/bin/bash
declare -a archs=(armeabi-v7a x86 arm64-v8a x86_64)
for arch in ${archs[@]}; do

    mkdir build
    cd build

    # project config (ndk <= r18)
    cmake .. -DCMAKE_BUILD_TYPE=$buildType                                                \
             -DCMAKE_SYSTEM_NAME=Android                                                  \
             -DCMAKE_SYSTEM_VERSION=26                                                    \
             -DCMAKE_ANDROID_NDK=$ANDROID_NDK_HOME                                        \
             -DCMAKE_ANDROID_ARCH_ABI=$arch                                               \
             -DCMAKE_ANDROID_NDK_TOOLCHAIN_VERSION=clang                                  \
             -DCMAKE_ANDROID_STL_TYPE=c++_shared                                          
    if [ $? -ne 0 ]; then
        echo "Failed to generate build files for Android arch $arch"
        exit 1
    fi

    # build it
    cmake --build . --config $buildType
    if [ $? -ne 0 ]; then
        echo "Build failed for Android arch $arch"
        exit 1
    fi

    # move it off to the side in an arch-specific folder
    cd ..
    rm -rf build_$arch
    mv build build_$arch
done

The above generated shared libraries can then be sym-linked into your android project as described below in the pre-built libraries section.

Including debug symbols during development

During development you’ll want to include debug symbols in your prebuilt libraries created above. To do this, complement the above (per architecture) CMake config command (the CMake command called in the architecture loop) with the following (-g generates debug symbols, -O0 disables optimisation):

-DCMAKE_C_FLAGS_RELEASE="-g -O0" \
-DCMAKE_CXX_FLAGS_RELEASE="-g -O0"

CMake with the NDK provided android.toolchain.cmake (NDK >= r19)

The NDK (as downloaded via Android Studio) now supplies a cmake toolchain file and can be invoked as follows:

cmake ./ \ 
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=armeabi-v7a                                                    \
  -DANDROID_NATIVE_API_LEVEL=29                                                \
  -DCMAKE_BUILD_TYPE=Release                                                   \ 
  -DCMAKE_INSTALL_PREFIX=`pwd -P`/build                                        \ 
  -DASSIMP_BUILD_STATIC_LIB="On"                                               \ 
  -DBUILD_SHARED_LIBS="Off" 
cmake --build . --config Release

ExternalProject_Add and the Android toolchain

If you use CMake’s ExternalProject_Add support to build external dependencies and want ExternalProject_Add to pick up your toolchain and associated cmake args (at the time of writing the version of CMake I’m using – 3.15 doesn’t appear to do this) then put this code block right at the top of your CMakeLists.txt file before your project() declaration (WARNING: to be absolutely sure what is being passed through enable the message string):

# put this before your project() declaration
get_cmake_property(vars CACHE_VARIABLES)
foreach(var ${vars})
  get_property(currentHelpString CACHE "${var}" PROPERTY HELPSTRING)
    if("${currentHelpString}" MATCHES "No help, variable specified on the command line." OR "${currentHelpString}" STREQUAL "")
        # message("${var} = [${${var}}]  --  ${currentHelpString}") # uncomment to see the variables being processed
        list(APPEND CL_ARGS "-D${var}=${${var}}")
    endif()
endforeach()

project(MyCMakeProject)

And then a typical ExternalProject_Add should then look like something along the lines of:

# zlib
ExternalProject_Add(zlib
    PREFIX              ${CMAKE_BINARY_DIR}/zlib
    GIT_REPOSITORY      https://github.com/madler/zlib.git
    GIT_TAG             "v1.2.11"
    INSTALL_DIR         ${THIRDPARTY_ROOT}
    UPDATE_COMMAND      ""
    CMAKE_ARGS          ${CL_ARGS}
        -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
        -DCMAKE_POSITION_INDEPENDENT_CODE=ON
)
add_library(zlib::zlib STATIC IMPORTED)
if(WIN32)
    set_target_properties(zlib::zlib PROPERTIES IMPORTED_LOCATION ${THIRDPARTY_ROOT}/lib/zlibstatic.lib)
else()
    set_target_properties(zlib::zlib PROPERTIES IMPORTED_LOCATION ${THIRDPARTY_ROOT}/lib/libz.a)
endif()

Adding prebuilt libraries to an android project

Currently I sym-link the generated (architecture-specific) libraries (we created above) into my android projects under their architecture-specific directories and then add an Android.mk file for each imported prebuilt library as per the NDK documentation so that the layout of my android app is:

app                            <-- the app root
    jni                        <-- native source and library root
        mylib                  <-- your library name (aka a 'module' or prebuilt library in NDK parlance)
            Android.mk         <-- describes the library / module (see below)
            armeabi-v7a        <-- 
                libmylib.so    <-- the armeabi-v7a version of the lib
            x86                <--
                libmylib.so    <-- etc

For this particular example then the Android.mk file looks like:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := mylib-prebuilt
LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libmylib.so
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include/
include $(PREBUILT_SHARED_LIBRARY)

A quick demo

With the above I added the ability to publish scenes to Android from my side project editor / engine / sdk:

Liked it? Take a second to support ben morris on Patreon!