Android NDK (Native Development Kit) is not considered to be one of the most developer friendly tools. In this article, I propose a solution that makes working with it much easier.
Introduction
Why would you want to use C in your Android project? I can give you two good reasons for that:
- Performance. If you are working on an app with a lot of calculations (games, CADs, image processing, cryptography etc.), you might want to consider implementing some of those calculations in a C library.
- Cross platform development. You can integrate C libraries in both Android and iOS apps.
It is actually pretty simple to integrate C code into an iOS application written in Swift. About this, you can read more here:
In Android on the other hand, it is a much more complex task to integrate a C library. In this article, I will show you how to simplify this task.
Background
This is not an introduction to Android NDK. If you are just starting to familiarize yourself with it, a better place to start is the official documentation:
The Official Code Generation
Luckily, there is a built in tool in the Java compiler, that can generate native bindings from a Java class. At the time of writing this article, Kotlin is not officially supported by this tool, but as Java and Kotlin has an excellent interoperability, so you can use this in Kotlin projects as well. Let's see this in action. Here is a small example, where we have a message
class:
public class Message {
public String subject;
public String text;
}
and a send message function, that should be bound to a native C function:
public class HelloJNI {
static {
System.loadLibrary("hello");
}
private native boolean sendMessage(Message message);
}
Let's run the following command:
javac -h . HelloJNI.java
This will generate the following C bindings for Java Native Interface (JNI):
#include <jni.h>
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jboolean JNICALL Java_HelloJNI_sendMessage(JNIEnv *, jobject, jobject);
#ifdef __cplusplus
}
#endif
#endif
The important part of this generated code is the following function definition:
JNIEXPORT jboolean JNICALL Java_HelloJNI_sendMessage(JNIEnv *, jobject, jobject);
The first parameter is the JNI environment, the second is the HelloJNI
instance and the third is the Message
instance. Note that the types are not mapped at all, so for the third parameter, you need to know that it is a Message
instance, and it has a member called "subject
", which is a string
. Should you want to read the value of subject
, you would need to write something like this:
jclass messageClass = (*jenv)->FindClass((JNIEnv *) jenv, "com/jnigen/model/Message");
jfieldID fieldId = (*jenv)->GetFieldID((JNIEnv *) jenv,
messageClass, "subject", "Ljava/lang/String;");
char* value = (*jenv)->GetStringUTFChars((JNIEnv *) jenv,
(*jenv)->GetObjectField((JNIEnv *) jenv, obj, fieldId), 0);
Yes, you understand correctly, this code is equivalent to:
String value = message.subject;
There must be a better way, right?
Before I show you my solution to this problem, I would like to point out one more inconvenience with this kind of code generation. We have an Android app here, that consumes a C library, yet it is the app that defines the interface, that the C library should provide. That would be a big issue if one member of your team would write C library, and another would write the Kotlin/Java code. Even if you work alone, this would prevent you to do things in the logical order, which is kind of annoying.
A Better Way of Generating Code
I have written a code generator that works he opposite way: It generates Kotlin code, and C bindings from C code. This project can be found here:
Let's see the same example, but now with the new generator. Using this generator, we would start by writing a C header:
#ifndef MESSAGE_H
#define MESSAGE_H
struct Message {
char* subject;
char* text;
};
int sendMessage(struct Message message);
#endif
From this header, my generator would create three files:
The message struct would have a Kotlin counterpart:
package com.jnigen.model
data class Message (var subject: String = String(), var text: String = String())
The native interface:
package com.jnigen
import com.jnigen.model.*
class JniApi {
external fun sendMessage(message: Message): Int
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
And the binding code for JNI:
#include <stdlib.h>
#include "../example/example.h"
#include "jni.h"
jclass getMessageClass(JNIEnv const *jenv) {
return (*jenv)->FindClass((JNIEnv *) jenv, "com/jnigen/model/Message");
}
jmethodID getMessageInitMethodId(JNIEnv const *jenv) {
return (*jenv)->GetMethodID((JNIEnv *) jenv, getMessageClass(jenv), "<init>", "()V");
}
jobject createMessage(JNIEnv const *jenv) {
return (*jenv)->NewObject((JNIEnv *) jenv, getMessageClass(jenv),
getMessageInitMethodId(jenv));
}
jfieldID getMessageFieldID(JNIEnv const *jenv, char *field, char *type) {
return (*jenv)->GetFieldID((JNIEnv *) jenv,
getMessageClass(
jenv),
field, type);
}
jobject convertMessageToJobject(JNIEnv const *jenv, struct Message value) {
jobject obj = createMessage(jenv);
(*jenv)->SetObjectField((JNIEnv*)jenv, obj,
getMessageFieldID(jenv, "subject", "Ljava/lang/String;"),
(*jenv)->NewStringUTF((JNIEnv *) jenv, value.subject));
(*jenv)->SetObjectField((JNIEnv*)jenv, obj,
getMessageFieldID(jenv, "text", "Ljava/lang/String;"),
(*jenv)->NewStringUTF((JNIEnv *) jenv, value.text));
return obj;
}
struct Message convertJobjectToMessage(JNIEnv const *jenv, jobject obj) {
struct Message result;
result.subject = (*jenv)->GetStringUTFChars((JNIEnv *) jenv,
(*jenv)->GetObjectField((JNIEnv *) jenv, obj,
getMessageFieldID(jenv, "subject", "Ljava/lang/String;")), 0);
result.text = (*jenv)->GetStringUTFChars((JNIEnv *) jenv,
(*jenv)->GetObjectField((JNIEnv *) jenv, obj,
getMessageFieldID(jenv, "text", "Ljava/lang/String;")), 0);
return result;
}
JNIEXPORT
jint JNICALL
Java_com_jnigen_JniApi_sendMessage(JNIEnv *jenv, jobject instance,
jobject message) {
int result = sendMessage(
convertJobjectToMessage(jenv, message)
);
return result;
}
Notice how much better the result of this code generation process is. As C structs are correctly mapped to Kotlin data classes, there is basically no need to write any JNI code by hand.
You just write a C library, run the generator, and call it from Kotlin. It is that easy! ;)
History
- 17th February, 2021: Initial version