Click here to Skip to main content
15,887,267 members
Articles / Mobile Apps / Android
Tip/Trick

Using C for Android Development (the Easy Way)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
17 Feb 2021CPOL3 min read 7.4K   7  
Binding C structs and functions to Kotlin is easier than you have thought!
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:

  1. 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.
  2. 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:

Java
public class Message {
    public String subject;
    public String text;
}

and a send message function, that should be bound to a native C function:

Java
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):

C
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sendMessage
 * Signature: (LMessage;)Z
 */
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:

C
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:

C
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:

Java
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:

C
#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:

Java
//Generated code. Do not edit!

package com.jnigen.model

data class Message (var subject: String = String(), var text: String = String())

The native interface:

Java
//Generated code. Do not edit!

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:

C
//  Generated code. Do not edit!

#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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Code Sharp Kft.
Hungary Hungary
Making webshops faster at codesharp.dev

Comments and Discussions

 
-- There are no messages in this forum --