Article in Code Manual category.
Understanding Android NDK with Kotlin: Part 1 – Concepts
The Android NDK is a set of tools that allow you to use C and C++ code in your Android app. It provides platform libraries to manage…
An Android developer's daily life is full of challenges, from updates in the ecosystem, migration to Kotlin to using Dagger2, RxJava and so on. On my journey to becoming a better developer, I got a chance play with the Android NDK on one of my projects. I was excited to work on it but at the same time worried because of the lack of documentation.
Through this article, I’ll be sharing my experiences with the NDK and how to use it with Kotlin.
What is Android NDK
The Native Development Kit (NDK) is a set of tools that allow you to use C and C++ code in your Android app. It provides platform libraries to manage native activities and access hardware components such as sensors and touch input.
The NDK may not be appropriate for most novice Android programmers who need to use only Java code and framework APIs to develop their apps. However, the NDK can be useful for the following cases:
- Squeeze extra performance out of a device to achieve low latency or run computationally intensive applications, such as games or physics simulations.
- Reuse code between your iOS and Android apps.
- Use libraries like FFMPEG, OpenCV, etc.
Java Native Interface
JNI is the medium of interaction between the Java runtime and native code. It involves more than one language and runtime so some familiarity with C/C++ is recommended.
- Create a new project
- Include C++ and Kotlin support
- Download the latest Android NDK, CMake and LLDB from the SDK manager.
CMake and LLDB
CMake is cross-platform, free and open-source software for managing the build process of software using a compiler-independent method. The build project contains a
CMakeLists.txt file in every directory that controls the build process. List of CMake useful functions here.
LLDB is a next generation, high-performance debugger. LLDB currently converts debug information into
clang types so that it can leverage the
clang compiler infrastructure. It allows LLDB to support the latest C, C++ language features and runtimes in expressions without having to reimplement any of its functionality.
JNI Primitive Types
JNI has its own primitive and reference data types. To work with native code (call native functions written in C/C++, pass arguments, get results, etc.) primitive types are used.
JNI Function Example
To call a native function from Kotlin we’ll have to add:
JNIEXPORTmacro: A macro is a fragment of code which has been given a name. Whenever the name is used, it is replaced by the contents of the macro.
In this case,
JNIEXPORT will be replaced by
__attribute__ ((visibility (“default”))).
- Function name should start with
Javaand name of the
classthat contains the appropriate method in Java or Kotlin code.
The native function should have at least two arguments:
JNIEnv* is a pointer to the pointer to function tables. It provides most of the JNI functions. All your native functions will receive a
JNIEnv as the first argument.
jobject jObj is the class instance that contains this function in Kotlin code. In my case, it’s an object of Math class.
JNI functions are available through an interface pointer. An interface pointer is a pointer to a pointer. This pointer points to an array of pointers, each of which points to an interface function. Every interface function is at a predefined offset inside the array.see figure below for the illustration.
Adding Header File
To add header files we can either use
#include<filename> to include standard library header files or
#include "filename" to include programmer-defined header files.
To avoid including headers multiple times by mistake, wrapping the header content in
#ifndef — #endif can be used, this avoids error during compilation.
JNI — Reference Types
To get an instance of Kotlin class we have to use the above-mentioned reference types.
Local, Weak and Global References
We can create local, weak and global references with jObject.
JNIEnv *jEnv provides most of the JNI functions respectively for creating references.
NewGlobalRef(JNIEnv *env, jobject
NewWeakGlobalRef(JNIEnv *env, jobject
NewLocalRef(JNIEnv *env, jobject
Similarly, we have functions to delete the reference:
void DeleteLocalRef(JNIEnv *env, jobject localRef);,
void DeleteGlobalRef(JNIEnv *env, jobject globalRef); and
void DeleteWeakGlobalRef(JNIEnv *env, jweak obj);.
Android.mk and Application.mk
Android.mk is the makefile for building the native project. It allows you to group your sources into modules. A module is either a static library, a shared library, or a standalone executable. You can define one or more modules in each
Android.mk file, and you can use the same source file in multiple modules.
Application.mk file is really a tiny GNU Makefile fragment that defines several variables for compilation. This makefile describes several variables that will help make the assembly more flexible.
This covered most of the basics required to start using Native SDK in your project.
In this article, we went through the basics of Android NDK. Hop in for the Part 2 where I’ll demonstrate a sample app using the NDK where we will see how to communicate between Kotlin and C++