Integrating an Android NDK library into an Android Studio application is under-documented and can lead to difficulty with complex build rules or JNI integration. One of our lead developers begins a series laying out one approach to adding an NDK module to an Android Studio application utilizing statically compiled libraries, custom data transfer objects and asynchronous processes.
As Android development has started to move towards a focus on Android Studio and Gradle, I have run into several occasions where I needed to ‘re-learn’ how to perform a specific development task in this new environment. Recently, I had a client who needed a decent portion of their work handled asynchronously with pre-built native libraries in a separate Android module. Android Studio’s NDK support is still coming into its own and there is fairly decent support at this point starting with 0.8.6 but there are QUITE a few ‘gotchas’ that I encountered... In this series of articles I hope to compile the information and results that I arrived at and help you get rolling on your native project.
I will assume that you are familiar with the Android Studio and Gradle development model and already have a project that you want to add the new native module to. If you are still working on making the switch from Eclipse, check out Jay’s article at https://sdgsystems.com/blog/migrating-eclipse-andro... for a terrific migration guide. Also, this guide is written with an eye towards development on Linux or Mac OSX, so you’ll have to fix up the paths if you’re developing on Windows.
Using Gradle with built-in NDK support
To start with, create a new module with
File -> New Module -> Android library
You’ll have the opportunity to name the application and module, but I recommend using the same name as your package name. In a fit of creativity, I’ll call this module ‘NdkModule’. In this example, I have not created an activity but you certainly CAN if you want to have a GUI portion of the module.
Next, add your source files to src/main/jni. If you are starting from scratch, you can add a basic main.c and main.h that looks like this:
main.c
#include "main.h"
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <malloc.h>
#include <unistd.h>
jint Java_com_sdgsystems_examples_android_ndk_ndkmodule_MainNative_callWithArguments(JNIEnv* env, jobject thiz, jstring deviceName, jint width, jint height) {
const char* dev_name = (*env)->GetStringUTFChars(env, deviceName, 0);
(*env)->ReleaseStringUTFChars(env, deviceName, dev_name);
//TODO something awesome
return 0;
}
// Required for the default JNI implementation
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
return JNI_VERSION_1_6;
}
main.h
#ifndef __MAIN_H__
#define __MAIN_H__
#include <jni.h>
#include <android/log.h>
// underscores are reserved characters in JNI referring to package boundaries.
// stick with camelCase moduleNames, classNames and methodNames
jint Java_com_sdgsystems_examples_android_ndk_ndkmodule_MainNative_callWithArguments(JNIEnv* env, jobject thiz, jstring deviceName, jint width, jint height);
#endif // __MAIN_H__
Add your Java files to src/main/java. I will detail out a more complicated example later in this article, but this is the bare minimum:
MainNative.java
package com.sdgsystems.examples.android.ndk.ndkmodule
public class MainNative {
private static final String TAG = "MainNative";
private native int callWithArguments(String deviceName, int width, int height);
static {
//NOTE: this comes from the module name that we will define in our build.gradle
System.loadLibrary("NdkModule");
}
public MainNative() {
//TODO implement a useful constructor
}
public int callNativeMethod(String deviceName, int width, int height) {
return callWithArguments(deviceName, width, height);
}
}
Android Studio will automatically add the new module to your settings.gradle but it’s good to know where it gets defined in case you need to remove it during your development process:
settings.gradle:
include … ,:NdkModule
Any project that needs to refer to your new module will need to add a dependency in its own build.gradle:
build.gradle (any project that needs to refer to your new module package)
dependencies {
…
compile project(':NdkModule')
}
Gradle needs a pointer to your NDK installation. You’ll notice that it already has a pointer to the sdk location in local.properties. If you don’t have the NDK yet, you can download it from https://developer.android.com/tools/sdk/ndk/. The path here should be the location of the ndk-build executable.
local.properties (main project directory)
ndk.dir=/path/to/ndk
Android Studio automatically creates a build.gradle for your new library but there are a couple of pieces that you’ll need to add:
build.gradle (simple module)
apply plugin: 'com.android.library'
android {
compileSdkVersion 19
buildToolsVersion "20.0.0"
defaultConfig {
applicationId "com.sdgsystems.examples.android.ndk.ndkmodule"
minSdkVersion 17
targetSdkVersion 19
versionCode 1
versionName "1.0"
// These directives will be used to tell Android studio how to compile your library
// NOTE: These are used to generate an Android.mk file. your current Android.mk file
// WILL BE IGNORED. I’ll talk about how to include a complex Android.mk file later
ndk {
//This will be the name of your library (so: libNdkModule.so)
moduleName "NdkModule"
cFlags "-std=c99"
ldLibs 'log'
}
}
// Point Gradle at the directory where you want to root all of your native code
// There is a default directory but I prefer to control which directories to use
// and there will be a point later where keeping control over this is important
sourceSets.main {
jni.srcDirs = [‘src/main/jni’];
}
buildTypes {
release {
runProguard false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
Thanks for reading! In part 2 of this series, I will discuss using an Android.mk NDK compilation process using pre-built native libraries.
Ben Friedberg, Lead Software Engineer
Comments