Otros Problemas de Programación

Esta sección presenta información sobre acceso a clases, métodos y campos, y cubre los threads, la memoria y la JVM1.

Problemas de Lenguaje

Hasta ahora, los ejemplos de métodos nativos han cuvierto llamadas solitarias a funciones C y c++ que o devuelven un resultado o modifican los parámetro pasados a la función. Sin embargo, C++ al igual que utiliza ejemplares de clases. si creamos una clase en un método nativo, la referencia a esta clase no tiene una clase equivalente en el lenguaje Java, lo que hace díficil llamar a funciones de la clase C++ que se creó primero.

Una forma de manejar esta situación es mantener un registtro de las clases C++ referencias y pasadas de vuelta a un proxy o al programa llamante. Para asegurarnos de que una clase C++ persiste a través de llamadas a método nativos, usamos el operador new de C++ para crear una referencia al objeto C++ en la pila.

El siguiente código proporciona un mapeo entre la base de datos Xbase y código en lenguaje Java. La base de datos Xbase tiene un API C++ y usa inicializaciónde clases para realizar operaciones subsecuentes en la base de datos. Cuando se crea el objeto clase, se devuelve un puntero a este objeto como una valor int al lenguaje Java. Podemos usar un valor long o mayor para máquinas mayores de 32 bits.

public class CallDB {
  public native int initdb();
  public native short opendb(String name, int ptr);
  public native short GetFieldNo(
                        String fieldname, int ptr);

  static {
    System.loadLibrary("dbmaplib");
  }

  public static void main(String args[]) {
    String prefix=null;
    CallDB db=new CallDB();
    int res=db.initdb();
    if(args.length>=1) {
      prefix=args[0];
    }
    System.out.println(db.opendb("MYFILE.DBF", res));
    System.out.println(db.GetFieldNo("LASTNAME", res));
    System.out.println(db.GetFieldNo("FIRSTNAME", res));
   }
}
El valor del resultado devuelto desde la llamada al método nativo initdb, se pasa a las sigueintes llamadas al método nativo. El código nativo incluido en la librería dbmaplib.cc des-referencia el objeto Java pasado como parámetro y recupera el objeto puntero. La línea xbDbf* Myfile=(xbDbf*)ptr; fuerza el valor del puntero init a ser un punetro del tipo Xbase xbDbf.
#include <jni.h>
#include <xbase/xbase.h>
#include "CallDB.h"

JNIEXPORT jint JNICALL Java_CallDB_initdb(
	JNIEnv *env, jobject jobj) {
  xbXBase* x;
  x= new xbXBase();
  xbDbf* Myfile;
  Myfile =new xbDbf(x);
  return ((jint)Myfile);
}

JNIEXPORT jshort JNICALL Java_CallDB_opendb(
	                   JNIEnv *env, jobject jobj, 
	                   jstring dbname, jint ptr) {
  xbDbf* Myfile=(xbDbf*)ptr;
  return((*Myfile).OpenDatabase( "MYFILE.DBF"));
}

JNIEXPORT jshort JNICALL Java_CallDB_GetFieldNo
                           (JNIEnv *env, jobject jobj, 
                           jstring fieldname, 
                           jint ptr) {
  xbDbf* Myfile=(xbDbf*)ptr;
  return((*Myfile).GetFieldNo(
	env->GetStringUTFChars(fieldname,0)));
}

Llamar a Métodos

La sección sobre los arrays iluminó algunas razones por las que llamar a método Java desde dentro de código nativo; por ejemplo, cuando necesitamos liberar el resultado que intentamos devolver. Otros usos de las llamadas a método java desde dentro de código nativo podría ser si necesitamos devolver más de un resultado o simplemente queremos modificar valores jaba desde dentro del código nativo.

Llamar a métodos Java desde dentro de código nativo implica estos tres pasos:

  1. Recuperar una Referencia a la Clase.
  2. Recuperar un identificador de método.
  3. LLamar a los métodos.

Recuperar una Referencia de Clase

Es primer paso es recuperar una referencia a una clase que contenga los métodos a los que queremos acceder. Para recuperar una referencia, podemos usar el método FindClass o aceder a los argumentos jobject p jclass para el método nativo:
Usa el método FindClass:

  JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
  (JNIEnv *env, jobject jobj){
  jclass cls = (*env)->FindClass(env, "ClassName");
  }

Usa el argumento jobject:

  JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
  (JNIEnv *env, jobject jobj){
  jclass cls=(*env)->GetObjectClass(env, jobj);
  }

Usa el argumento jclass:

  JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
  (JNIEnv *env, jclass jcls){
  jclass cls=jcls;
  }

Recuperar un identificador de Método

Una vez que hemos obtenido la clase, el segundo paso es llamar a la función GetMethodID para recuperar un identificador para un método que seleccionemos de la clase. El identificador es necesario cuando llamamos al método de este ejemplar de la clase. Como el lenguaje Java soporta sobrecarga de método, también necesitamos específicar la firma particular del método al que queremos llamar. Para encontar qué firma usa nuestro método Java, ejecutamos el comando javap de esta forma:
  javap -s Class
La firma del método usasa se muestra como un comentario después de cada declaración de método como se ve aquí:
bash# javap -s ArrayHandler
Compiled from ArrayHandler.java
public class ArrayHandler extends java.lang.Object {
  java.lang.String arrayResults[];
   /*   [Ljava/lang/String;   */
  static {};
   /*   ()V   */
  public ArrayHandler();
   /*   ()V   */
  public void displayArray();
   /*   ()V   */
  public static void main(java.lang.String[]);
   /*   ([Ljava/lang/String;)V   */
  public native void returnArray();
   /*   ()V   */
  public void sendArrayResults(java.lang.String[]);
   /*   ([Ljava/lang/String;)V   */
}
Usamos la función GetMethodID para llamar a métodos de ejemplar de un ejemplar del objeto. o usamos la función GetStaticMethodID para llamar a un método estático. Sus listas de argumentos son iguales.

Llamar a Métodos

Tercero, se llama al método de ejemplar correspndiente usando una función Call<type>Method. El valor type puede ser Void, Object, Boolean, Byte, Char, Short, Int, Long, Float, o Double.

Los paramétros para el método pueden pasarse como una lista separada por coma, un array de valores a la función Call<type>MethodA, o como una va_list. El va_list es una construccuón usada frecuentemente como lista de argumentos en C. CallMethodV es la función usada para pasar un va_list ().

Los métodos estáticos son llamados de una forma similar excepto en que el nombre del método incluye un indenficador Satic adicional, CallStaticByteMethodA, y se usa el valor jclass en lugar del valor jobject.

El siguiente ejemplo devuelve un objeto array llamando al método sendArrayResults desde la clase ArrayHandler.

// ArrayHandler.java
public class ArrayHandler {
  private String arrayResults[];
  int arraySize=-1;

  public native void returnArray();

  static{
    System.loadLibrary("nativelib");
  }
 
  public void sendArrayResults(String results[]) {
    arraySize=results.length;
    arrayResults=new String[arraySize];
    System.arraycopy(results,0,
                     arrayResults,0,arraySize);
  }

  public void displayArray() {
    for (int i=0; i<arraySize; i++) {
      System.out.println("array element "+i+ "= " + arrayResults[i]);
    }
  }

  public static void main(String args[]) {
    String ar[];
    ArrayHandler ah= new ArrayHandler();
    ah.returnArray();
    ah.displayArray();
  }
}
El código nativo C++ se define de esta forma:
#include <jni.h>
#include <iostream.h>
#include "ArrayHandler.h"

JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){

  jobjectArray ret;
  int i;
  jclass cls;
  jmethodID mid;

  char *message[5]= {"first", 
		"second", 
		"third", 
		"fourth", 
		"fifth"};

  ret=(jobjectArray)env->NewObjectArray(5,
      env->FindClass("java/lang/String"),
      env->NewStringUTF(""));

  for(i=0;i<5;i++) {
    env->SetObjectArrayElement(
	ret,i,env->NewStringUTF(message[i]));
  }

  cls=env->GetObjectClass(jobj);
  mid=env->GetMethodID(cls, 
	"sendArrayResults", 
	"([Ljava/lang/String;)V");
  if (mid == 0) {
    cout "<<Can't find method sendArrayResults";
    return;
  }

  env->ExceptionClear();
  env->CallVoidMethod(jobj, mid, ret);
  if(env->ExceptionOccurred()) {
    cout << "error occured copying array back" <<endl;
    env->ExceptionDescribe();
    env->ExceptionClear();
  }
  return;
}
Para construir esto sobre Linux, ejecutamos los siguientes comandos:
  javac ArrayHandler.java
  javah -jni ArrayHandler

  g++  -o libnativelib.so 
	-shared -Wl,-soname,libnative.so
	-I/export/home/jdk1.2/include 
	-I/export/home/jdk1.2/include/linux nativelib.cc  
	-lc
Si queremos especificar un método de superclase, por ejemplo para llamar al constructor de padre, podemos hacerlo llamando a las funciones CallNonvirtual<type>Method.

Un punto importante cuando llamamos a métodos Java o a campos desde dentro del código nativo es que necesitamos capturar las excepciones lanzadas. La función ExceptionClear limpia cualquier excepción pendiente miesntras que la función ExceptionOccured chequea para ver si se ha lanzado alguna excepción en la sesión actual JNI.

Acceder a Campos

Acceder a campos Java desde dentro de código nativo es similar a llamar a métodos Java. Sin emnargo, el campo es recuperado con un ID de campo en lugar de un ID de método.

Lo primero que necesitamos es recuperar el ID de un campo. Podemos usar la función GetFieldID, especificando el nombre del campo y la firma en lugar del nombre y la firma del método. Una vez que tenemos el ID del campo, llamamos a una función Get<type>Field. El <type> es el mismo tipo nativo que está siendo devuelto excepto que se quita la j y la primera letra se pone en mayúsculas. Por ejemplo el valor <type> es Int para el tipo nativo jint, y Byte para el tipo nativo jbyte.

El resultado de la función Get<type>Field es devuelto como el tipo nativo. Por ejemplo, para recuperar el campo arraySize de la clase ArrayHandler, llamamos a GetIntField como se ve en el siguiente ejemplo.

El campo puede ser seleccionado llamando a las funciones env->SetIntField(jobj, fid, arraysize) . Los campos estáticos pueden ser configurados llamando a SetStaticIntField(jclass, fid, arraysize) y recuperados llamando a GetStaticIntField(jobj, fid).

#include <jni.h>
#include <iostream.h>
#include "ArrayHandler.h"

JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
(JNIEnv *env, jobject jobj){

    jobjectArray ret;
    int i;
    jint arraysize;
    jclass cls;
    jmethodID mid;
    jfieldID fid;

    char *message[5]= {"first",
                "second",
                "third",
                "fourth",
                "fifth"};

    ret=(jobjectArray)env->NewObjectArray(5,
        env->FindClass("java/lang/String"),
        env->NewStringUTF(""));

    for(i=0;i<5;i++) {
      env->SetObjectArrayElement(
        ret,i,env->NewStringUTF(message[i]));
    }

    cls=env->GetObjectClass(jobj);
    mid=env->GetMethodID(cls,
        "sendArrayResults",
        "([Ljava/lang/String;)V");
    if (mid == 0) {
        cout <<Can't find method sendArrayResults";
        return;
    }

    env->ExceptionClear();
    env->CallVoidMethod(jobj, mid, ret);
    if(env->ExceptionOccurred()) {
       cout << "error occured copying 
                        array back" << endl;
       env->ExceptionDescribe();
       env->ExceptionClear();
    }
    fid=env->GetFieldID(cls, "arraySize",  "I");
    if (fid == 0) {
        cout <<Can't find field arraySize";
        return;
    }
    arraysize=env->GetIntField(jobj, fid);
    if(!env->ExceptionOccurred()) {
       cout<< "size=" << arraysize << endl;
    } else {
       env->ExceptionClear();
    }
    return;
}

Threads y Sincronización

Aunque la librería nativa se carga una vez por cada clase, los threads individuales de una aplicación escrita en Java usan su propio puntero interface cuando llaman a un método nativo. Si necesitamos restringir el acceso a un objeto Java desde dentro del código nativo, podemos asegurarnos de los métodos Java a los que llamamos tienen sincronización explícita o podemos usar las funciones MonitorEnter y MonitorExit.

En el lenguaje Java, el código está protegido por un monitor siempre que especifiquemos la palabra clave synchronized. En Java el monitor que entra y sale de las rutinas normalmente está oculto para el desarrollador de la aplicación. En JNI, necesitamos delinear explícitamente los puntos de la entrada y de salida del código de seguridad del thread.

El siguiente ejemplo usa un objeto Boolean para reestringir el acceso a la función CallVoidMethod.

  env->ExceptionClear();
  env->MonitorEnter(lock);
  env->CallVoidMethod(jobj, mid, ret);
  env->MonitorExit(lock);
  if(env->ExceptionOccurred()) {
    cout << "error occured copying array back" << endl;
    env->ExceptionDescribe();
    env->ExceptionClear();
  }
Podríamos encontrar que en caso donde queremos accder a recursos locales del sistema como un manejador MFC windows o una cola de mensajes, es mejor usar un Thread Java y acceder a la cola de eventos nativa o al sistema de mensajes dentro del código nativo.

Problemas de Memoria

Por defecto, JNI usa referencias locales cuando crea objetos dentro de un método nativo. Esto significa que cuando el método retorna, las referencias están disponibles para el recolector de basura. Si queremos que un objeto persista a través de las llamadas a un método nativo, debemos usar una referencia golbal. Una referencia global se crea desde una referencia local llamando a NewGlobalReference sobre la referencia local.

Podemos marcar explíctamente para el recolector de basura llamando a DeleteGlobalRef sobre la referencia. También podemos crear una referencia global al estilo weak que sea accesible desde fuera del método, pero puede ser recolectado por el recolector de basura. Para crear una de estas referencias, llamamos a NewWeakGlobalRef y DeleteWeakGlobalRef para marcar la referencia para la recolección de basura.

Incluso podemos marcar explícitamente una referencia local para la recolección de basura llamando al método env->DeleteLocalRef(localobject). Esto es útil si estamo usando una gran cantidad de datos temporales:

  static jobject stringarray=0;

  JNIEXPORT void JNICALL Java_ArrayHandler_returnArray
  (JNIEnv *env, jobject jobj){

    jobjectArray ret;
    int i;
    jint arraysize;
    int asize;
    jclass cls, tmpcls;
    jmethodID mid;
    jfieldID fid;

    char *message[5]= {"first", 
		"second", 
		"third", 
		"fourth", 
		"fifth"};

    ret=(jobjectArray)env->NewObjectArray(5,
        env->FindClass("java/lang/String"),
        env->NewStringUTF(""));

  //Make the array available globally
    stringarray=env->NewGlobalRef(ret);

  //Process array
  // ...

  //clear local reference when finished..
    env->DeleteLocalRef(ret);
  }

Invocaciones

La sección sobre llamadas a métodos nos mostraba como llamar a un método o campo Java usando el interface JNI y una clase cargada usando la función FindClass. Con un poco más de código, podemos crear un programa que invoque a la máquina virtual Java e incluya su propio puntero al interface JNI que puede ser usado para crear ejemplares de clases Java. En Java 2, el programa de ejecución llamando java es una pequeña aplicación JNI que hace exactamente esto.

Podemos crear una máquina virtual Java con una llamada a JNI_CreateJavaVM, y desconectar la máquina virtual Java creada con una llamada a JNI_DestroyJavaVM. Una JVM también podría necesitar algunas propiedades adicionales de entorno. Estas propiedades podrían pasarse a la función JNI_CreateJavaVM en un estructura JavaVMInitArgs.

La estructura JavaVMInitArgs contiene un puntero a un valor JavaVMOption usado para almacenar información del entorno como el classpath y la versión de la máquina virtual Java, o propiedades del sistema que podrían pasarse normalmente en la línea de comandos del programa.

Cuando retorna la función JNI_CreateJavaVM, podemos llamar a método y crear ejemplares de clases usando las funciones FindClass y NewObject de la misma forma que lo haríamos con código nativo embebido.


Nota: La invocación de la máquina virtual Java sólo se usa para threads nativos en máquinas virtuales Java. Algunas antiguas máquinas virtuales Java tienen una opción de threads verdes que es estable para el uso de invocaciones, Sobre una plataforma Unix, podríamos necesitar enlazar explícitamente con -lthread o -lpthread.
El siguiente programa invoca una máquina virtual Java, carga la clase ArrayHandler y recupera el campo arraySize que debería tener el valor menos uno. Las opciones de la máquina virtual Java incluyen el path actual en el classpath y desactivar del compilador Just-In_Time (JIT) -Djava.compiler=NONE.
#include <jni.h>

void main(int argc, char *argv[], char **envp) {
  JavaVMOption options[2];
  JavaVMInitArgs vm_args;
  JavaVM *jvm;
  JNIEnv *env;
  long result;
  jmethodID mid;
  jfieldID fid;
  jobject jobj;
  jclass cls;
  int i, asize;

  options[0].optionString = ".";
  options[1].optionString = "-Djava.compiler=NONE";

  vm_args.version = JNI_VERSION_1_2;
  vm_args.options = options;
  vm_args.nOptions = 2;
  vm_args.ignoreUnrecognized = JNI_FALSE;

  result = JNI_CreateJavaVM(
             &jvm,(void **)&env, &vm_args);
  if(result == JNI_ERR ) {
    printf("Error invoking the JVM");
    exit (-1);
  }

  cls = (*env)->FindClass(env,"ArrayHandler");
  if( cls == NULL ) {
    printf("can't find class ArrayHandler\n");
    exit (-1);
  }
  (*env)->ExceptionClear(env);
  mid=(*env)->GetMethodID(env, cls, "<init>", "()V");
  jobj=(*env)->NewObject(env, cls, mid);
  fid=(*env)->GetFieldID(env, cls, "arraySize", "I");
  asize=(*env)->GetIntField(env, jobj, fid);

  printf("size of array is %d",asize);
  (*jvm)->DestroyJavaVM(jvm);
}

Adjuntar Threads

Después de invocar la máquina virtual Java, hay un thread local ejecutándose en ella. Podemos crear más threads en el sistema operativo local y adjuntar threads en la máquina virtual Java para estos nuevos threads. Podriamos querer hacer esto su nuestra aplicación nativa es multi-threads.

Adjuntamos el thread local a la máquina virtual Java con una llamada a AttachCurrentThread. Necesitamos suministrar punteros al ejemplar de la máquina virtual Java y al entorno JNI. En la plataforma Java 2, podemos específicar en el tercer parámetro el nombre del thread y/o el grupo bajo el que queremos que viva nuestro thread. Es importante eliminar cualquier thread que haya sido préviamente adjuntado; de otra forma, el programa no saldrá cuando llamemos a DestroyJavaVM.

#include <jni.h>
#include <pthread.h>

JavaVM *jvm;

void *native_thread(void *arg) {
  JNIEnv *env;
  jclass cls;
  jmethodID mid;
  jfieldID fid;
  jint result;
  jobject jobj;
  JavaVMAttachArgs args;
  jint asize;
  
  args.version= JNI_VERSION_1_2;
  args.name="user";
  args.group=NULL;
  result=(*jvm)->AttachCurrentThread(
	jvm, (void **)&env, &args);

  cls = (*env)->FindClass(env,"ArrayHandler");
  if( cls == NULL ) {
    printf("can't find class ArrayHandler\n");
    exit (-1);
  }
  (*env)->ExceptionClear(env);
  mid=(*env)->GetMethodID(env, cls, "<init>", "()V");
  jobj=(*env)->NewObject(env, cls, mid);
  fid=(*env)->GetFieldID(env, cls, "arraySize", "I");
  asize=(*env)->GetIntField(env, jobj, fid);
  printf("size of array is %d\n",asize);
  (*jvm)->DetachCurrentThread(jvm);
}

void main(int argc, char *argv[], char **envp) {
  JavaVMOption *options;
  JavaVMInitArgs vm_args;
  JNIEnv *env;
  jint result;
  pthread_t tid;
  int thr_id;
  int i;

  options = (void *)malloc(3 * sizeof(JavaVMOption));

  options[0].optionString = "-Djava.class.path=.";
  options[1].optionString = "-Djava.compiler=NONE";

  vm_args.version = JNI_VERSION_1_2;
  vm_args.options = options;
  vm_args.nOptions = 2;
  vm_args.ignoreUnrecognized = JNI_FALSE;

  result = JNI_CreateJavaVM(&jvm,(void **)&env, &vm_args);
  if(result == JNI_ERR ) {
    printf("Error invoking the JVM");
    exit (-1);
  }

  thr_id=pthread_create(&tid, NULL, native_thread, NULL);

// If you don't have join, sleep instead
//sleep(1000);
  pthread_join(tid, NULL);
  (*jvm)->DestroyJavaVM(jvm);
  exit(0);
}

_______
1 Cuando se usan en toda esta site, los términos, "Java virtual machine" o "JVM" significa una máquina virtual de la plataforma Java.


Ozito