Skip to main content

Kotlin JNI

Introduction to JNI in Kotlin

Java Native Interface (JNI) is a powerful feature that allows Java code (and by extension, Kotlin code) to interact with native applications and libraries written in other programming languages such as C, C++, or Assembly. Since Kotlin runs on the Java Virtual Machine (JVM), it can leverage JNI just like Java does.

In this tutorial, we'll explore how to use JNI with Kotlin, allowing you to incorporate native code into your Kotlin applications. This is particularly useful when:

  • You need to access platform-specific features
  • You want to reuse existing native libraries
  • You need performance-critical sections of code to run as close to the hardware as possible
  • You're developing applications that need direct access to system resources

Understanding JNI Basics

Before diving into implementation details, let's understand the core components of JNI:

  1. Native Methods: These are methods declared in Kotlin/Java but implemented in native code
  2. Native Libraries: Compiled C/C++ code that contains the implementations of native methods
  3. JNI Functions: The interface that allows native code to interact with the JVM

Setting Up JNI with Kotlin

Let's walk through the process of creating a simple JNI application with Kotlin:

Step 1: Set up your project structure

Create a project structure like this:

project/
├── src/
│ ├── main/
│ │ ├── kotlin/ # Kotlin source files
│ │ └── cpp/ # C++ source files
│ └── test/
├── build.gradle.kts # Gradle build script
└── settings.gradle.kts # Gradle settings

Step 2: Declare native methods in Kotlin

Create a Kotlin file that declares the native methods:

kotlin
package com.example.jni

class NativeUtils {
// Declare a native method
external fun sayHello(name: String): String

// Load the native library when the class is loaded
companion object {
init {
System.loadLibrary("nativeutils")
}
}
}

The external keyword in Kotlin (equivalent to native in Java) indicates that the method is implemented in native code. The System.loadLibrary() call loads the native library that will contain the implementation.

Step 3: Generate header files

After compiling your Kotlin code, you need to generate C/C++ header files. In Java, you would use the javah tool, but it's been deprecated. Instead, you'll create the header file manually or use a JNI helper tool.

Here's what a manually created header file might look like for our example:

cpp
// nativeutils.h
#include <jni.h>

#ifndef _Included_com_example_jni_NativeUtils
#define _Included_com_example_jni_NativeUtils

#ifdef __cplusplus
extern "C" {
#endif

/*
* Class: com_example_jni_NativeUtils
* Method: sayHello
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_example_jni_NativeUtils_sayHello
(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Step 4: Implement the native method in C/C++

Now create a C/C++ file that implements the native method:

cpp
// nativeutils.cpp
#include "nativeutils.h"
#include <string>

JNIEXPORT jstring JNICALL Java_com_example_jni_NativeUtils_sayHello
(JNIEnv *env, jobject obj, jstring name) {

// Convert jstring to C++ string
const char *nameStr = env->GetStringUTFChars(name, NULL);
if (nameStr == NULL) {
return NULL; // Out of memory
}

// Create the greeting
std::string greeting = "Hello, " + std::string(nameStr) + " from C++!";

// Release the string resources
env->ReleaseStringUTFChars(name, nameStr);

// Convert C++ string back to jstring and return
return env->NewStringUTF(greeting.c_str());
}

Step 5: Compile the native library

You'll need to compile the C/C++ code into a shared library. The exact command depends on your operating system:

On Linux:

bash
g++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux nativeutils.cpp -o libnativeutils.so

On macOS:

bash
g++ -shared -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin nativeutils.cpp -o libnativeutils.dylib

On Windows:

bash
g++ -shared -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 nativeutils.cpp -o nativeutils.dll

Step 6: Use the native method in Kotlin

Now you can call the native method from your Kotlin code:

kotlin
fun main() {
val nativeUtils = NativeUtils()
val result = nativeUtils.sayHello("Kotlin Developer")
println(result)
}

Output:

Hello, Kotlin Developer from C++!

Using Gradle for JNI

For a more streamlined workflow, you can use Gradle with the C++ plugin to automate the process:

kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "1.8.0"
id("cpp-library")
}

library {
// Configure C++ library
targetMachines.add(machines.linux.x86_64)
targetMachines.add(machines.windows.x86_64)
targetMachines.add(machines.macOS.x86_64)
}

tasks.register<Exec>("generateJniHeaders") {
dependsOn("compileKotlin")
// Commands to generate JNI headers
}

tasks.named("compileReleaseCpp") {
dependsOn("generateJniHeaders")
}

JNI Data Types and Conversions

When working with JNI, you need to convert between Java/Kotlin types and native C/C++ types:

Kotlin/Java TypeJNI TypeC/C++ Type
booleanjbooleanunsigned char
bytejbytesigned char
charjcharunsigned short
shortjshortshort
intjintint
longjlonglong long
floatjfloatfloat
doublejdoubledouble
Stringjstringjobject
arraysj<Type>Arrayjobject
objectsjobjectjobject

Example of Array Handling

Here's an example showing how to pass and manipulate arrays between Kotlin and C++:

Kotlin code:

kotlin
class NativeArrayProcessor {
external fun processIntArray(input: IntArray): IntArray

companion object {
init {
System.loadLibrary("arrayprocessor")
}
}
}

fun main() {
val processor = NativeArrayProcessor()
val input = intArrayOf(1, 2, 3, 4, 5)
val result = processor.processIntArray(input)
println("Result: ${result.joinToString()}")
}

C++ implementation:

cpp
JNIEXPORT jintArray JNICALL Java_com_example_jni_NativeArrayProcessor_processIntArray
(JNIEnv *env, jobject obj, jintArray input) {

// Get the length of the array
jsize length = env->GetArrayLength(input);

// Get a pointer to the array elements
jint *elements = env->GetIntArrayElements(input, NULL);
if (elements == NULL) {
return NULL; // Out of memory
}

// Process the array (multiply each element by 2)
for (int i = 0; i < length; i++) {
elements[i] *= 2;
}

// Create a new array for the result
jintArray result = env->NewIntArray(length);
if (result == NULL) {
env->ReleaseIntArrayElements(input, elements, 0);
return NULL; // Out of memory
}

// Set the processed elements to the result array
env->SetIntArrayRegion(result, 0, length, elements);

// Release resources
env->ReleaseIntArrayElements(input, elements, 0);

return result;
}

Output:

Result: 2, 4, 6, 8, 10

Real-World Use Cases for JNI

Here are some practical applications where JNI can be particularly useful:

1. Hardware Access

Imagine you need to access a custom hardware device from your Kotlin application:

kotlin
class HardwareAccess {
external fun initializeDevice(): Boolean
external fun readSensorData(): FloatArray
external fun closeDevice()

companion object {
init {
System.loadLibrary("hwaccess")
}
}
}

2. Performance-Critical Algorithms

For computationally intensive tasks like image processing:

kotlin
class ImageProcessor {
external fun applyFilter(pixels: IntArray, width: Int, height: Int, filterType: Int): IntArray

companion object {
const val FILTER_BLUR = 0
const val FILTER_SHARPEN = 1

init {
System.loadLibrary("imageproc")
}
}
}

3. Integration with Existing C/C++ Libraries

Access established C/C++ libraries like OpenCV for computer vision:

kotlin
class OpenCVBridge {
external fun detectFaces(imageData: ByteArray, width: Int, height: Int): Array<Rectangle>

companion object {
init {
System.loadLibrary("opencv_java")
System.loadLibrary("opencvbridge")
}
}
}

Best Practices and Considerations

When working with JNI in Kotlin, keep these best practices in mind:

  1. Error Handling: Check for NULL returns and handle exceptions properly in both Kotlin and native code.
  2. Resource Management: Always release native resources like memory allocations to prevent leaks.
  3. Threading: Be aware of JNI's threading rules, especially when using callbacks from native code.
  4. Platform Compatibility: Build your native libraries for all target platforms.
  5. Performance Monitoring: JNI calls have overhead, so measure performance to ensure the native code actually improves performance.
  6. Security: Validate all data passed between the JVM and native code, especially if processing user input.

Common JNI Pitfalls

  • String Handling: Remember that Java strings are immutable and Unicode, while C/C++ strings are mutable and typically UTF-8.
  • Exception Handling: Native code needs to explicitly check for and throw exceptions.
  • Memory Management: JNI requires manual memory management in the native code.
  • Type Conversion: Be careful with type sizes and signedness differences.

Summary

Kotlin JNI allows you to integrate native code written in C/C++ with your Kotlin applications, providing access to platform-specific features, high-performance algorithms, and existing native libraries. While powerful, JNI comes with complexity due to manual memory management, type conversions, and cross-platform concerns.

In this tutorial, we've covered:

  • Setting up JNI in a Kotlin project
  • Declaring and implementing native methods
  • Converting data between Kotlin and C/C++
  • Real-world use cases for JNI
  • Best practices and common pitfalls

With these fundamentals, you're ready to explore more advanced JNI applications and confidently integrate native code into your Kotlin projects.

Additional Resources

Exercises

  1. Create a simple "Hello World" application using Kotlin JNI.
  2. Modify the array processing example to calculate the square root of each element.
  3. Write a JNI program that reads system information (CPU, memory, etc.) using native code.
  4. Create a Kotlin wrapper for a simple C library of your choice.
  5. Implement a native method that creates and returns a complex object (e.g., a list of custom objects) to Kotlin.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)