Está en la página 1de 36

Java Native Interface (JNI)

1
Tabla de Contenido
1 Introducción
2 Las librerías de enlace estático y dinámico
3 Ejemplo básico con dos clases
3.1 Escribir Código Java
3.2 Compilar el Código Java
3.3 Crear el archivo de Cabecera
3.4 Escribir la función C
3.5 Crear la Librería Dinámica
3.5.1 Unix
3.5.2 Windows
3.6 Ejecutar el Programa
4 Ejemplo básico con una clase
4.1 Declarar el método nativo como miembro de una clase.
4.2 Crear el archivo de cabecera nativo
4.3 Implementar el método nativo
4.4 Compilar el archivo nativo
4.5 Ejecutar el programa
5 Tipos de datos fundamentales, arreglos y objetos
5.1 Parámetros de un método nativo
5.2 Correspondencia de tipos
5.3 Acceso a tipos de datos fundamentales
5.4 Acceso a String
5.4.1 Obtener el texto de un String
5.4.2 Crear un nuevo String
5.4.3 Ejemplo
5.5 Acceso a arreglos
5.5.1 Acceso a arreglos de tipos de datos fundamentales
5.5.2 Acceso a arreglos de referencias
6 Acceso a objetos
6.1 La signatura de tipo

2
1. Introducción
La plataforma Java 2 incorpora la interfaz de programación Java Native Interface (JNI), para
permitir que se puedan escribir programas en otros lenguajes diferentes a Java y mantener la
portabilidad entre todas las plataformas. También, JNI permite que el código Java que se ejecute
en una Máquina Virtual de Java pueda interactuar con aplicaciones y librerías escritas en otros
lenguajes, como C, C++ y ensamblador. Además, la interfaz de programación de aplicación
“Invocation API” permite que se puedan llamar a código de Máquina Virtual Java desde
aplicaciones nativas.

Un método nativo es un método Java (una instancia de un objeto o una clase) cuya
implementación se ha realizado en otro lenguaje de programación fuera del código Java, por
ejemplo, C. Vamos a ver cómo se integran métodos nativos en clases Java. Actualmente, el
lenguaje Java solamente proporciona mecanismos para integrar código C en programas Java.

El objetivo de utilizar JNI para escribir métodos nativos es el de permitir que los
programadores puedan manejar aquellas situaciones en las que una aplicación no puede ser
escrita estrictamente en Java. Por ejemplo algunas de las situaciones en las que puede ser
necesario recurrir a métodos nativos son las siguientes:

o La librería de clases estándar de Java no soporta las características dependientes de


plataforma necesarias para la ejecución de la aplicación.
o Ya hay una aplicación o librería escrita en otro lenguaje y se quiere que sea accesible a
los programas Java.
o Puede necesitarse escribir una pequeña porción de código crítica en cuanto a su tiempo
de ejecución, en un lenguaje de bajo nivel, ensamblador por ejemplo, y hacer que los
programas Java llamen a esas funciones, para realizar aplicaciones en tiempo real.

La programación a través del entorno que proporciona JNI para implementar métodos nativos
involucra varias operaciones, y proporciona a estos métodos nativos una gran flexibilidad, de
forma que:

o Los métodos nativos pueden llamar a métodos Java con suma facilidad.
o Un método nativo puede utilizar los objetos Java del mismo modo que el propio Java los
usan.
o Un método nativo puede crear objetos Java, incluyendo arreglos y objetos de tipo String,
y luego inspeccionarlos y utilizarlos para sus propias operaciones.
o Un método nativo también puede inspeccionar y utilizar objetos creados por parte de
código Java de la aplicación.
o Un método nativo puede actualizar objetos Java ya creados para que luego sean usados
por la parte de código Java de la aplicación.

En resumen, las partes Java y nativa de la aplicación pueden crear, actualizar, acceder y
compartir objetos java.

JNI permite utilizar las ventajas que ofrece el lenguaje de programación Java desde métodos
nativos; en concreto, permite capturar y lanzar excepciones desde métodos nativos y que estas
excepciones sean manejadas en la aplicación Java. Finalmente, los métodos nativos pueden

3
utilizar JNI para realizar la comprobación de tipos estricta que proporciona Java en tiempo de
ejecución.

La Figura 1 resume todas las características anteriores, mostrando cómo JNI sirve de enlace entre
Java y aplicaciones nativas, por ejemplo, escritas en lenguaje C.

Excepciones Clases Máquina Virtual


Java

Java Native Interface

C
Funciones Librerías

Figura 1. Muestra cómo JNI sirve de enlace entre Java y aplicaciones nativas

En resumen, JNI es un mecanismo que nos permite ejecutar código nativo desde Java y
viceversa, en donde el código nativo son funciones escritas en un lenguaje de programación
como C o C++ para el sistema operativo (a partir de ahora SO) donde se está ejecutando la
máquina virtual.

Vamos a llamar máquina virtual a un entorno de programación formado por:

• El programa que ejecuta los byte codes Java


• Las librerías (API) de Java

Llamamos host environment a el ordenador en que se ejecuta el SO y a las librerías nativas de


este ordenador; estas librerías se suelen escribir en C o C++ y se compilan dando lugar a códigos
binarios del hardware en que estamos trabajando.

De esta forma podemos decir que las máquinas virtuales Java se hacen para un determinado
host environment.

La máquina virtual que se usa en la Mac OS X es el JRE (Java Runtime Environment) de Apple,
aunque hay otras máquinas virtuales Java (también llamadas plataformas) como la de Sun que es
la implementación de referencia y la más usada en sistemas como Windows, Linux y Solaris.
IBM tiene una máquina virtual para Windows, para AIX y para Linux. Borland por su parte
también tiene su propia máquina virtual en Windows. Microsoft también hizo una máquina
virtual para Windows, pero está desestimada por no seguir las especificaciones de Sun, que
ahora es Oracle.

JNI tiene un interfaz bidireccional que permite a las aplicaciones Java llamar a código nativo y
viceversa. Es decir JNI soporta dos tipos de interfaces:

4
1. Native methods. Que permiten a Java llamar a funciones implementadas en librerías
nativas
2. Invocation Interface. Que nos permite incrustar una máquina virtual Java en una
aplicación nativa. Por ejemplo, una máquina virtual Java en un navegador de Internet
escrito en C. Para ello, la aplicación nativa llama a librerías nativas de la máquina virtual
y luego usa el llamado invocation interface para ejecutar métodos Java en la máquina
virtual.

5
2. Las librerías de enlace estático y dinámico
Las librerías de enlace estático son archivos destinados a almacenar funciones, clases y
variables globales y se crean a partir de varios archivos .o (UNIX) o .obj (Windows). En UNIX
suelen tener la extensión .a y en Windows la extensión .lib

Mac OS X usa la extensión de los sistemas UNIX tradicionales.

Las funciones de la librería estática se incluyen dentro del programa ejecutable durante la fase de
enlazado, con lo que una vez generado el ejecutable ya no es necesario disponer de las librerías
de enlace estático.

Las librerías de enlace dinámico son archivos cuyas funciones no se incrustan al programa
ejecutable durante la fase de enlazado, sino que en tiempo de ejecución el programa busca el
archivo, carga su contenido en memoria y enlaza su contenido según va siendo necesario, es
decir según vamos llamando a las funciones.

Esto tiene la ventaja de que varios programas pueden compartir las mismas librerías, lo cual
reduce el gasto de disco duro, especialmente con las llamadas al sistema (APIs) que suelen ser
usadas por muchas aplicaciones a la vez.

Su extensión también varía dependiendo del SO en que se esté. Extensiones típicas son:

Sistema Operativo Extensión


Mac OS X .dylib
UNIX .so
Windows .dll

Para que los programas encuentren las librerías de enlace dinámico, estas deben ponerse en
unos determinados directorios. Cada SO sigue sus reglas:

Sistema Operativo Regla de búsqueda de librerías de enlace dinámico


Busca en el directorio donde está la aplicación y en la variable de
Mac OS X
entorno DYLD_LIBRARY_PATH
Busca en los directorios indicados en la variable de entorno
UNIX
LD_LIBRARY_PATH 1
Busca en el directorio donde está la aplicación ( .exe ) y en los
Windows directorios indicados por la variable de entorno PATH, si no lo encuentra
busca en el directorio system o System32 de Windows

Fijar las variables de entorno

Para fijar estas variables de entorno existen comandos que varían dependiendo del sistema donde
estemos. Por ejemplo, si se quiere incluir el directorio actual en el path de búsqueda de librerías
de enlace dinámico.

1
En algunos sistemas también podemos indicar rutas de búsqueda de librerías en el archivo
/etc/ld.so.conf que es cargado por /sbin/ldconfig al arrancar.

6
En Mac OS X de 64 bits se haría:

Configurar variables de entorno PATH en Mac OS X

Editar el archivo .bash_profile y colocar ahí tus rutas. Este archivo se ejecuta cada vez que
iniciamos sesión en la terminal y está ligado a cada usuario, por lo que otros usuarios del sistema
no se verán afectados por tus cambios.

Paso 1: Abre tu terminal de comandos.


Paso 2:
1. Si el archivo no existe solo debes crearlo ejecutando el siguiente comando en tu terminal:
touch ~/.bash_profile

2. Abre el archivo en tu editor preferido o a través del siguiente comando en tu terminal:


open ~/.bash_profile

Paso 3: Agrega tus rutas al final del archivo.


export PATH=${PATH}:/tu_ruta

Los ":" sirven para concatenar rutas, de manera que cada vez que necesites agregar una
nueva ruta solo debes colocar ":" y tu nueva ruta al final de la asignación del PATH.

Ejemplo real de como está configurado en mi equipo para que puedas darte una mejor
idea:

export ANDROID_HOME=/Users/MeridaDesignStudio/Development/adt-bundle/sdk
export COMPOSER=/Users/MeridaDesignStudio/.composer/vendor/bin
export PATH=${PATH}:$ANDROID_HOME/tools:$ANDROID_HOME/platform-tools:$COMPOSER

Como puedes ver en el ejemplo, se pueden crear variables de tus rutas y luego usarlas en
la asignación del PATH.
Paso 4: Guarda el archivo y cierra el editor.
Paso 5: Forza la ejecución del archivo .bash_profile para ver los cambios inmediatamente sin
tener que reiniciar. En tu terminal ejecuta el siguiente comando:
source ~/.bash_profile

Configurar variables de entorno JAVA_HOME

Establezca la variable de entorno JAVA_HOME para que apunte al directorio instalado de JDK
(que contendrá el subdirectorio de include que se usará en el siguiente paso):

$ export JAVA_HOME = /your/java/installed/dir

En Linux se tendría que hacer:

Variable de entorno PATH

7
Para PATH (valor del entorno) escriba el comando como se muestra a continuación, en la
“Terminal” usando su ruta de instalación … Nota: (la ruta predeterminada es la que se muestra,
pero si tiene instalado OpenJDK en otra ubicación, establezca esa ruta).

export PATH = $PATH:/usr/lib/jvm/java-9-openjdk/bin

Variable de entorno JAVA_HOME

Para JAVA_HOME (Variable de entorno) escriba el comando como se muestra a continuación,


en la “Terminal” utilizando su ruta de instalación … (Nota: la ruta predeterminada es la que se
muestra, pero si tiene instalado OpenJDK en otra ubicación, establezca esa ruta).

export JAVA_HOME = /usr/lib/jvm/java-9-openjdk

Para comprobar si la instalación se realiza correctamente, escriba lo siguiente en el terminal y


verá que java se está ejecutando en su máquina.

java -version

Y en Windows se haría:

Incluir el directorio "bin" de JDK en el PATH

El Shell de Windows busca el directorio actual y los directorios listados en la PATH ( variable
del sistema ) para los programas ejecutables. Los programas de JDK (como el compilador Java
javac.exe y el tiempo de ejecución de Java java.exe) residen en el subdirectorio "bin" del
directorio instalado de JDK. Debe incluir "bin" en el PATH para ejecutar los programas JDK.

Para editar la "Ruta" en el entorno PATH en Windows 7/8/10:

1. Inicie "Panel de control" ⇒ (Opcional) Sistema y seguridad ⇒ Sistema ⇒ Haga clic en


"Configuración avanzada del sistema" en el panel izquierdo.
2. Cambie a la pestaña "Avanzado" ⇒ Presione el botón "Variables de entorno".
3. Debajo de "Variables del sistema" (el panel inferior), desplácese hacia abajo para
seleccionar "Ruta" ⇒ Haga clic en "Editar ...".
4. Para Windows 10 (nuevas versiones):
Verá una TABLA que enumera todas las entradas PATH existentes (de lo contrario, vaya
al siguiente paso). Haga clic en "Nuevo" ⇒ Ingrese al directorio "bin" de JDK
"c:\Program Files\Java\jdk-10.0.{xx}\bin" (Reemplace {xx} con su número de
instalación !!!) ⇒ Seleccione "Subir" para mover esta entrada hasta el TOP.

Antes de Windows 10 más reciente:


(Para estar SEGURO, copie el contenido del "Valor variable" en el Bloc de notas antes de
cambiarlo.)
En el campo "Valor variable", INSERTE "c:\Program Files\Java\jdk-10.0.{xx}\bin"
(¡¡Reemplace {xx} con su número de instalación!) EN FRENTE de todos los directorios
existentes, seguido de un punto y coma ( ; ) que separa el directorio bin del JDK del resto
de los directorios existentes. NO BORRAR ninguna entrada existente; de lo contrario,
algunas aplicaciones existentes pueden no ejecutarse.
8
5. Nombre de la variable: PATH
Valor de la variable: c:\Archivos de programa\Java\jdk-10.0.{XX}\bin; [ No borre las
entradas que salen ... ]

Notas: A partir de JDK 1.8, la instalación creó un directorio


"c:\ProgramData\Oracle\Java\javapath" y se agregó al PATH. Contiene solo ejecutables JRE
(java.exe , javaw.exe y javaws.exe), pero NO los ejecutables de JDK (por ejemplo, javac.exe).

Para establecer la variable de entorno JAVA_HOME:

1. Primero, encuentre su directorio instalado JDK. Para JDK 10, el valor predeterminado es
"c:\Program Files\Java\jdk-10.0.xx ", donde xx es el número de actualización. Utilice su
"Explorador de archivos" para ver este directorio y tome nota de su directorio instalado
JDK.
2. Compruebe si JAVA_HOME ya está configurado. Iniciar un CMD y emitir:
SET JAVA_HOME

Si JAVA_HOME mensaje "Variable de entorno JAVA_HOME no definida", continúe


con el siguiente paso.
Si obtiene "JAVA_HOME=C:\Program Files\Java\jdk-10.0.{xx}", verifique que esté
configurado correctamente en su directorio JDK. Si no, pasa al siguiente paso.
3. Para configurar la variable de entorno JAVA_HOME en Windows 10/8/7: Inicie "Panel
de control" ⇒ (Opcional) Sistema y seguridad ⇒ Sistema ⇒ Configuración avanzada del
sistema ⇒ Cambie a la pestaña "Avanzada" ⇒ Variables de entorno ⇒ Variables del
sistema (panel inferior) ⇒ "Nuevo" (o busque "JAVA_HOME" y "Editar" si ya está
configurado) ⇒ En "Nombre de variable", ingrese "JAVA_HOME" ⇒ En "Valor de
variable", ingrese el directorio instalado de JDK que anotó en el Paso 1. (En la versión
más reciente de Windows 10: puede presionar el botón "Examinar Directorio ..." y
seleccionar el directorio instalado JDK para evitar errores tipográficos).
4. Para verificar, REINICIE una CMD (reinicio necesario para actualizar el entorno) y
emita:
SET JAVA_HOME
JAVA_HOME = c:\ Archivos de programa \ Java \ jdk-10.0. {X} <== Verifique que este es su directorio instalado de JDK

Notas: Las variables de entorno de Windows (como JAVA_HOME , PATH ) NO distinguen


entre mayúsculas y minúsculas.

Generar una librería de enlace dinámico

La forma de generar una librería de enlace dinámico es casi siempre la misma:

1. Escribimos los archivos .c o .cpp con el código fuente


2. Los compilamos dando una opción al compilador que indique que en vez de un
ejecutable queremos generar una librería de enlace dinámico.

La opción 2 también varía dependiendo del compilador y SO en que estemos:

9
Encontrar la compilación correcta para su plataforma operativa (Windows, Mac OS X, Linux),
para su JDK (64 bits) y descubrir las opciones correctas del compilador es la parte más difícil
para que funcione el JNI.

En Mac OS X con JDK de 64 bits

1. // Para mí máquina @
/Library/Java/JavaVirtualMachines/jdk1.8.0_xx.jdk/Contents/Home

$ echo $JAVA_HOME

2. Compile el programa C Archivo.c en el módulo de uso compartido dinámico


libArchivo.dylib usando gcc , que se incluye en todos los sistemas operativos
Unixes/Mac:

$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" -dynamiclib


-o libArchivo.dylib Archivo.c

3. Ejecute el programa Java:

$ java -Djava.library.path=. ClaseJNIJava

En Linux con JDK de 64 bits

1. Compile el programa C Archivo.c en el módulo compartir libArchivo.so usando gcc ,


que se incluye en todos los Unixes:

$ gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -


shared -o libArchivo.so Archivo.c

2. Ejecute el programa Java:

$ java -Djava.library.path=. ClaseJNIJava

En Windows con JDK de 64 bits

Vamos a utilizar Cygwin. Debe tomar nota de lo siguiente para Windows:

• Windows/Intel usa estos conjuntos de instrucciones: x86 es un conjunto de instrucciones


de 32 bits; i868 es una versión mejorada de x86 (también de 32 bits); x86_64 (o amd64)
es un conjunto de instrucciones de 64 bits.
• Un compilador de 32 bits se puede ejecutar en Windows de 32 bits o 64 bits (compatible
con versiones anteriores), pero el compilador de 64 bits solo se puede ejecutar en
Windows de 64 bits.
• Un compilador de 64 bits podría producir un destino de 32 bits o 64 bits.
• Si utiliza el GCC de Cygwin, el objetivo podría ser Windows nativo o Cygwin. Si el
destino es Windows nativo, el código se puede distribuir y ejecutar bajo Windows. Sin
embargo, si el objetivo es Cygwin, para distribuir, debe distribuir el entorno de ejecución
de Cygwin (cygwin1.dll). Esto se debe a que Cygwin es un emulador de Unix en
Windows.
• Lo anterior explica las muchas versiones de GCC bajo Cygwin.
10
Para JDK de 64 bits, debe encontrar un compilador que produzca el destino de Windows nativo
de 64 bits. Esto es proporcionado por MinGW-W64. Puede instalar MinGW-W64 bajo Cygwin,
seleccionando los paquetes "mingw64-x86_64-gcc-core" (compilador C) y "mingw64-x86_64-
gcc-g++" (compilador C ++). Los ejecutables son "x86_64-w64-mingw32-gcc" (Compilador C)
y "x86_64-w64-mingw32-g++ " (Compilador C ++), respectivamente. También se puede utilizar
el ejecutable gcc (Compilador C) y g++ (Compilador C ++), que son los ejecutables
recomendados.

A continuación, use los siguientes comandos para compilar archivo .c (Archivo.c) en archivo .dll
(Arch.dll). En Windows, hacemos referencia a la variable de entorno JAVA_HOME como
%JAVA_HOME% en el comando:

> gcc -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o


Arch.dll Archivo.c

Las opciones de compilador utilizadas son:

• -I headerDir: para especificar el directorio del encabezado. En este caso "jni.h" (en
"%JAVA_HOME%\include") y "jni_md.h" (en "%JAVA_HOME%\include\win32"),
donde JAVA_HOME es una variable de entorno establecida en el directorio instalado de
JDK.
• -shared : para generar biblioteca compartida.
• -o outputFilename: para establecer el nombre de archivo de salida "hello.dll".

También puedes compilar y enlazar en dos pasos:

// Compila solo "Archivo.c" con el indicador -c. La salida es "Archivo.o"


> gcc -c -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" Archivo.c

// // Enlace "Archivo.o" a la biblioteca compartida "Arch.dll"


> gcc -shared -o Arch.dll Archivo.o

Arch.dll verifica el tipo de archivo resultante a través de la utilidad "file", que indica que
"Arch.dll" es una DLL de Windows nativa de 64 bits (x86_64).

> file hello.dll


Arch.dll: PE32+ executable (DLL) (console) x86-64, for MS Windows

En los siguientes subtemas se muestran los pasos necesarios para mezclar código nativo C y
programas Java.

11
3 El ejemplo básico con dos clases
En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_1".

Recurriremos a nuestro saludo; en este caso, el programa HolaMundo tiene dos clases Java: la
primera, Main, implementa el método main() y la segunda, HolaMundo, tiene un método nativo
que presenta el mensaje de saludo. La implementación de este segundo método se realizará en C.

Las acciones que se debe realizar, para conseguir que la versión del saludo funcione, serán las
que se desarrolle en los subtemas siguientes.

3.1 ESCRIBIR CODIGO JAVA

En primer lugar, se debe crear una clase Java, HolaMundo, que declare un método nativo.
También se debe crear el programa principal que cree el objeto HolaMundo y llame al método
nativo.

Las siguientes líneas de código definen la clase HolaMundo, que consta de un método y un
segmento estático de código:

// HolaMundo.java
// Clase HolaMundo
class HolaMundo
{
public native void presentaSaludo();

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

El método loadLibrary(libraryName) que se detalla a continuación:

System.loadLibrary("hola");

Recibe el nombre de la librería de enlace dinámico "hola" y la carga. La librería debe cargarse
antes de llamar a cualquier método nativo.

El nombre que pasamos en "hola" depende de la plataforma donde estemos:

• En Mac OS X el archivo se debería llamar libhola.jnilib y el libraryName pasaríamos


"hola"º
• En Linux o Solaris el archivo se debería llamar libhola.so y el libraryName pasaríamos
"hola"
• En Windows el archivo se debería llamar hola.dll y el libraryName pasaríamos
“HolaMundo”

Es decir a loadLibrary() siempre le pasamos "hola"

12
Se puede decir que la implementación del método presentaSaludo() de la clase HolaMundo está
escrito en otro lenguaje, porque la palabra reservada native aparece como parte de la definición
del método. Esta definición, proporciona solamente la definición para presentaSaludo() y no
proporciona ninguna implementación para él. La implementación la proporcionaremos desde un
archivo fuente separado; escrito en lenguaje C.

La definición para presentaSaludo() también indica que el método es un método público, no


acepta argumentos y no devuelve ningún valor. Al igual que cualquier otro método, los métodos
nativos deben estar definidos dentro de una clase Java.

El código C que implementa el método presentaSaludo() debe ser compilado en una librería
dinámica y cargado en la clase Java que lo necesite. Esta carga, mapea la implementación del
método nativo sobre su definición.

El siguiente bloque de código carga la librería dinámica, en este caso “hola”. El sistema Java
ejecutará un bloque de código estático de la clase cuando la cargue.

Todo el código anterior forma parte del archivo "HolaMundo.java", que contiene la clase
HolaMundo. En un archivo separado, "Main.java", se va a crear una aplicación Java que
instancie a la clase HolaMundo y llame al método nativo presentaSaludo().

// Main.java
// Aplicación Java
class Main
{
public static void main( String args[] )
{
new HolaMundo().presentaSaludo();
}
}

Como se puede observar, se llama al método nativo del mismo modo que a cualquier otro
método Java; se añade el nombre del método al final del nombre del objeto con un punto ("."). El
conjunto de paréntesis que sigue al nombre del método encierra los argumentos que se le pasen.
En este caso, el método presentaSaludo() no recibe ningún tipo de argumento.

En resumen, en el código Java para que se puedan incorporar métodos nativos hay que hacer
dos cosas:

o Escribir la declaración de cada método nativo que se vaya a utilizar. Eso sería igual que la
declaración de un método normal de una interfaz Java, incorporándole la palabra native.

public native void presentaSaludo();

o Colocar la carga de la librería que contiene el código nativo a través de un bloque de la clase
estática.

Static
{
System.loadLibrary( "hola" )
}

13
Y ya se puede incorporar en el método principal la llamada que permita invocar al método
nativo:

public static void main( String args[] )


{
new HolaMundo().presentaSaludo();
}

3.2 COMPILAR EL CÓDIGO JAVA

Si se utiliza el compilador javac para compilar el código Java que se desarrolló.

Compilar los dos archivos fuentes de código Java que se crearon en el directorio de trabajo, para
lo cual cargar la línea de comandos: botón derecho en Menú de Inicio de Windows, para elegir la
opción Ejecutar, luego digitar cmd y enter. Por último, teclear los siguientes comandos en la
línea de comandos:

> D: (P.e. mí unidad de datos)


> cd D:\AAEjerciciosJava\01_Ejercicios_JNI\Proyecto_1_1 (P.e. mi disco duro)
> javac HolaMundo.java
> javac Main.java

3.3 CREAR EL ARCHIVO DE CABECERA

Ahora se debe utilizar la aplicación javah para conseguir el archivo de cabecera .h. El archivo de
cabecera define una estructura que representa la clase HolaMundo sobre código C y proporciona
la definición de una función C para la implementación del método nativo presentaSaludo()
definido en ese clase.

A partir de JDK 8, debe usar "javac -h" para compilar el programa Java y generar el archivo de
encabezado C / C ++ denominado HolaMundo.h de la siguiente manera:

> javac –h . HolaMundo.java

La opción "-h dir" genera el encabezado C/C++ y lo coloca en el directorio especificado (en el
ejemplo anterior, '.' para el directorio actual).

El archivo que creará, será un archivo de cabecera del mismo nombre que la clase y con
extensión .h. Por ejemplo, el comando anterior habrá creado el archivo HolaMundo.h, cuyo
contenido será el siguiente:

/* DO NOT EDIT THIS FILE - it is machine generated */


#include <jni.h>
/* Header for class HolaMundo */

#ifndef _Included_HolaMundo
#define _Included_HolaMundo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HolaMundo
* Method: presentaSaludo
14
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HolaMundo_presentaSaludo
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

La directiva #include <jni.h> incluye el archivo jni.h, que proporciona la información


para que el código C pueda interactuar con el sistema Java.

Este archivo de cabecera contiene la llamada de la función C que está declarada como:

JNIEXPORT void JNICALL Java_HolaMundo_presentaSaludo


(JNIEnv *, jobject);

Esta es la definición de la función C que se debe escribir para implementar el método nativo
presentaSaludo() de la clase HolaMundo. Si HolaMundo llamase a otros métodos nativos, las
definiciones de las funciones también aparecerían aquí.

El nombre de la función C que implementa el método nativo está derivado del nombre del
paquete, el nombre de la clase y el nombre del método nativo. Así, el método nativo
presentaSaludo() dentro de la clase HolaMundo es HolaMundo_presentaSaludo(). En este
ejemplo, no hay nombre de paquete porque HolaMundo se considera englobado dentro del
paquete por defecto.

La función C acepta parámetros, aunque el método nativo definido en la clase Java no acepte
ninguno, dichos parámetros son:

• El primer parámetro es un puntero a la interfaz JNIEnv, a través del cual el código


nativo accede a los parámetros y objetos que se les pasan desde la aplicación Java.
• El segundo parámetro es una referencia a la instancia actual del objeto, se puede pensar
en este parámetro como si fuese la variable this de C++. En nuestro caso, ignoramos el
parámetro this.

3.4 ESCRIBIR LA FUNCION C

Escribir la función C para el método nativo en un archivo fuente de código C. La


implementación será una función habitual C, que luego integraremos con la clase Java. La
definición de la función C debe ser la misma que la que se ha generada en el archivo
HolaMundo.h.

La implementación planteada se almacena en un archivo C. Por convenio, el código nativo se


coloca en un archivo con el mismo nombre de la clase Java, añadiéndole el postfijo “Imp”. En
este caso, el archivo será "HolaMundoImp.c", y contendrá las siguientes líneas de código:

// HolaMundoImp.c
#include <jni.h>
#include "HolaMundo.h"
#include <stdio.h>
15
JNIEXPORT void JNICALL
Java_HolaMundo_presentaSaludo (JNIEnv * env, jobject obj)
{
printf("Hola Mundo, desde C");
return;
}

Como se puede ver, la implementación no puede ser más sencilla: hace una llamada a la función
printf() para presentar el saludo y sale.

En el código se incluyen tres archivos de cabecera:

• jni.h
Proporciona la información para que el código C pueda interactuar con el sistema Java
• HolaMundo.h
Es el archivo de cabecera que hemos generado para nuestra clase. Contiene la estructura C
que representa la clase Java para la que estamos escribiendo el método nativo y la definición
de la función para ese método nativo.
• stdio.h
Es necesario incluirlo porque utilizamos la función printf() de la librería estándar de C, cuya
declaración se encuentra en este archivo de cabecera.

3.5 CREAR LA LIBRERIA DINAMICA

Se utiliza el compilador C para compilar el archivo .h y el archivo fuente .c; para crear una
librería dinámica. Ahora para generarlo utilizar el compilador C del sistema, haciendo que el
archivo "HolaMundoImp.c" genere una librería dinámica de nombre hola, que será la que el
sistema Java cargue cuando ejecute la aplicación que se está construyendo. Ejecutar el siguiente
comando:

> gcc -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o


hola.dll HolaMundoImp.c

Cuando se escriben métodos nativos, siempre habrá que garantizar incluir los archivos "jni.h" y
"jni_md.h", que son proporcionado por JDK y está disponible en "<JAVA_HOME>\include" y
"<JAVA_HOME>\include\win32" (para Windows), respectivamente.

3.6 EJECUTAR EL PROGRAMA

Y, por fin, utilizar el intérprete de Java, java, para ejecutar el programa que construido,
siguiendo todos los pasos anteriormente descritos. Si se teclea el comando:

> java Main

Se obtendrá el resultado siguiente:


Hola Mundo, desde C

Si no aparece este mensaje de saludo y lo que aparece en pantalla son expresiones como
UnsatisfiedLinkError, es porque no se tiene fijado correctamente el camino de la librería
16
dinámica que hemos generado. Este camino es la lista de directorios que el sistema Java utilizará
para buscar las librerías que debe cargar. Debemos asegurarnos de que el directorio donde se
encuentra nuestra librería hola recién creada, figura entre ellos.

Si se fija el camino correcto y se ejecuta de nuevo el programa, se verá que ahora sí aparece el
mensaje de saludo que esperábamos.

También puede aparecer la excepción en el hilo "main": "UnsatisfiedLinkError: :


..I\Proyecto_1_0\hola.dll: Can't load IA 32-bit .dll on a AMD 64-bit platform”, que significa "No
se puede cargar .dll IA de 32 bits en una plataforma AMD de 64 bits".

17
4 El ejemplo básico con una clase
En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_2".

Vamos a realizar un programa Java con una clase que llama a una función de librería C que dice:
“Hola mundo”. El uso de una sola clase no es adecuado, porque no se podría usar en otra
aplicación, debido a que ya es una aplicación.

Para ello vamos a dar los siguientes pasos:

4.1 Declarar el método nativo como miembro de una clase

Los métodos nativos llevan el modificador native y están sin implementar ya que su
implementación estará en una librería nativa, luego hacemos:

// HolaMundo.java
// Clase HolaMundo
public class HolaMundo
{
private native void saluda();

public static void main(String[] args)


{
new HolaMundo().saluda();
}

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

4.2 Crear el archivo de cabecera nativo

Ahora tenemos que generar el archivo .class y un archivo .h con el prototipo de los métodos
nativos a implementar.

Para ello usamos el comando:

> javac –h . HolaMundo.java

Siendo HolaMundo el nombre de la clase compilada a la que queremos generar el archivo .h

El archivo generado tiene la forma:

#include <jni.h>
/* Header for class HolaMundo */

#ifndef _Included_HolaMundo
#define _Included_HolaMundo
#ifdef __cplusplus
extern "C" {
#endif
18
/*
* Class: HolaMundo
* Method: saluda
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HolaMundo_saluda
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

4.3 Implementar el método nativo

La implementación de la función debe tener el mismo prototipo que la cabecera, y podemos


hacerla así:

// HolaMundoImp.c
#include <stdio.h>
#include "HolaMundo.h"

JNIEXPORT void JNICALL Java_HolaMundo_saluda (JNIEnv *env, jobject obj)


{
printf("Hola Mundo\n");
return;
}

Los parámetros JNIEnv y jobject se ponen siempre en los métodos nativos, y ya veremos luego
para que sirven.

4.4 Compilar el archivo nativo

Usando los comandos que explicamos en el apartado 2 para crear la librería de enlace dinámico,
con el comando:

> gcc -I "%JAVA_HOME%\include" -I "%JAVA_HOME%\include\win32" -shared -o


hola.dll HolaMundoImp.c

4.5 Ejecutar el programa

La ejecución del programa se realizará con el comando:

> java HolaMundo

La salida del programa será la siguiente:


Hola Mundo, desde C

Al ejecutar el programa Java se producirá una UnsatisfiedLinkError si la función loadLibrary()


no encuentra la libraría. Recuérdese que es necesario fijar las variables de entorno que indican
donde están las librerías de enlace dinámico como explicamos antes.

19
Alternativamente podemos indicar el path de la librería de enlace dinámico con la propiedad del
sistema:

java -Djava. library.path=. HolaMundo


Hola Mundo

Para que la busque en el directorio actual.

20
5 Tipos de datos fundamentales, arreglos y objetos
En este apartado veremos que correspondencia existe entre los tipos de datos Java y sus
correspondientes tipos de datos C. Después veremos cómo intercambiar datos entre Java y C.

Empezaremos viendo los tipos de datos fundamentales para luego pasar a estudiar tipos más
complejos como los arreglos o los objetos.

5.1 Parámetros de un método nativo

Como ya vimos, un método nativo siempre tiene al menos dos parámetros:

JNIEXPORT void JNICALL Java_HolaMundo_saluda (JNIEnv* env, jobject obj);

1. El primer argumento env apunta a una tabla de punteros a funciones, que son las funciones
que usaremos para acceder a los datos Java de JNI.

2. El segundo argumento jobject varía dependiendo de si es un método de instancia o un


método de clase (estático).

• Si es un método de instancia se trata de un jobject que actúa como un puntero this al


objeto Java.
• Si es un método de clase, se trata de una referencia jclass a un objeto que representa la
clase en la cual están definidos los métodos estáticos.

5.2 Correspondencia de tipos

En Java existen principalmente dos tipos de datos:

1. Tipos fundamentales. Como int, float o double. Su correspondencia con tipos C es directa,
ya que en el archivo jni.h encontramos definiciones de tipos C equivalentes. Por ejemplo
para float encontramos el tipo jfloat.

La siguiente tabla muestra todos los tipos fundamentales Java y sus correspondientes tipos C.

21
Tipo Java Tipo C Descripción
boolean jboolean 8 bits sin signo
byte jbyte 8 bits con signo
char jchar 16 bits sin signo
short jshort 16 bits con signo
int jint 32 bits con signo
long jlong 64 bits con signo
float jfloat 32 bits formato IEEE
double jdouble 64 bits formato IEEE

2. Referencias. Apuntan a objetos, cadenas de caracteres y arreglos.

JNI pasa los objetos a los métodos nativos como referencias opacas, es decir como punteros C
que apuntan a estructuras internas sólo conocidas por la máquina virtual concreta que estemos
usando. Los campos de esta estructura no se dan a conocer al programador, sino que el
programador accede a ellos a través de funciones JNI.

Por ejemplo el tipo JNI correspondiente a java.lang.String es jstring. El valor exacto de los
campos a los que apunta jstring es irrelevante al programador, sino que éste accede al texto del
objeto usando:

const jbyte* GetStringUTFChars(JNIEnv* env, jstring string, jboolean* isCopy);

La función devuelve un arreglo de caracteres UTF8 correspondientes al jstring Java (Unicode 16


bits).

isCopy luego explicaremos para que vale.

Todas las referencias en JNI son del tipo jobject, aunque por mejorar el control de tipos se han
creado tipos derivados como jstring o jobjectArreglo.

jobject
jclass
jstring
jarreglo
jobjectArreglo
jbooleanArreglo
jbyteArreglo
jcharArreglo
jshortArreglo
jintArreglo
jlongArreglo
jfloatArreglo
jdoubleArreglo
jthrowable

5.3 Acceso a tipos de datos fundamentales

La conversión entre variables Java y tipos de datos fundamentales es como ya dijimos de forma
inmediata.
22
Por ejemplo si queremos hacer un método nativo que sume dos números.

En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_3",
luego se crea la clase Parametros en el archivo Parametros.java:

// Parametros.java
// clase Parametros
public class Parametros
{
private native int suma(int a, int b);

public static void main(String[] args)


{
Parametros p = new Parametros();
System.out.println("3 + 4 = " + p.suma(3, 4));
}

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

El tipo int de Java corresponde con el tipo jint de C, luego tendremos que hacernos un archivo
Parametros.c así:

// Parametros.c
#include <jni.h>
#include "Parametros.h"

JNIEXPORT jint JNICALL Java_Parametros_suma (JNIEnv* env, jobject obj,


jint u , jint v)
{
return u + v;
}

El prototipo exacto de la función nativa suma() lo encontramos en el archivo Parametros.h tras


ejecutar javah.

Luego de ejecutar la clase Parametros,class, la salida del programa será la siguiente:

3+4=7

5.4 Acceso a String

String, es una clase, que estará representado por el tipo C jstring. Para acceder al contenido de
este objeto existen funciones que convierten un String Java en cadenas C. Estás funciones nos
permiten convertir tanto a Unicode como a UTF-8.

5.4.1 Obtener el texto de un String

Para obtener el texto UTF-8 correspondiente a un String Java tiene la función


GetStringUTFChars().

23
• La función GetStringUTFChars() devuelve un puntero a un arreglo de caracteres UTF-8
del string enviado desde java.

const jbyte* GetStringUTFChars(JNIEnv* env, jstring string, jboolean* isCopy);

isCopy puede ser NULL si no nos devolverá si ha hecho una copia en otro arreglo o ese
arreglo en el buffer original. isCopy valdrá JNI_TRUE o JNI_FALSE.

Luego para llamar a esta función haríamos:

const jbyte* str = (*env)->GetStringUTFChars(env, text, NULL);


if (str == NULL)
return NULL; // OutOfMemoryError fue lanzada

text es un jstring que hemos recibido como parámetro desde Java.

El lanzamiento de excepciones en JNI es distinto al lanzamiento de excepciones en Java.


Cuando se lanza una excepción en JNI, no se retrocede por la pila de llamadas hasta
encontrar el catch, Sino que la excepción queda pendiente, y cuando salimos del método
nativo es cuando se lanza la excepción.

• El buffer obtenido por la llamada anterior no se libera hasta que lo liberamos usando:

void ReleaseStringUTFChars(JNIEnv* env, jstring string, const char* utfbuffer);

• Podemos conocer la longitud de una cadena UTF-8 usando:

jsize GetStringUTFLength(JNIEnv* env, jstring string);

Además, de las funciones GetStringUTFChars() y ReleaseStringUTFChars() tenemos las


funciones:

• const jchar* GetStringChars(JNIEnv* env, jstring string, jboolean* isCopy);


• void RealeaseStringChars(JNIEnv* env, jstring string, const jchar* charsbuffer);

Que nos sirven para obtener un buffer con la representación Unicode del texto del jstring.

• Las cadenas UTF-8 siempre acaban en '\0', pero no las cadenas Unicode, con lo que
tendremos que usar la siguiente función para saber la longitud de una cadena Unicode:

jsize GetStringLength(JNIEnv* env, jstring string);

Obsérvese que tanto GetStringUTFChars() como GetStringChars() tienen un argumento isCopy,


que indica si el buffer devuelto es una copia del buffer donde está el string de la máquina virtual,
o es el mismo String, en cuyo caso nunca deberíamos modificar este buffer o modificaríamos
realmente el String java, lo cual violaría el principio de inmutabilidad de los String Java.

En general no es posible predecir si la máquina virtual hará copia del buffer o no. En principio si
la máquina virtual utiliza un formato para almacenar los strings, por ejemplo. Unicode, entonces
sabemos que GetStringUTFChars() siempre devolverá una copia, y GetStringChars() devolverá
24
una copia o no. El devolver una copia implica hacer una copia del buffer, pero a cambio la
máquina virtual podrá ejecutar la recogida de basura. Si devuelve el buffer original no podrá
realizar recogida de basura del objeto String de Java aunque en Java ya nadie lo apunte, porque
lo estamos apuntando desde C.

Nota: Como normalmente un String le cogeremos, procesaremos y liberaremos rápidamente, es


mejor que no se haga una copia de éste.

En el JSDK 1.2 surgieron nuevas funciones que, no nos garantizan, pero si aumentan la
probabilidad de obtener directamente un puntero al arreglo de caracteres de la máquina virtual,
estas funciones son:

• const jchar* GetStringCritical(JNIEnv* env, jstring string, jboolean* isCopy);


• void ReleaseStringCritical(JNIEnv* env, jstring string, jchar* char arreglo);

A cambio debemos de tratar el código entre las llamadas a GetStringCritical() y


ReleaseStringCritical() como una sección crítica, respecto a que no debemos de hacer nada que
bloquee al hilo o llamar a otra función de JNI, sólo podemos hacer cosas como copiar el
contenido del buffer a otro buffer.

No existen las correspondientes GetStringUTFCritical() y ReleaseStringUTFCritical() ya que las


máquinas virtuales suelen representar internamente los string en Unicode, y seguramente tendrán
que hacer una copia para obtener su representación UTF.

5.4.2 Crear un nuevo String

También podemos crear nuevas instancias de un java.lang.String desde un método nativo usando
las funciones JNI:

• jstring NewStringUTF(JNIEnv* env, const char* bytes);

Esta función recibe un arreglo de caracteres UTF y crea un objeto jstring

• jstring NewString(JNIEnv* env, const jchar* ubuffer, jsize length);

Esta función recibe un arreglo de caracteres Unicode y crea un jstring

Ambas funciones producen una OutOfMemoryError si fallan en cuyo caso devuelven NULL,
con lo que siempre hay que comprobar el retorno de la función.

5.4.3 Ejemplo

Como ejemplo proponemos hacer un método nativo que recibe como parámetro un String con un
mensaje de prompt a dar a un usuario por consola y recoge de consola el texto escrito por el
usuario que nos lo devuelve como un String.

En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_4", luego
creamos la clase Parametros.

// Parametros.java
25
// Clase Parametros
public class Parametros {

private native String pideTexto(String prompt);

public static void main( String args[] ) {

Parametros p = new Parametros();

String texto = p.pideTexto("Escriba un texto: ");


System.out.println("El texto devuelto es: " + texto);
}

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

La implementación del método nativo en el archivo Parametros.c sería:

// Parametros.c
#include <jni.h>
#include <string.h>
#include "Parametros.h"

JNIEXPORT jstring JNICALL


Java_Parametros_pideTexto (JNIEnv *env, jobject obj, jstring prompt)
{
char buffer[128];

// Obtenemos el texto pasado como parámetro


const jbyte* prompt_c = (*env)->GetStringUTFChars( env, prompt, NULL );

puts(prompt_c);
(*env)->ReleaseStringUTFChars( env, prompt, prompt_c );

// Pedimos un texto por consola


// Suponemos que no escribirá el usuario más
// de 127 caracteres
gets(buffer);

return (*env)->NewStringUTF( env, buffer );


}

La corrida de la aplicación es:

Escriba un texto:
Hola <ENTER>
El texto devuelto es: hola

Aquí es importante que la extensión del archivo Parametros.c sea .c y no .cpp, ya que el puntero
env está declarado de forma distinta en C que en C++.

5.5 Acceso a arreglos

JNI considera dos tipos de arreglos:

26
o Arreglos de tipos de datos fundamentales. Son arreglos cuyos elementos son tipos de
datos fundamentales como boolean o int
o Arreglos de referencias. Son arreglos cuyos elementos son objetos o bien otros arreglos
(arreglos de arreglos)

Por ejemplo:

int[] iarr; // Arreglo de tipos fundamentales


float[] farr; // Arreglo de tipos fundamentales
Object[] oarr; // Arreglo de referencias
int[][] iarr2; // Arreglo de referencias

5.5.1 Acceso a arreglos de tipos de datos fundamentales

Para acceder a arreglos de tipos fundamentales existen funciones JNI que nos devuelven el
arreglo en una variable de tipo jarray o derivada.

Existen tipos derivados para los arreglos de tipos de elementos más comunes.

jarray
jobjectArray
jbooleanArray
jbyteArray
jcharArray
jshortArray
jintArray
jlongArray
jfloatArray
jdoubleArray

• Una vez que tengamos una variable nativa de tipo jarray o derivado podemos obtener la
longitud del arreglo con:

jsize GetArrayLength(JNIEnv* env, jarray arreglo);

• Después podemos acceder a los datos del arreglo usando funciones JNI. Para cada tipo
fundamental existe su correspondiente función JNI.

jboolean* GetBooleanArrayElements (JNIEnv* env, jbooleanArray arreglo, jboolean* isCopy);


jbyte* GetByteArrayElements(JNIEnv* env, jbyteArray arreglo, jbyte* isCopy);
jchar* GetCharArrayElements(JNIEnv* env, jcharArray arreglo, jchar* isCopy);
jshort* GetShortArrayElements (JNIEnv* env, jshortArray arreglo, jshort* isCopy);
jint* GetlntArrayElements(JNIEnv* env, jintArray arreglo, jint* isCopy);
jlong* GetLongArrayElements(JNIEnv* env, jlongArray arreglo, jlong* isCopy);
jfloat* GetFloatArrayElements (JNIEnv* env, jfloatArray arreglo, jfloat* isCopy);
jdouble* GetDoubleArrayElements (JNIEnv* env, jdoubleArray arreglo, jdouble* iscopy);

isCopy nos dice si el puntero devuelto es al arreglo original o si se ha hecho una copia de
éste.

• Una vez que se nos devuelve un puntero, éste es válido hasta que lo liberamos con su
correspondiente función:
27
void ReleaseTipoArrayElements(JNIEnv* env, jbooleanArray arreglo, jboolean* buffer, jint mode);

De la cual también hay una para cada tipo de dato fundamental.

mode puede tomar uno de estos valores:

Modo Acción
0 Copia el contenido de buffer en el arreglo Java y libera buffer
JNI_COMMIT Copia el contenido de buffer en el arreglo Java pero no libera buffer
JNI_ABORT Libera buffer sin copiar su contenido en el arreglo Java

En JDK 1.2 se añadieron las funciones JNI:

• void* GetPrimitiveArrayCritical(JNIEnv* env, jarray arreglo, jboolean* isCopy);


• void ReleasePrimitiveArrayCritical (JNIEnv* env, jarray arreglo, void* carreglo, jint
mode);

Que al igual que pasaba con String las funciones procuran enviar el arreglo original si es
posible, con lo que el código nativo no debe bloquearse o llamar a otras funciones JNI entre
las llamadas a GetPrimitiveArrayCritical() y ReleasePrimitiveArrayCritical()

Por último también existen funciones para crear arreglos Java desde el código nativo:

• jbooleanArray NewBooleanArray(JNIEnv* env, jsize length);


• jbyteArray NewByteArray(JNIEnv* env, jsize length);
• jcharArray NewCharArray(JNIEnv* env, jsize length);
• jshortArray NewShortArray(JNIEnv* env, jsize length);
• jintArray NewlntArray(JNIEnv* env, jsize length);
• jlongArray NewLongArray(JNIEnv* env, jsize length);
• jfloatArray NewFloatArray(JNIEnv* env, jsize length);
• jdoubleArray NewDoubleArray(JNIEnv* env, jsize length);

Ejemplo

Como ejemplo podríamos hacer un método que recibe dos arreglos unidimensionales y devuelve
un nuevo arreglo con la suma de estos.

En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_5", luego
creamos la clase Parametros.

En la clase Java metemos el método nativo:

// Parametros.java
// Clase Parametros
public class Parametros {
private native int suma(int a, int b);
private native String pideTexto(String prompt);
private native int[] sumaArreglos(int[]A, int[]B);

public static void main( String args[] ) {

28
Parametros p = new Parametros();

System.out.println( "3 + 4= " + p.suma(3,4) );

String texto = p.pideTexto("Escriba un texto: ");


System.out.println("El texto devuelto es: " + texto);

int[] A = {1,2,3};
int[] B = {3,2,1};
int[] C = p.sumaArreglos(A, B);
if ( C == null )
System.out.println("No se puede hacer la suma");
else {
System.out.println("La suma de arreglos es: ");
for ( int i = 0; i < C.length; i++ )
System.out.print(C[i] + " ");
System.out.println();
}
}

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

Y la función C a implementar sería:


// Parametros.c
#include <jni.h>
#include "Parametros.h"

JNIEXPORT jint JNICALL


Java_Parametros_suma (JNIEnv *env, jobject obj, jint u, jint v)
{
return u + v;
}

JNIEXPORT jstring JNICALL


Java_Parametros_pideTexto (JNIEnv *env, jobject obj, jstring prompt)
{
char buffer[128];

// Obtenemos el texto pasado como parámetro


const jbyte* prompt_c = (*env)->GetStringUTFChars( env, prompt, NULL );

printf( "%s", prompt_c );


(*env)->ReleaseStringUTFChars( env, prompt, prompt_c );

// Pedimos un texto por consola


// Suponemos que no escribirá el usuario más
// de 127 caracteres
scanf( "%s", buffer );

return (*env)->NewStringUTF( env, buffer );


}

JNIEXPORT jintArray JNICALL


Java_Parametros_sumaArreglos (JNIEnv *env, jobject obj, jintArray arregloA,
jintArray arregloB)
29
{
jintArray arregloC;

jint* A;
jint* B;
jint* C;
jsize i;

// Medimos el arreglo
jsize longitud = (*env)->GetArrayLength( env, arregloA );
if ( longitud != (*env)->GetArrayLength( env, arregloB ) )
return NULL; // No coinciden las longitudes
// Creamos un nuevo arreglo con la solución
arregloC = (*env)->NewIntArray( env, longitud );

A = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloA, NULL );


B = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloB, NULL );
C = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloC, NULL );

for ( i =0; i < longitud; i++ )


C[i] = A[i] + B[i];

(*env)->ReleasePrimitiveArrayCritical( env, arregloA, A, JNI_ABORT );


(*env)->ReleasePrimitiveArrayCritical( env, arregloB, B, JNI_ABORT );
(*env)->ReleasePrimitiveArrayCritical( env, arregloC, C, 0 );

return arregloC;
}

La corrida de la aplicación es:

3+4=7
Escriba un texto: Hola <ENTER>
El texto devuelto es: Hola
La suma de arreglos es:
444

5.5.2 Acceso a arreglos de referencias

JNI tiene dos funciones para el acceso a los arreglos de referencias:

• jobject GetObjectArrayElement (JNIEnv* env, jobjectArray arreglo, jsize index);


• void SetObjectArrayElement(JNIEnv* env, jobjectArray arreglo, jsize index, jobject
value);

A diferencia de los arreglos de tipos de datos fundamentales, no podemos acceder a todos los
elementos a la vez, sino que tenemos que procesar elemento a elemento con estas funciones.

Para crear un arreglo de referencias tenemos la función:

• jarray NewObjectArray(JNIEnv* env, jsize length, jclass elementType, jobject initialElement);

initialElement es el valor inicial al que se fijan todos los elementos del arreglo, y puede ser
NULL.

30
elementType indica el tipo de los elementos del arreglo, y no puede ser NULL. Para obtener este
tipo se suele usar la función:

• jclass FindClass(JNIEnv* env, const char* name);

Nota: De momento basta decir que para el ejemplo que vamos a hacer en name, pasaremos "[I"
que significa que queremos un arreglo de arreglos de enteros. Al final se explicará lo indicado.

Ejemplo

Como ejemplo vamos hacer un método sumaMatrices(), que recibe dos matrices bidimensionales
y devuelve otra matriz con la suma de estas.

En primer lugar creamos un directorio para la aplicación, en este caso se llama " Proyecto_1_6", luego
creamos la clase Parametros.

// Parametros.java
// Clase Parametros
public class Parametros {
private native int suma(int a, int b);
private native String pideTexto(String prompt);
private native int[] sumaArreglos(int[] A, int[] B);
private native int[][] sumaMatrices(int[][] A, int[][] B);

public static void main( String args[] ) {

Parametros p = new Parametros();

System.out.println( "3 + 4 = " + p.suma(3,4) );

String texto = p.pideTexto("Escriba un texto: ");


System.out.println("El texto devuelto es: " + texto);

int[] A = {1,2,3};
int[] B = {3,2,1};
int[] C = p.sumaArreglos(A, B);
if ( C == null )
System.out.println("No se puede hacer la suma");
else {
System.out.println("La suma de arreglos es: ");
for ( int i = 0; i < C.length; i++ )
System.out.print(C[i] + " ");
System.out.println();
}

int [][] M1 = { {2,3},{4,6},{2,4} };


int [][] M2 = { {4,3},{0,2},{1,5} };
int [][] M3 = p.sumaMatrices(M1, M2);

if ( M3 == null )
System.out.println("Falló la suma");
else {
System.out.println("La suma de la matriz es:");
for ( int i = 0; i < M3.length; i++ ) {
for ( int j = 0; j < M3[i].length; j++ )
System.out.print(M3[i][j] + " ");
System.out.println();
31
}
}

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

La función nativa se implementaría así:

// Parametros.c
#include <jni.h>
#include "Parametros.h"

JNIEXPORT jint JNICALL


Java_Parametros_suma (JNIEnv *env, jobject obj, jint u, jint v)
{
return u + v;
}

JNIEXPORT jstring JNICALL


Java_Parametros_pideTexto (JNIEnv *env, jobject obj, jstring prompt)
{
char buffer[128];

// Obtenemos el texto pasado como parámetro


const jbyte* prompt_c = (*env)->GetStringUTFChars( env, prompt, NULL );

printf( "%s", prompt_c );


(*env)->ReleaseStringUTFChars( env, prompt, prompt_c );

// Pedimos un texto por consola


// Suponemos que no escribirá el usuario más
// de 127 caracteres
scanf( "%s", buffer );

return (*env)->NewStringUTF( env, buffer );


}

JNIEXPORT jintArray JNICALL


Java_Parametros_sumaArreglos (JNIEnv *env, jobject obj, jintArray arregloA,
jintArray arregloB)
{
jintArray arregloC;

jint* A;
jint* B;
jint* C;
jsize i;

// Medimos el arreglo
jsize longitud = (*env)->GetArrayLength( env, arregloA );
if ( longitud != (*env)->GetArrayLength( env, arregloB ) )
return NULL; // No coinciden las longitudes
// Creamos un nuevo arreglo con la solución
arregloC = (*env)->NewIntArray( env, longitud );

32
A = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloA, NULL );
B = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloB, NULL );
C = (jint*)(*env)->GetPrimitiveArrayCritical( env, arregloC, NULL );

for ( i=0; i<longitud; i++ )


C[i] = A[i] + B[i];

(*env)->ReleasePrimitiveArrayCritical( env, arregloA, A, JNI_ABORT );


(*env)->ReleasePrimitiveArrayCritical( env, arregloB, B, JNI_ABORT );
(*env)->ReleasePrimitiveArrayCritical( env, arregloC, C, 0 );

return arregloC;
}

JNIEXPORT jobjectArray JNICALL


Java_Parametros_sumaMatrices (JNIEnv *env, jobject obj,
jobjectArray arregloA, jobjectArray arregloB)
{
jobjectArray arregloC;
jsize i;
jclass tipo_arreglo;
jsize n_vectores = (*env)->GetArrayLength( env, arregloA );

if ( n_vectores != (*env)->GetArrayLength( env, arregloB ) )


return NULL; // No puede sumar los arreglos por ser distintos

//Falta determinar si los dos arreglos tienen el mismo numero de columnas

tipo_arreglo = (*env)->FindClass( env, "[I" );


if ( tipo_arreglo == NULL )
return NULL; // Se produjo una excepción

arregloC = (*env)->NewObjectArray( env, n_vectores, tipo_arreglo, NULL );

if ( arregloC == NULL )
return NULL; // Excepción OutOfMemoryError lanzada

for ( i=0; i<n_vectores; i++ ) {


jsize j;
jint * bufferA;
jint * bufferB;
jint * bufferC;

jintArray vectorA = (jintArray)(*env)->GetObjectArrayElement( env,


arregloA, i );
jintArray vectorB = (jintArray)(*env)->GetObjectArrayElement( env,
arregloB, i );

jsize longitud_vector = (*env)->GetArrayLength( env, vectorA );


jintArray vectorC = (*env)->NewIntArray( env, longitud_vector );

bufferA = (*env)->GetIntArrayElements( env, vectorA, NULL );


bufferB = (*env)->GetIntArrayElements( env, vectorB, NULL );
bufferC = (*env)->GetIntArrayElements( env, vectorC, NULL );

for ( j=0; j<longitud_vector; j++ )


bufferC[j] = bufferA[j] + bufferB[j];

(*env)->ReleaseIntArrayElements( env, vectorA, bufferA, JNI_ABORT );


(*env)->ReleaseIntArrayElements( env, vectorB, bufferB, JNI_ABORT );
33
(*env)->ReleaseIntArrayElements( env, vectorC, bufferC, 0 );
(*env)->SetObjectArrayElement( env, arregloC, i, vectorC );
(*env)->DeleteLocalRef( env, vectorA );
(*env)->DeleteLocalRef( env, vectorB );
(*env)->DeleteLocalRef( env, vectorC );
}

return arregloC;

La corrida de la aplicación es:

3+4=7
Escriba un texto: Hola
El texto devuelto es: Hola
La suma de arreglos es:
444
La suma de la matriz es:
66
48
39

La llamada a DeleteLocalRef() lo que hace es evitar que la máquina virtual pueda quedarse sin
memoria.

34
6 Acceso a objetos

En este apartado aprenderemos a acceder a los atributos de un objeto Java, así como a llamar a
sus métodos (no se va a ver en este curso).

6.1 La signatura de tipo

Para describir los tipos de los atributos y de los métodos de un objeto, Java utiliza las signaturas
de tipo, también llamadas descriptores de miembro (member descriptor).

Una signatura de tipo es una cadena C que describe el tipo de un miembro (atributos o métodos)
de la clase.

P.e. “Ljava/lang/String;” indica que un atributo es del tipo java.lang.String

Los tipos fundamentales se representan con letras:

Signatura de tipo Tipo Java


V void
Z boolean
B byte
C char
S short
I int
J long
F flota
D double

Por ejemplo para representar un int usamos “I” y para representar un float “F”.

Para representar un arreglo se pone un [ delante del tipo. P.e. para representar un double [] se usa
“[D” y parar epresentar un int [] [] se usa “[ [I”.

Por último, para representar una clase se precede por una L, se pone el nombre completo de la
clase y se acaba en ;. P.e. para representar la clase java.util.Date se usaría “Ljava/util/Date;“.
Obsérvese que se sustituyen los . por /

Los métodos también tienen una signatura de tipo en cuyo caso los parámetros del método se
ponen primero entre paréntesis y el retorno del método después. P.e. para un método como:

public int getWidth()

Tenemos la signatura de tipo: “()I“. Aquí hay que tener cuidado de no poner (V) I” que sería
incorrecto. Sin embargo si tenemos el método:

public void drawLine(int width)

su signatura de tipo sería ”(I) V”, es decir para indicar el retomo si se pone la V

35
O para el método:

public java.lang.String formatDate(java.util.Date)

Tenemos la signatura de tipo:”(Ljava/util/Date;)Ljava.lang.String;”

Si el método recibe varios parámetros se ponen todos seguidos sin usar ningún símbolo como
separador.

Por ejemplo para:

public int suma(int a, mt b)

La signatura de tipo del método sería: “(II) I”

Podemos sacar las signaturas de tipo de los miembros de una clase usando el comando javap:

% javap -s -p Parametros
Compiled from Parametros.java
public class Parametros extends java.lang.Objeot {
publio Parametros ();
/* ()V */
private native mt suma(int, intt);
/* (II)I */
private native java.lang.String
pideTexto (java.lang.String);
/* (Ljava/lang/String;)Ljava/lang/String; */
private native int sumaArreglos(int[], int[]) [];
/* ([I[I]) [I */
private native int sumaMatrioes(int[] [], int[ ] [ ]) [ ] [ ];
/* ([[I[[I) [[I */
publio statio void main(java.lang.String[]);
/* ([Ljava/lang/String;)V */
static {};
/* ()V */

El parámetros -s es para que saque las signaturas de tipo C y no la signatura de tipo Java, y -p es
para que también saque los miembros privados.

Una vez tengamos la signatura de tipo, podemos pasársela a funciones como:

jclass FindClass(JNIEnv* env, const ohar* name);

La cual, como vimos en el ejemplo anterior nos devuelve una variable que representa a la clase,
y que luego esta variable nos la piden otras funciones de JNI.

Por ejemplo cuando hacíamos:

Tipo_arreglo = (*env)->Findclass(env, “[I”);

en el ejemplo anterior.

36

También podría gustarte