Faculté
des Sciences de Luminy | Présentation
du langage Java Henri Garreta |
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 :
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".
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.
Pas de surprise, le fichier précédent se compile par la commande
javac MonBazar.java
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 :
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.
#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.
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).
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).
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);
}
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