jueves, 14 de marzo de 2013

No JNI


Cuando se desea usar una librería del sistema operativo (.dll, .so, etc…)  desde una aplicación Java la solución que nos aporta el lenguaje Java es usar JNI y los métodos “native”. La arquitectura es la siguiente:



Las librerías se cargan al solo una vez y no es posible eliminarlas ni volver a cargarlas durante la ejecución de la aplicación Java. Yo quiero proponer otra alternativa (a la que llamaremos por control) y luego comprarla con JNI para ver cual sería mejor. 

Por la parte de C, lo que propongo es no usar los métodos JNIEXPORT de la librería y crear una aplicación C que recibe por entrada estándar una secuencia de bytes que indicara que comando ejecutar y con que parámetros y escribirá en la salida estándar los resultados de la operación que ejecute.
Por la parte de Java, lo que propongo es no usar los método nativos ni cargar las librerías. La aplicación Java ejecutará la aplicación C y tomará el control de la ejecución. Al tomar el control puede:

  • Parar la aplicación C y volver a lanzarla.
  • Capturar la salida estándar de la aplicación C por lo que puede escribir y leer de ella.


La nueva arquitectura seria:



La forma en Java de lanzar una aplicación y tomar el control es la siguiente:
Clase Main:
public class MainNoJNI
{
    public static void main (String arg[]) throws Exception{
        
       
    
        String process = "D:\\fuentes\\noJNI\\noJNI\\Release\\noJNI.exe";
        Runtime rtPrimary = Runtime.getRuntime();
        final Process pidPrimary = rtPrimary.exec(process);
        
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {  
            public void run() {  
                pidPrimary.destroy();  
                }  
        })); 
      
        
        DataOutputStream dataOutPrimary = new
        DataOutputStream(pidPrimary.getOutputStream());
        
        Date inicio = new Date();
        byte [] entrada = generarBytesAltaCliente(new AltaEntrada());
        
        dataOutPrimary.write( entrada );
        dataOutPrimary.flush();
        dataOutPrimary.close();

        DataInputStream dataInPrimary = new
        DataInputStream(pidPrimary.getInputStream());
        
        // vamos a suponer que la salida es como mucho de 10 bytes
        byte[] salida = new byte[10];
        dataInPrimary.read( salida, 0, 10 );
        
        AltaSalida altaSalida = interpretaSalida(salida);
        Date fin = new Date();
        
        //System.out.println("Con control has tardado : " + (fin.getTime() - inicio.getTime()) + " milisegundos");
        System.out.println( (fin.getTime() - inicio.getTime()) );
        
        pidPrimary.destroy();
    }

    private static AltaSalida interpretaSalida( byte[] salida )
    {
        AltaSalida altaSalida = new AltaSalida();
        
        altaSalida.respuesta = salida[0];
        
        byte [] codigo = new byte [4];
        codigo[0] = salida[1];
        codigo[1]= salida[2];
        codigo[2]= salida[3];
        codigo[3]= salida[4];
        altaSalida.codigoUsuario =  new String(codigo);
        
        altaSalida.departamento =  salida[5];   
        
        altaSalida.esFiable=  salida[6];  
        
        
        altaSalida.fechaAltaDia = salida[7];
        
        altaSalida.fechaAltaMes= salida[8];
        
        altaSalida.fechaAltaAnyo = salida[9];
        
        
        return altaSalida;
    }

    private static byte[]  generarBytesAltaCliente(AltaEntrada altaEntrada)
    {

        // vamos a suponer que la entrada es como mucho de 10 bytes
        byte[] salida = new byte[10];
        salida[0] = altaEntrada.tipo; // 1 byte para el tipo de operacion. Alta usuario
        
        byte[] nombre = altaEntrada.nombre.getBytes();
        
        salida[1] = nombre[1]; 
        salida[2] = nombre[2]; 
        salida[3] = nombre[2]; 
        salida[4] = nombre[3]; // 4 bytes para el nombre 
        
        
        salida[5] = altaEntrada.edad; // 1 byte para la edad
        
        salida[6] = altaEntrada.sexo; // 1 byte el sexo
        
        byte[] ret = new byte[2];
        ret[0] = (byte)(altaEntrada.codigoPais & 0xff);
        ret[1] = (byte)((altaEntrada.codigoPais >> 8) & 0xff);
        
       
        
        salida[9] = altaEntrada.codigoCiudad; // 1 bytes para el codigo del ciudad        
        
        
        return salida;
    }
}

La clase entrada seria:

public class AltaEntrada
{

    public byte tipo;
    
    public String nombre;
    
    public byte edad;    
    
    public byte sexo;
    
    public short codigoPais;
    
    public byte codigoCiudad;
    
  
    
    public AltaEntrada (){
        tipo = 1;
        
        nombre = "pepe";
        
        edad = 20;    
        
        sexo = 1;
        
        codigoPais = 300;
        
        codigoCiudad = 100;
        
   
    }
    
}

La clase salida:

public class AltaSalida
{

    
    public byte respuesta;
    
    public String codigoUsuario;
    
    public byte departamento;    
    
    public byte esFiable;    
    
    public byte fechaAltaDia;
    
    public byte fechaAltaMes;
    
    public byte fechaAltaAnyo;
    
    
}

El codigo C:

#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include <string>

using namespace std;

string alta (string leido){

  string salida;
  char  nombre[4];    
  char edad;        
  char sexo;    
  unsigned short codigoPais;    
  char codigoCiudad;

  // parseamos
  leido.copy(nombre, 4, 1);
  edad = leido.at(5);
  sexo = leido.at(6);
  codigoPais = (leido.at(7)<<8) | leido.at(8);
  codigoCiudad = leido.at(9);

  // lo que tarde la operacion
  Sleep(1000);


  // creamos la salida
  salida.append(1,0);
  salida.append(1,'p');
  salida.append(1,'e');
  salida.append(1,'p');
  salida.append(1,'e');
  salida.append(1,12);
  salida.append(1,1);
  salida.append(1,11);
  salida.append(1,10);
  salida.append(1,5);

return salida;
}

int _tmain(int argc, _TCHAR* argv[])
{
char comando = -1;

string leido;
string respuesta;
// el comando 0 es cierra la aplicacion
cin >> leido;

comando = leido.at(0);
// es un alta
if (comando == 1) 
respuesta = alta (leido);
else
respuesta = -1;

cout << respuesta ;

return 0;
}





Vamos ha hacer una comparativa con varios conceptos para ver que opción puede resultar mejor:


Concepto
JNI
Por control
Memoria
Se suma a al tamaño de la aplicación Java
Va a parte de la aplicación Java
Forma de comunicación
Por memoria
Por entrada/salida estándar
Unidad de comunicación
Objetos/metodos – mapeo
Cadena – parseo
Error en C
Se cierra la aplicación/hilo
Se puede detectar y actuar en consecuencia. La aplicación/hilo continúa
Instancias
Una
Infinitas
Cambio en ejecución
No se puede
Se puede
Rendimiento
Igual

Que es cada concepto:
  •          Memoria: donde se coloca la parte C en memoria.
  •          Forma de comunicación: como se comunica la parte Java con la parte C.
  •          Unidad de comunicación: como Java le dice a C que ejecute un método y como C le da la respuesta de la ejecución.
  •          Error en C: que sucede en la parte Java si se produce un error grabe en la parte C.
  •           Instancias: instancias de la parte C que se pueden hacer desde Java.
  •          Cambio en ejecución: capacidad de cambiar la parte C en ejecución por otra parte C que tenga los mismos métodos.


Teniendo en cuenta estos conceptos, la arquitectura “por control” parece mejor excepto quizás en el apartado de Unidad de ejecución ya que JNI trata de que no se note cuando la ejecución de la aplicación Java pasa por ser C ni que tampoco se note el fin de esa ejecución C y la ejecución de la aplicación Java vuelve a ser Java. Desde el punto de la programación, desde Java es mejor pero en la parte C los objetos no son mapeados directamente si no que están en un objeto indeterminado y cada campo del objeto hay que buscarlo uno a uno. Si son muchos el código se complica y no queda tan claro que sea mas “limpio” que pasear un array de bytes.
¿Pero que pasa si no usas un objeto y lo que haces es pasarlo parámetro a parámetro? 

No hay comentarios: