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.