1. Introducción
Como sabemos, una de las principales fortalezas de Java es su portabilidad, lo que significa que una vez que escribimos y compilamos el código, el resultado de este proceso es un bytecode independiente de la plataforma.
En pocas palabras, esto se puede ejecutar en cualquier máquina o dispositivo capaz de ejecutar una máquina virtual Java, y funcionará tan perfectamente como podríamos esperar.
Sin embargo, a veces necesitamos usar código compilado de forma nativa para una arquitectura específica .
Puede haber algunas razones para necesitar usar código nativo:
- La necesidad de manejar algo de hardware.
- Mejora del rendimiento para un proceso muy exigente
- Una biblioteca existente que queremos reutilizar en lugar de reescribirla en Java.
Para lograr esto, el JDK introduce un puente entre el código de bytes que se ejecuta en nuestra JVM y el código nativo (generalmente escrito en C o C ++).
La herramienta se llama Java Native Interface. En este artículo veremos cómo es escribir un código con él.
2. Cómo funciona
2.1. Métodos nativos: la JVM cumple con el código compilado
Java proporciona la palabra clave nativa que se utiliza para indicar que la implementación del método la proporcionará un código nativo.
Normalmente, al hacer un programa ejecutable nativo, podemos optar por usar bibliotecas estáticas o compartidas:
- Bibliotecas estáticas: todos los archivos binarios de la biblioteca se incluirán como parte de nuestro ejecutable durante el proceso de vinculación. Por lo tanto, ya no necesitaremos las bibliotecas, pero aumentará el tamaño de nuestro archivo ejecutable.
- Libs compartidas: el ejecutable final solo tiene referencias a las librerías, no al código en sí. Requiere que el entorno en el que ejecutamos nuestro ejecutable tenga acceso a todos los archivos de las librerías que utiliza nuestro programa.
Esto último es lo que tiene sentido para JNI, ya que no podemos mezclar código de bytes y código compilado de forma nativa en el mismo archivo binario.
Por lo tanto, nuestra biblioteca compartida mantendrá el código nativo por separado dentro de su archivo .so / .dll / .dylib (dependiendo del sistema operativo que estemos usando) en lugar de ser parte de nuestras clases.
La palabra clave nativa transforma nuestro método en una especie de método abstracto:
private native void aNativeMethod();
Con la principal diferencia de que en lugar de ser implementado por otra clase Java, se implementará en una biblioteca compartida nativa separada .
Se construirá una tabla con punteros en memoria a la implementación de todos nuestros métodos nativos para que puedan ser llamados desde nuestro código Java.
2.2. Componentes necesarios
Aquí hay una breve descripción de los componentes clave que debemos tener en cuenta. Los explicaremos más adelante en este artículo.
- Código Java: nuestras clases. Incluirán al menos un método nativo .
- Código nativo: la lógica real de nuestros métodos nativos, generalmente codificados en C o C ++.
- Archivo de encabezado JNI: este archivo de encabezado para C / C ++ ( incluye / jni.h en el directorio JDK) incluye todas las definiciones de elementos JNI que podemos usar en nuestros programas nativos.
- Compilador C / C ++: podemos elegir entre GCC, Clang, Visual Studio o cualquier otro que queramos en la medida en que sea capaz de generar una biblioteca compartida nativa para nuestra plataforma.
2.3. Elementos JNI en código (Java y C / C ++)
Elementos de Java:
- Palabra clave "nativa": como ya hemos cubierto, cualquier método marcado como nativo debe implementarse en una biblioteca compartida nativa.
- System.loadLibrary (String libname) : un método estático que carga una biblioteca compartida desde el sistema de archivos en la memoria y hace que sus funciones exportadas estén disponibles para nuestro código Java.
Elementos C / C ++ (muchos de ellos definidos dentro de jni.h )
- JNIEXPORT: marca la función en la biblioteca compartida como exportable, por lo que se incluirá en la tabla de funciones y, por lo tanto, JNI puede encontrarla.
- JNICALL: combinado con JNIEXPORT , garantiza que nuestros métodos estén disponibles para el marco JNI
- JNIEnv: una estructura que contiene métodos que podemos usar nuestro código nativo para acceder a elementos de Java
- JavaVM: una estructura que nos permite manipular una JVM en ejecución (o incluso iniciar una nueva) añadiéndole hilos, destruyéndola, etc.
3. Hola mundo JNI
A continuación, veamos cómo funciona JNI en la práctica.
En este tutorial, usaremos C ++ como idioma nativo y G ++ como compilador y enlazador.
Podemos usar cualquier otro compilador de nuestra preferencia, pero aquí se explica cómo instalar G ++ en Ubuntu, Windows y MacOS:
- Ubuntu Linux: ejecute el comando "sudo apt-get install build-essential" en una terminal
- Windows: instalar MinGW
- MacOS: ejecute el comando “g ++” en una terminal y, si aún no está presente, lo instalará.
3.1. Creando la clase Java
Comencemos a crear nuestro primer programa JNI implementando un clásico "Hola mundo".
Para empezar, creamos la siguiente clase Java que incluye el método nativo que realizará el trabajo:
package com.baeldung.jni; public class HelloWorldJNI { static { System.loadLibrary("native"); } public static void main(String[] args) { new HelloWorldJNI().sayHello(); } // Declare a native method sayHello() that receives no arguments and returns void private native void sayHello(); }
Como podemos ver, cargamos la biblioteca compartida en un bloque estático . Esto asegura que estará listo cuando lo necesitemos y desde donde lo necesitemos.
Alternativamente, en este programa trivial, podríamos cargar la biblioteca justo antes de llamar a nuestro método nativo porque no estamos usando la biblioteca nativa en ningún otro lugar.
3.2. Implementar un método en C ++
Ahora, necesitamos crear la implementación de nuestro método nativo en C ++.
Dentro de C ++ la definición y la puesta en práctica se suelen almacenar en .h y Cpp archivos respectivamente.
First, to create the definition of the method, we have to use the -h flag of the Java compiler:
javac -h . HelloWorldJNI.java
This will generate a com_baeldung_jni_HelloWorldJNI.h file with all the native methods included in the class passed as a parameter, in this case, only one:
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject);
As we can see, the function name is automatically generated using the fully qualified package, class and method name.
Also, something interesting that we can notice is that we're getting two parameters passed to our function; a pointer to the current JNIEnv; and also the Java object that the method is attached to, the instance of our HelloWorldJNI class.
Now, we have to create a new .cpp file for the implementation of the sayHello function. This is where we'll perform actions that print “Hello World” to console.
We'll name our .cpp file with the same name as the .h one containing the header and add this code to implement the native function:
JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv* env, jobject thisObject) { std::cout << "Hello from C++ !!" << std::endl; }
3.3. Compiling And Linking
At this point, we have all parts we need in place and have a connection between them.
We need to build our shared library from the C++ code and run it!
To do so, we have to use G++ compiler, not forgetting to include the JNI headers from our Java JDK installation.
Ubuntu version:
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
Windows version:
g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
MacOS version;
g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o
Once we have the code compiled for our platform into the file com_baeldung_jni_HelloWorldJNI.o, we have to include it in a new shared library. Whatever we decide to name it is the argument passed into the method System.loadLibrary.
We named ours “native”, and we'll load it when running our Java code.
The G++ linker then links the C++ object files into our bridged library.
Ubuntu version:
g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc
Windows version:
g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias
MacOS version:
g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc
And that's it!
We can now run our program from the command line.
However, we need to add the full path to the directory containing the library we've just generated. This way Java will know where to look for our native libs:
java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI
Console output:
Hello from C++ !!
4. Using Advanced JNI Features
Saying hello is nice but not very useful. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.
4.1. Adding Parameters To Our Native Methods
We'll add some parameters to our native methods. Let's create a new class called ExampleParametersJNI with two native methods using parameters and returns of different types:
private native long sumIntegers(int first, int second); private native String sayHelloToMe(String name, boolean isFemale);
And then, repeat the procedure to create a new .h file with “javac -h” as we did before.
Now create the corresponding .cpp file with the implementation of the new C++ method:
... JNIEXPORT jlong JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers (JNIEnv* env, jobject thisObject, jint first, jint second) { std::cout << "C++: The numbers received are : " << first << " and " << second
NewStringUTF(fullName.c_str()); } ...
We've used the pointer *env of type JNIEnv to access the methods provided by the JNI environment instance.
JNIEnv allows us, in this case, to pass Java Strings into our C++ code and back out without worrying about the implementation.
We can check the equivalence of Java types and C JNI types into Oracle official documentation.
To test our code, we've to repeat all the compilation steps of the previous HelloWorld example.
4.2. Using Objects and Calling Java Methods From Native Code
In this last example, we're going to see how we can manipulate Java objects into our native C++ code.
We'll start creating a new class UserData that we'll use to store some user info:
package com.baeldung.jni; public class UserData { public String name; public double balance; public String getUserInfo() { return "[name]=" + name + ", [balance]=" + balance; } }
Then, we'll create another Java class called ExampleObjectsJNI with some native methods with which we'll manage objects of type UserData:
... public native UserData createUser(String name, double balance); public native String printUserData(UserData user);
One more time, let's create the .h header and then the C++ implementation of our native methods on a new .cpp file:
JNIEXPORT jobject JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_createUser (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) { // Create the object of the class UserData jclass userDataClass = env->FindClass("com/baeldung/jni/UserData"); jobject newUserData = env->AllocObject(userDataClass); // Get the UserData fields to be set jfieldID nameField = env->GetFieldID(userDataClass , "name", "Ljava/lang/String;"); jfieldID balanceField = env->GetFieldID(userDataClass , "balance", "D"); env->SetObjectField(newUserData, nameField, name); env->SetDoubleField(newUserData, balanceField, balance); return newUserData; } JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleObjectsJNI_printUserData (JNIEnv *env, jobject thisObject, jobject userData) { // Find the id of the Java method to be called jclass userDataClass=env->GetObjectClass(userData); jmethodID methodId=env->GetMethodID(userDataClass, "getUserInfo", "()Ljava/lang/String;"); jstring result = (jstring)env->CallObjectMethod(userData, methodId); return result; }
Again, we're using the JNIEnv *env pointer to access the needed classes, objects, fields and methods from the running JVM.
Normally, we just need to provide the full class name to access a Java class, or the correct method name and signature to access an object method.
We're even creating an instance of the class com.baeldung.jni.UserData in our native code. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.
We can check all other methods of JNIEnv into the Oracle official documentation.
4. Disadvantages Of Using JNI
JNI bridging does have its pitfalls.
The main downside being the dependency on the underlying platform; we essentially lose the “write once, run anywhere” feature of Java. This means that we'll have to build a new lib for each new combination of platform and architecture we want to support. Imagine the impact that this could have on the build process if we supported Windows, Linux, Android, MacOS…
JNI not only adds a layer of complexity to our program. It also adds a costly layer of communication between the code running into the JVM and our native code: we need to convert the data exchanged in both ways between Java and C++ in a marshaling/unmarshaling process.
Sometimes there isn't even a direct conversion between types so we'll have to write our equivalent.
5. Conclusion
Compiling the code for a specific platform (usually) makes it faster than running bytecode.
This makes it useful when we need to speed up a demanding process. Also, when we don't have other alternatives such as when we need to use a library that manages a device.
However, this comes at a price as we'll have to maintain additional code for each different platform we support.
Es por eso que generalmente es una buena idea usar JNI solo en los casos en que no haya una alternativa de Java .
Como siempre, el código de este artículo está disponible en GitHub.