Guide de JNI (interface native Java)

Guide de JNI (interface native Java)

1. introduction

Comme nous le savons, l’un des principaux atouts de Java est sa portabilité, c’est-à-dire qu’une fois le code écrit et compilé, le résultat de ce processus est un bytecode indépendant de la plate-forme.

En termes simples, cela peut fonctionner sur n'importe quelle machine ou périphérique capable d'exécuter une machine virtuelle Java, et cela fonctionnera de manière transparente, comme on pouvait s'y attendre.

Cependant, parfoiswe do actually need to use code that’s natively-compiled for a specific architecture.

Il pourrait y avoir des raisons pour avoir besoin d'utiliser du code natif:

  • La nécessité de gérer du matériel

  • Amélioration de la performance pour un processus très exigeant

  • Une bibliothèque existante que nous voulons réutiliser au lieu de la réécrire en Java.

To achieve this, the JDK introduces a bridge between the bytecode running in our JVM and the native code (généralement écrit en C ou C ++).

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

2. Comment ça fonctionne

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

Java fournit le mot-clénative 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 fichiers binaires de la bibliothèque seront inclus dans l'exécutable au cours du processus de liaison. Ainsi, nous n’aurons plus besoin des bibliothèques, mais cela augmentera la taille de notre fichier exécutable.

  • Bibliothèques partagées - l'exécutable final ne contient que des références aux bibliothèques, pas le 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énative transforme notre méthode en une sorte de méthode abstraite:

private native void aNativeMethod();

Avec la principale différence queinstead of being implemented by another Java class, it will be implemented in a separated native shared library.

Une table avec des pointeurs en mémoire pour la mise en œuvre de toutes nos méthodes natives sera construite de manière à pouvoir être appelée à 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 en détail plus loin dans cet article

  • Java Code - nos classes. Ils incluront au moins une méthodenative.

  • Code natif - la logique réelle de nos méthodes natives, généralement codée en C ou C ++.

  • Fichier d'en-tête JNI - ce fichier d'en-tête pour C / C ++ (include/jni.h dans le répertoire JDK) comprend toutes les définitions des éléments JNI que nous pouvons utiliser dans nos programmes natifs.

  • C/C++ Compiler – we can choose between GCC, Clang, Visual Studio, or any other we like as far as it’s able to generate a native shared library for our platform.

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

Éléments Java:

  • Mot clé "natif" - comme nous l'avons déjà expliqué, 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 du système de fichiers en mémoire et rend ses fonctions exportées disponibles pour notre code Java.

C/C++ elements (many of them defined within jni.h)

  • JNIEXPORT- marque la fonction dans la bibliothèque partagée comme étant exportable. Elle sera donc incluse dans la table des fonctions. JNI pourra donc la trouver.

  • JNICALL - combiné avecJNIEXPORT, 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 machine virtuelle Java en cours d'exécution (ou même d'en démarrer une nouvelle) en lui ajoutant des threads, en la détruisant, etc.

3. Bonjour le monde JNI

Ensuite,let’s look at how JNI works in practice.

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écuter la commande“sudo apt-get install build-essential” dans un terminal

  • Windows -Install 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.example.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,we load the shared library in a static block. Cela garantit qu'il sera prêt lorsque nous en aurons besoin et d'où que nous en ayons besoin.

Sinon, 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 ++

Nous devons maintenant 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 les fichiers.h et.cpp.

Tout d'abord,to create the definition of the method, we have to use the -h flag of the Java compiler:

javac -h . HelloWorldJNI.java

Cela générera un fichiercom_example_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_example_jni_HelloWorldJNI_sayHello
  (JNIEnv *, jobject);

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

En outre, quelque chose d'intéressant que nous pouvons remarquer est que nous obtenons deux paramètres passés à notre fonction; un pointeur vers lesJNIEnv; actuels et aussi l'objet Java auquel la méthode est attachée, l'instance de notre classeHelloWorldJNI.

Maintenant, nous devons créer un nouveau fichier.cpp pour l'implémentation de la fonctionsayHello. This is where we’ll perform actions that print “Hello World” to 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_example_jni_HelloWorldJNI_sayHello
  (JNIEnv* env, jobject thisObject) {
    std::cout << "Hello from C++ !!" << std::endl;
}

3.3. Compiler et relier

À 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 ++,not forgetting to include the JNI headers from our Java JDK installation.

Version Ubuntu:

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

Version Windows:

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

Version MacOS;

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

Une fois que nous avons compilé le code pour notre plate-forme dans le fichiercom_example_jni_HelloWorldJNI.o, nous devons l'inclure dans une nouvelle bibliothèque partagée. Whatever we decide to name it is the argument passed into the method 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_example_jni_HelloWorldJNI.o -lc

Version Windows:

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

Version MacOS:

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

Et c'est tout!

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

Cependant,we need to add the full path to the directory containing the library we’ve just generated. De cette façon, Java saura où chercher nos bibliothèques natives:

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

Sortie de la console:

Hello from C++ !!

4. Utilisation des fonctionnalités JNI avancées

Dire bonjour est bien mais pas très utile. Usually, we would like to exchange data between Java and C++ code and manage this data in our program.

4.1. Ajout de paramètres à nos méthodes natives

Nous ajouterons quelques paramètres à nos méthodes natives. Créons une nouvelle classe appeléeExampleParametersJNI avec deux méthodes natives utilisant des paramètres et des retours de types différents:

private native long sumIntegers(int first, int second);

private native String sayHelloToMe(String name, boolean isFemale);

Répétez ensuite la procédure pour créer un nouveau fichier .h avec «javac -h», comme nous l’avions fait auparavant.

Créez maintenant le fichier .cpp correspondant avec l'implémentation de la nouvelle méthode C ++:

...
JNIEXPORT jlong JNICALL Java_com_example_jni_ExampleParametersJNI_sumIntegers
  (JNIEnv* env, jobject thisObject, jint first, jint second) {
    std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
    return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_example_jni_ExampleParametersJNI_sayHelloToMe
  (JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
    const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
    std::string title;
    if(isFemale) {
        title = "Ms. ";
    }
    else {
        title = "Mr. ";
    }

    std::string fullName = title + nameCharPointer;
    return env->NewStringUTF(fullName.c_str());
}
...

Nous avons utilisé le pointeur*env de typeJNIEnv pour accéder aux méthodes fournies par l'instance d'environnement JNI.

JNIEnv nous permet, dans ce cas, de passer JavaStrings dans notre code C ++ et de revenir en arrière sans se soucier de l'implémentation.

On peut vérifier l'équivalence des types Java et des types C JNI enOracle official documentation.

Pour tester notre code, nous devons répéter toutes les étapes de compilation de l'exempleHelloWorld précédent.

4.2. Utilisation d'objets et appel de méthodes Java à partir de code natif

Dans ce dernier exemple, nous allons voir comment nous pouvons manipuler des objets Java dans notre code C ++ natif.

Nous allons commencer à créer une nouvelle classeUserData que nous utiliserons pour stocker des informations utilisateur:

package com.example.jni;

public class UserData {

    public String name;
    public double balance;

    public String getUserInfo() {
        return "[name]=" + name + ", [balance]=" + balance;
    }
}

Ensuite, nous allons créer une autre classe Java appeléeExampleObjectsJNI avec des méthodes natives avec lesquelles nous gérerons les objets de typeUserData:

...
public native UserData createUser(String name, double balance);

public native String printUserData(UserData user);

Encore une fois, créons l'en-tête.h puis l'implémentation C ++ de nos méthodes natives sur un nouveau fichier.cpp:

JNIEXPORT jobject JNICALL Java_com_example_jni_ExampleObjectsJNI_createUser
  (JNIEnv *env, jobject thisObject, jstring name, jdouble balance) {

    // Create the object of the class UserData
    jclass userDataClass = env->FindClass("com/example/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_example_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;
}

Encore une fois, nous utilisons le pointeurJNIEnv *env pour accéder aux classes, objets, champs et méthodes nécessaires à partir de la JVM en cours d'exécution.

Normalement, il suffit de fournir le nom complet de la classe pour accéder à une classe Java, ou le nom et la signature de méthode corrects pour accéder à une méthode objet.

Nous créons même une instance de la classecom.example.jni.UserData dans notre code natif. Once we have the instance, we can manipulate all its properties and methods in a way similar to Java reflection.

Nous pouvons vérifier toutes les autres méthodes deJNIEnv dans lesOracle official documentation.

4. Inconvénients de l'utilisation de JNI

Le pontage JNI a ses pièges.

Le principal inconvénient est la dépendance à la plate-forme sous-jacente; Fonctionwe essentially lose the “write once, run anywhere” de Java. Cela signifie que nous devrons créer une nouvelle bibliothèque pour chaque nouvelle combinaison de plate-forme et d'architecture que nous souhaitons prendre en charge. Imaginez l’impact que cela pourrait avoir sur le processus de construction si nous prenions en charge Windows, Linux, Android, MacOS…

JNI ajoute non seulement une couche de complexité à notre programme. It also adds a costly layer of communication entre le code fonctionnant dans la JVM et notre code natif: nous devons convertir les données échangées dans les deux sens entre Java et C ++ dans un processus de marshaling / unmarshaling.

Parfois, il n’ya même pas de conversion directe entre les types, nous devrons donc écrire notre équivalent.

5. Conclusion

Compiler le code pour une plate-forme spécifique (généralement) le rend plus rapide que l’exécution de bytecode.

Cela le rend utile lorsque nous devons accélérer un processus exigeant. De plus, lorsque nous n'avons pas d'autres alternatives, par exemple lorsque nous devons utiliser une bibliothèque qui gère un appareil.

Cependant, cela a un prix car nous devrons maintenir un code supplémentaire pour chaque plate-forme différente que nous prenons en charge.

C’est pourquoi il est généralement judicieux deonly use JNI in the cases where there’s no Java alternative.

Comme toujours, le code de cet article est disponibleover on GitHub.