Skip to content

Instantly share code, notes, and snippets.

@teocci
Last active June 9, 2016 07:49
Show Gist options
  • Save teocci/dbe5260ac209ac8fbaed923745066cf9 to your computer and use it in GitHub Desktop.
Save teocci/dbe5260ac209ac8fbaed923745066cf9 to your computer and use it in GitHub Desktop.

NDK support in Android Studio

NDK support requires the use of Android NDK (r10e or above) and the android gradle-experimental plugin (0.7.0-alpha1). NDK support for the gradle(-stable) plugin, check this article on the NDK and Android Studio. This user guides provides general details on how to use it and highlights the difference between the experimental plugin and the original plugin. (This guide is based on the gradle-experimental 0.7.0 and Android Studio 2.1)

Setting up the gradle-experimental plugin

A typical Android Studio project may have a directory structure as follows. Files that we need to change are marked with a "*":

.
├── app/
│   ├── app.iml
│   ├── build.gradle*
│   └── src/
├── build.gradle*
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties*
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── MyApplication.iml
└── settings.gradle

The gradle-experimental 0.7.0-alpha1 pluging requires the gradle-2.10 use. Start by setting the project gradle/wrapper/gradle-wrapper.properties:

#Wed Apr 10 15:27:10 PDT 2013
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip

We also need to change the reference to the android gradle plugin to the gradle-experimental plugin, in the ./build.gradle file:

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle-experimental:0.7.0'
    }
}

The new experimental plugin is based on Gradle’s new component model mechanism(DSL), while allows significant reduction in configuration time. There are some significant changes in the DSL between the new plugin and the traditional one. But, it also includes NDK integration for building JNI applications. The android plugins com.android.model.application and com.android.model.library are replacing the former com.android.application and com.android.library plugins.

We should migrate apps and libs build.gradle files to use these new plugins. Here is an example of the same configuration, with the original DSL and the experimental:

Original DSL

apply plugin: 'com.android.application'
 
android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion
 
    defaultConfig {
        applicationId "com.ph0b.example"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 4
        versionName "1.0.1"
 
        ndk {
            moduleName "yourmodule"
            ldLibs "log"
            stl "gnustl_static"
            cFlags "-std=c++11 -fexceptions"
        }
    }
 
    signingConfigs {
        release {
            storeFile file(STORE_FILE)
            storePassword STORE_PASSWORD
            keyAlias KEY_ALIAS
            keyPassword KEY_PASSWORD
        }
    }
 
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.txt'
            signingConfig signingConfigs.release
        }
        debug {
            jniDebuggable true
        }
    }
}

Experimental DSL

apply plugin: 'com.android.model.application'
 
model {
    android {
        compileSdkVersion rootProject.ext.compileSdkVersion
        buildToolsVersion rootProject.ext.buildToolsVersion
    
        defaultConfig {
            applicationId "com.ph0b.example"
            minSdkVersion.apiLevel 15
            targetSdkVersion.apiLevel 23
            versionCode 4
            versionName "1.0.1"
        }
    }
 
    android.ndk {
        moduleName "mymodule"
        ldLibs.addAll(['log'])
        cppFlags.add("-std=c++11")
        cppFlags.add("-fexceptions")
		platformVersion 15
        stl 'gnustl_shared'
    }
 
    android.signingConfigs {
        create("release") {
            keyAlias KEY_ALIAS
            keyPassword STORE_PASSWORD
            storeFile file(STORE_FILE)
            storePassword KEY_PASSWORD
        }
    }
 
    android.buildTypes {
        release {
            shrinkResources true
            useProguard true
            proguardFiles.add(file('proguard-rules.txt'))
            signingConfig = $("android.signingConfigs.release")
        }
    }
}

To summarize the changes required:

  • all the android declarations are now going under model {}
  • assignments now have to use explicitly ‘=‘
  • collections must not be overwritten instead we should use .removeAll(), .add(), .addAll()
  • variants and other new configurations have to be declared using ‘create()‘
  • properties like xxxSdkVersion have changed to xxxSdkVersion.apiLevel

We notice that in both DSLs, there is a configuration block for the NDK. This is where we should set all the NDK configuration when using gradle, because by default Android.mk and Application.mk files will be simply ignored.

NDK support in Android Studio

To activate the NDK support inside Android Studio, you only need to have a NDK module declared inside your application or library build.gradle:

model {
    android {
    //...
    ndk {
        moduleName "mymodule"
    }
  }
}

Once it is done, in the Java sources, we can create a method prefixed with the native keyword then press ALT+Enter to generate its C or C++ implementation.

The implementation will be added under "jni" directory, inside an existing cpp file if there is one, or inside a new one.

In order to get started with NDK modules, ported samples to use the new gradle-experimental plugin can be found at: https://github.com/googlesamples/android-ndk

Here is everything you can configure for a ndk module:

    android.ndk {
        moduleName = "mymodule"
        ldLibs.addAll(['log'])
        ldFlags.add("")
        toolchain = "clang"
        toolchainVersion = "3.9"
        abiFilters.add("x86")
        CFlags.add("")
        cppFlags.add("")
        debuggable = false
        renderscriptNdkMode = false
        stl = "system"
        platformVersion = 15
    }

Since 0.7.0, you can also add ABI-specific configurations:

	android.abis {
		create("x86") {
			cppFlags.add('-DENABLE_SSSE3')
			ldLibs.add('')
			ldFlags('')
		}
		create("armeabi-v7a") {
			cppFlags.addAll(["-mhard-float", "-D_NDK_MATH_NO_SOFTFP=1", "-mfloat-abi=hard"])
			ldLibs.add("m_hard")
			ldFlags.add("-Wl,--no-warn-mismatch")
		}
	}

Debugging a NDK project

We have to create and use a new Run/Debug configuration from the "Android Native" default to get the Android Studio debug capabilities. This debug variant will have the ndk.debuggable flag set to true by default.

Anvanced NDK features on Android Studio

Many advanced features, such as the ability to have dependencies between native libraries, reuse prebuilts, tune specific toolchain options, and having dynamic version codes while still having our project in a good shape. These features are a bit complex to achieve, as the gradle-experimental plugin is still undergoing a lot of improvements across versions.

Getting the APP_PLATFORM right

When you’re building a NDK module, the android platform you’re compiling it against is a quite important setting, as it basically determines the minimum platform your module will be guaranteed to run on.

With earlier versions than gradle-experimental:0.3.0-alpha4, the chosen platform was the one set as compileSdkVersion. Fortunately with subsequent releases, you can now set android.ndk.platformVersion independently, and you should make it the same as your minSdkVersion.

Using external libraries and separate modules

-- with sources If you have access to your 3rd party libraries source code, you can embed it into your project and make it statically compile with your code.

There is an example of this with the native_app_glue library from the NDK, inside the native-activity sample. For example, you can copy the library sources inside a subfolder inside your jni folder and add a reference to its directory so the includes are properly resolved:

    android.ndk {
        //...
        cppFlags.add('-I' + file("src/main/jni/native_app_glue"))
    }

-- with sources in different modules From 0.6.0-alpha7 version, you can finally have clean dependencies between native libraries, by setting the dependency on another module from your model: build.gradle

    android.sources {
        main {
            jni {
                dependencies {
                    project ":yourlib" buildType "release" productFlavor "flavor1" linkage "shared"
                }
            }
        }
    }

In order to keep debugging working, you may have to edit your app-native run configuration, to add /build/intermediates/binaries/release/obj/[abi] to the symbol directories.

--with native prebuilts This technique works with static and shared prebuilts too! Inside your model, you’ll have to add a “lib repository”:

    repositories {
        libs(PrebuiltLibraries) {
            yourlib {
                headers.srcDir "src/main/jni/prebuilts/include"
                binaries.withType(SharedLibraryBinary) {
                    sharedLibraryFile = file("src/main/jni/prebuilts/${targetPlatform.getName()}/libyourlib.so")
                }
            }
        }
    }

And declare the dependency on this library:

    android.sources {
        main {
            jni {
                dependencies {
                    library "yourlib" linkage "shared" 
                }
            }
        }
    }

Shared linkage is the default, but of course you can use static prebuilts by using a static linkage, and declaring StaticLibraryBinary/staticLibraryFile variables.

When having dependencies on shared libraries, you need to make sure to integrate these libs inside your APK. It will be the case if they’re under jniLibs, otherwise you can add them manually:


    android.sources {
        main {
            jniLibs {
                source {
                    srcDir "src/main/jni/prebuilts"
                }
            }
        }
    }

Not supported Multiple APKs

When we publish multiple APKs (for instance, per architecture), we have to give them different version Version Codes. Even using splits or abiFilters and flavors is not possible, yet. The good news is we can mix the use of the stable gradle plugin, and the new experimental one, even while keeping debug features working! Please follow this gist.

Using Android.mk/Application.mk

If the built-in gradle support isn’t suitable to your needs, you can get rid of it, while keeping the goodness of Android Studio C++ editing.

Declare a module that correctly represents your configuration, as this will help AS to correctly resolve all the symbols you’re using and keep the editing capabilities:

android.ndk { // keeping it to make AS correctly support C++ code editing
        moduleName "mymodule"
        ldLibs.add('log')
        cppFlags.add('-std=c++11')
        cppFlags.add('-fexceptions')
        cppFlags.add('-I' + file("src/main/jni/prebuilts/include"))
        stl = 'gnustl_shared'
    }

Then, set the jniLibs location to libs, the default directory in which ndk-build will put the generated libs, and deactivate built-in compilation tasks:

model {
    //...
    android.sources {
        main {
          jniLibs {
            source {
                srcDir 'src/main/libs'
            }
        }
    }
}
 
tasks.all {
	task ->
	if (task.name.startsWith('compile') && task.name.contains('MainC')) {
		task.enabled = false
	}
	if (task.name.startsWith('link')) {
		task.enabled = false
	}
}

This way, we call ndk-build(.cmd) from the root of the src/main directory. ndk-build will use usual Android.mk/Application.mk files under the jni folder, the libs will be generated inside libs/ as usual and get included inside your APK.

We can also add the call to ndk-build in gradle configuration so it’s done automatically:

import org.apache.tools.ant.taskdefs.condition.Os
 
model {
    ...
    android.sources {
        main {
          jniLibs {
            source {
                srcDir 'src/main/libs'
            }
        }
    }
}
 
// call regular ndk-build(.cmd) script from app directory
task ndkBuild(type: Exec) {
    if (Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'ndk-build.cmd', '-C', file('src/main').absolutePath
    } else {
        commandLine 'ndk-build', '-C', file('src/main').absolutePath
    }
}
 
tasks.all {
	task ->
	if (task.name.startsWith('compile') && task.name.contains('MainC')) {
		task.enabled = false
	}
	if (task.name.startsWith('link')) {
		task.enabled = false
	}
	if (task.name.endsWith('SharedLibrary') ) {
		task.dependsOn ndkBuild
	}
}
apply plugin: 'com.android.application'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "com.example.yourapp"
minSdkVersion 16
targetSdkVersion 23
versionCode 1230
versionName "1.2.3"
}
splits {
abi {
enable true
reset()
include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
universalApk true
}
}
// map for the version code
project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]
applicationVariants.all { variant ->
// assign different version code for each output
variant.outputs.each { output ->
output.versionCodeOverride =
project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + defaultConfig.versionCode
}
}
buildTypes {
debug {
jniDebuggable true
}
}
}
dependencies {
compile project(':lib')
}
apply plugin: 'com.android.model.library'
model {
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig.with {
minSdkVersion.apiLevel = 16
targetSdkVersion.apiLevel = 23
}
}
android.ndk {
moduleName = "mylib"
ldLibs.addAll(['log'])
cppFlags.add("-std=c++11")
cppFlags.add("-fexceptions")
platformVersion = 15
}
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle-experimental:0.6.0'
classpath 'com.android.tools.build:gradle:2.0.0'
}
}
allprojects {
repositories {
jcenter()
}
}
ext {
compileSdkVersion = 23
buildToolsVersion = "23.0.2"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment