sábado, 7 de junio de 2014

Tutorial JNI en Linux parte 1

Este es el primer tutorial de JNI este tutorial forma parte de los de Java solo que es aparte ya que si los escribiera hasta que alcanzáramos este "nivel" de programación seria tardado ya que es de nivel avanzado y en los tutoriales de Java vamos en el 9 de nivel básico,faltarían 11 para entrar en este tema,así que lo veremos de una vez.

Bien primero que es JNI:

JNI son las siglas de de Java Native Interface, JNI es un framework con el cual podemos ejecutar métodos nativos(escritos en C,C++ y assembler),en la maquina virtual de Java,esto hace que podamos usar  soluciones de bajo nivel en un programa Java,la JNI se usa para ejecutar código mas veloz que el que este ejecuta en su maquina virtual.

Una consideración de JNI,ademas de otras que existen es que si implementamos métodos nativos Java pierde su "encanto" de que se puede portar donde sea,ya que si abran trabajado con C,C++ sabrán que los programas se hacen específicos para las distintas arquitecturas.

Una cosa buena de JNI es que al aprender a usarlo también nos servirá con Android cuando usemos el NDK.

Antes de empezar,este espacio de trabajo usare:

Sistema : Ubuntu 14.04 o Gentoo

Java version 8 : java version "1.8.0_05"

Editor de texto : Vim

Lenguaje de soporte : C

Compilador : gcc

Depurador : gdb

Terminal : gnome-terminal o konsole

Tutorial

Primero que nada vamos a crear una clase java que implemente un metodo nativo,vamos a hacer el ejemplo de Hola Mundo con JNI,para ello debemos implementar una función nativa en la clase la cual lleva dos modificadores  el tipo de funcion y no lleva implementacion,puede lucir asi:
private native void saluda();

El modificador private se usa porque se manipulara desde la propia clase solamente y en inversa el método nativo podrá acceder a elementos de la clase.

native es la palabra reservada para saber que sera un método nativo.

void es el tipo de función o de retorno,en este caso seria un procedimiento al no regresar nada.

saluda() es el nombre del procedimiento y en este caso no lleva parámetros.

Tenemos que hacer una cosa mas,tenemos que cargar la librería nativa,esto lo hacemos llamando la librería .so,que seria por ejemplo libHolaMundo.so, lo hacemos usando el bloque estático static que como mencionamos alguna vez este bloque se carga antes de cualquier cosa,dentro de el usamos la función System.loadLibrary le pasamos como argumento un String con el nombre de la librería sin lib y sin .so:

Por ejemplo:

System.loadLibrary("HolaMundo");

Bien así se ve el código entero:


// Archivo HolaMundo.java

class HolaMundo {
    private native void saluda();
    static {
        System.loadLibrary("HolaMundo");
    }
    
    public static void main(String []args) {
        System.out.println("Hola desde Java");
        new HolaMundo().saluda();
    }
    
}

Ahora debemos compilarlo:

javac HolaMundo.java

Esto nos genera el .class,aun no lo podemos ejecutar ya que necesitamos la librería,lo que debemos hacer es crearla,para ello debemos compilar un .c como .so,pero primero necesitamos crearlo y para ello primero debemos crear una cabecera .h,lo bueno es que java tiene esta utilidad:

Creamos el header:

javah HolaMundo


Ahora bien nos genera el .h y aunque es muy importante no debemos editarlo,incluso nos lo advierten en la primera linea:

Lo que debemos de ver es la funcion que crea,ya que es la que debemos implementar en el .c del mismo:


1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <jni.h>
/* Header for class HolaMundo */

#ifndef _Included_HolaMundo
#define _Included_HolaMundo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HolaMundo
 * Method:    saluda
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HolaMundo_saluda
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

En este caso:

JNIEXPORT void JNICALL Java_HolaMundo_saluda  (JNIEnv *, jobject);
Como vemos contiene dos Macros:

JNIEXPORT y JNICALL,los cuales se encargan de ejecutar correctamente la función.

Cambien tenemos el tipo de función en este caso void,como es un tipo similar al de c no hay problema ,pero veremos otros si fuera por ejemplo una función int,veríamos un jint,o una función String veríamos un jstring,etc.

Vemos el nombre de la función dividido en tres partes:

Java, el nombre de la clase y el nombre de la función escrita en la clase.

Por ultimo vemos dos argumentos?

Pero porque dos argumentos si no le pasamos nada desde java,estos son un JNIEnv que es el entorno,el que usaremos para toda la manipulación de datos de java,y un jobject que es la representación de la clase misma de Java(la que llama al programa en C).

Bien ahora si vamos a crear el .c,el cual no hará casi nada solo implementar esa función:

En la primera linea debemos o deberíamos llamar a jni.h pero como ya lo llamamos en el HolaMundo.h no es necesario,aunque podemos hacerlo,luego incluimos el stdio.h como siempre y por ultimo creamos la función que nos muestra el .h,prácticamente podemos copiar y pegar,pero no ya que aunque el .h define los dos argumentos que vamos usar no define los nombres,así queda el .c:


1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include "HolaMundo.h"


JNIEXPORT void JNICALL Java_HolaMundo_saluda
  (JNIEnv * env, jobject obj)
{
    puts("Hola desde JNI");
}

Bien ahora solo queda crear el .so,para ello debemos compilar llamando las librerias necesarias,en mi caso(por la versión de java que uso):

gcc -shared -I/usr/lib/jvm/java-8-oracle/include/ -I/usr/lib/jvm/java-8-oracle/include/linux/ -o libHolaMundo.so HolaMundo.c -Wall

Lo que esta en rojo es lo que debemos cambiar(o tal vez no):

Eso es todo ahora basta ejecutar,pero debemos redefinir o agrear el PATH actual al  LD_LIBRARY_PATH,para que podamos usar la librería que acabamos de crear ya que actualmente esta en . y no donde java lo pide :

export LD_LIBRARY_PATH=.
Ejecutamos;

java HolaMundo




Si deseamos modificar un archivo debemos ejecutar todo de nuevo,el javac,el javah,el gcc así que podemos crear un script en bash para automatizarlo,bueno eso me sirve a mi,vean este seria:


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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
NAME=HolaMundo # Editar

PJAVA=java-8-oracle # Editar

function genera(){
echo "Ejecutando javac $NAME.java"

javac $NAME.java

echo "Ejecutando javah $NAME"

javah $NAME
}

function compila() {
echo "Ejecutando gcc"

gcc -shared -I/usr/lib/jvm/$PJAVA/include/ -I/usr/lib/jvm/$PJAVA/include/linux/ -o lib$NAME.so $NAME.c -Wall

}

function ejecuta() {
echo -e "Ejecutando $NAME\n"

java $NAME
}

function ayuda() {
echo "$0
         -h : ayuda
         -g : generar .h
         -e : ejecutar
         -c : compila
         -a : ejecutar todo
    "
}

case $1 in
    -e)
        ejecuta
    ;;
    -g)
        genera
    ;;
    -c)
        compila
    ;;
    -a)
        genera
        compila
        ejecuta
    ;;
    -h|"")
        ayuda
    ;;
esac

Así solo ejecutamos:

bash exec.sh -a


Todo lo que acabamos de hacer,crear el .java el .h,el .c,el .sh(opcional) siempre lo vamos a hacer,así que esa era la introducción a JNI,ahora lo que sigue es saber usar el API,manejar funciones y tipos de datos de jni.h,usar,obtener,devolver y modificar objetos de java desde el código .c.

Vamos a hacer una función saludar mejorada esta función saludara a un nombre que le pasemos,para eso modificamos la función de la clase HolaMundo.java para que quede así:

private native void saluda(String nombre);

También modificamos la llamada,que esta mas abajo:


new HolaMundo().saluda("nombre");


Ahora compilamos y generamos el .h:

bash exec.sh -g

o

javac HolaMundo.java

javah HolaMundo

Bien luego debemos editar el .c,ya que si vemos el .h,cambio y nos pasa un argumento mas:

jstring

Para usar la String que nos pasa Java debemos crear una referencia a un tipo const char, y luego apuntar al String,primero seria así:

const char * nombre;

Luego tenemos que pedir la String con la función GetStringUTFChars o GetStringChars de jni la cual nos devuelve el String en UTF-8 o Unicode,le pasamos tres argumentos,el primero es el entorno,el segundo es el jstring y el ultimo un jboolean,este no nos interesa,podemos ponerlo en NULL o en JNI_TRUE.

Para llamar una función de JNI usamos un puntero de env simulando una clase de C++,así:

(env*)->funcion

Así es como quedaría:

nombre = (*env)->GetStringUTFChars(env,name,NULL);

name es el nombre del parámetro jstring,no del nombre del const char.

Ya se podria usar este código,pero tenemos que seguir las normas de todo programador C, ver si se creo el puntero(si no es igual a NULL) y liberar la memoria del puntero.

Lo primero es fácil:

if (nombre == NULL) {
       return; // Terminamos la funcion
}

Lo segundo se logra con una funcion propia de la API,no es simplemente usar free(),esa funcion es:

ReleaseStringUTFChars o ReleaseStringChars dependiendo con cual creamos el puntero.

Le posamos tres argumentos,el entorno,el jstring y el puntero a jstring,el const char,en este caso:

(*env)->ReleaseStringUTFChars(env,name,nombre);

Eso es todo,asi queda el código completo:





1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include "HolaMundo.h"

JNIEXPORT void JNICALL Java_HolaMundo_saluda
  (JNIEnv * env, jobject obj,jstring name)
{
    const char *nombre;
    nombre = (*env)->GetStringUTFChars(env,name,NULL);
    if (nombre == NULL)
    {
        return;
    }

    printf("Hola desde JNI : %s\n",nombre);

    (*env)->ReleaseStringUTFChars(env,name,nombre);
    
}

Compilamos:

gcc -shared -I/usr/lib/jvm/$PJAVA/include/ -I/usr/lib/jvm/$PJAVA/include/linux/ -o lib$NAME.so $NAME.c -Wall

o

bash exec.sh -c

Y ejecutamos:

java HolaMundo

o

bash exec.sh -e


Por ultimo vamos a ver como podriamos retornar una String,supongamos que queremos hacer un algoritmo en C que cambie las palabras del string por el simbolo que sigue,esto es facil en C,luego lo retormamos y lo imprimimos en Java:

Así queda la clase Java:




1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Archivo : HolaMundo.java

class HolaMundo {
    private native void saluda(String nombre);
    private native String encripta(String palabra);
    static {
        System.loadLibrary("HolaMundo");
    }
    
    public static void main(String []args) {
        System.out.println("Hola desde Java");
        new HolaMundo().saluda("Atheyus");
        System.out.println(new HolaMundo().encripta("Tiempo de Tux"));
    }
    
}

Compilamos y generamos el .h:

bash exec.sh -g
El HolaMundo.h luce asi en estos momentos:


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
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HolaMundo */

#ifndef _Included_HolaMundo
#define _Included_HolaMundo
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HolaMundo
 * Method:    saluda
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HolaMundo_saluda
  (JNIEnv *, jobject, jstring);

/*
 * Class:     HolaMundo
 * Method:    encripta
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_HolaMundo_encripta
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

Lo implementamos en el .c,así queda:


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
37
38
39
#include <stdio.h>
#include <string.h>
#include "HolaMundo.h"

JNIEXPORT void JNICALL Java_HolaMundo_saluda
  (JNIEnv * env, jobject obj,jstring name)
{
    const char *nombre;
    nombre = (*env)->GetStringUTFChars(env,name,NULL);
    if (nombre == NULL)
    {
        return;
    }

    printf("Hola desde JNI : %s\n",nombre);

    (*env)->ReleaseStringUTFChars(env,name,nombre);
}

JNIEXPORT jstring JNICALL Java_HolaMundo_encripta
  (JNIEnv * env, jobject obj, jstring word)
{
    const char *nombre;
    nombre = (*env)->GetStringUTFChars(env,word,NULL);
    if (nombre == NULL)
    {
        return NULL;
    }
    int size = (*env)->GetStringUTFLength(env,word);
    char encripta[size];
    char tnombre[size];
    strcpy(tnombre,nombre);
    int i;
    for (i=0;i<size;i++)
        encripta[i] = (tnombre[i]+1);
    jstring word_en = (*env)->NewStringUTF(env,encripta);
    (*env)->ReleaseStringUTFChars(env,word,nombre);
    return word_en;
}

Eso es todo compilamos y ejecutamos:


Para lograr eso en Java usaríamos StringBuilder,u otra opción pesada,pero en C asi seria,muy veloz,no voy a explicar las funciones dejo la documentación de ellas y de otras mas en el enlace que sigue:

Enlace

No hay comentarios.:

Publicar un comentario

Los comentarios serán revisados antes de ser publicados.