Skip to content

Instantly share code, notes, and snippets.

@okamayana
Created October 7, 2015 05:18
Show Gist options
  • Save okamayana/b3d588ebb752391818aa to your computer and use it in GitHub Desktop.
Save okamayana/b3d588ebb752391818aa to your computer and use it in GitHub Desktop.
Android Studio: Setting up the IDE for proper JNI NDK integration

Purpose

The purpose of this document is to outline the necessary steps to set up a development environment for Android application development with NDK integration.

Environment setup

This document was written with the following setup:

  • Android Studio version 1.3.2
  • Android NDK version r10e

Adding the NDK to your PATH environment variable

Although not required specifically for this guide, it is usually important to have the NDK added to your PATH environment variable. If it is not, add this line to your (assuming bash is your preferred shell) ~/.bashrc:

export ANDROID_NDK_HOME="<ANDROID_NDK_INSTALLATION>"
export PATH="$ANDROID_NDK_HOME:$PATH"

Source ~/.bashrc (if you have any open shells) for the change to take effect:

. ~/.bashrc

Including pre-built libraries

Default JNI pre-built libraries directory structure

If you have previously-built native libraries (*.so files) that you would like to use in your Android application, you will need to place them under <PROJECT>/<MODULE>/src/main/java/jniLibs/<TARGET_ARCHITECTURE>/. By default, Android will automatically include native libraries placed in the above directory, categorized by the pre-built's target architecture.

For example, if you're trying to use a native library called mylib and you have compiled mylib for armeabi, armeabi-v7a, and x86 architectures, then your module directory should look like the following:

<PROJECT>/
+-- <MODULE>/
    +-- src/
        +-- main/
            +-- java/
                +-- jniLibs/
                    +-- armeabi/
                        +-- libmylib.so
                    +-- armeabi-v7a/
                        +-- libmylib.so
                    +-- x86/
                        +-- libmylib.so

You can now load and use mylib in Android by calling (in Java, usually in a static block):

static {
    System.loadLibrary("mylib")  // module name, without the trailing ".so" or leading "lib"
}

Custom JNI pre-built libraries directory structure

If you prefer to have your pre-built JNI libraries in another location, you can specify it in your module's build.gradle file (not the top-level project's build.gradle!!), under android.sourceSets.main.jniLibs.srcDir:

android {
    // ... android gradle settings

    sourceSets.main {
        jniLibs.srcDir 'path/to/myJniLibs'  // change to a directory of your choice
    }
}

Compiling C/C++ code directly from Android Studio

The process of writing C/C++ code, compiling them separately, adding the resulting *.so files to your Android project, and (finally) compiling the Android project can be pretty tedious. Android Studio provides limited NDK integration, which is often sufficient for simpler projects, but can result in unwanted behaviours for more complex ones. In short, we need to disable Android Studio's limited native integration features, and handle NDK support manually by adding changes to our app module's build.gradle file.

To be able to write C/C++ code and compile them together with your Android app module, you will first need to do the following:

  1. Add and set ndk.dir property inside the top-level project's local.properties file to point to your NDK installation. For example:

    ndk.dir=/home/odhita/Software/android-ndk-r10e
  2. Create an Android.mk makefile in <MODULE>/src/main/jni with (at the very minimium) the following contents:

    LOCAL_PATH := $(call my-dir)
    
    include $(CLEAR_VARS)
    
    LOCAL_MODULE    := <MY_NATIVE_MODULE_NAME>
    LOCAL_SRC_FILES := <MY_NATIVE_MODULE_SOURCE_FILES>
    
    include $(BUILD_SHARED_LIBRARY)

    This file is used by the build system (such as Gradle) to describe native source code. It is essentially a partial GNU Makefile that glues your C/C++ code to the build system. Please refer to the official Android NDK guide on Android.mk for more information.

  3. Create an Application.mk makefile in <MODULE>/src/main/jni with (at the very minimum) the following contents:

    APP_ABI := all

    Similarly to Android.mk, Application.mk is a partial GNU Makefile that describes native modules required by your Android application. Please refer to the official Android NDK guide on Application.mk for more information.

  4. In the app module's build.gradle file, disable Android Studio's automatic ndk-build call by resetting android.sourceSets.main.jni.srcDirs as follows.

    You will also want to set android.sourceSets.main.jniLibs.srcDirs to src/main/libs, since this is where the manual ndk-build call will place its resulting output *.so files in, and you will want them included in the app's APK:

    android {
        // ... android gradle settings
    
        sourceSets.main {
            jni.srcDirs = []                // disable automatic ndk-build call
            jniLibs.srcDir 'src/main/libs'  // ensures your native library will be included in the APK
        }   
    }
  5. In the app module's build.gradle file, define functions getNdkDir() and getNdkBuildCmd(). getNdkDir() will use the ANDROID_NDK_HOME environment variable if it is set, otherwise it will use the ndk.dir definition in the top-level project's local.properties file. getNdkCmd() will return the appropriate ndk-build binary depending on the host OS. Together, these two functions will build a valid path to ndk-build.

    For the getNdkBuildCmd() function to work, you'll need to place an import statement for the external library org.apache.tools.ant.taskdefs.condition.Os at the top of the build.gradle file. For reference:

    import org.apache.tools.ant.taskdefs.condition.Os
    
    apply plugin: 'com.android.application'
    
    android {
        // ... android gradle settings
    }
    
    dependencies {
        // ... module dependencies
    }
    
    def getNdkDir() {
        if (System.env.ANDROID_NDK_HOME != null)
            return System.env.ANDROID_NDK_HOME
    
        Properties properties = new Properties()
        properties.load(project.rootProject.file('local.properties').newDataInputStream())
        def ndkdir = properties.getProperty('ndk.dir', null)
        if (ndkdir == null)
            throw new GradleException("NDK location not found. Define location with ndk.dir in the" +
                    "local.properties file or with an ANDROID_NDK_HOME environment variable.")
        
        return ndkdir
    }
    
    def getNdkBuildCmd() {
        def ndkbuild = getNdkDir() + "/ndk-build"
        if (Os.isFamily(Os.FAMILY_WINDOWS))
            ndkbuild += ".cmd"
    
        return ndkbuild
    }
  6. In the app module's build.gradle file, under android, define the tasks :ndkClean and :ndkBuild. As their names suggest, :ndkClean cleans your native build output directory, and :ndkBuild builds your native code:

    android {
        // ... android gradle settings
    
        task ndkBuild(type: Exec) {
            workingDir file('src/main')
            commandLine getNdkBuildCmd()
        }
    
        task ndkClean(type: Exec) {
            workingDir file('src/main')
            commandLine getNdkBuildCmd(), 'clean'
        }
    }
  7. In the app module's build.gradle file, under android, add the following task dependencies:

    android {
        // ... android gradle settings
    
        // ensures ndkBuild task is run before any JavaCompile tasks
        tasks.withType(JavaCompile) {
            compileTask -> compileTask.dependsOn ndkBuild
        }
    
        // ensures ndkClean task is run before clean task
        clean.dependsOn ndkClean
    }

For completeness, here is how your build.gradle should look like:

import org.apache.tools.ant.taskdefs.condition.Os

apply plugin: 'com.android.application'

android {
    // ... android gradle settings

    sourceSets.main {
        jniLibs.srcDir 'src/main/libs'
        jni.srcDirs = []
    }

    task ndkBuild(type: Exec) {
        workingDir file('src/main')
        commandLine getNdkBuildCmd()
    }

    task ndkClean(type: Exec) {
        workingDir file('src/main')
        commandLine getNdkBuildCmd(), 'clean'
    }

    tasks.withType(JavaCompile) {
        compileTask -> compileTask.dependsOn ndkBuild
    }

    clean.dependsOn ndkClean
}

dependencies {
    // ... module dependencies
}

def getNdkDir() {
    if (System.env.ANDROID_NDK_ROOT != null)
        return System.env.ANDROID_NDK_ROOT

    Properties properties = new Properties()
    properties.load(project.rootProject.file('local.properties').newDataInputStream())
    def ndkdir = properties.getProperty('ndk.dir', null)
    if (ndkdir == null)
        throw new GradleException("NDK location not found. Define location with ndk.dir in the" +
                "local.properties file or with an ANDROID_NDK_ROOT environment variable.")

    return ndkdir
}

def getNdkBuildCmd() {
    def ndkbuild = getNdkDir() + "/ndk-build"
    if (Os.isFamily(Os.FAMILY_WINDOWS))
        ndkbuild += ".cmd"

    return ndkbuild
}

At this point, whenever you build or run your app through Android Studio, your native code will also be built and integrated properly with your app, directly as part of the build process.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment