Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jonnybrooks/13c6cbae832e7d1662e6c1431380d42d to your computer and use it in GitHub Desktop.
Save jonnybrooks/13c6cbae832e7d1662e6c1431380d42d to your computer and use it in GitHub Desktop.
Creaing a purely C++ Android app with the NDK (and without Android Studio)

Creating a purely C++ Java app with the NDK (and without Android Studio)

There are a few main steps to getting this working:

  1. Install tools - Android SDK (along /w JDK and build-tools), and the NDK
  2. Configure project - setting up AndroidManifest.xml, adding android_native_app_glue to your project
  3. Build - setup Android.mk for use with ndk-build, and write a simple build script to pipe it all together

Install tools

If you haven't already, you'll need to install the Android SDK (which I think includes the JDK? Otherwise grab that as well), the Android NDK, and the SDK build-tools. You can do that using SDK manager. Downloading these will get you the following tools:

  • ndk-build (NDK) - for building the final .so file for the .apk
  • aapt (build-tools) - this is for packaging your .apk (app) file
  • jarsigner (JDK) - for signing the .apk
  • keytool (JDK) - for creating a key store for use with jarsigner
  • zipalign (build-tools) - for compressing the .apk
  • (also ndk-gdb if you intend to debug using that)

The NDK download will also include a couple files you need to link against. These files are called android_native_app_glue.h/c. They are thoroughly explained in the native-activity sample provided by Google - I highly recommend reading the entire sample!

Configure project

First you'll want to set up your AndroidManifest.xml file in the root of your project. Mine is pretty small so I'll just include the whole thing inline:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="digital.panthalassa.hitlist" android:versionCode="1" android:versionName="1.0">
    <uses-sdk android:minSdkVersion="22" android:targetSdkVersion="29" />
    <uses-permission android:name="android.permission.SET_DEBUG_APP"></uses-permission>
	<application android:allowBackup="false" android:fullBackupContent="false" android:icon="@drawable/color_icon" android:label="@string/app_name" android:hasCode="false" android:debuggable="true">
		<activity android:name="android.app.NativeActivity" android:label="@string/app_name" android:configChanges="keyboardHidden" android:screenOrientation="portrait">
        	<meta-data android:name="android.app.lib_name" android:value="native-activity" />
			<intent-filter>
				<action android:name="android.intent.action.MAIN" />
				<category android:name="android.intent.category.LAUNCHER" />
			</intent-filter>
		</activity>
	</application>
</manifest>

I then recommend that you copy the android_native_app_glue.h and android_native_app_glue.c files into your project in prep for linking. Mine were at the following path:

$ANDROID_SDK_ROOT/ndk/21.3.6528147/sources/android/native_app_glue/

($ANDROID_SDK_ROOT is simply an environment variable I made which points to the root of the SDK). You won't need to include them directly in your main cpp file, you can just define it to link on the command line using ndk-build.

Build

Building isn't too complicated, it just involves a lot of moving pieces.

Firstly it requires writing a configuration file called Android.mk for use with ndk-build. It is essentially just a make file with a few custom macros useful for Android builds.

In your Android project root (i.e. next to AndroidManifest.xml), create a directory called jni/ (it must be called jni/ in order for ndk-build to find it!), and within that directory create a file called Android.mk. Again, the file is super small, so I'll just include the whole thing inline:

LOCAL_PATH := /home/jonny/code/hitlist/code

include $(CLEAR_VARS)
LOCAL_CFLAGS += -Wno-writable-strings -O2
LOCAL_LDLIBS := -llog -landroid
LOCAL_SRC_FILES := $(LOCAL_PATH)/android/android_native_app_glue.c $(LOCAL_PATH)/android_hitlist.cpp
LOCAL_MODULE := native-activity # important caveat, see NB below for details
include $(BUILD_SHARED_LIBRARY)

NB: The LOCAL_MODULE name must exactly match the name given in the following AndroidManifest.xml field (specified above):

<meta-data android:name="android.app.lib_name" android:value="native-activity" />

If they don't match, this won't work!

With that in place, you can go about writing a build script which takes all the necessary steps to build an .apk for your desired architecture. I will include my personal build script (with additional comments) in an attached file in this gist. I build on Linux, but it should be a fairly trivial translation from .sh into a batch file or some such! I also have a "run" script which deploys the .apk to an emulator, which I'll include too as a bonus.

Happy coding!

#!/bin/bash
set -e -x
# define some convenient ways to refer to our build tools
aapt="$ANDROID_SDK_ROOT/build-tools/29.0.2/aapt"
zipalign="$ANDROID_SDK_ROOT/build-tools/29.0.2/zipalign"
# Define the android API you're targeting here
android_platform="android-29"
# Define the architecture you're targeting. Options are armeabi-v7a, arm64-v8a, x86, x86_64
target_libcpp="arm64-v8a" # for Snapdragon 845, OnePlus 6T
# set up build directories
rm -rf build/*.apk build/apk
mkdir -p "build/apk/lib"
# build shared lib with ndk-build
pushd code/
$ANDROID_NDK_ROOT/ndk-build NDK_DEBUG=1 # for more info on ndk-build, see https://developer.android.com/ndk/guides/ndk-build
popd
# copy .so to build/apk/lib/$target_libcpp (this directory hierarchy is necessary so don't change it!)
cp -a code/libs/$target_libcpp build/apk/lib
# create the apk
$aapt package --debug-mode -f -M "code/AndroidManifest.xml" -S "code/res" \
-I "$ANDROID_SDK_ROOT/platforms/$android_platform/android.jar" \
-A "data/" -F "build/apk/hitlist.unsigned.apk" "build/apk"
# create the keystore for signing the APK - keytool and jarsigner are utilities provided by the JDK
# NB: This only needs to happen once so in reality I comment this command out after the first run
keytool -genkeypair -keystore "build/debug.keystore" -storepass android -keypass android -alias androiddebugkey -dname "cn=jonny" -keyalg RSA -keysize 2048 -validity 20000
# sign the APK
jarsigner -sigalg SHA1withRSA -digestalg SHA1 -storepass android -keypass android -keystore "build/debug.keystore" \
-signedjar "build/apk/hitlist.signed.apk" "build/apk/hitlist.unsigned.apk" androiddebugkey
# compress the APK - an optional step, but a standard one
$zipalign 4 "build/apk/hitlist.signed.apk" "build/hitlist.apk"
# clean apk files
rm -rf build/apk
#!/bin/bash
set -e -x
# run the build script first
./build_android
# install and run the apk using [adb](https://developer.android.com/studio/command-line/adb)
adb install --user 0 -r build/hitlist.apk
adb shell am start -a android.intent.action.MAIN -n digital.panthalassa.hitlist/android.app.NativeActivity
# this trap sets up a command to kill the running app if the user hits Ctrl+C in the terminal
trap "adb shell am force-stop digital.panthalassa.hitlist" SIGINT
# begin logging the app's output in the terminal
adb logcat --pid=`adb shell pidof -s digital.panthalassa.hitlist`
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment