Faculté des Sciences de Luminy
Présentation du langage Java
Henri Garreta
rev. 02.09.06

Java Native Interface (JNI)

Les questions suivantes ont été posées par certains d’entre vous :

Bien entendu, la réponse est « oui » aux quatre questions. Le premier cas – le plus utile – est développé ici à titre d’exemple. Mais celui-là, et les trois autres, sont bien expliqués dans la documentation officielle :

Appeler une fonction écrite en C : un petit exemple complet

    1. Écriture du fichier source Java
    2. Compilation
    3. Production du fichier ".h"
    4. Activation de Dev-C++
    5. Écriture du fichier source C
    6. Compilation
    7. Exécution
    8. Un second exemple
    9. Le cas de SunOS et Solaris

Supposons que nous soyons en train de développer une certaine classe MonBazar et que nous n’appréciions pas la difficulté[1] qu’il y a en Java à lire une ligne au clavier. Nous avons donc décidé d’écrire en C une fonction pour afficher une question et lire la réponse de l’utilisateur par un appel de la fonction fgets bien connue, c’est-à-dire une fonction qui fait essentiellement ceci (question et reponse sont des tableaux de caractères) :

fputs(question, stdout);
fgets(reponse, sizeof reponse, stdin);

En Java, le problème se pose ainsi : la classe MonBazar aura une méthode lireLigne qualifiée native. Cela signifie que l’entête de la méthode sera écrit normalement, mais pas son corps. Celui-ci sera fourni sous la forme d’une bibliothèque, construite avec les outils C du système hôte (gcc, Dev-C++, etc.)., et on veillera à ce que cette bibliothèque soit chargée à l’exécution, lorsque la classe démarre.

Pour fixer les idées, nous supposerons que nous travaillons sous Windows, que nous développons en C avec Dev-C++ et que nous avons installé JDK dans un dossier nommé "C:\java\jdkX.X.X". Si au lieu de cela nous avions travaillé sous Linux, Solaris ou un autre système, ou sous Windows avec un autre compilateur, il y a aurait eu dans la suite quelques petites différences, bien expliquées dans les documentations citées, notamment le Tutorial.

Voir aussi la section B, "Le cas de SunOS / Solaris".

1. Écriture du fichier source Java

La première étape est l’écriture du fichier source Java :

public class MonBazar {
    public native String lireLigne(String question);

    static {
        System.loadLibrary("maBibliotek");
    }


    public static void main(String[] args) {
        MonBazar unObjet = new MonBazar();
        String reponse = unObjet.lireLigne("ton nom? ");
        System.out.println("Bonjour " + reponse);
    }
}

Le début de la classe ci-dessus présente lireLigne comme une méthode d’instance (on aurait pu en faire une méthode static, c’est-à-dire de classe, la suite n’en aurait été que plus facile), publique (ce qui n’a aucune conséquence sur la possibilité ou la manière de l’écrire en C), native (cela signifie précisément qu’elle sera écrite en C), rendant une String comme résultat et prenant une String comme argument.

Le bloc static renferme un code qui sera exécuté, une seule fois, lors du chargement de la classe, c’est-à-dire ici lors du démarrage de l’application. En Java, toute classe peut comporter un tel code d’initialisation de la classe ; on peut y faire toutes sortes d’opérations utiles, pourvu qu’elles ne requièrent pas l’existence préalable d’instances de la classe. Dans notre cas, cela se limite à charger une bibliothèque censée contenir le code de lireLigne, écrit en C et compilé.

La manière dont un nom de fichier est fabriqué à partir de l’argument de loadLibrary dépend du système d’exploitation sous-jacent. Sous Windows, une bibliothèque nommée maBibliotek est supposée se trouver dans un fichier nommé maBibliotek.dll. Sous UNIX le fichier devra s’appeler plutôt libmaBibliotek.so

Pour le reste, notre classe MonBazar est un jouet limité à une méthode principale purement démonstrative, réduite à la création d’une instance (il faut bien, puisque lireLigne est une méthode d’instance) et à l’appel de lireLigne afin d’en vérifier le bon fonctionnement.

2. Compilation

Pas de surprise, le fichier précédent se compile par la commande

javac MonBazar.java 

3. Production du fichier ".h"

Une partie du code C que nous devons fournir sera écrit par Java. Cela s’obtient avec la commande javah, comme ceci :

javah -jni MonBazar 

(Notez qu’il faut que la classe MonBazar ait été préalablement compilée). Cela produit un fichier MonBazar.h au contenu passablement ésotérique :

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class MonBazar */

#ifndef _Included_MonBazar
#define _Included_MonBazar

#ifdef __cplusplus
extern "C" {
#endif

/*
* Class: MonBazar
* Method: lireLigne
* Signature: (Ljava/lang/String;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_MonBazar_lireLigne(JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif

#endif

Bien entendu, les identificateurs JNIEXPORT, jstring, JNICALL, JNIEnv, jobject, jstring etc., sont des pseudo-constantes introduites avec #define... ou des types définis avec typedef... dans le fichier <jni.h>. Dans une large mesure, Java nous dispense de connaître les détails des définitions de ces éléments, on l’en remercie chaleureusement!

Le fichier produit par javah est nécessaire pour compiler le code que nous allons écrire en C. Sa partie intéressante est la déclaration en C de la fonction native :

JNIEXPORT jstring JNICALL Java_MonBazar_lireLigne(JNIEnv *, jobject, jstring);

Bien entendu, le prototype de la fonction C que nous écrirons devra coïncider exactement avec celui-ci. Nous apprenons en particulier que la méthode MonBazar.lireLigne s’appellera en C : Java_MonBazar_lireLigne. Ce nom, inventé par javah, aurait été encore plus long si la classe MonBazar n’avait pas été dans le package sans nom ou bien s’il y avait eu surcharge (plusieurs méthodes nommées lireLigne).

Vue du côté Java, cette méthode a un unique argument, la chaîne correspondant à la question à poser. Vue du côté C elle en a trois :

4. Activation de Dev-C++

Pour développer notre fonction C nous allons lancer Dev-C++ en pensant que nous devons produire une DLL (Dynamic Link Library, ou bibliothèque à liens dynamiques) nommée maBibliotek.dll. Il faut donc faire la commande Fichier > Nouveau > Projet... puis choisir comme type DLL et comme nom maBibliotek :

(Quand on vous le proposera, enregistrez le projet sous le nom proposé, maBibliotek.dev) Dev-C++ crée un projet avec deux fichiers sommaires, nommés dllmain.c et dll.h ou quelque chose comme cela. Jetez ces deux fichiers et créez-en un nouveau, nommé par exemple monCode.c. C’est dans celui-là que vous allez taper votre code C.

Auparavant il y a encore un petit réglage à faire : allez dans Projet > Options du projet > Paramètres et dans le champ "Compilateur :" ajoutez le texte

-I"C:/java/jdkX.X.X/include" -I"C:/java/jdkX.X.X/include/win32"

(nous avons supposé que C:/java/jdkX.X.X/ est le dossier dans lequel on a installé le JDK) :

Cela permettra au compilateur C de trouver le fichier dont il est question dans la directive #include <jni.h>. Laissez les autres options proposées par Dev-C++ (--no-export-all-symbols, --add-stdcall-alias, etc.) comme elles sont.

5. Ecriture du fichier sourc C

Voici enfin le fameux code natif que nous devons écrire en C. La fonction obtenue semble elle aussi écrite en martien, mais c’est surtout parce qu’elle est très courte (rien qu’un printf suivi d’un gets !), donc les primitives bizarres pour le passage des arguments y deviennent très voyantes. Si le travail à faire avait été plus important on aurait tout de suite reconnu un bon morceau de C bien de chez nous :
#include <stdio.h>
#include <jni.h>
#include "MonBazar.h"

JNIEXPORT jstring JNICALL
Java_MonBazar_lireLigne(JNIEnv *envir, jobject objet, jstring question) {
char tampon[128];

const char *tmp = (*envir)->GetStringUTFChars(envir, question, 0);
printf("%s", tmp);
(*envir)->ReleaseStringUTFChars(envir, question, tmp);

fgets(tampon, 128, stdin);
return (*envir)->NewStringUTF(envir, tampon);
}

MonBazar.h est le fichier produit tout à l’heure par javah, tandis que jni.h est un fichier livré avec le JDK.

Les trois fonctions bizarres appelées ci-dessus sont nécessaires parce que les caractères ne sont pas la même chose en Java (16 bits, codage Unicode) et en C (8 bits, codage UTC). GetStringUTFChars et ReleaseStringUTFChars obtiennent une chaîne C faite à partir d’une chaîne Java, tandis que NewStringUTF fabrique une chaîne Java à partir d’une chaîne C.

6. Compilation

La compilation s’obtient comme d’habitude dans Dev-C++ (commande Exécuter > Compiler, ou F9). Si on a suivi les instructions précédentes (notamment à propos des chemins -I"C:/java/jdkX.X.X/include" et -I"C:/java/jdkX.X.X/include/win32", qu’il faut adapter à son propre cas en remplaçant C:/java/jdkX.X.X/ par le dossier dans lequel on a installé le JDK) tout doit bien se passer. Dev-C++ produit un fichier maBibliotek.dll qu’il faut laisser à la portée de Java (par exemple, dans le même dossier que la classe à exécuter).

7. Exécution

En principe cela marche très bien :

C:\tmp> java MonBazar
ton nom? Henri
Bonjour Henri

Si la compilation s’est bien déroulée, l’exécution en Java peut échouer de deux manières principales :

c:\tmp> java MonBazar
Exception in thread "main" java.lang.UnsatisfiedLinkError: no maBibliotek in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1403)
at java.lang.Runtime.loadLibrary0(Runtime.java:788)
at java.lang.System.loadLibrary(System.java:832)
at MonBazar.(MonBazar.java:6)

cela signifie que, lors du chargement de la classe, Java n’a pas trouvé le fichier maBibliotek.dll. Ce fichier existe-t-il ? Où l’avez-vous placé ?

Autre manière, pour la classe MonBazar, de ne pas fonctionner :

c:\tmp> java MonBazar
Exception in thread "main" java.lang.UnsatisfiedLinkError: lireLigne
at MonBazar.lireLigne(Native Method)
at MonBazar.main(MonBazar.java:11)

Ici ce n’est pas la même chose : Java a bien trouvé le fichier maBibliotek.dll, mais il n’y a pas trouvé dedans la fonction cherchée, lireLigne. C’est une situation bien plus glauque. Vérifiez que vous avez donné à la fonction écrite en C le prototype (i.e.: le nom et la liste des arguments) que javah a mis dans le fichier ".h" produit. Sinon, vérifiez les options de compilation. Il y a ici des questions sur (a) la programmation Windows et (b) l’emploi du compilateur C qui sortent du cadre de ce cours (dépasser les compétences du prof est une manière canonique de sortir du cadre d’un cours).

8. Un second exemple

Comment exploiter dans un programme Java une fonction C ayant des arguments « par adresse » ? A titre d’exemple, voici une telle fonction ; elle reçoit l’adresse d’une variable entière et elle en incrémente la valeur de 2 unités :

void uneFonction(int *ptrInt) {
*ptrInt = *ptrInt + 2;
}

Voici une classe qui va - indirectement - utiliser la fonction précédente. Ce qui en Java ressemble le plus à un argument par adresse est un objet (puisque les objets sont toujours accédés par référence), ce qui explique l’introduction de la classe auxiliaire Entier :

class Entier {
int valeur;
}

public class MaClasse {
public native void incrementer(Entier nombre);

static {
System.loadLibrary("maBibliotek");
}

public static void main(String[] args) {
MaClasse unObjet = new MaClasse();
Entier n = new Entier();
for (int i = 0; i < 5; i++) {
System.out.println(n.valeur);
unObjet.incrementer(n);
}
}
}

Et voici l’élément le plus important, en tout cas le plus complexe : le fichier qui fait le lien entre les deux précédents

#include <stdio.h>
#include <jni.h>
#include "MaClasse.h"

extern void uneFonction(int *);

JNIEXPORT void JNICALL
Java_MaClasse_incrementer(JNIEnv *envir, jobject objet, jobject nombre) {
jclass classe = (*envir)->GetObjectClass(envir, nombre);
jfieldID idChamp = (*envir)->GetFieldID(envir, classe, "valeur", "I");

int valeur = (*envir)->GetIntField(envir, nombre, idChamp);
uneFonction(&valeur);
(*envir)->SetIntField(envir, nombre, idChamp, valeur);
}

9. Le cas de SunOS / Solaris

Pour ce dialecte d’UNIX les étapes ci-dessus restent valables, sauf ce qui traite de Dev-C++. D’autre part, il faut savoir que dans ce système une bibliothèque nommée maBibliotek doit occuper un fichier qui

Par conséquent, si le ficher source de la bibliothèque se nomme maBibliotek.c, la commande pour le compiler sera

gcc maBibliotek.c -shared -fpic -o libmaBibliotek.so

D’autre part, la deuxième contrainte sur la variable LD_LIBRARY_PATH amènera souvent à ajouter les deux lignes suivantes dans son fichier .bash_profile :

LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
export PATH

Notez que le manque de sérieux à propos de cette variable LD_LIBRARY_PATH se paie généralement par l’erreur, commentée plus haut : java.lang.UnsatisfiedLinkError: no maBibliotek in java.library.path


[1] Le code développé ici est donné pour illustrer le mécanisme JNI mais il est en soi peu utile, car depuis la version 5 de Java on dispose de bons outils pour effectuer de manière simple et pratique des lectures sur l'entrée standard.