Guide de JNI (Java Native Interface)

1. Introduction

Comme nous le savons, l'une des principales forces de Java est sa portabilité - ce qui signifie qu'une fois que nous écrivons et compilons du code, le résultat de ce processus est un bytecode indépendant de la plate-forme.

En termes simples, cela peut s'exécuter sur n'importe quelle machine ou périphérique capable d'exécuter une machine virtuelle Java, et cela fonctionnera aussi parfaitement que prévu.

Cependant, nous devons parfois utiliser du code compilé de manière native pour une architecture spécifique .

Il peut y avoir des raisons pour lesquelles utiliser du code natif:

  • La nécessité de gérer du matériel
  • Amélioration des performances pour un processus très exigeant
  • Une bibliothèque existante que nous voulons réutiliser au lieu de la réécrire en Java.

Pour y parvenir, le JDK introduit un pont entre le bytecode exécuté dans notre JVM et le code natif (généralement écrit en C ou C ++).

L'outil s'appelle Java Native Interface. Dans cet article, nous verrons comment écrire du code avec.

2. Comment ça marche

2.1. Méthodes natives: la JVM rencontre le code compilé

Java fournit le mot-clé natif utilisé pour indiquer que l'implémentation de la méthode sera fournie par un code natif.

Normalement, lors de la création d'un programme exécutable natif, nous pouvons choisir d'utiliser des bibliothèques statiques ou partagées:

  • Bibliothèques statiques - tous les binaires de la bibliothèque seront inclus dans notre exécutable pendant le processus de liaison. Ainsi, nous n'aurons plus besoin des bibliothèques, mais cela augmentera la taille de notre fichier exécutable.
  • Les bibliothèques partagées - l'exécutable final n'a que des références aux bibliothèques, pas au code lui-même. Cela nécessite que l'environnement dans lequel nous exécutons notre exécutable ait accès à tous les fichiers des bibliothèques utilisées par notre programme.

Ce dernier est ce qui a du sens pour JNI car nous ne pouvons pas mélanger le bytecode et le code compilé nativement dans le même fichier binaire.

Par conséquent, notre bibliothèque partagée conservera le code natif séparément dans son fichier .so / .dll / .dylib (selon le système d'exploitation que nous utilisons) au lieu de faire partie de nos classes.

Le mot-clé natif transforme notre méthode en une sorte de méthode abstraite:

private native void aNativeMethod();

Avec la principale différence qu'au lieu d'être implémentée par une autre classe Java, elle sera implémentée dans une bibliothèque partagée native séparée .

Une table avec des pointeurs en mémoire vers l'implémentation de toutes nos méthodes natives sera construite afin qu'elles puissent être appelées à partir de notre code Java.

2.2. Composants nécessaires

Voici une brève description des éléments clés dont nous devons tenir compte. Nous les expliquerons plus loin dans cet article

  • Code Java - nos classes. Ils comprendront au moins une méthode native .
  • Code natif - la logique réelle de nos méthodes natives, généralement codées en C ou C ++.
  • Fichier d'en-tête JNI - ce fichier d'en-tête pour C / C ++ ( incluez / jni.h dans le répertoire JDK) comprend toutes les définitions des éléments JNI que nous pouvons utiliser dans nos programmes natifs.
  • Compilateur C / C ++ - nous pouvons choisir entre GCC, Clang, Visual Studio ou tout autre que nous aimons dans la mesure où il est capable de générer une bibliothèque partagée native pour notre plate-forme.

2.3. Éléments JNI dans le code (Java et C / C ++)

Éléments Java:

  • Mot clé «native» - comme nous l'avons déjà vu, toute méthode marquée comme native doit être implémentée dans une bibliothèque native partagée.
  • System.loadLibrary (String libname) - une méthode statique qui charge une bibliothèque partagée à partir du système de fichiers en mémoire et rend ses fonctions exportées disponibles pour notre code Java.

Éléments C / C ++ (dont beaucoup sont définis dans jni.h )

  • JNIEXPORT- marque la fonction dans la bibliothèque partagée comme exportable afin qu'elle soit incluse dans la table de fonction, et ainsi JNI peut la trouver
  • JNICALL - combiné avec JNIEXPORT , il garantit que nos méthodes sont disponibles pour le framework JNI
  • JNIEnv - une structure contenant des méthodes que nous pouvons utiliser notre code natif pour accéder aux éléments Java
  • JavaVM - une structure qui nous permet de manipuler une JVM en cours d'exécution (ou même d'en démarrer une nouvelle) en y ajoutant des threads, en la détruisant, etc.

3. Hello World JNI

Ensuite, regardons comment JNI fonctionne dans la pratique.

Dans ce didacticiel, nous utiliserons C ++ comme langage natif et G ++ comme compilateur et éditeur de liens.

Nous pouvons utiliser n'importe quel autre compilateur de notre préférence, mais voici comment installer G ++ sur Ubuntu, Windows et MacOS:

  • Ubuntu Linux - exécutez la commande «sudo apt-get install build-essential» dans un terminal
  • Windows - Installez MinGW
  • MacOS - exécutez la commande «g ++» dans un terminal et s'il n'est pas encore présent, il l'installera.

3.1. Création de la classe Java

Commençons par créer notre premier programme JNI en implémentant un "Hello World" classique.

Pour commencer, nous créons la classe Java suivante qui inclut la méthode native qui effectuera le travail:

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(); }

Comme nous pouvons le voir, nous chargeons la bibliothèque partagée dans un bloc statique . Cela garantit qu'il sera prêt quand nous en aurons besoin et où que nous en ayons besoin.

Alternativement, dans ce programme trivial, nous pourrions à la place charger la bibliothèque juste avant d'appeler notre méthode native car nous n'utilisons la bibliothèque native nulle part ailleurs.

3.2. Implémentation d'une méthode en C ++

Maintenant, nous devons créer l'implémentation de notre méthode native en C ++.

Dans C ++, la définition et l'implémentation sont généralement stockées respectivement dans des fichiers .h et .cpp .

Tout d'abord, pour créer la définition de la méthode, nous devons utiliser l' indicateur -h du compilateur Java :

javac -h . HelloWorldJNI.java

Cela générera un fichier com_baeldung_jni_HelloWorldJNI.h avec toutes les méthodes natives incluses dans la classe passées en paramètre, dans ce cas, une seule:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv *, jobject); 

Comme nous pouvons le voir, le nom de la fonction est automatiquement généré en utilisant le nom complet du package, de la classe et de la méthode.

Aussi, quelque chose d'intéressant que nous pouvons remarquer est que nous obtenons deux paramètres passés à notre fonction; un pointeur vers le JNIEnv actuel ; et aussi l'objet Java auquel la méthode est attachée, l'instance de notre classe HelloWorldJNI .

Maintenant, nous devons créer un nouveau fichier .cpp pour l'implémentation de la fonction sayHello . C'est ici que nous allons effectuer des actions qui affichent «Hello World» sur la console.

Nous nommerons notre fichier .cpp avec le même nom que celui .h contenant l'en-tête et ajouterons ce code pour implémenter la fonction native:

JNIEXPORT void JNICALL Java_com_baeldung_jni_HelloWorldJNI_sayHello (JNIEnv* env, jobject thisObject) { std::cout << "Hello from C++ !!" << std::endl; } 

3.3. Compilation et liaison

À ce stade, nous avons toutes les pièces dont nous avons besoin en place et avons une connexion entre elles.

Nous devons construire notre bibliothèque partagée à partir du code C ++ et l'exécuter!

Pour ce faire, nous devons utiliser le compilateur G ++, sans oublier d'inclure les en-têtes JNI de notre installation Java JDK .

Version Ubuntu:

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Version Windows:

g++ -c -I%JAVA_HOME%\include -I%JAVA_HOME%\include\win32 com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Version MacOS;

g++ -c -fPIC -I${JAVA_HOME}/include -I${JAVA_HOME}/include/darwin com_baeldung_jni_HelloWorldJNI.cpp -o com_baeldung_jni_HelloWorldJNI.o

Une fois que nous avons compilé le code pour notre plate-forme dans le fichier com_baeldung_jni_HelloWorldJNI.o , nous devons l'inclure dans une nouvelle bibliothèque partagée. Quel que soit le nom que nous décidons de nommer, c'est l'argument passé dans la méthode System.loadLibrary .

Nous avons nommé le nôtre «natif», et nous le chargerons lors de l'exécution de notre code Java.

L'éditeur de liens G ++ lie ensuite les fichiers objets C ++ dans notre bibliothèque pontée.

Version Ubuntu:

g++ -shared -fPIC -o libnative.so com_baeldung_jni_HelloWorldJNI.o -lc

Version Windows:

g++ -shared -o native.dll com_baeldung_jni_HelloWorldJNI.o -Wl,--add-stdcall-alias

Version MacOS:

g++ -dynamiclib -o libnative.dylib com_baeldung_jni_HelloWorldJNI.o -lc

Et c'est tout!

Nous pouvons maintenant exécuter notre programme à partir de la ligne de commande.

Cependant, nous devons ajouter le chemin complet du répertoire contenant la bibliothèque que nous venons de générer. De cette façon, Java saura où chercher nos bibliothèques natives:

java -cp . -Djava.library.path=/NATIVE_SHARED_LIB_FOLDER com.baeldung.jni.HelloWorldJNI

Sortie de la console:

Hello from C++ !!

4. Utilisation des fonctionnalités JNI avancées

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.

C'est pourquoi c'est généralement une bonne idée de n'utiliser JNI que dans les cas où il n'y a pas d'alternative Java .

Comme toujours, le code de cet article est disponible à l'adresse over sur GitHub.