Ejemplo de JNI, usar código en C desde Java

Escrito por el , actualizado el .
gnu-linux java planeta-codigo programacion
Enlace permanente Comentarios

Para tareas muy específicas que requieran alto rendimiento, baja latencia, tiempo real o haya restricciones de tiempo el lenguaje Java y la JVM pueden mostrar algunas limitaciones obligando a escribir alguna sección crítica de un programa en un lenguaje nativo como C o C++. Para hacer posible la integración entre Java y C existe en Java la API JNI. En este artículo mostraré como realizar un programa Java que emite el mensaje Hola Mundo desde una biblioteca compartida en C y usando JNI.

Java

GNU

Linux

Nunca hasta ahora había tenido necesidad de crear un programa que no estuviese completamente escrito en el lenguaje Java. La API de Java ofrece multitud de clases para cualquier funcionalidad que necesitemos desde estructuras de datos hasta algoritmos de búsqueda o criptografía. También porque el rendimiento de un programa en Java es suficiente y similar a un programa equivalente escrito en C o C++ gracias a las optimizaciones que implementa la máquina virtual de Java o JVM aún siendo los programas Java compilados a una representación intermedia de bytecode independiente de la arquitectura de procesador y sistema operativo en archivos de extensión class y posteriormente interpretados y traducidos a la arquitectura de ejecución, lo que le proporciona a Java la conocida frase “Write once, run anywhere”.

Sin embargo, en casos que se necesita un alto rendimiento para tareas muy específicas o evitar las imposiciones de la máquina virtual como las paradas que realiza para el recolector de basura una solución es escribir esa funcionalidad crítica en lenguaje C, C++ e incluso en Go. El caso de necesidad que me he encontrado es acceder a un sensor de temperatura DHT11 del kit de iniciación a la electrónica para la Raspberry Pi para leer de él la temperatura y humedad. La forma que tiene el sensor DHT11 de proporcionar los datos tiene restricciones de tiempo, cuando se le requieren los valores envía 80 bits de datos donde un pulso de 27μs significa un 0 y un pulso de más de ese tiempo hasta 70μs significa un 1. Estas restricciones de tiempo del sensor y el hecho de que es en una modesta en potencia Raspberry Pi 1 donde lo usaré hace que Java no sea capaz de leer correctamente los valores del sensor.

Acceder desde Java a código nativo en C requiere usar Java Native Interface o por sus siglas JNI. Lo primero que hay que realizar es crear una clase que declare los métodos que serán implementados de forma nativa declarando estos métodos usando la palabra reservada native y que serán enlazados por la JVM cargando una librería compartida con System.loadLibrary(). Creada la clase Java se ha de generar el archivo de cabecera .h propia del lenguaje C con el programa de utilidad del JDK javah. Con el archivo de cabecera se implementa la función y se crea una librería compartida en GNU/Linux usando el compilador gcc. Con la librería compartida se puede iniciar el programa Java. Si la biblioteca compartida no se encuentra se lanzará una excepción del tipo UnsatisfiedLinkError.

Excepción UnsatisfiedLinkError cuando no se encuentra la librería de código nativo

Excepción UnsatisfiedLinkError cuando no se encuentra la librería de código nativo

Algunas otras necesidades para hacer uso de JNI son:

  • Acceder a características dependientes de la plataforma necesitadas por la aplicación que no están soportadas en la librería estándar de Java.
  • Ya hay una librería escrita en otro lenguaje y se quiere hacer accesible a código Java a través de JNI.
  • Se quiere implementar una pequeña parte de código crítico en un lenguaje de bajo nivel como ensamblador.

Desde los métodos de código nativo se puede:

  • Crear, inspeccionar y actualizar objetos Java (incluyendo arrays y strings).
  • Llamar a métodos Java.
  • Capturar y lanzar excepciones.
  • Cargar y obtener información de clases.
  • Realizar validación de tipos en tiempo de ejecución.

Los comandos para generar el archivo de cabecera de C y compilarlo con el código nativo en una librería compartida con gcc son:

1
2
$ javah -d src/main/c -classpath buil/classes/main io.github.picodotdev.blogbitix.javaraspberrypi.JniHelloWorld
$ gcc -I"/usr/lib/jvm/java-8-openjdk/include" -I"/usr/lib/jvm/java-8-openjdk/include/linux" -shared -fPIC -L/usr/lib -o src/main/resources/libjnihelloworld-amd64.so src/main/c/JniHelloWorld.c
build.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
apply plugin: 'java'
apply plugin: 'idea'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    mavenCentral()
}

...

task javah1(type:Exec) {
    commandLine "javah", "-d", "src/main/c", "-classpath", sourceSets.main.output.classesDir, "io.github.picodotdev.blogbitix.javaraspberrypi.JniHelloWorld"
}

...

task javah(dependsOn: ['javah1', 'javah2']){
}

task gcc1(type:Exec, dependsOn: 'javah') {
    commandLine "gcc", "-shared", "-fPIC", "-I/usr/lib/jvm/java-8-openjdk/include", "-I/usr/lib/jvm/java-8-openjdk/include/linux", "-o", "src/main/resources/libjnihelloworld-amd64.so", "src/main/c/JniHelloWorld.c"
}

task gcc(dependsOn: ['gcc1']){
}

...

task executeJniHelloWorldLocal(type: JavaExec, dependsOn: ['build', 'gcc']) {
    main = 'io.github.picodotdev.blogbitix.javaraspberrypi.JniHelloWorld'
    classpath = sourceSets.main.runtimeClasspath
}

...
build.gradle

La cabecera usa varias definiciones de tipos definidas en los archivos jni.h y el archivo que variará según el sistema operativo jni_md.h. En la estructura JNIEnv con múltiples funciones de integración en C y Java, también varias definiciones de los tipos Java para usarlos en C como jobject, jstring, jint, jboolean, jlong, jdouble, jchar, etc.

El programa que emite el mensaje Hello World! desde código nativo en C debe cargar y enlazar la librería de código nativo con el código de la clase Java. Esto se muestra en el bloque de inicialización static de la clase, en este caso usándo el método System.load(), la librería de código nativo de extensión .so en GNU/Linux como en este caso al construirse el proyecto se incluye en el archivo .jar del artefacto resultante se extráe al directorio temporal y se carga desde esa ubicación temporal. En el programa se llama al método print implementado en código nativo y en el código C se usa la función printf de la librería stdio para emitir el mensaje:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.github.picodotdev.blogbitix.javaraspberrypi;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public class JniHelloWorld {

    static {
        String architecture = System.getProperty("os.arch");
        String library = String.format("/libjnihelloworld-%s.so", architecture);
        try (InputStream is = JniHelloWorld.class.getResourceAsStream(library)) {
            Path path = File.createTempFile("libjnihelloworld", "so").toPath();
            Files.copy(is, path, StandardCopyOption.REPLACE_EXISTING);
            System.load(path.toAbsolutePath().toString());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private native void print();

    public static void main(String[] args) {
        new JniHelloWorld().print();
    }
}
JniHelloWorld.java
1
2
3
4
5
6
7
8
#include <stdio.h>
#include "io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld.h"

JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld_print(JNIEnv *env, jobject obj)
{
    printf("Hello World!\n");
    return;
}
JniHelloWorld.c
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld */

#ifndef _Included_io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld
#define _Included_io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_io_github_picodotdev_blogbitix_javaraspberrypi_JniHelloWorld_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
JniHelloWorld.h

La librería compartida para un sistema amd64 la he compilado en mi equipo de escritorio y para la versión arm en la Raspberry Pi e incluido en el directorio src/main/resources de código fuente del ejemplo.

1
2
3
$ ./gradlew build
$ java -classpath build/classes/main:build/resources/main io.github.picodotdev.blogbitix.javaraspberrypi.JniHelloWorld
$ ssh -t 192.168.1.101 'cd /home/raspberrypi/scripts/javaraspberrypi && java -classpath "*" io.github.picodotdev.blogbitix.javaraspberrypi.JniHelloWorld'
execute.sh

Mensaje en la terminal emitido desde código nativo (amd64) Mensaje en la terminal emitido desde código nativo (ARM)

Mensaje en la terminal emitido desde código nativo en un sistema amd64 y ARM

Ente ejemplo usa Java 8 y requiere instalar el compilador gcc para compilar la librería con código nativo. Gradle ofrece soporte para compilar código nativo con su plugin, sin embargo, he preferido usar y conocer los comandos javah y gcc sin usar Gradle. En el siguiente artículo mostraré el ejemplo del sensor DHT11 usando JNI y código nativo en C llamando a métodos de un objeto Java desde código C.

Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew executeJniHelloWorldLocal


Comparte el artículo: