Al mencionar JNI, muchos programadores experimentan inconscientemente algún temor inexplicable. JNI parece sospechosamente difícil, y a primera vista su mecanismo parece mágico. Sin embargo, los que lo analizan más de cerca aprecian mucho sus propiedades.
Si no ha oído hablar de esta tecnología, Java Native Interface o JNI es un mecanismo estándar de Java que permite que el código Java interactúe con el código C y C++. La Wikipedia dice que “JNI permite a los programadores escribir métodos nativos para manejar situaciones en las que una aplicación no puede escribirse completamente en el lenguaje de programación Java, por ejemplo, cuando la biblioteca de clases estándar de Java no es compatible con las características específicas de la plataforma o la biblioteca del programa”, lo que significa que en una aplicación Android podemos utilizar una biblioteca C++ que necesitemos e interactuar con ella directamente desde el código Java y viceversa.
Suena muy bien, ¿eh?
Pues aquí tiene tres razones por las que nos encanta JNI:
- JNI hace posibles algunos procesos que no están implementados en Java. Como los comandos sensibles al hardware o los comandos directos de la API del SO, por ejemplo. Para los desarrolladores de Android, abre muchas oportunidades fuera de Dalvik. El código C/C++ compilado funcionará en cualquier dispositivo Java porque JNI es independiente de Dalvik, ya que fue diseñado para JVM.
- Posibilidad de aumentar el rendimiento de la aplicación con la ayuda de bibliotecas de bajo nivel para cosas como gráficos, cálculos, diferentes tipos de renderizado, etc.
- Ya se ha escrito un gran número de bibliotecas para todas las diferentes tareas. Y poder reutilizar el código sin reescribirlo en otro lenguaje facilita mucho la vida del desarrollador. De este modo, bibliotecas tan populares como FFmpeg y OpenCV están disponibles para los desarrolladores de Android.
Pero veamos más de cerca esta tecnología. Como dijo Linus Torvalds “Hablar es barato. Muéstrame el código”
El esquema de interacción tiene este aspecto:
Para llamar a un método C++ desde Java, es necesario:
- Cree un método en una clase Java
123private native void NombreDeLaFunción( parámetros ); - Cree una cabecera y un archivo cpp en una carpeta jni. Este contendrá el código C++ llamado por la función nativa mencionada anteriormente.
- En la cabecera definimos su firma así
12345extern "C" {JNIEXPORT void JNICALL Java_my_package_NativeCallsClass_myFunction(JNIEnv *, jclass);} - extern “C” es necesario para evitar que el compilador de C++ cambie los nombres de las funciones declaradas.
- JNIEXPORT es un modificador necesario para JNI.
- Los tipos de datos con el prefijo “j “: jdouble, jobject, jstring, etc – reflejan los objetos y tipos de Java en C/C++.
- JNIEnv* es una interfaz para Java. Permite llamar a métodos Java, crear objetos Java y hacer otras cosas más útiles de Java.
- El segundo parámetro crucial es jobject o jclass, respecto a si el método es estático. Si lo es, el argumento será jclass (un enlace a una clase en la que se declara el código) y si es estático, será jobject (un enlace a un objeto en el que se llamó al método).
En realidad, no tiene que escribir el código manualmente. Puede utilizar una utilidad de javah, pero me pareció más fácil y claro hacerlo por mí mismo. La función en sí se realiza en un archivo .cpp. - Vuelva a Java donde definimos la función nativa y añada
123System.loadLibrary( "Library name by which we compile cpp files mentioned above" );
en el mismo comienzo del archivo, por encima de la declaración del método nativo. El nombre de la biblioteca se mantiene en Android.mk.
1234LOCAL_MODULE:= Name# If we assemble with .mk files - También me gustaría prestar su atención a los archivos como Android.mk y Application.mk. En Android.mk guardamos los nombres de todos los archivos .cpp de la carpeta jni que vamos a compilar, cualquier bandera específica y también las rutas a las cabeceras y a las librerías adicionales, en otras palabras, algunos parámetros y ajustes de enlace y otras cosas necesarias para ensamblar una librería.
En Android.mk guardamos parámetros de ensamblaje adicionales como la versión de plataforma requerida, el tipo de arquitectura, etc.
Déjeme resumirlo todo. Llamar a C++ desde Java:
- Creamos una función con un modificador nativo y la llamamos desde cualquier método Java
- El compilador de Java genera el bytecode
- El compilador C/C++ crea una biblioteca dinámica .so
- Cuando ejecutamos la aplicación, el dispositivo Java comienza a procesar el bytecode
- Cuando se encuentra con la llamada de loadLibrary, añade un archivo .so al proceso
- Cuando se encuentra con la llamada del método nativo, busca un método en los archivos .so abiertos por su firma
- Si el método está presente, será llamado. Si no, la aplicación se bloquea
Pero, ¿y si necesitamos hacer lo contrario? (llamar a un método Java desde código C/C++)
A veces necesitamos llamar a un método desde un nativo de Java. Como cuando hay una operación de larga duración en el nativo y tenemos que seguir su progreso. Esto se llama “callback”.
La lógica de una devolución de llamada:
El código Java llama a un método C++ ->
Cuando se procesa, el método llama a su SDK y envía la información que necesita. Para enviar esta información a la aplicación, tiene que hacer lo siguiente:
- En la carpeta jni, crea una clase AndroidGUIHandler que extiende IGUIHandler. Sus métodos reciben parámetros del SDK (wstring y otros), los convierten en un formato compatible con Java y llaman a un método Java enviando esos parámetros.
- (Los métodos de la clase AndroidGUIHandler no tendrán ninguna firma y se parecerán a los métodos C++).
- Antes de llamar a métodos Java, primero necesita conectarse a un hilo Java con la ayuda de una clase envolvente. Luego, necesita utilizar otra clase envolvente para las devoluciones de llamada. En esa clase, utilizando los métodos jni GetObjectClass, GetMethodID, usted busca los métodos Java que necesita llamar (se utiliza el mecanismo de reflexión durante la búsqueda de clases y métodos). Y luego llama a los métodos jni estándar como CallIntMethod, CallVoidMethod para llamar a los métodos encontrados previamente desde Java y enviarles toda la información necesaria desde el SDK.
Y esa es la base de la tecnología dada. Yo diría que si no le gusta JNI, probablemente no lo conoce lo suficiente. Pero, como siempre, tiene algunos fallos:
- JNI no atrapa excepciones como NullPointerException o IllegalArgumentException debido al menor rendimiento y porque la mayoría de las funciones de las bibliotecas C no pueden manejar ese tipo de problemas
- PERO: JNI permite utilizar la excepción de Java. Así que casi podemos anular este defecto procesando el código JNI manualmente y comprobando los códigos de error y lanzando luego las excepciones en Java
- La dificultad de trabajar con JNI desde hilos nativos. Para simplificar la interacción, hay que escribir una clase envolvente que haga todas las manipulaciones necesarias
- Aumento del tamaño de un archivo apk
- transición “costosa” del código Java al nativo y viceversa
- La depuración de un código C++ es un problema en sí mismo
- En algunos casos, trabajar con JNI podría ser MUCHO más lento que con un análogo de Java
- Pero la principal desventaja de JNI es que cualquier código nativo ata firmemente su aplicación Java a una plataforma concreta. Y su uso arruina la concepción de Write Once o Run Anywhere. Y eso es con lo que tendrá que lidiar
Y, a pesar de todos sus defectos (nadie es perfecto), esta tecnología es querida y apreciada.