Android NDK Adventures

This is meant to serve as a reference and quick tutorial for future NDK projects. Going through this hassle once was more than enough.

Dependencies: Compiling C/C++ Libraries from Source

Makefile Projects

http://kvurd.com/blog/compiling-a-cpp-library-for-android-with-android-studio/

build.sh

#!/bin/sh

# Points to a standalone Android toolchain
# See https://developer.android.com/ndk/guides/standalone_toolchain.html
# and http://www.myandroidonline.com/2015/06/09/compile-assimp-open-source-library-for-android/
export ANDROID_NDK_TOOLCHAIN=$HOME/android/toolchain-armeabi

# Do not touch these
export PATH=$ANDROID_NDK_TOOLCHAIN/bin:$PATH
export CC="arm-linux-androideabi-gcc"
export CXX="arm-linux-androideabi-g++"

# Add additional args here as appropriate
../configure --prefix=$(pwd) --host arm-linux-androideabi

make
make install

The build.sh script is meant to be placed inside a directory such as build/ inside the project's source tree, to avoid polluting the project's root directory.

CMake Projects

http://www.myandroidonline.com/2015/06/09/compile-assimp-open-source-library-for-android/

build.sh

#!/bin/sh

# Points to a standalone Android toolchain
# See https://developer.android.com/ndk/guides/standalone_toolchain.html
# and http://www.myandroidonline.com/2015/06/09/compile-assimp-open-source-library-for-android/
export ANDROID_NDK_TOOLCHAIN=$HOME/android/toolchain-armeabi

# Points to the Android SDK
ANDROID_SDK=$HOME/android/sdk

# This file must be downloaded from the android-cmake project on github:
# https://github.com/taka-no-me/android-cmake](https://github.com/taka-no-me/android-cmake
export ANDTOOLCHAIN=$HOME/android/android-cmake/android.toolchain.cmake

# Do not touch these
export PATH=$PATH:$ANDROID_SDK/tools
export PATH=$PATH:$ANDROID_SDK/platform-tools
export PATH=$PATH:$ANDROID_SDK/android-toolchain/bin

# Add additional args here as appropriate
cmake -DCMAKE_TOOLCHAIN_FILE=$ANDTOOLCHAIN \
      -DANDROID_NDK=$ANDROID_NDK \
      -DCMAKE_BUILD_TYPE=Release \
      -DANDROID_ABI="armeabi-v7a" \
      -DANDROID_NATIVE_API_LEVEL=android-21 \
      -DANDROID_FORCE_ARM_BUILD=TRUE \
      -DCMAKE_INSTALL_PREFIX=install \
      ..

# This is to remove the versioned shared libs in assimp.
# Might be useful for other CMake-based projects, otherwise remove this line.
sed -i s/-soname,libassimp.so.3/-soname,libassimp.so/g code/CMakeFiles/assimp.dir/link.txt

make
make install

Shared Library Patching

http://computervisionandjava.blogspot.com.es/2015/05/trouble-with-versioned-shared-libraries.html http://stackoverflow.com/questions/11491065/linking-with-versioned-shared-library-in-android-ndk

Most C/C++ projects / build scripts generate versioned shared libraries. For example, many scripts will generate a libfoo.so.x.y, a libfoo.so.x, and a libfoo.so, the former being the actual library and the latter being soft links. Android does not support versioned libs and expects a simple libfoo.so file, however, so if a simple libfoo.so can't be generated, the versioned shared library libfoo.so.x.y.z must be patched.

To "unversion" a shared library libfoo.so.x.y.z:

  1. Run objdump -p libfoo.so.x.y.z | grep so.
  2. Run rpl -R -e libxxx.so.y libxxx_y.so libfoo.so.x.y.z to rename libxxx.so.y to libxxx_y.so in the exports table. Also rename the soname following the same rule.
  3. Rename the shared library to match its soname and rename its dependencies to match the new names.

This works because libfoo.so.x and libfoo_x.so have the same length. If we patch a name with a differently sized string, the resulting shared library becomes corrupted.

Example:

Running objdump on a library libfoo:

libfoo.so.x.y.z:     file format elf32-little
NEEDED               libbar.so.3
NEEDED               libbaz.so.15
SONAME               libfoo.so.x

Patching the dependency table and soname:

libfoo.so.x.y.z:     file format elf32-little
NEEDED               libbar_3.so
NEEDED               libbaz_15.so
SONAME               libfoo_x.so

And then renaming all files:

libfoo.so.x.y.z -> libfoo_x.so
libbar.so.3     -> libbar_3.so
libbaz.so.15    -> libbaz_15.so

Building the Main C++ Project

Android Studio's support for the NDK is essentially unusable at the time of writing, which is remarkable, to say the least.

Create a directory jni and the place source files there, together with Android.mk and Application.mk:

Project/
    app/
    build/
    gradle/
    ...
    jni/
        Android.mk
        Application.mk
        main.cc

Here we assume the typical project layout that is created when starting a new project using Android Studio.

To build:

  1. Run ndk-build inside jni. This will generate the main shared library and copy the library and its dependencies to Project/libs.
  2. Copy libs/* to app/src/main/jniLibs.

The libraries in jniLibs are automatically packed with the APK when the Java project is built.

Calling the C++ Library from Java

Java Side

public class Native {

    // Libraries must be loaded from least dependent to most dependent.
    // If X depends on Y and Y is not already loaded upon loading X,
    // a linker error will occur.
    //
    // If library file is libfoo.so, pass "foo" to System.loadLibrary().
    static {
        System.loadLibrary("C"); // libC.so
        System.loadLibrary("B"); // libB.so
        System.loadLibrary("A"); // libA.so
    }

    public static native String func1();
    public static native void func2();
}

C++ Side

Functions must follow a specific naming convention. If we had a function

void foo(int x)

and we wanted to call it from class Native inside package com.example.hello, then the function signature would have to be:

JNIEXPORT void JNICALL Java_com_example_hello_Native_foo(JNIEnv*, jclass, jint x)

See the JNI documentation for details.