Buscar
Social
Ofertas laborales ES
« Vivir para ver, ver para leer, leer para ... | Main | Conexrión Java-C y C-Java »
lunes
oct012001

JNI (Java Native Interface)


JNI (Java Native Interface)




Desde sus principios Java dejo apoyarse en otros lenguajes de programación, pero su primera forma contaban con algunos problemas que algunos fabricantes trataron de resolver con sus diferentes interfaces. Para evitar los problemas de mantenimiento Sun crea el interfaces JNI.



JNI es el nuevo interfaces de código nativo que emplea Sun. JNI nos permite ejecutar código Java y comunicarnos con librerías escritas en otros lenguajes, como pueden ser C y C++. La mayor ventaja del JNI es que se puede programar una aplicación o librería nativa y trabajar en cualquier maquina virtual Java que soporte JNI.



Existen otros interfaces diferentes de métodos nativos como pueden ser:



  • Métodos nativos de JDK1.0: Tenia algunas deficiencias como son que los accesos a campos de los objetos Java se realizaban mediante estructuras de C. Este método podemos verlo en mi página club.idecnet.com/~frodrigu.
  • JRI (Java Runtime Interfaces): Es el interfaces de Netscape y es en el que se ha basado Sun para su JNI.
  • RNI (Raw Native Interfaces): Es uno de los métodos de bajo nivel de Microsoft.
  • Java / COM: El método de alto nivel de Microsoft para usar objetos COM como si de un objeto Java se tratase.



¿Cuando necesitamos JNI?



Intentaremos siempre escribir nuestra aplicación completamente en Java para evitar incompatibilidades con las plataformas, pero existen algunas situaciones en las que no sera posible. Algunas de las situaciones en las que tendremos que escribir métodos nativos son:



  • Cuando queramos utilizar una característica dependiente de la plataforma en donde vayamos a ejecutar nuestra aplicación como puede ser una tarjeta de recogida de datos.
  • Cuando tengamos que trabajar con librerías escritas en otro lenguajes y son imprescindibles para nuestra aplicación, y no podemos pasar esta librerías a Java.
  • Métodos críticos en ejecución, esto con los nuevos avances en tecnología Java cada vez será menos preciso utilizar métodos nativos para lograr velocidad, con la perdida de portabilidad que esto supone.
  • Para acceder a Java desde código nativo utilizamos las funciones de JNI, estas funciones están disponibles gracias al interfaces de punteros. Un interfaces de punteros es un puntero a un array de punteros que apuntara a funciones del interfaces, en la Figura A podemos ver gráficamente como es un interfaces de punteros JNI.








En la Figura A se ve como existe un puntero por cada interfaces JNI que tengamos, este apunta a un arrays de punteros, cada puntero del array apunta a las funciones de interfaces. Con esta estructura podremos tener una maquina virtual que soporte varias versiones de funciones de JNI, existiendo un puntero por cada interfaces. Los métodos nativos reciben un puntero al interfaces de JNI.



Como enlazamos los métodos nativos con Java



Al igual que pasaba con el sistema antiguo del JDK 1.0, con la función loadLibrary, cargamos la librería donde se encuentran nuestros métodos nativos un ejemplo de como quedaría el código sería.



Class mia
{
native int alguna_funcion(int I);
static
{
System.loadLibrary("MiLibreria");
}
}


El nombre de la librería dependerá del entorno en el que estemos desarrollando así en Windows 95 esa librería se llamaría MiLibreria.dll y en un sistema Unix como puede ser Linux libMiLibreria.so. La librería tendrá que estar en uno de los caminos que tengamos especificados para tal fin en nuestro entorno de desarrollo. En Unix por ejemplo seria los caminos que tengamos especificados en la variable del sistema LD_LIBRARY_PATH.



Estas librerías tendrán que ser dinámicas, si el sistema operativo en el que estuviéramos
trabajando no permitiese librerías dinámicas se tendrán que enlazar con la maquina virtual.



El nombre de las funciones en las librerías dinámicas se formara de la siguiente manera:




  1. El prefijo Java_ que impedirá que otras funciones de la librería que no son métodos nativos de Java se confundan con ellos.
  2. El nombre de la clase, si esta clase pertenece a un paquete el nombre de este aparecerá al principio y se separara de este con un guión bajo "_". Para la creación de nombre puede ser necesarios la utilización de caracteres de escape los podemos ver en la Tabla A.
  3. Separada del nombre de la clase con un guión bajo el nombre de la clase.
  4. Para poder tener sobrecarga de funciones también en los métodos nativos tendremos dos guiones bajos y después los tipos de argumentos de la clase que se codifican de la siguiente manera. En la Tabla B podemos ver la nomenclatura de los tipos.


Caracteres de Escape

Secuencia de escape Descripción
_0XXX Carácter Unicode XXXX
_1 Guión bajo "_"
_2 Punto y coma ";"
_3 Cierre corchete "["


Los parámetros serán pasados por copia, los tipos primitivos como enteros, flotantes, etc. Los demás tipos se pasaran por referencia. Todos los objetos pasados a un método nativo serán tratadas como locales de esta forma serán liberados cuando salgamos del método automáticamente. El momento de la liberación de los objetos locales del método dependerá de la maquina virtual y de la necesidad de esta para ejecutar el recolector de basura.











Compilación de métodos nativos



Para poder crear una librería de métodos nativos primero tendremos que compilar la clase que declare los métodos nativos la forma de decláralos es anteponiendo native al método, una vez que tememos la clase pasaremos la herramienta javah que nos permitirá crear ficheros cabecera a partir de una clase Java. La herramienta javah tendrá que ejecutarse con la opción -jni, para que nos genere una cabecera de prototipos métodos nativos con el estilo JNI. En el Listado 1 podemos ver un ejemplo de Makefile para la generación de programas Java con métodos nativos en un entorno Windows 95.




# Ejemplo de Makefile
#
# Fco. Javier Rodriguez Navarro (c)
#
# Uso: nmake nmakefile
#

# Poner en esta variable el camino a nuestro JDK
JDK =d:\jdk1.1.5
CFLAGS = -Zi -DWIN32 -I$(JDK)\include -I$(JDK)\include\win32
.SUFFIXES: .Java .class

ejecutable: Pruebas.class Ejemplo1.class Ej1_lib.dll

Pruebas.class: Pruebas.java
$(JDK)\bin\javac Pruebas.java

Ejemplo1.class: Ejemplo1.java
$(JDK)\bin\javac Pruebas.java

Ejemplo1.obj: Ejemplo1.h

Ejemplo1.h: Ejemplo1.class
$(JDK)\bin\javah -jni Ejemplo1

Ej1_lib.dll: Ejemplo1.obj
link -dll -out:Ej1_lib.dll Ejemplo1.obj

limpia:
-del *.class
-del *.obj
-del *.dll
-del *.lib
-del *.h



Para cualquier otro entorno deberemos cambiar las opciones de compilación y enlace, estas opciones las tenemos en la variable CFLAGS.





Funciones JNI



Mediante las funciones de JNI podremos acceder a variables de objetos, manipular arrays, etc. Es responsabilidad del programador no pasar objetos nulos a las funciones que no puedan recibir este tipo de objetos.



El acceso a las funciones se realiza mediante el argumento JNIEnv que nos llega como parámetro. JNIEnv es un puntero a la estructura JNINativeInterface que almacena los punteros a las funciones, si miramos el fichero jni.h (que se encuentra en el directorio de includes) podremos ver todas las funciones del JNI. Para realizar una llamada a una función de JNI tendremos que hacer una referencia a la entrada correspondiente del puntero JNIEnv, así para llamar a la función GetVersion que explicaremos a continuación escribiríamos en nuestro código.



(*env)->GetVersion(env);


env es la variable que contiene el puntero JNIEnv este puntero nos llega siempre en nuestros métodos nativos, también tenemos un segundo parámetros que es de tipo jobject que nos referencia a nosotros mismos.



jint GetVersion(JNIEnv *)


Esta función nos dará la versión de JNI que estamos utilizando, la versión es devuelta en formato de 32 bits, los primeros 16 bits son para el primer número de la versión y los otros 16 bits son el segundo número de versión.



Para la versión 1.1.x tendremos un resultado 0x00010001 que en decimal seria 65537 y para la versión 1.2 tendremos 0x00010002 que en decimal seria 65538.



En la versión 1.2 de Java cada cargador de clase será el propietario de sus librerías nativas, para esto aparecen dos funciones:



  • JNI_OnLoad: Es llamada por la maquina virtual cuando se carga la librería de métodos nativos y retorna la versión de JNI necesaria por la librería. Si la librería no informa de la versión asume que la versión de JNI que se necesitara es la 1.1, si la maquina virtual no reconociera la versión que retorna JNI_OnLoad la librería en este caso no podría ser cargada.

  • JNIOnUnload: La maquina virtual llamara a esta función cuando se llame al recolector de memoria no utilizada (recolector de basura).


Accesos a campos de la clase



A continuación veremos la forma de acceder a las variables de la clase desde nuestros métodos nativos. En el Listado 2 se puede ver el ejemplo en Java de una clase que llama a métodos nativos, como se declaran estos métodos y como cargamos la librería que contiene a estos métodos.



/**************************************************************
* Ejemplo1.java
*
* Descripcion: Ejemplo de como acceder a campos de la clase,
* tanto para recoger sus valore o para cambiarlos.
*
* Autor: Fco. Javier Rodriguez Navarro (c)
*
**************************************************************/
import java.io.*;
public class Ejemplo1
{
static int valor_estatico ;
int valor_a;
int valor_b;
int valor_suma;

protected native void leemos();
private native int suma(int a, int b);
public native void multiplica(int valor);

public void ejecuta()
{
// Cargamos la librería del ejemplo1.
System.loadLibrary("Ej1_lib");

// Recogemos los valores de a y b desde C.
leemos();

// Realizamos la suma de los valores recogidos
valor_suma = suma(valor_a, valor_b);
System.out.println("Java: La suma realizada es: " + valor_suma);

// Se multiplica por el estático.
valor_estatico = 5;
multiplica(valor_suma);
System.out.println("Java: El valor estático quedo en: "+valor_estatico);
}
}


La librería de los métodos nativos deberá encontrarse en el camino que tengamos en las variables PATH para los sistemas de Windows y LD_LIBRARY_PATH en los sistemas Unix.



El programa no tiene ninguna utilidad y es meramente didáctico y lo que realiza es lo siguiente:




  1. Recogemos los datos de dos variables desde C, esto se realiza mediante el método nativo leemos, y ponemos los valores en las variables valor_a, y valor_b.
  2. Realizamos una suma de las dos variables en el método nativos suma.
  3. Cambiamos el valor de la variable estática valor_estatico por la multiplicación de ella y el parámetro que le pasamos al método nativo multiplica. Se ha realizado esta función para tener un ejemplo de acceso a variables estáticas ya que difieren un poco de las variables que no lo son.


En el Listado 3 se puede ver la implementación de los métodos nativos y el uso de las funciones de JNI que se utilizan para recoger y modificar los valores de las variables de la clase, estas funciones son las que pasamos a explicar a continuación.



/***********************************************************
* Ejemplo1.c
*
* Descripcion: Métodos nativos para la clase Ejemplo1.
*
* Fco. Javier Rodriguez Navarro (c)
*
**********************************************************/
#include "Ejemplo1.h"
/******************* Método leemos *************************************/
JNIEXPORT void JNICALL Java_Ejemplo1_leemos (JNIEnv *env, jobject object)
{
int a, b;
jclass Miclase;
jfieldID ID_Campo;

// Recogemos valores.
printf("C: Valor de a: ");
scanf("%d",&a);
printf("\nC: Valor de b: ");
scanf("%d",&b);

// Pasamos los valores a las variables de la clase.
Miclase = (*env)->GetObjectClass(env, object); // Recogemos clase.
ID_Campo = (*env)->GetFieldID(env, Miclase, "valor_a", "I");
(*env)->SetIntField(env, object, ID_Campo, a);
ID_Campo = (*env)->GetFieldID(env, Miclase, "valor_b", "I");
(*env)->SetIntField(env, object, ID_Campo, b);
}

/****************** Método suma **************************************/
JNIEXPORT jint JNICALL Java_Ejemplo1_suma(JNIEnv *env, jobject object, jint a, jint b)
{
int resultado;
resultado = a + b;
return resultado;
}

/*************** Metdod multiplica ***************************************/
JNIEXPORT void JNICALL Java_Ejemplo1_multiplica (JNIEnv *env , jobject object, jint valor)
{
jint v_estatico;
jclass Miclase;
jfieldID ID_Campo;

// Recogemos el valor de la variable estática.
Miclase = (*env)->GetObjectClass(env, object); // Recogemos clase.
ID_Campo = (*env)->GetStaticFieldID(env, Miclase, "valor_estatico", "I");
v_estatico = (*env)->GetStaticIntField(env, Miclase, ID_Campo);

printf("C: El valor recogido es: %d\n",v_estatico);
v_estatico = valor * v_estatico;
printf("C: Cambiamos el valor estático a %d\n",v_estatico);

(*env)->SetStaticIntField(env, Miclase, ID_Campo, v_estatico);
}

 


JfieldID GetFieldID(JNIEnv *, jclass, const char *, const char *)


Esta función nos da el ID de la variable este es el primer paso antes de poder recoger o actualizar el valor con las funciones que veremos a continuación. Si no encuentra la variable nos devolverá NULL y saltara la excepción NoSuchFieldError. Los parámetros son los siguientes:



  • El primer parámetro es un puntero al entorno JNI que nos llega por parámetro.
  • El segundo será la clase de la que deseamos obtener la variable. Si lo que tenemos es un objeto, como pasa cuando queremos tener acceso a una variable de nuestra propia clase, (el segundo parámetro de nuestro método nativo es la referencia a nosotros mismos) se obtiene la clase con la función de JNI GetObjectClass esta función será explicada cuando hablemos de la manipulación de objetos.
  • El tercer parámetro será el nombre de la variable que deseamos tener acceso.
  • El cuarto es el tipo de la variable, la definición de las variables se pueden ver en la Tabla B, si queremos asegurarnos del tipo de variable podemos usar la utilidad javap con el parámetro s para obtener la representación interna de la variables de la clase.

    javap -s





Nomenclatura de tipos de datos

Tipo Java Tipo Nativo Nomenclatura
boolean jboolean Z
byte jbyte B
char jchar C
short jshort S
int jint I
long jlong J
float jfloat F
double jdouble D
void void No aplica
Otro tipo Ltipo; El ; se escribirá _2
Arrays tipo[ el [ se escribirá _3



Tipo_Nativo GetField(JNIEnv *, jobject, jfieldID)


Esta función nos permite obtener el valor de la variable del ID que le pidamos, este ID se habrá obtenido con la función anterior, tendremos una función para cada tipo. En la Tabla C podemos ver todas las funciones que nos permiten recoger los valores de la variables no estáticas.



Funciones para obtener valores de variables

Función Tipo nativo que recoge
GetObjectField jobject
GetBooleanField jboolean
GetByteField jbyte
GetCharField jchar
GetShortField jshort
GetIntField jint
getLongField jlong
GetFloatField jfloat
GetDoubleField jdouble


void SetField(JNIEnv *, jobject, jfieldID, Tipo_Nativo)


Esta función al contrario de la anterior nos sirve para modificar una variable de un objeto.



  • En el primer parámetro, como siempre, tendremos el puntero al entorno JNI.
  • El segundo parámetro, al igual que en la función anterior, es el objeto donde se encuentra la variable que deseamos modificar.
  • El tercero es el ID de la variable a modificar que se habrá obtenido con la función GetFieldID.
  • El cuarto será el nuevo valor con el cual actualizamos la variable.


Al igual que pasaba con la función anterior y con las que veremos a continuación que tengan que tratar con diferentes tipos, existirá una para cada tipo de dato primitivo.



En el método nativo leemos que se encuentra implementado en la función Java_Ejemplo1_leemos del programa C que vemos en el Listado 3 se puede ver como primero recogemos la clase con GetObjectClass para poder con ella obtener el ID de la variable con la función GetFieldID y como paso final actualizamos las variables de la clase con la función correspondiente al tipo que se desea actualizar SetField en nuestro caso al tratarse de un entero utilizamos la función SetIntField.



JfieldID GetStaticFieldID(JNIEnv *, jclass, const char *, const char *)


Al igual que en la variables no estáticas se tiene que obtener primero el ID de la variable. Los parámetros también son el puntero al entorno JNI, la clase de la que se quiere obtener la variable, nombre de la variable y el tipo de la variable.



Tipo_Natico GetStaticField(JNIEnv *, jclass, jfieldID)


Esta función nos dará el valor de la variable estática que nos indica el ultimo parámetro, la diferencia con su homologa de variables no estáticas es que en este caso no se le pasa el objeto sino la clase, esto es debido a que las variables estáticas son comunes a una clase y no al objeto como ocurre con la variables no estaticas.



Void SetStaticField(JNIEnv *, jclass, jfieldID, Tipo_Nativo)


Esta función modifica la variable correspondiente al ID que se indica en el tercer parámetro y que se habrá obtenido con la función GetStaticFieldID, esta función como se explico anteriormente obtiene el ID de variables estáticas. En el cuarto parámetro indicaremos el nuevo valor que deseamos que tenga esta variable.



Llamadas a métodos



Después de haber visto como acceder a los campos de una clase, explicaremos como llamar a métodos de la clase. Para esto nos apoyaremos en el Listado 4, en el podemos ver también como crear un clase desde C que estudiaremos más adelante, también tenemos dos clases creadas que nos servirán de apoyo:



"ClaseA": Que tiene un constructor sin parámetros, el método "unmetodo" sin parámetros ni valor de retorno.

"ClaseHijaA": Hereda de ClaseA y esta compuesta por dos constructores uno sin parámetros y el otro con dos parámetros. También tiene un método llamado "unmetodo" que es igual al de su clase padre en cuanto a parámetros y valor de retorno.



#include "Ejemplo2.h"
/*
* Class: Ejemplo2
* Method: metodo_C
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_Ejemplo2_metodo_1C(JNIEnv *env, jobject object)
{
jobject LaHijaA, LaOtraHija, LaOtraHija2;
jclass LaClase, LaClaseA;
jmethodID ID_Metodo;
int resultado;
jvalue parámetros[2];
// Creamos las clases
LaClaseA = (*env)->FindClass(env, "ClaseA");

LaClase = (*env)->FindClass(env, "ClaseHijaA");
ID_Metodo = (*env)->GetMethodID(env, LaClase, "", "()V");
LaHijaA = (*env)->NewObject(env, LaClase, ID_Metodo);

// Paso mediante parámetros variables.
ID_Metodo = (*env)->GetMethodID(env, LaClase, "", "(II)V");
LaOtraHija = (*env)->NewObject(env, LaClase, ID_Metodo, 12,98);
// Paso Mediante arryas de parámetros.
ID_Metodo = (*env)->GetMethodID(env, LaClase, "", "(II)V");
parámetros[0].i= 23;
parámetros[1].i= 98;
LaOtraHija2 = (*env)->NewObjectA(env, LaClase, ID_Metodo, parámetros);

// Ejecucion de unmetodo
ID_Metodo = (*env)->GetMethodID(env, LaClase, "unmetodo", "()V");
(*env)->CallVoidMethod(env, LaHijaA, ID_Metodo);

ID_Metodo = (*env)->GetMethodID(env, LaClaseA, "unmetodo", "()V");
(*env)->CallNonvirtualVoidMethod(env, LaHijaA, LaClaseA, ID_Metodo);

// Ejecucion de un método Estático.
ID_Metodo = (*env)->GetStaticMethodID(env, LaClase, "ElEstatico", "(I)I");
resultado = (*env)->CallStaticIntMethod(env, LaClase, ID_Metodo, 9);
}


Antes de poder llamar al cualquier método de la clase tendremos que recoger el ID del método, igual que hacíamos para tener acceso a las variables de la clase, mediante la función GetMethodID y GetStaticMethodID para los métodos estáticos. Una vez que tenemos el identificador del método lo llamaremos con una función que se adapte a nuestro métodos. Las funciones que tenemos para la llamada a métodos son las siguientes:








jmethodID GetMethodID(JNIEnv *, jclass, const char *, const char *)


Esta función nos retornara el ID de un método para que podamos llamarle en cualquier momento, si no encuentra el método devolverá NULL y saltara la excepción NoSuchMethodError. Los parámetros son:



  • El primer parámetro tenemos el puntero al entorno de JNI.
  • El segundo parámetro será la clase en la que se encuentra el método del que se desea obtener el ID del método.
  • El tercer parámetro es el nombre del método del que deseamos coger el ID, si lo que se quiere obtener es el ID del constructor el nombre será "".
  • El cuarto y último son los tipos de parámetros que recibirá el método encerrados entre paréntesis y el tipo de retorno, si los parámetros son uno entero y carácter con un valor de retorno tipo long tendremos que poner "(IC)J". En los constructores el valor de retorno es siempre de tipo void "V".


Para obtener el ID de un método estático utilizaremos la función GetStaticMethodID que es igual que la anterior con la diferencia del segundo parámetro que en lugar de ser un objeto lo que le pasaremos es una clase (jclass).



Tipo_Nativo CallMethod(JNIEnv *, jobject, jmethodID, ...)
Tipo_Nativo CallMethodA(JNIEnv *, jobject, jmethodID, jvalue *)
Tipo_Nativo CallMethodV(JNIEnv *, jobject, jmethodID, va_list)


Estas funciones nos servirán para llamar al método una vez hayamos obtenido el ID de este con la función anterior. Las excepciones que se pueden producir son las que el método al que llamamos pueda producir. Los parámetros son:



  • El primero el entorno de JNI.
  • El segundo el objeto que contiene el método que queremos llamar.
  • El tercero el ID del método a llamar y que se habrá obtenido con la función GetMethodID.
  • El cuarto son los parámetros del método y se podrán pasar de tres formas distintas, cada forma tiene su propia función, que explicamos a continuación:


    • CallMethod: Mediante parámetros variables de C, de esta forma pasaremos los valores que creamos oportunos igual que como cuando pasamos parámetros a cualquier otra función.
    • CallMethodA: Los parámetros son pasados mediante un array de argumentos, este array de argumentos es de tipo jvalue que es una unión como la siguiente:

      unión jvalue
      {
      jboolean z;
      jbyte b;
      jchar c;
      jshort s;
      jint i;
      jlong j;
      jfloat f;
      jdouble d;
      jobject l;
      }

    • CallMethodV: Los parámetros son pasados mediante va_list .



Al igual que pasaba con las funciones para ver variables de la clase deberemos llamar a la función que se corresponda con el tipo que nos devolverá el métodos.


Tipo_Nativo CallNonvirtualMethod(JNIEnv *, jobject, jclass, jmethodID, ...)
Tipo_Nativo CallNonvirtualMethodA(JNIEnv *, jobject, jclass, jmethodID, jvalue *)
Tipo_Nativo CallNonvirtualMethodV(JNIEnv *, jobject, jclass, jmethodID, va_list)


Estas tres funciones son similares a las anteriores con la diferencia de invocar al método de la clase, por este motivo tenemos que introducir un parámetro más que la clase de la que se quiere ejecutar el método, de esta forma podremos llamar a los métodos de la clase padre como se puede ver en el Listado 4. Para llamar a métodos estáticos tendremos que recoger el ID del método con GetStaticMethodID y llamar a las funciones:



CallStaticMethod(JNIEnv *, jclass, jmethod, ...)
CallStaticMethodA(JNIEnv *, jclass, jmethod, jvalue *)
CallStaticMethod(JNIEnv *, jclass, jmethod, va_list)


El segundo parámetro como es lógico cambia ya que el método estático pertenece a la clase y no al objeto, es por este motivo por lo que le pasaremos un tipo jclass. Después utilizaremos la función que corresponda con el tipo devuelto por nuestro método y la que mejor se adapte a nuestra forma de pasar argumentos.



Operaciones con clases y objetos



A continuación pasamos a explicar las funciones que nos permitirán realizar operaciones con objetos y clases, algunas de estas funciones ya han sido vistas anteriormente.



jobject AllocObject(JNIEnv *, jclass)


Esta función nos permite crear una instancia de un objeto sin que se llame al constructor de este. Los parámetros son en primer lugar el entorno JNI y el segundo es un tipo jclass que será la clase que deseamos crear instancia, este nos retorna un tipo jobject que será la instancia de la clase o un NULL en caso de que no se haya podido crear. Las excepciones que pueden producirse con esta función serán:



  1. InstantiationException: Si la clase es un interfaces o una clase abstracta, para evitarla deberemos crear una clase heredada de esta que defina los métodos abstractos de esta.
  2. OutOfMemoryError: Si el sistema se ha quedado sin memoria.


jobject NewObject(JNIEnv *, jclass, jmethodID, ...)
jobject NewObjectA(JNIEnv *, jclass, jmethodID, jvalue *)
jobject NewObjectV(JNIEnv *, jclass, jmethodID, va_list)


Estas funciones nos crearan un objeto de la clase indicada como podemos ver en el Listado 4. Los parámetros que pasamos son:



  • Primero el entorno JNI.
  • Segundo la clase que queremos crear.
  • Tercero el ID del constructor que se habrá obtenido con GetMethodID donde el nombre del método tendrá que ser "", y el tipo de retorno "V" (void).
  • Son los parámetros del constructor. La forma de pasar los parámetros, al igual que nos pasaba al invocar los métodos, dependerá de como nos convenga enviarlos utilizando para ellos la función que más se adapte a nuestras necesidades.


Si el objeto no se ha podido crear nos devolverá NULL y las excepciones que pueden saltar son las mismas que en el caso de la función AllocObject.



jclass GetObjectClass(JNIEnv *, jobject):


Nos permite obtener la clase del objeto pasado como segundo parámetro.



jboolean IsInstanceOf(JNIEnv *, jobject, jclass)


Con esta función podemos saber si un objeto que pasamos en el segundo parámetro es instancia de la clase pasada como tercer parámetro. El valor que devuelve esta función será JN_TRUE si el objeto es instancia de la clase y JNI_FALSE en caso contrario.



jboolean IsSameObject(JNIEnv *, jobject, jobject)


Nos permite saber si los dos objetos que pasamos como parámetro son iguales, devolviendo JNI_TRUE en el caso de que sean iguales y JNI_FALSE si son distintos.



jobject NewGlobalRef(JNIEnv *, jobject)
void DeleteGlobalRef(JNIEnv *, jobject)


Crea una referencia global del objeto que se pasa como segundo parámetro, si no se puede crear nos devolverá NULL. Las referencias globales han de ser borradas con la función DeleteGlobalRef se pasara como segundo parámetro un objeto que será la referencia global a borrar.



En la versión 1.2 aparece un tipo especial de referencia global "Weak Global Reference". Esta tipo de referencia se puede utilizar igual que las locales o globales, pero será recogida automáticamente por el recolector si esta solo referenciada a WEAK. Las funciones nuevas que aparecen para poder manipular estas referencias son: NewWeakGlobalRef y DeleteWeakGlobalRef.



void DeleteLocalRef(JNIEnv *, jobject)


Las referencias locales son sólo validas mientras estemos en el método que tiene estas referencias locales, una vez que salgamos de el serán borradas por el recolector de basura. Esto tiene un coste para el sistema que algunas veces nos puede interesar liberar memoria de referencias locales que ya no se van a utilizar. Para poder liberar una referencia local tenemos la función DeleteLocalRef que permite al programador borrar manualmente una referencia local.



En la versión 1.2 aparecen una serie de funciones que nos permite un manejo más flexible de la variables locales. Una de las funciones que tenemos nuevas es "jint EnsureLocalCapacity(JNIEnv *, jint) " que nos permitirá asegurar el mínimo numero de referencias locales que se indica en el segundo parámetro, si puede asegurar nos esta capacidad nos devolverá un cero, la maquina virtual nos asegura automáticamente 16 referencias locales. Podemos crear un nuevo espacio de referencias locales "jint PushLocalFrame(JNIEnv *, jint)" o liberar este espacio de referencias mediante "jobject PopLocalFrame(JNIEnv *, jobject)".



Al igual que pasaba con las referencias globales en la versión 1.2 nos proporcionan una nueva función para crear referencia locales que es "jobject NewLocalRef(JNIEnv *, jobject)".



jclass FindClass(JNIEnv *, const char *)


Esta función carga una definición de clase como se puede ver en el Listado 4 la clase la buscar en los caminos de la variable de entorno CLASSPATH incluidos los ficheros ZIP.



En el primer parámetro tendremos el entorno JNI y como segundo el nombre de la clase a cargar, si esta en un paquete tendremos que indicar el nombre del paquete separando los subpaquetes mediante "/". Si no hemos podido localizar la clase devolverá NULL y saltara la excepción NoClassDefFoundError. Otras excepciones que pueden producirse son:



  1. ClassFormatError: Los datos de la clase no tienen un formato correcto.
  2. OutOfMemoryError: No tenemos memoria en el sistema, cuando nos llega esta excepción puede ser un buen momento para liberar espacio de referencias globales y locales que ya no nos sirve.


En la versión 1.2 FindClass podremos utilizar un cargador de clase asociado al método nativo que nos permita no solo las clases que se localizan en la variable CLASSPATH.



Operaciones con String



Las cadenas de caracteres que nos llegan desde Java no pueden ser utilizadas directamente en C y deberán ser pasadas al formato de este lenguaje, a la inversa nos encontramos con el mismo problema y el formato de las cadenas de caracteres de C no puede ser utilizado directamente por Java, así que tendremos que transfórmarlo con las funciones que nos ofrece el interfaces JNI y que pasamos a explicar basándonos en ejemplo de del Listado 5.



#include "Ejemplo3.h"
#include
/*
* Class: Ejemplo3
* Method: metodo_C
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Ejemplo3_metodo_1C(JNIEnv *env, jobject object, jstring envio)
{
jstring enviamos;
const unsigned char *cadenaUTF;
unsigned char código;
int cont,longitud;
// Preparamos el String a devolver.
enviamos = (*env)->NewStringUTF(env, "Se creo en C");

// Recogemos o que nos llega.
cadenaUTF = (*env)->GetStringUTFChars(env, envio, 0);
printf("Nos llego a Java: %s\n",cadenaUTF);
(*env)->ReleaseStringUTFChars(env, envio, cadenaUTF);
longitud = (*env)->GetStringUTFLength(env, envio);
for (cont=0; cont < longitud; cont++)
{
if (cadenaUTF[cont] < 0x80 )
{
printf("%c",cadenaUTF[cont]);
}
else
{
printf("(%c)",((cadenaUTF[cont++] & 0x1F) << 6) + (cadenaUTF[cont] &0x3F));
}
}
printf("%X, ",cadenaUTF[longitud]);

return enviamos;
}


Una cadena de caracteres de Java se puede convertir a formato C con una representación Unicode o UTF-8. El formato Unicode solo lo deberemos utilizar si nuestro sistema operativo utiliza este tipo de caracteres.



Un carácter UTF-8 puede ocupar 1, 2, o 3 bytes dependiendo del código de carácter a representa, el rango de caracteres va desde 1 hasta 65535, los caracteres que van desde 1 al 128 (80H) se representaran con un byte como se puede ver en (a) de la Figura B.








Los caracteres entre 128 (80H) y 2047 (07FFH) se representarán con dos bytes con el formato que vemos en (b) de la Figura B. En el Listado 5 vemos un ejemplo de descodificación de caracteres entre los rangos 1 y 2047.



Los caracteres comprendidos entre los rangos 2048 (0800H) y 65536 (FFFFH) se representan con tres bytes como se ve en ( c) de la Figura B.



jstring NewString(JNIEnv *, const char *)
jstring NewStringUTF(JNIEnv *, const char *)


Estas funciones construyen una cadena de caracteres con formato de Java y se les pasa como parámetros una cadena de caracteres Unicode o UTF-8 dependiendo de la función que utilicemos. Si no se ha podido construir la cadena de caracteres devolverán NULL y la excepción que puede saltar es OutOfMemoryError que nos estará indicando que el sistema no tiene memoria para crear esta cadena.



jsize GetStringLength(JNIEnv *, jstring)
jsizeStringUTFLength(JNIEnv *, jstring)


Con estas funciones podemos ver los que nos ocupa una cadena de caracteres de Java en formato Unicode o UTF-8 respectivamente.



const char * GetStringChars(JNIEnv *, jstring, jboolean *)
const char * GetStringUTFChars(JNIEnv *, jstring, jboolean *)


Con estas funciones creamos un puntero a una cadena de caracteres Unicode o UTF-8 respectivamente con el contenido de la cadena Java que se le pasa como segundo parámetro.



El tercer parámetro es un puntero a jboolean que nos permite ver si esta copia se ha realizado con éxito o no, si la copia se realizo con éxito se pondrá a JNI_TRUE en caso contrario será puesto a JNI_FALSE.



Una vez que no necesitemos esta cadena de caracteres deberemos liberarla de memoria con las funciones ReleaseStringChars y ReleaseStringUTFChars respectivamente, los parámetros que tendremos que pasarles son:



  1. Primero el puntero del entorno JNI.
  2. Segundo la cadena de caracteres Java que sirvió de copia en las funciones GetStringChars y GetStringUTFChars.
  3. Tercero el puntero a carácter que se tiene que liberar y que es el que nos fue devuelto por una de las funciones GetStringChars o GetStringUTFChars.


La versión 1.2 nos proporciona dos nuevas funciones para convertir subconjunto de cadenas de caracteres Java a Unicode o UTF, estas nuevas funciones son:



void GetStringRegion(JNIEnv *, jstring, jsize, jsize, char *)
void GetStringUTFRegion(JNIEnv *, jstring, jsize, jsize, char *)


En el tercer y cuarto parámetro deberemos poner el principio y la longitud de la cadena y el resultado lo tendremos en el quinto parámetro. Si los valores de inicio y longitud no son correctos con respecto a la longitud de la cadena de caracteres Java que se pasa en el segundo parámetro se producirá la excepción StringIndexOutOfBoundException.



Operaciones con Arrays



En el Listado 6 podemos ver como accedemos a un array de Java y como podemos desde C crear un array en la clase Java. Desde C también podemos obtener la longitud de los arrays de Java, esto lo realizamos con la siguiente función "jsize GetArrayLength(JNIEnv *, jarray)".



#include "Ejemplo4.h"
/*
* Class: Ejemplo4
* Method: metodo_C
* Signature: ([I)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_Ejemplo4_metodo_1C(JNIEnv *env, jobject object, jintArray origen)
{
int longitud;
int cont;
jint *datos;
jfloatArray resultado;
jfloat *nuevosdatos;
jfieldID ID_Campo;
jclass LaClase;

longitud = (*env)->GetArrayLength(env, origen);
/* Los datos que han llegado son */
datos = (*env)->GetIntArrayElements(env, origen, 0);
for (cont = 0; cont < longitud ; cont++)
{
printf("%d, ", datos[cont]);
}
printf("\n");

// Gabamos resultados en array de la clase.
resultado = (*env)->NewFloatArray(env, longitud*2);
nuevosdatos = (jfloat *) calloc(sizeof(jfloat), longitud*2);
for (cont=0; cont < longitud; cont ++)
{
nuevosdatos[cont] = datos[cont] / 2.0;
nuevosdatos[longitud + cont] = datos[cont] * 2;
}
(*env)->SetFloatArrayRegion(env, resultado, 0, longitud*2,nuevosdatos);
LaClase = (*env)->GetObjectClass(env, object);
ID_Campo = (*env)->GetFieldID(env, LaClase, "destino", "[F");
(*env)->SetObjectField(env, object, ID_Campo, resultado);
(*env)->ReleaseIntArrayElements(env, origen, datos, 0);
}


Otras funciones que nos permiten manejar arrays son las siguientes:



jarray NewObjectArray(JNIEnv *, jsize, jclass, jobject)


Nos permite crear un array de objetos, para crear un array multidimensional tenemos que pensar que es una array de arrays, según veremos no tenemos una función que nos permita crear un arrays de arrays pero si tomamos al array como un objeto. Esta función nos podrá servir para realizar arrays multidimensionales.



Esta función nos retornara NULL si no puede crear el array y si la causa es por carecer de memoria se producirá la excepción OutOfMemoryError.



Los parámetros que deberemos pasar son:



  • El primer parámetro se pasa el entorno JNI.
  • El segundo parámetro indica las longitud del array.
  • El tercer parámetro es la clase del elemento del array.
  • El cuarto será el elemento con el que inicializaremos los elementos del array.


jobject GetObjectArrayElemnt(JNIEnv *, jobjectArray, jsize)


Nos permite recoger el objeto que se encuentra en la posición que le indicamos como tercer parámetro. Si el parámetro tercero indica un índice no valido del array se producirá la excepción ArrayIndexOutOfBoundsException.



void SetObjectArrayElement(JNIEnv *, jobjectArray, jsize, jobject)


Nos permite cambiar el valor de un elemento del array, los parámetros son:



  • Primero el entorno JNI.
  • Segundo el array que deseamos modificar.
  • Tercero el del elemento del array anterior que deseamos actualizar su valor.
  • Cuarto el valor que pasamos.


Si el elemento indicado no es un elemento valido se producirá la excepción ArrayIndexOutOfBoundsException, si la clase que queremos introducir como nuevo valor no pertenece al tipo del array se producirá la excepción ArrayStoreException.



NewArray(JNIEnv *, jsize)


Nos permite crear un array de un tipo determinado cuya tamaño se le indica en el segundo parámetro. Tendremos una función para cada tipo básico. Si el array no puede ser construido nos devolverá NULL. Podemos ver un ejemplo de su uso en el Listado 6.



Los tipos de array básicos se pueden ver en la siguiente tabla.



Tipos de array

Tipo Nativo TipoArray
jboolean jbooleanArray
jbyte jbyteArray
jchar jcharArray
jshort jshortArray
jint jintArray
jlong jlongArray
jfloat jfloatArray
jdouble jdoubleArray


*GetArrayElements(JNIENV *, , jboolean *)


Desde C no podemos acceder directamente a los elementos de un array Java. Lo primero que tenemos que hacer es convertirlo a tipo C, esta función es la encargada de realizar esta conversión. El segundo parámetro es el array Java a convertir a formato C (un puntero al tipo del array) y el tercero nos indicara si se ha podido realizar la conversión o no, en caso de que la copia se haya realizado retornara JNI_TRUE en caso contrario JNI_FALSE.



Después de utilizar el array C deberemos borrarlos para liberar memoria y esto lo realizaremos con la función:



void ReleaseArrayElements(JNIEnv *, , *, jint)


Esta función libera el puntero al array en formato nativo, el segundo parámetro es el array Java y el tercero el array en formato nativo (puntero al tipo del array). El cuarto parámetro nos indicara la forma de borrado que podrá ser:



  • 0 Se elimina la copia y se libera el puntero, este es el método que más utilizaremos.
  • JNI_COMMIT la copia será borrada pero no liberamos el puntero por lo tanto no se libera la memoria ocupada por el mismo.
  • JNI_ABORT liberamos el puntero pero no eliminamos la copia de los elementos.


Otro conjunto de funciones que nos permitirá manipular el array en regiones será:



void GetArrayRegion(JNIEnv *, , jsize, jsize, *)


Nos permite coger una porción de array Java pasado como segundo parámetro y especificada por un inicia (parámetro tercero) y de longitud (parámetro cuarto), a un array de tipo nativo que se encuentra en el quinto parámetro. Si la región indicada no es valida producirá una excepción ArrayIndexOutOfBoundsExcepction.



Void SetArrayRegion(JNIEnv *, , jsize, jsize, *)


Realiza una copia de un array nativo a un array Java, tendremos que especificar que porción de array queremos copiar con los parámetros tercero y cuarto. Esta función no produce ninguna excepción ya que C no es capaz de saber la longitud de sus arrays.



Para pasar estos datos a una array de la clase Java tendremos que obtener la variable Java obteniendo el ID y pasarlo como si de un objeto se tratara, un ejemplo lo podemos ver en el Listado 6.



Excepciones



Muchas de las funciones de JNI pueden producir excepciones, estas excepciones se pueden capturar desde Java. Además de poder lanzar excepciones podremos capturarlas desde JNI incluso es psoible producir errores graves que no permitan una recuperación y por lo tanto un retorno a la maquina virtual.



Si alguna vez tenemos un error tan grave que no podemos ni siquiera volver a nuestra clase Java, podemos lanzar un error fatal con un texto explicativo de lo que ha pasado y salir del programa inmediatamente. Para esto tenemos la función "void FatalError(JNIEnv *, const char *)" en la que como segundo parámetro tenemos una descripción del error. Tenemos una función que nos permite la depuración informando de las excepciones por la salida estándar de error (stderr), esta función es "void exceptionDescribe(JNIEnv *)".



Si queremos saber desde nuestro método nativo si una excepción a saltado o no y de esta forma intentar solucionar el problema desde nuestro método nativo utilizaremos la función del JNI "jthrowable ExceptionOcurred(JNIEnv *)". Después de tratar la excepción se podrá limpiar para que no salte al retornar a Java mediante la función "void ExceptionClear(JNIEnv *)". Las dos funciones siguientes nos permitirán lanzar excepciones:



jint Throw(JNIEnv *, jthrowable)
jint ThrowNew(JNIEnv *, jclass, const char *)


Si nos devuelven 0 es que se habremos podido lanzar la excepción. La segunda función nos permite construir una excepción con un mensaje que nos explique el motivo del error.



Interfaces con la maquina virtual



Una de las mejoras que tenemos con el JNI es que podemos crear aplicaciones nativas y estas utilizar clases realizadas en Java. El JNI nos permite cargar, inicializar e invocar a la maquina virtual de Java, para de esta forma desde nuestro programa escrito en lenguaje nativo poder crear clases Java y ejecutar sus métodos.



Cuando creamos una maquina virtual de Java en nuestro programa nativo, obtenemos un puntero al interfaces de la maquina virtual que nos permitirá destruirla y a su vez tratar con el thread de la maquina. Si queremos obtener este entorno en un método nativo de un programa Java para así tener un mayor control sobre la maquina virtual podemos ejecutar la función "jint GetJavaVM(JNIEnv *, JavaVM **)" que nos devuelve un puntero al interfaces de la maquina virtual Java, correspondiente al thread de esta maquina virtual.



Cuando creemos una aplicación nativa que tiene que utilizar clases de Java, se tendrán que crear una maquina virtual en esta aplicación. Para poder realizar esto necesitaremos que nuestro programa se enlace con la librería de Java. Algunos ejemplo de línea de compilación son:



Para Windows



cl -I\include -I\include\win32 -MT \
-link \lib\javai.lib


Para Linux



cc -I/include -I/include/genunix \
-L/lib/ixxx/green_threads -ljava


Para Sun



cc -I/include -I/include/solaris \
-L/lib/sparc/green_threads -ljava


será el directorio donde se tenga instalado el entorno Java.
es el programa C que creara una maquina virtual Java.









A continuación explicaremos como ejecutar un método de una clase Java desde un programa C. En el Listado 7 se puede ver un ejemplo de esto.




/********************************************************************
* Ejemplo5.c
*
* Descripcion: Este Ejemplo nos permite crear Maquinas virtuales.
*
* Autor: Fco. Javier Rodriguez Navarro
*
*********************************************************************/
#include
int main()
{
JDK1_1InitArgs datos;
JNIEnv *env;
JavaVM *mvj;
jclass LaClase;
jobject El_Objeto;
jmethodID ID_Metodo;
jint Resultado;

datos.version = 0x00010001; /* Indicamos la version */
JNI_GetDefaultJavaVMInitArgs(&datos);

Resultado = JNI_CreateJavaVM(&mvj, &env, &datos);
if (Resultado < 0)
{
printf("Error al crear la maquina virtual");
}
else
{
/* Localizamos la clase y ejecutamos el método */
LaClase = (*env)->FindClass(env, "Ejemplo5");
ID_Metodo = (*env)->GetStaticMethodID(env, LaClase, "inicio", "()V");
(*env)->CallStaticVoidMethod(env, LaClase, ID_Metodo);

(*mvj)->DestroyJavaVM(mvj);
printf("Esto es otra vez C\n");
}

}


Lo primero que hacemos es coger los datos de inicialización de la maquina virtual, lo realizamos con la función:



jint JNI_GetDefaultJavaVMInitArgs(void *)


Esta función recoge la configuración por defecto de la maquina virtual, dependiendo de la maquina virtual podemos tener diferentes tipos de datos. Para poder tener una inicialización siempre estándar tenemos el primer campo de la estructura que será el de la versión.



El campo de versión se rellenara con la versión del JDK en el caso de la 1.1 el contenido deberá de ser 0x00010001 y la estructura de la versión 1.1 es JDK1_1InitArgs con los siguientes campos:



  • jint versión: Nos indica la versión de la maquina virtual.
  • char **properties: Es un array de String con el formato "propiedad=valor".
  • jint checkSource: Indica si chequea el código de una clase.
  • jint nativeStackSize: Tamaño de la pila para los thread de la maquina virtual.
  • jint javaStackSize: Tamaño de la pila Java, la pila de Java es el equivalente a un Stack de cualquier otro lenguaje donde se guardarán las variables locales y resultados temporales.
  • jint minHeapSize: Tamaño inicial de Heap, esta es la parte de memoria donde se asignan los objetos de nueva creación, el tamaño del Heap puede aumentar hasta el valor indicado por maxHeapSize.
  • jint verifyMode: Esta variable nos indica cuando verifica los byte codes de Java, los valores que puede tener es "0" nunca se verifican, "1" Solo los códigos remotos y "2" siempre.
  • const char *classpath: Los directorios donde podemos localizar las clases.
  • jint (*) (FILE *, const char *, va_list): Es un puntero a la función que imprime los valores de la maquina virtual. Podremos cambiarlos para que se guarden en un fichero de log.
  • void (*)(jint ): Puntero a la función de salida de la maquina virtual.
  • void (*)(): Puntero a la función que aborta la maquina virtual.
  • jint enableClassGC: Indica si esta habilitado la clase recolectora de basura.
  • jint enableVerboseGC: Permitirá que los mensajes del recolector de basura se impriman.


Una vez que tenemos los datos de la maquina virtual podemos crear una maquina virtual con la función:



jint JNI_CreateJavaVM(JavaVM **, JNIEnv **, void *)


Esta función nos permite crear una maquina virtual de Java, una vez que tenemos esta maquina virtual podremos cargar las clases y métodos que queramos para trabajar desde nuestro programa C con clases de Java. Esta función nos devolverá 0 en caso de que no hayan existidos problemas o un número negativo en caso contrario. Los parámetros de esta función son los siguientes:



  • Primero es un puntero a la maquina virtual y nos permitirá descargar la maquina virtual una vez que hayamos terminado con nuestras clases de Java. Utilizaremos la función "jint DestroyVM(JavaVM *)", si no ha podido descargar la maquina virtual nos devolverá un número negativo.
    Tenemos una función que nos permitirá todas las maquinas virtuales que han sido creadas "jint JNI_GetCreatedJavaVMs(JavaVM **, jsize, jsize *)" el tercer parámetro nos indicara la maquinas virtuales que han sido creadas, y el primero será un array con los punteros de la maquinas virtuales creadas. En la versión de JDK 1.1 no se permite crear más de una maquina virtual, por lo que esta función nos puede parecer fuera de lugar, de todas formas con ella no podremos saber si ya se creo una maquina virtual y cual es su puntero para así poder actuar con ella.
    Otra función especifica de la maquina virtual es "jint AttachCurrentThread(JavaVM *, JNIEnv **, void *)" que nos permitirá recoger el puntero al JNI de la maquina virtual.
  • Segundo parámetro es el puntero al JNI que nos permitirá crear y ejecutar métodos de clase, así como las otras funciones que se han visto durante todo este artículo.
  • Tercero el entorno de la maquina virtual que se ha podido cambiar antes de llamar a la creación de la maquina.
















Fco. Javier Rodriguez Navarro trabaja como responsable de desarrollos en Tms con el objetivo de crear sistemas abiertos y escalables, de ahí la afición por Java.


Cada vez que tiene un rato libre se lanza a viajar y pasear para conocer nuevas gentes, paisajes y costumbres.



Para cualquier duda o tirón de orejas, e-mail a:
frodrigu_ARROBA_idecnet.com




Reader Comments

There are no comments for this journal entry. To create a new comment, use the form below.
Comentarios deshabilitados
Comentarios deshabilitados en esta noticia.