Développons en Java 2.30 | |
Copyright (C) 1999-2022 Jean-Michel DOUDOUX | (date de publication : 15/06/2022) |
|
Niveau : | Confirmé |
JNI est l'acronyme de Java Native Interface. C'est une technologie qui permet d'utiliser du code natif, notamment C, dans une classe Java.
L'inconvénient majeur de cette technologie est d'annuler la portabilité du code Java. En contre-partie cette technologie peut être très utile dans plusieurs cas :
La mise en oeuvre de JNI nécessite plusieurs étapes :
La bibliothèque est donc dépendante du système d'exploitation pour lequel elle est développée : .dll pour les systèmes de type Windows, .so pour les systèmes de type Unix, ...
Ce chapitre contient plusieurs sections :
La déclaration dans le code source Java est très facile puisqu'il suffit de déclarer la signature de la méthode avec le modificateur native. Le modificateur permet au compilateur de savoir que cette méthode est contenue dans une bibliothèque native.
Il ne doit pas y avoir d'implémentation même pas un corps vide pour une méthode déclarée native.
Exemple : |
class TestJNI1 {
public native void afficherBonjour();
static {
System.loadLibrary("mabibjni");
}
public static void main(String[] args) {
new TestJNI1().afficherBonjour();
}
} |
Pour pouvoir utiliser une méthode native, il faut tout d'abord charger la bibliothèque. Pour réaliser ce chargement, il faut utiliser la méthode statique loadLibrary() de la classe system et obligatoirement s'assurer que la bibliothèque est chargée avant le premier appel de la méthode native.
Le plus simple pour assurer ce chargement est de le demander dans un morceau de code d'initialisation statique de la classe.
Exemple : |
class TestJNI1 {
public native void afficherBonjour();
static {
System.loadLibrary("mabibjni");
}
} |
Le nom de la bibliothèque fournie en paramètre doit être indépendant de la plate-forme utilisée : il faut préciser le nom de la bibliothèque sans son extension. Le nom sera automatiquement adapté selon le système d'exploitation sur lequel le code Java est exécuté.
L'utilisation de la méthode native dans le code Java se fait de la même façon qu'une méthode classique.
Exemple : |
class TestJNI1 {
public native void afficherBonjour();
static {
System.loadLibrary("mabibjni");
}
public static void main(String[] args) {
new TestJNI1().afficherBonjour();
}
} |
Jusqu'à la version 8 de Java, l'outil javah fourni avec le JDK permet de générer un fichier d'en-tête qui va contenir la définition dans le langage C des fonctions correspondant aux méthodes déclarées natives dans le code source Java.
Javah utilise le bytecode pour générer le fichier .h. Il faut donc que la classe Java soit préalablement compilée.
La syntaxe est donc : javah -jni nom_fichier_sans_extension
Exemple : |
D:\java\test\jni>dir
03/12/2003 14:39 <DIR> .
03/12/2003 14:39 <DIR> ..
03/12/2003 14:39 230 TestJNI1.java
2 fichier(s) 230 octets
2 Rép(s) 2 200 772 608 octets libres
D:\java\test\jni>javac TestJNI1.java
D:\java\test\jni>javah -jni TestJNI1
D:\java\test\jni>dir
Répertoire de D:\java\test\jni
03/12/2003 14:39 <DIR> .
03/12/2003 14:39 <DIR> ..
03/12/2003 14:39 459 TestJNI1.class
03/12/2003 14:39 399 TestJNI1.h
03/12/2003 14:39 230 TestJNI1.java
3 fichier(s) 1 088 octets
2 Rép(s) 2 198 208 512 octets libres
D:\java\test\jni> |
Le fichier TestJNI1.h généré est le suivant :
Exemple : |
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class TestJNI1 */
#ifndef _Included_TestJNI1
#define _Included_TestJNI1
#ifdef __cplusplus
extern " C " {
#endif
/*
* Class: TestJNI1
* Method: afficherBonjour
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_TestJNI1_afficherBonjour(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif |
Le nom de chaque fonction native respecte le format suivant :
Java_nomPleinementQualifieDeLaClasse_NomDeLaMethode
Ce fichier doit être utilisé dans l'implémentation du code de la fonction.
Même si la méthode native est déclarée sans paramètre, il y a toujours deux paramètres passés à la fonction native :
A partir de Java 9, l'outil javah n'est plus disponible. Il faut utiliser l'option -h du compilateur javac en lui précisant le chemin d'un répertoire dans lequel le compilateur va lui-même le fichier d'en-tête.
Exemple : |
D:\java\test\jni>dir
23/01/2021 20:39 <DIR> .
23/01/2021 20:39 <DIR> ..
23/01/2021 20:39 230 TestJNI1.java
2 fichier(s) 230 octets
2 Rép(s) 2 200 772 608 octets libres
D:\java\test\jni>javac -h . TestJNI1.java
D:\java\test\jni>dir
Répertoire de D:\java\test\jni
23/01/2021 20:40 <DIR> .
23/01/2021 20:40 <DIR> ..
23/01/2021 20:40 459 TestJNI1.class
23/01/2021 20:40 408 TestJNI1.h
23/01/2021 20:39 230 TestJNI1.java
3 fichier(s) 1 088 octets
2 Rép(s) 2 198 208 503 octets libres
D:\java\test\jni> |
La bibliothèque contenant la ou les fonctions qui seront appelées doit être écrite dans un langage (c ou c++) et compilée.
Pour l'écriture en C, facilitée par la génération du fichier.h, il est nécessaire en plus des includes liées au code des fonctions d'inclure deux fichiers d'en-tête :
Exemple : TestJNI.c |
#include <jni.h>
#include <stdio.h>
#include "TestJNI1.h"
JNIEXPORT void JNICALL
Java_TestJNI1_afficherBonjour(JNIEnv *env, jobject obj)
{
printf(" Bonjour\n ");
return;
} |
Il faut compiler ce fichier source sous la forme d'un fichier objet .o
Exemple : avec MinGW sous Windows |
D:\java\test\jni>gcc -c -I"C:\j2sdk1.4.2_02\include" -I"C:\j2sdk1.4.2_02\include
\win32" -o TestJNI.o TestJNI.c |
Il faut ensuite définir un fichier .def qui contient la définition des fonctions exportées par la bibliothèque
Exemple : TestJNI.def |
EXPORTS
Java_TestJNI1_afficherBonjour |
Il ne reste plus qu'à générer la dll.
Exemple : TestJNI.def |
D:\java\test\jni>gcc -shared -o mabibjni.dll TestJNI.o TestJNI.def
Warning: resolving _Java_TestJNI1_afficherBonjour by linking to _Java_TestJNI1_a
fficherBonjour@8
Use-enable-stdcall-fixup to disable these warnings
Use-disable-stdcall-fixup to disable these fixups
D:\java\test\jni>dir
Répertoire de D:\java\test\jni
03/12/2003 16:22 <DIR> .
03/12/2003 16:22 <DIR> ..
03/12/2003 16:22 12 017 mabibjni.dll
03/12/2003 15:58 193 TestJNI.c
03/12/2003 16:20 40 TestJNI.def
03/12/2003 16:04 543 TestJNI.o
03/12/2003 14:39 459 TestJNI1.class
03/12/2003 14:39 399 TestJNI1.h
03/12/2003 14:39 230 TestJNI1.java
9 fichier(s) 14 074 octets
2 Rép(s) 2 198 392 832 octets libres
D:\java\test\jni> |
Il ne reste plus qu'à exécuter le code Java dans une machine virtuelle.
Exemple : |
D:\java\test\jni>java TestJNI1
Bonjour
D:\java\test\jni> |
Il est intéressant de noter que tant que la signature de la méthode native ne change pas, il est inutile de recompiler la classe Java si la fonction dans la bibliothèque est modifiée et recompilée.
Une méthode a quasiment toujours besoin de paramètres et souvent besoin de retourner une valeur.
Cette section va définir et utiliser une méthode native qui ajoute deux entiers et renvoie le résultat de l'addition.
Exemple : le code Java |
class TestJNI1 {
public native int ajouter(int a, int b);
static {
System.loadLibrary("mabibjni");
}
public static void main(String[] args) {
TestJNI1 maclasse = new TestJNI1();
System.out.println("2 + 3 = " + maclasse.ajouter(2,3));
}
} |
La déclaration de la méthode n'a rien de particulier hormis le modificateur native.
La signature de la fonction dans le fichier .h tient compte des paramètres.
Exemple : |
JNIEXPORT jint JNICALL Java_TestJNI1_ajouter
(JNIEnv *, jobject, jint, jint); |
Les deux paramètres sont ajoutés dans la signature de la fonction avec un type particulier jint, défini avec un typedef dans le fichier jni.h. Il y a d'ailleurs des définitions pour toutes les primitives.
Primitive Java | Type natif |
boolean | jboolean |
byte | jbyte |
char | jchar |
double | jdouble |
int | jint |
float | jfloat |
long | jlong |
short | jshort |
void | void |
Il suffit ensuite d'écrire l'implémentation du code natif.
Exemple : |
#include <jni.h>
#include <stdio.h>
#include "TestJNI2.h"
JNIEXPORT jint JNICALL Java_TestJNI2_ajouter
(JNIEnv *env, jobject obj, jint a, jint b)
{
return a + b;
} |
Il faut ensuite compiler le code :
Exemple : |
D:\java\test\jni>gcc -c -I"C:\j2sdk1.4.2_02\include" -I"C:\j2sdk1.4.2_02\include
\win32" -o TestJNI2.o TestJNI2.c |
Il faut définir le fichier .def : l'exemple ci-dessous construit une bibliothèque contenant les fonctions natives des deux classes Java précédemment définies.
Exemple : |
EXPORTS
Java_TestJNI1_afficherBonjour
Java_TestJNI2_ajouter |
Il suffit de générer la bibliothèque.
Exemple : |
D:\java\test\jni>gcc -shared -o mabibjni.dll TestJNI.c TestJNI2.c TestJNI.def
Warning: resolving _Java_TestJNI1_afficherBonjour by linking to _Java_TestJNI1_a
fficherBonjour@8
Use-enable-stdcall-fixup to disable these warnings
Use-disable-stdcall-fixup to disable these fixups
Warning: resolving _Java_TestJNI2_ajouter by linking to _Java_TestJNI2_ajouter@1
6 |
Il ne reste plus qu'à exécuter le code Java
Exemple : |
D:\java\test\jni>java TestJNI2
2 + 3 = 5 |
Les objets sont passés par référence en utilisant une variable de type jobject. Plusieurs autres types sont prédéfinis par JNI pour des objets fréquemment utilisés :
Objet C | Objet Java |
jobject | java.lang.object |
jstring | java.lang.String |
jclass | java.lang.Class |
jthrowable | java.lang.Throwable |
jarray | type de base pour les tableaux |
jintArray | int[] |
jlongArray | long[] |
jfloatArray | float[] |
jdoubleArray | double[] |
jobjectArray | Object[] |
jbooleanArray | boolean[] |
jbyteArray | byte[] |
jcharArray | char[] |
jshortArray | short[] |
Exemple : concaténation de deux chaînes de caractères |
class TestJNI3 {
public native String concat(String a, String b);
static {
System.loadLibrary("mabibjni");
}
public static void main(String[] args) {
TestJNI3 maclasse = new TestJNI3();
System.out.println("abc + cde = " + maclasse.concat("abc","cde"));
}
} |
La déclaration de la fonction native dans le fichier TestJNI3.h est la suivante :
Exemple : |
/*
* Class: TestJNI3
* Method: concat
* Signature: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_TestJNI3_concat
(JNIEnv *, jobject, jstring, jstring); |
Pour utiliser les paramètres de type jstring dans le code natif, il faut les transformer en utilisant des fonctions proposées par l'interface JNIEnv car le type String de Java n'est pas directement compatible avec les chaînes de caractères C (char *). Il existe des fonctions pour transformer des chaînes codées en UTF-8 ou en Unicode.
Les méthodes pour traiter les chaînes au format UTF-8 sont :
Les méthodes équivalentes pour les chaines de caractères au format Unicode sont : GetStringChars(), NewString(), GetStringUTFLength() et ReleaseStringChars()
Exemple : TestJNI3.c |
#include <jni.h>
#include <stdio.h>
#include "TestJNI3.h"
JNIEXPORT jstring JNICALL Java_TestJNI3_concat
(JNIEnv *env, jobject obj, jstring chaine1, jstring chaine2){
char resultat[256];
const char *str1 = (*env)->GetStringUTFChars(env, chaine1, 0);
const char *str2 = (*env)->GetStringUTFChars(env, chaine2, 0);
sprintf(resultat,"%s%s", str1, str2);
(*env)->ReleaseStringUTFChars(env, chaine1, str1);
(*env)->ReleaseStringUTFChars(env, chaine2, str2);
return (*env)->NewStringUTF(env, resultat);
} |
Attention : ce code est très simpliste car il ne vérifie pas un éventuel débordement du tableau nommé resultat.
Après la compilation des différents éléments, l'exécution affiche le résultat escompté.
Exemple : |
D:\java\test\jni>java TestJNI3
abc + cde = abccde |
|