Capítulo 7 JNI (Java Native Interface)

Anuncio
Capítulo 7
JNI (Java Native Interface)
7.1.
Introducción1
La interfaz JNI (Java Native Interface) es una herramienta de Java que permite a aplicaciones
escritas en Java ejecutar código nativo. Así mismo también es posible la situación inversa, la ejecución de código Java desde código nativo. Estas funcionalidades permiten a los programadores
Java hacer uso de desarrollos en código nativo, ahorrando tiempo en el desarrollo de tareas específicas que en el caso de que no existiera JNI habría que reprogramar. JNI proporciona una interfaz
estandarizada para el acceso a aplicaciones nativas independientemente de la implementación de
la máquina virtual.
Antes de entrar más en profundidad en JNI, es necesario definir algunos conceptos fundamentales
para entender el papel que juega en Java:
Plataforma Java: es el conjunto formado por la máquina virtual de Java (Java Virtual Machine) y su API. La API de Java consiste en una serie de clases predefinidas que realizan un
gran número de tareas y sirven de base para la implementación de aplicaciones.
Entorno huésped (host environment): representa el sistema operativo, un conjunto de librerías nativas y el juego de instrucciones de la CPU. Las aplicaciones nativas se escriben
en lenguajes de programación nativos como C/C++, se compilan en código máquina y se
enlazan con otras librerías nativas. Por todo lo anterior las aplicaciones nativas son dependientes del entorno huésped en contraposición de las aplicaciones Java que pueden ejecutarse
en cualquier plataforma Java estándar. Esto se debe a que las plataformas Java se sitúan
encima del entorno huesped, abstrayendo a las aplicaciones Java del entorno en el que en
realidad se están ejecutando.
Una vez definidos estos conceptos, en la siguiente figura se muestra la situación de JNI dentro del
conjunto “plataforma Java-entorno huésped”. JNI se sitúa entre la implementación de la máquina
virtual y la aplicación nativa, permitiendo, como ya se ha comentado anteriormente, la ejecución
de código nativo desde Java y viceversa.
JNI integra código Java y código nativo, soportando dos tipos de código nativo:
Librería nativa: es una colección de funciones implementadas en código nativo y compiladas
para una determinada máquina y sistema operativo. Es el tipo de código nativo al que puede
llamar una aplicación Java desde JNI usando los llamados métodos nativos. Estos métodos
permiten encapsular la llamada a librerías nativas dentro de métodos Java.
1 Este capítulo está basado en el documento JNI (Java Native Interface). Programmer’s Guide and
Specification.[42]
53
54
7.2. Proceso de desarrollo en JNI
Figura 7.1: Contexto de JNI
Aplicación nativa: JNI permite a través de su interfaz de invocación que una aplicación
nativa englobe una implementación de la máquina virtual. De este modo, puede ejecutar
aplicaciones Java.
7.1.1.
Objetivos de JNI
Los objetivos de JNI son los siguientes:
1. Compatibilidad binaria: se garantiza que las distintas implementaciones de la máquina
virtual para una determinada plataforma son compatibles con el código binario de las aplicaciones nativas. Por tanto, los programadores sólo tienen que implementar una versión de
las aplicaciones nativas para una plataforma determinada.
2. Eficiencia: por el hecho de estandarizar el acceso a código nativo en diferentes máquinas
virtuales, se introduce cierta sobrecarga en la ejecución de los programas. JNI pretende llegar
a una solución de compromiso entre eficiencia y normalización del acceso a código nativo.
3. Funcionalidad: la interfaz ofrece la suficiente información de la máquina virtual para que
las aplicaciones nativas puedan realizar tareas útiles.
7.2.
Proceso de desarrollo en JNI
En este apartado se va exponer el proceso de implementación de una llamada a código nativo
desde Java utilizando un método nativo, ya que es la forma más común de uso de JNI. Como
ejemplo, se llamará a una librería nativa que imprimirá “Hola Mundo” por pantalla (en este caso
la aplicación se ejecuta en un PC). A continuación se muestran los pasos a seguir:
1. Creación de una clase que declare el código nativo. En este caso ‘‘HolaMundo.java”
2. Compilación de la clase.
3. Uso de javah (que se encuentra en el kit de desarrollo de Java) para crear el archivo de
cabecera de la librería nativa (HolaMundo.h).
4. Implementación de las funciones declaradas en el archivo de cabecera generado en el paso 3
(HolaMundo.c).
5. Compilación de la librería nativa. La librería generada debe ser dinámica, por lo que la
extensión del archivo debe ser “.so” (linux), “dylib” (Mac Os X) o “.dll” (Windows).
6. Ejecución de la clase que llama al código nativo.
7. JNI (Java Native Interface)
7.2.1.
55
Declaración del método nativo y compilación de la clase
Dentro de la clase HolaMundo.java, se declara el método nativo imprime:
class HolaMundo {
private native void imprime ();
public static void main ( String [] args ) {
new HolaMundo (). imprime ();
}
static {
System . loadLibrary ( " HolaMundo " );
}
}
Se puede observar que el método imprime tiene dos diferencias evidentes con respecto a la declaración de métodos que se suele realizar en Java. Una primera diferencia es el uso del modificador
native. El modificador native debe estar presente en la declaración de todos los métodos nativos.
Una segunda diferencia es que no se implementa el método, ya que la implementación se produce
en la librería nativa.
Por otra parte, existe un bloque de código con el modificador static. Esto indica a la máquina
virtual que el bloque en cuestión debe ejecutarse en primer lugar. La utilidad del modificador
static en esta clase es asegurarse de que la librería nativa se va a cargar mediante la llamada de
System.loadLibrary antes de que se haga referencia a ella en la llamada a imprime.
Una vez implementada la clase se pasa a compilarla, mediante la SDK, resultando el fichero
“HolaMundo.class”.
7.2.2.
Creación del fichero de cabecera
El archivo de cabecera de la librería nativa se genera a través de la herramienta javah, como se
explico anteriormente:
javah HolaMundo
El archivo generado se llama “HolaMundo.h”. En el caso en el que la clase que se diera como entrada
perteneciera a un paquete, como por ejemplo “prueba.jni”, el archivo de cabecera resultante sería
“prueba_jni_HolaMundo.h”.
En cuanto al contenido del archivo de cabecera, en este caso sólo contiene la declaración de una
función:
JNIEXPORT void JNICALL
J ava _H ol aMu nd o_ im pri me ( JNIEnv * , jobject );
En esta declaración se pueden observar dos macros (JNIEXPORT y JNICALL) y dos argumentos de
entrada ( un puntero a JNIEnv y jobject). Estos cuatro elementos se verán más adelante, aunque
cabe señalar que jobject representa el objeto que realiza la llamada al método nativo, en este caso
una instancia de la clase “HolaMundo”. Los nombres de los métodos nativos se determinan anteponiendo el prefijo “Java_” más el nombre completo de la clase (incluyendo el paquete al que pertenece) y el nombre del método, separando las palabras por el caracter “_” en vez de por puntos, como
ocurre en Java. De esta forma, el método imprime perteneciente a la clase ejemplo.jni.HolaMundo
se declara en el fichero de cabecera como Java_ejemplo_jni_HolaMundo_imprime.
7.2.3.
Implementación y compilación del método nativo
La implementación del método nativo se muestra seguidamente:
# include < jni .h >
# include < stdio .h >
# include " HelloWorld . h "
56
7.3. Tipos de datos y funciones de trasformación
JNIEXPORT void JNICALL
J a v a _ H ola Mu nd o_ imp ri me ( JNIEnv * env , jobject obj )
printf (" Hola Mundo \ n ");
return ;
}
{
Entre los ficheros de cabecera utilizados se encuentra “jni.h”. Este archivo contiene las definiciones
de todas las funciones, tipos y macros que se necesitan para implementar un método nativo.
Debido a la sencillez de este ejemplo, es posible ignorar los argumentos de entrada de la función.
Como se dijo en el apartado anterior, más tarde se explicará la utilidad de estos argumentos.
Una vez implementado el método nativo, se procede a su compilación, utilizando el compilador
más adecuado para el tipo de máquina y sistema operativo. Como ya se comentó, el resultado
de la compilación debe ser una librería dinámica para poder cargarla desde código Java mediante
System.load.Library.
7.2.4.
Ejecución del programa
Si ahora se ejecuta la aplicación Java, debe imprimirse en la consola “Hola Mundo”.
Se debe poner atención en definir correctamente la variable de entorno para las librerías nativas.
Esta variable de entorno indica a la máquina virtual dónde tiene que buscar las librerías que
tiene que cargar. En sistemas Windows ésta debe estar en el directorio actual o en alguno de los
directorios definidos en la variable PATH. Además de los directorios definidos en PATH, se puede
definir dentro de la llamada al lanzador de aplicaciones (java para el JRE de Sun) el directorio
donde buscar librerías nativas, estableciento la propiedad java.library.path:
java - Djava . library . path = C :\ librerias HolaMundo
7.3.
Tipos de datos y funciones de trasformación
En el ejemplo anterior no se pasaba ningún argumento a la función nativa. Pero esto no siempre
va a ser así. En este apartado se detalla la correspondencia entre los tipos en Java y en JNI así
como las funciones que permiten obtener cadenas de caracteres en C partiendo de objetos String.
Antes de explicar estos tipos y funciones, es preciso aclarar cómo se produce la llamada a métodos
nativos desde Java. Para ello se vuelve a recordar la definición del método nativo imprime, visto
en el apartado anterior:
JNIEXPORT void JNICALL
J a v a _ H ola Mu nd o_ imp ri me ( JNIEnv * env , jobject obj )
printf (" Hola Mundo \ n ");
return ;
}
{
Las macros JNIEXPORT y JNICALL garantizan que la función se va a exportar en la librería, y que
el compilador generará el código siguiendo el formato de llamada correcto. El argumento env es
un puntero a la interfaz JNIEnv. La interfaz JNIEnv contiene un puntero a una tabla de funciones.
Cada una de las entradas de esta tabla representa una función de JNI. A través de las funciones
de JNI, un método nativo puede acceder a los objetos y métodos de Java.
En Java los tipos de datos se dividen en tipos primitivos (int, float o char, por ejemplo) y
referenciados, como clases, objetos y cadenas. En JNI los tipos primitivos de Java tienen su propia
definición. Por ejemplo un int en java se coresponde en JNI a un jint (que se define como un
entero de 32 bits) y un float a un jfloat. Las cadenas de caracteres en Java (java.lang.String)
se coresponden con el tipo jstring, que como todos los objetos en Java son pasados a los métodos
57
7. JNI (Java Native Interface)
nativos como “referencias opacas”. Una referencia opaca es un tipo de puntero que apunta a
estructuras internas de la máquina virtual de Java. El programador, por tanto, no tiene acceso
a estas estructuras, por lo que para obtener los valores de dichas referencias debe hacer uso de
las funciones que ofrece JNI. En los siguientes apartados se mostrarán algunas de estas funciones,
empezando por las referentes a la conversión de objetos String a cadenas de caracteres en C así
como a la creación de objetos String desde código nativo.
7.3.1.
Conversión y creación de cadenas de caracteres
En JNI es posible convertir los objetos jstring en cadenas de caracteres en C, tanto UTF-8
como Unicode. El siguiente método nativo imprimeCadena, es una extensión del método imprime
visto en el primer ejemplo. En este caso, el método nativo imprime la cadena que se le pasa como
argumento desde Java.
JNIEXPORT jint JNICALL
J a v a _ H o l a M u n d o _ i m p r i m e C a d e n a ( JNIEnv * env , jobject obj , jstring cadena )
{
char * mensaje ;
jint resultado ;
/** conversion de la cadena **/
mensaje = (* env ) - > GetStringUTFChars ( env , cadena , NULL );
if ( mensaje != NULL )
{
printf (" %s \ n " , mensaje );
/** liberación de la cadena nativa **/
(* env ) - > Rel ease Strin gUTF Char s ( env , cadena , mensaje );
resultado = 0;
} else {
resultado = 1;
}
return resultado ;
}
La función GetStringUTFChars convierte la cadena de caracteres Unicode que representa el tipo
jstring a una cadena en formato UTF-8 en C. Como se puede observar, es importante comprobar
que realmente se ha producido la conversión de la cadena, ya que puede ocurrir que no haya
suficiente memoria para construir la cadena de caracteres en C. Cuando se ha terminado de
utilizar la cadena convertida, es necesario liberar la memoria utilizada para almacenarla con la
función ReleaseStringUTFChars.
En JNI, además de la conversión de cadenas de caracteres es posible la creación de objetos jstring
desde código nativo, usando la función NewStringUTF, cuya definición se presenta a continuación:
jstring NewStringUTF ( JNIEnv * env , const char * bytes );
Donde bytes representa la cadena de caracteres de C/C++ en formato UTF-8 que se desea
convertir en un objeto jstring. Si no hay memoria suficiente para crear el objeto, se genera la
excepción OutOfMemoryError.
En las funciones que se acaban de explicar, las cadenas en C/C++ están en formato UTF-8. Para
los sistemas operativos que permiten cadenas en formato Unicode, existen unas variantes de las
funciones anteriores, que se detallan en la siguiente tabla:
Cadenas UTF-8
GetStringUTFChars
ReleaseStringUTFChars
NewStringUTF
Cadenas Unicode
GetStringChars
ReleaseStringChars
NewString
58
7.3. Tipos de datos y funciones de trasformación
7.3.2.
Acceso a cadenas
En el caso de que se pasen como argumento a una función nativa cadenas de tipos primitivos o
referenciados, estas se representan como algún subtipo de jarray. Entre los subtipos de jarray se
encuentran entre otros jintArray, jbyteArray o jobjectArray. Estas cadenas no pueden ser gestionadas directamente por el código nativo. En su lugar, es preciso utilizar funciones que permitan
convertirlas en cadenas de C/C++, tal y como se muestra en el método nativo sumaCadena:
JNIEXPORT jint JNICALL
J a v a _ I n tA r r a y_ s u m aC a d e na ( JNIEnv * env , jobject obj , jintArray arr ) {
jint * buf ;
jint length ;
jint i , sum = 0;
length = (* env ) - > GetArrayLength ( env , arr );
buf = ( jint *) malloc ( sizeof ( jint )* length );
(* env ) - > GetIntArrayRegion ( env , arr , 0 , length , buf );
for ( i = 0; i < length ; i ++) {
sum += buf [ i ];
}
free ( buf );
return sum ;
}
Para calcular el tamaño del buffer se utiliza GetArrayLength, y tras reservar la memoria se llama
a la función Get<Tipo>ArrayRegion, que es la encargada de copiar length elementos, desde la
posición inicial (0).
Además de la función Get<tipo>ArrayRegion, se puede utilizar Get<tipo>ArrayElements, con
lo cual se logra no tener que conocer la longitud de la cadena en Java para reservar la memoria,
ya que la función la reserva por el programador. Para liberar la memoria reservada por JNI, se
usa la función Release<tipo>ArrayElements. Para modificar cadenas de tipos primitivos desde
código nativo se utiliza la función Set<Tipo>ArrayRegion, que consiste básicamente en la función
inversa a Get<Tipo>ArrayRegion.
Si lo que se desea es crear cadenas de Java desde código nativo, se debe hacer uso de la función
New<Tipo>Array, función a la que es necesario pasarle la cadena nativa así como su longitud.
7.3.3.
Acceso a campos de objetos
Una vez que ya se ha descrito cómo acceder a tipos primitivos y cadenas, se procede ahora a
detallar las herramientas disponibles en JNI para acceder a los campos de cualquier instancia de
una clase.
Para poder acceder a los campos de un objeto, es necesario completar los siguientes pasos:
1. Conocer la clase a la que pertenece el objeto. Esta operación se realiza mediante la llamada
a GetObjectClass.
2. Extraer los identificadores de los campos a los que se desean acceder. Los campos se acceden en JNI usando identificadores de campo, definidos como variables jfieldID. Estos
identificadores se obtienen a través de la invocación de la función GetFieldID.
3. Acceder a los campos mediante los identificadores extraídos. Este acceso se produce a través
de la llamada a GetObjectField.
Se ilustran estos pasos mediante la clase Sumador que se define de la siguiente forma:
7. JNI (Java Native Interface)
class Sumador {
int a ;
int b ;
59
/* primer sumando */
/* segundo sumando */
int resultado ;
private native void suma ();
public static void main ( Sring args []){
Sumador sumador = new Sumador ();
sumador . a = 2;
sumador . b = 5;
sumador . suma ();
System . out . println ( sumador . a + "+" + sumador . b + "="
+ sumador . resultado );
}
static {
System . loadLibrary (" sumalib ");
}
}
Donde la definición del método nativo Java_Sumador_suma es:
JNIEXPORT void JNICALL
Jav a_Sumador_suma ( JNIEnv * env , jobject obj ){
jint a , b , resultado ;
jfieldID a_fid , b_fid , res_fid ;
jclass cls ;
cls = (* env ) - > GetObjectClass ( env , obj );
a_fid = (* env ) - > GetFieldID ( env , cls ," a " ," I ");
if ( a_fid != NULL )
{
b_fid = (* env ) - > GetFieldID ( env , cls ," b " ," I ");
if ( b_fid != NULL )
{
res_fid =(* env ) - > GetFieldID ( env , cls ," resultado " ," I ");
if ( res_fid != NULL ){
a = (* env ) - > GetIntField ( env , a_fid );
b = (* env ) - > GetIntField ( env , b_fid );
resultado = a + b ;
(* env ) - > SetIntField ( env , obj , res_fid , resultado );
}
}
}
return ;
}
En JNI, el identificador del campo se obtiene mediante GetFieldID, indicando la clase a la que
pertenece el objeto, el nombre del campo y el descriptor del campo. El descriptor del campo es la
forma de identificar el tipo (ya sea primitivo o referenciado) del campo. En la siguiente tabla se
muestra la correspondencia entre los tipos primitivos y su descriptor:
60
7.3. Tipos de datos y funciones de trasformación
Tipo
boolean
byte
char
short
int
long
float
double
Descriptor de Campo
Z
B
C
S
I
L
F
D
Si se quiere obtener el identificador de un campo de tipo referenciado, se utiliza el nombre completo
de la clase, iniciando el nombre con el caracter “L”, sustituyendo el caracter separador “.” por “/”
y finalizando la cadena con “;”. Por ejemplo, si se quiere acceder a un campo String, el descriptor
es “Ljava/lang/String;”. Si lo que se desea es obtener el identificador de una tabla, se utiliza el
prefijo “[“. Por ejemplo, en el caso del acceso a una tabla de enteros, el descriptor sería “[I” .
7.3.4.
Llamada a métodos de objetos
Desde código nativo también es posible realizar llamadas a métodos de objetos. Esto se realiza de
manera similar a como se accede a los campos:
1. Se averigua la clase a la que pertenece el objeto mediante la función GetObjectClass.
2. Se extrae el identificador del método (representado por jmethodID) a través de la llamada
a la función GetMethodID.
3. Se ejecuta el método llamando a la función que corresponda dentro de la familia de funciones
Call<Tipo>Method.
Para mostar estos pasos, se define la clase Escritor que cuenta con los métodos escribe e
imprimeMensaje:
class Escritor {
private native void escribe ( String fichero , String texto );
void imprimeMensajeError (){
System . out . println ( Error al abrir el archivo );
}
public static void main ( String args []){
Escritor escritor = new Escritor ();
escritor . escribe (/ directorio / fichero1 . txt , Hola Mundo );
}
static {
System . loadLibrary (" escribelib ");
}
}
El método escribe es un método nativo al que se le pasan dos argumentos, uno es el texto que
se desea escribir y el otro el fichero donde se guardará el texto escrito. Se muestra a continuación
la definición de la función Java_Escritor_escribe:
JNIEXPORT void JNICALL
J a v a _ E s crit or_es crib e ( JNIEnv * env , jobject * obj ,
jstring fichero , jstring texto ){
7. JNI (Java Native Interface)
61
char * texto_nativo ;
char * fichero_nativo ;
FILE * descriptor ;
jclass cls ;
jmethodID imprime_id ;
texto_nativo = (* env ) - > GetStringUTFChars ( env , texto , NULL );
if ( texto_nativo != NULL ){
fichero_nativo = (* env ) - > GetStringUTFChars ( env , fichero , NULL );
if ( fichero_nativo != NULL ){
descriptor = fopen ( fichero_nativo , wb );
if ( descriptor != NULL ){
fprintf ( descriptor , %s \n , texto_nativo );
fclose ( descriptor );
} else {
cls = (* env ) - > GetObjectClass ( obj );
imprime_id = (* env ) - > GetMethodID ( env , cls ,
imprimeMensajeError ,() V );
if ( imprime_id != NULL ){
(* env ) - > CallVoidMethod ( env , obj , imprime_id );
}
}
}
}
}
}
Como se puede observar, dentro del método nativo escribe, si se falla al abrir el fichero se
llama al método imprimeMensajeError. En el proceso de obtención del identificador del método
se llama a la función GetMethodID con tres argumentos: el primer argumento es el puntero a
JNIEnv, el segundo el objeto que contiene el método al que se pretende llamar y en último lugar
se pasa el descriptor del método. Los descriptores de los métodos son similares a los descriptores
de campos, ya explicados. En el caso de los descriptores de métodos, se diferencia el tipo devuelto
y los que se pasan como parámetros. Los tipos de los argumentos se describen entre parántesis,
y seguidamente se especifica el tipo que devuelve. De este modo, si una función no toma ningún
argumento y devuelve un entero, su descriptor es “()I”. En cambio, si se le pasa como argumento
un String y devuelve un entero, entonces sería “(Ljava/lang/String;)I”.
Para ahorrarse la aplicación de estas reglas a cada método que se desee acceder, es posible utilizar
la aplicación javap de la SDK de Java. Esta aplicación ejecutada con la opción “-s” desglosa los
nombres de los campos y métodos contenidos en un fichero “.class”, junto con sus descriptores.
7.4.
Construcción de objetos
En JNI también es posible la instanciación de clases. La instanciación de clases es de utilidad
cuando en la función nativa se devuelve una variable de tipo referenciado. Para la instanciación
de objetos en código nativo es necesario seguir los siguientes pasos:
1. Obtención de la clase que se pretende instanciar, almacenándola en una variable de tipo
jclass. Para ello, si no se dispone de un objeto instanciado almacenado en una variable
jobject, se puede utilizar la función FindClass, que necesita el nombre completo de la clase.
62
7.4. Construcción de objetos
2. Creación del objeto mediante la función NewObject. NewObject reserva memoria para el
objeto y llama a uno de sus constructores. Para llamar un constructor concreto, primero
ha de extraerse su identificador mediante GetMethodID, determinando como nombre del
método “<init>” y como tipo devuelto “V”.
En la siguiente función creaCadena, se ilustra el procedimiento que se acaba de explicar. La
funcionalidad de creaCadena es emular a la función NewString, que como ya se ha comentado
crea un objeto String pasándole una cadena de caracteres Unicode de C.
jstring creaCadena ( JNIEnv * env , jchar * chars , jint len ) {
jclass claseString ;
jmethodID cid ;
jcharArray cadena ;
jstring resultado ;
/* obtencion de la clase y el constructor de String */
claseString = (* env ) - > FindClass ( env , " java / lang / String ");
if ( stringClass == NULL ) {
return NULL ;
}
cid = (* env ) - > GetMethodID ( env , claseString ,
" < init >" , "([ C ) V ");
if ( cid == NULL ) {
return NULL ;
}
/* construccion de la cadena de caracteres */
cadena = (* env ) - > NewCharArray ( env , len );
if ( cadena == NULL ) {
return NULL ;
}
(* env ) - > SetCharArrayRegion ( env , cadena , 0 , len , chars );
/* construccion del objeto String */
resultado = (* env ) - > NewObject ( env , claseString , cid , cadena );
(* env ) - > DeleteLocalRef ( env , cadena );
(* env ) - > DeleteLocalRef ( env , claseString );
return resultado ;
}
En el ejemplo, primero se ha construido una cadena de caracteres Java (cadena) para después
llamar al contructor de la clase String que toma como argumento la cadena que se acaba de
construir, mediante la inclusión del identificador del contructor y de la cadena de elementos char
como argumentos de NewObject. La obtención del identificador del constructor (en la que se utiliza
la función GetMethodID), puede servir como ejemplo de lo explicado en el paso dos. Finalmente se
liberan las referencias locales que se han usado durante la creación del objeto String (claseString
y cadena) usando las función DeleteLocalRef.
Llegado a este punto resulta conveniente definir los tipos de referencias en JNI. Como se explicó
en el apartado 7.3, los tipos referenciados se gestionan en JNI como referencias opacas. Estas
referencias se dividen en tres tipos según sus características. Las referencias más comunes son las
referencias locales. Las referencias locales sólo tienen validez dentro de la función nativa que las ha
definido y no se conserva su valor entre sucesivas llamadas al método. Esto significa que es inútil
retener el valor de una referencia local declarándola como variables estática, ya que la máquina
virtual se encarga de liberar los recursos a los que apunta la referencia local cada vez que se termina
la ejecución del método nativo. Las variables locales, por tanto, son liberadas automáticamente por
la máquina virtual. No obstante, pueden liberarse las referencias locales explícitamente mediante
la función DeleteLocalRef.
7. JNI (Java Native Interface)
63
En el otro extremo se encuentran las referencias globales, que son creadas partiendo de una referencia local a través de la llamada a la función NewGlobalReference. Este tipo de referencias no
puede ser liberado por la máquina virtual, lo que permite que se puedan almacenar entre ejecuciones de un determinado método nativo sin que se produzcan incoherencias. Por otro lado, dado que
este tipo de referencias no se pueden liberar automáticamente, se hace necesaria para su liberación
la llamada a la función DeleteGlobalReference.
Por último, existe un tercer tipo de referencia llamada referencia global débil. Una referencia global
débil no se libera cuando termina la ejecución del método nativo, auque sí se libera el objeto
Java subyacente. Esta clase de referencias son útiles para los casos en que se quiera conservar la
referencia pero no mantener el objeto cargado en la máquina virtual, como por ejemplo cuando
se desea conservar la referencia a una clase entre ejecuciones del método o en distintos hilos. Por
lo tanto, es posible que en sucesivas ejecuciones de un método nativo, una referencia global débil
apunte a un objeto que ya ha sido destruido por la máquina virtual. Para estos casos, así como
para comprobar que dos referencias cualquiera apuntan al mismo objeto, se utiliza la función
IsSameObject. Por ejemplo, si se tienen dos referencias locales (obj1 y obj2) que puede que
referencien al mismo objeto, se usaría IsSameObject de la siguiente forma:
(* env ) - > IsSameObject ( env , obj1 , obj2 );
El valor devuelto por la función será JNI_TRUE si referencian al mismo objeto y JNI_FALSE en otro
caso. Si lo que se intenta es saber si una referencia local o global (por ejemplo obj1) apunta a un
objeto ya liberado, la llamada a IsSameObject se realiza de esta forma:
(* env ) - > IsSameObject ( env , obj1 , NULL );
Obteniendo JNI_TRUE si obj1 referencia a un objeto ya liberado y JNI_FALSE si todavía se mantiene
vivo. Esto es equivalente a escribir:
obj1 == NULL
Esto es así porque el hecho de que una variable global ya no apunte a un objeto cargado en
memoria es equivalente a la liberacioón de la referencia, y por tanto el valor se ha puesto a NULL.
Para la comprobación de que una variable local débil aún hace referencia a un objeto Java válido
se utiliza IsSameObject de forma análoga al caso anterior :
(* env ) - > IsSameObject ( env , obj_debil , NULL );
La llamada a esta función es necesaria para conocer si se ha liberado el objeto al que apunta obj_debil ya que las variables globales débiles no se liberan automáticamente y por tanto
conservan el valor aunque ya se haya liberado el objeto al que apuntaban.
7.5.
Hilos y JNI
Como es sabido, Java permite la ejecución de múltiples hilos dentro de un proceso. Por lo tanto, es
necesario disponer de una serie de funcionalidades que permitan a los programadores gestionar los
hilos de una manera parecida en como se hace en Java. En los siguientes apartados se describirán
estas herramientas así como las limitaciones que existen en cuanto al uso de las variables de JNI
entre distintos hilos.
7.5.1.
Restricciones
En un entorno multihilo, en JNI existen una serie de limitaciones que se han de tener en cuenta
a fin de que varios hilos puedan ejecutar un método nativo simultánamente:
El puntero a JNIEnv no puede pasarse de un hilo a otro, ya que cada JNIEnv está asociado
al hilo en el que fue creado.
64
7.5. Hilos y JNI
Las variables locales no se pueden conservar de un hilo a otro, porque al cambiar de hilo
las variables locales pierden su valor. Para pasar una variable local a otro hilo, es necesario
convertirla a global.
7.5.2.
Obtención del puntero a JNIEnv
Como se acaba de explicar, los punteros a JNIEnv no se pueden pasar entre hilos, ya que su valor
está ligado al hilo en el que se crearon. No obstante hay circunstancias que requiren la obtención
del puntero a JNIEnv en otro hilo distinto al del método nativo llamado por la aplicación Java.
Un ejemplo típico es el paso de una función que haga uso de JNI como “callback”2 de una función
nativa. En ese caso, la función no es llamada desde Java, pero en cambio sí necesita el puntero a
JNIEnv para poder ejecutarse con éxito.
Para estos casos se utiliza la función AttachCurrentThread de la interfaz de invocación (tipo
JavaVM). La interfaz de invocación permite cargar la máquina virtual desde código nativo, pasándole los parámetros correspondientes, como se hace al ejecutar la aplicación “java”. Así pues,
es necesaria la obtención del puntero JavaVM para poder obtener el puntero a JNIEnv El uso de
AttachCurrentThread se ilustra a continuación:
...
JavaVM * vm ;
JavaVMAttachArgs args ;
...
/* Obtención de vm */
...
args . version = JNI_VERSION_1_2 ;
args . name = NULL ;
args . group = NULL ;
vm - > A ttachCurrentThread ( jvm , ( void **)& env , & args )
/* Llamadas a las funciones de JNI usando env */
...
La obtención del puntero a JavaVM se puede conseguir de diversas formas: mediante la llamada a
la función JNI_GetCreatedJavaVMs o dentro de un método nativo usando la función GetJavaVM.
El puntero a JavaVM obtenido se puede almacenar para que pueda ser usado en otros hilos, ya que
el VM conserva su validez entre hilos distintos.
En cuanto a la función AttachCurrentThread, tiene como argumentos de entrada el puntero a
la máquina virtual y una estructura llamada JavaVMAttachArgs. Esta estructura cuenta con los
campos version, name y group. La version indica la versión de la interfaz JNIEnv que se desea
obtener, siendo las opciones JNI_VERSION_1_1 y JNI_VERSION_1_2. El campo name por su parte,
especifica el nombre del hilo Java (es decir una instancia de java.lang.Thread) que se creará. Si
no se desea poner un nombre al hilo se establece name a NULL. Finalmente group indica al grupo
en el que se desea adscribir el hilo que se va a crear.
Una vez obtenido el puntero a JNIEnv, se pueden realizar las llamadas a las funciones que se han
explicado a lo largo de este capítulo.
2 Un callback es una función que se pasa a otra como argumento, para que esta última llame a la primera cuando
lo considere necesario.
Descargar