In the previous article, we described how to build a simple Android executable, which
uses Boost C++ libraries. This is good example to see how the process works and to understand the internals;
however, for practical purposes we need to know how to build ready-to-use Android applications
which can be submitted to Google Play Store, for example.
The officially supported way to create such applications is with the help of Android Studio.
Unfortunately, Android Studio don't support native-enabled applications as good as it does Java-based applications.
NDK support is very limited at the current moment. Thus, the only supported NDK applications
are those consisting of just one module (final shared library) built from the sources located in the 'jni' folder,
without any dependencies, without splitting by modules (e.g. to a set of static and shared libraries), etc.
There is no ability to do customizations (except for very limited set of options), available for developers in the gradle script
used by Android Studio to build Android applications:
build.gradle
defaultConfig {
...
ndk {
moduleName "my-module-name"
cFlags "-std=c++11 -fexceptions"
ldLibs "log"
stl "gnustl_shared"
abiFilter "armeabi-v7a"
}
}
}
The only supported settings for NDK build are moduleName, cFlags, ldLibs, stl and abiFilter;
we can't specify additional dependencies (such as Boost libraries) here. We also can't specify
a set of paths to instruct the linker where to search libraries, as well as many other settings.
This happens because gradle plug-in (used by Android Studio to build the projects) ignore existing
Application.mk and Android.mk from the 'jni' folder. Instead it generates its own Android.mk
on the fly, using settings from the build script.
Practically, the only working way to build fully featured NDK-enabled applications in Android Studio would be to completely
disable its limited NDK support and call the $NDK/ndk-build command explicitly.
Here we'll describe step-by-step how to do it.
We'll create a simple Android application in Android Studio from scratch, and then add
native parts there. We assume you already have installed Android Studio and set up the Android SDK; we also assume
you've downloaded and unpacked CrystaX NDK somewhere on your computer.
Java part
First off, open Android Studio and create a new Android project there:
Select "Android 4.0.3" target:
Select blank activity:
Accept default names and press the "Finish" button:
Layout
Now, modify app/res/layout/activity_main.xml so it looks like the following:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">
<TextView android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
MainActivity
Add the following lines into your MainActivity.onCreate():
TextView field = (TextView)findViewById(R.id.text);
field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
Add declaration of native method into MainActivity class:
private native String getGPSCoordinates(String rootPath);
Also, don't forget to add loading of the native library to the static initialization block:
static {
System.loadLibrary("test-boost");
}
Final content of MainActivity.java should be as below:
MainActivity.java
package net.crystax.examples.testboost;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.TextView;
public class MainActivity extends ActionBarActivity {
static {
System.loadLibrary("test-boost");
}
private native String getGPSCoordinates(String rootPath);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView field = (TextView)findViewById(R.id.text);
field.setText(getGPSCoordinates(getFilesDir().getAbsolutePath()));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
We have finished with Java part of the application; let's switch to the native part now.
Native part
Create the folder where native sources will be located:
Use the default 'main' source set in next window and press the "Finish" button:
Sources
Create the following files in the just-created folder (app/src/main/jni):
Android.mk
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := test-boost
LOCAL_SRC_FILES := test.cpp gps.cpp
LOCAL_STATIC_LIBRARIES := boost_serialization_static
LOCAL_LDLIBS := -llog
include $(BUILD_SHARED_LIBRARY)
$(call import-module,boost/1.57.0)
gps.hpp
#ifndef GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
#define GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
#include <string>
// include headers that implement a archive in simple text format
#include <boost/archive/text_oarchive.hpp>
#include <boost/archive/text_iarchive.hpp>
/////////////////////////////////////////////////////////////
// gps coordinate
//
// illustrates serialization for a simple type
//
class gps_position
{
private:
friend class boost::serialization::access;
friend std::ostream &operator<<(std::ostream &, gps_position const &);
// When the class Archive corresponds to an output archive, the
// & operator is defined similar to <<. Likewise, when the class Archive
// is a type of input archive the & operator is defined similar to >>.
template<class Archive>
void serialize(Archive & ar, const unsigned int version)
{
ar & degrees;
ar & minutes;
ar & seconds;
}
int degrees;
int minutes;
float seconds;
public:
gps_position(){}
gps_position(int d, int m, float s) :
degrees(d), minutes(m), seconds(s)
{}
bool operator==(gps_position const &g) const
{
return degrees == g.degrees &&
minutes == g.minutes &&
seconds == g.seconds;
}
bool operator!=(gps_position const &g) const
{
return !(*this == g);
}
};
void save(std::string const &root, gps_position const &g);
void load(std::string const &root, gps_position &g);
#endif // GPS_HPP_7D5AF29629F64210BE00F3AF697BA650
gps.cpp
#include <fstream>
#include "gps.hpp"
const char *FILENAME = "gps.dat";
std::ostream &operator<<(std::ostream &s, gps_position const &g)
{
s << "GPS(" << g.degrees << "/" << g.minutes << "/" << g.seconds << ")";
return s;
}
void save(std::string const &root, gps_position const &g)
{
// create and open a character archive for output
std::ofstream ofs(root + "/" + FILENAME);
boost::archive::text_oarchive oa(ofs);
// write class instance to archive
oa << g;
// archive and stream closed when destructors are called
}
void load(std::string const &root, gps_position &g)
{
// create and open an archive for input
std::ifstream ifs(root + "/" + FILENAME);
boost::archive::text_iarchive ia(ifs);
// read class state from archive
ia >> g;
// archive and stream closed when destructors are called
}
test.cpp
#include <jni.h>
#include <string.h>
#include <stdlib.h>
#include <string>
#include <exception>
#include <sstream>
#include <android/log.h>
#define LOG(fmt, ...) __android_log_print(ANDROID_LOG_INFO, "TEST-BOOST", fmt, ##__VA_ARGS__)
#include "gps.hpp"
std::string gps(std::string const &root)
{
const gps_position g(35, 59, 24.567f);
save(root, g);
gps_position newg;
load(root, newg);
std::ostringstream ostr;
if (g != newg)
return std::string();
ostr << "GPS coordinates: " << newg;
return ostr.str();
}
extern "C"
jstring
Java_net_crystax_examples_testboost_MainActivity_getGPSCoordinates( JNIEnv* env,
jobject thiz,
jstring rootPath )
{
const char *s = env->GetStringUTFChars(rootPath, 0);
std::string root(s);
env->ReleaseStringUTFChars(rootPath, s);
LOG("root: %s", root.c_str());
try {
std::string ret = gps(root);
return env->NewStringUTF(ret.c_str());
}
catch (std::exception &e) {
LOG("ERROR: %s", e.what());
abort();
}
catch (...) {
LOG("Unknown error");
abort();
}
}
Build script
Now, we need to modify the build script to allow building the native part as well as the Java one.
To do that, we first need to open local.properties and add the path to the CrystaX NDK, like below:
sdk.dir=/opt/android/android-sdk-mac
ndk.dir=/opt/android/crystax-ndk-10.1.0
For Windows users, backslashes and colons in the path should be escaped:
sdk.dir=C\:\\android\\android-sdk-mac
ndk.dir=C\:\\android\\crystax-ndk-10.1.0
And, finally, open and edit build.gradle:
Make it to be exactly like the following:
build.gradle
import org.apache.tools.ant.taskdefs.condition.Os
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion "21.1.2"
defaultConfig {
applicationId "net.crystax.examples.testboost"
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets.main.jni.srcDirs = [] // disable automatic ndk-build call, which ignore our Android.mk
sourceSets.main.jniLibs.srcDir 'src/main/libs'
// call regular ndk-build(.cmd) script from app directory
task ndkBuild(type: Exec) {
workingDir file('src/main')
commandLine getNdkBuildCmd()
}
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn ndkBuild
}
task cleanNative(type: Exec) {
workingDir file('src/main')
commandLine getNdkBuildCmd(), 'clean'
}
clean.dependsOn cleanNative
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
}
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
}
Here is the diff for those who are interested what exactly we've added to build.gradle:
build.gradle.diff
diff --git a/build.gradle b/build.gradle
index a6b8c98..08dce1c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,3 +1,5 @@
+import org.apache.tools.ant.taskdefs.condition.Os
+
apply plugin: 'com.android.application'
android {
@@ -17,9 +19,50 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
+
+ sourceSets.main.jni.srcDirs = [] // disable automatic ndk-build call, which ignore our Android.mk
+ sourceSets.main.jniLibs.srcDir 'src/main/libs'
+
+ // call regular ndk-build(.cmd) script from app directory
+ task ndkBuild(type: Exec) {
+ workingDir file('src/main')
+ commandLine getNdkBuildCmd()
+ }
+
+ tasks.withType(JavaCompile) {
+ compileTask -> compileTask.dependsOn ndkBuild
+ }
+
+ task cleanNative(type: Exec) {
+ workingDir file('src/main')
+ commandLine getNdkBuildCmd(), 'clean'
+ }
+
+ clean.dependsOn cleanNative
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:21.0.3'
}
+
+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
+}
File tree
The source file tree of the TestBoost/app folder should look like the following:
.
├── app.iml
├── build.gradle
├── proguard-rules.pro
└── src
├── androidTest
│ └── java
│ └── net
│ └── crystax
│ └── examples
│ └── testboost
│ └── ApplicationTest.java
└── main
├── AndroidManifest.xml
├── java
│ └── net
│ └── crystax
│ └── examples
│ └── testboost
│ └── MainActivity.java
├── jni
│ ├── Android.mk
│ ├── Application.mk
│ ├── gps.cpp
│ ├── gps.hpp
│ └── test.cpp
└── res
.......
Final result
That's it! Now build the project as usual (Build -> Make Module 'app') and run it on the device.
Here is a screenshot of the running application: