|
Les méthodes natives
Vous êtes certainement convaincu que le code
écrit en langage Java a un certain nombre d'avantages sur le code écrit dans les
langages comme C ou C++,même en ce qui concerne les applications spécifiques à
une plate-forme. Il n'est bien sûr pas question ici de portabilité. Tandis
qu'une solution "100% pure Java" est en principe sympathique, pour être
réaliste, en ce qui concerne une application, il existe des situations où vous
voudrez écrire (ou utiliser) du code écrit dans un autre langage( un tel code
est appelé code natif).Il y a trois raisons évidentes pour lesquelles ce peut
être le bon choix:
-
Vous
disposez d'une quantité substantielle de code testé et débogué, disponible
dans cet autre langage.
Le portage du code en langage Java prendrait beaucoup de temps et le code
résultant devrait être de nouveau testé et débogué.
-
Votre
application nécessite l'accès à des unités ou des fonctionnalités système et
l'utilisation de la technologie Java serait au mieux fastidieuse, et au pire
impossible.
-
La
vitesse d'exécution du code est essentielle.
Si vous
êtes dan l'une de ces trois situations, il peut être judicieux d'appeler le code
natif à partir de programmes écrits en Java. Voyons maintenant comment mettre en
oeuvre le code natif.
Appeler une
fonction C à partir du langage Java
Supposons
que vous ayez une fonction C qui réalise une tâche que vous appréciez, et que
pour une raison ou une autre, vous vouliez vous dispenser de la réimplémenter en
langage Java. Pour les besoins de la démonstration, nous allons admettre qu'il
s'agit de l'incontournable fonction printf. Vous voulez pouvoir appeler
printf à partir de vos programmes. Le langage Java utilise le mot clé
native, et il est évident que vous devrez encapsuler la fonction printf
dans une classe. Vous pouvez parfaitement écrire quelque chose comme:
public class Printf
{
public native String printf(String s);
}
et vous pourrez en fait compiler cette classe, mais
quand vous voudrez l'utiliser dans un programme, la machine virtuelle va vous
dire qu'elle ne peut pas trouver printf, et elle va émettre une erreur
UnsatisfiedLinkError. L'astuce consiste donc à fournir les informations
d'exécution nécessaires pour qu'elle puisse lier cette classe. Comme vous le
verrez bientôt, avec le JDK cela implique un processus en trois étapes:
- Générer un pseudo-code C pour une fonction qui fait la
translation entre l'appel de la plate-forme Java et la fonction C réelle. Le
pseudo-code réalise cette translation en prenant les informations de
paramètres de la pile de la machine virtuelle, pour les passer à la fonction C
compilée.
- Créer une bibliothèque partagée spéciale et exporter le
pseudo-code à partir de celle-ci.
- Utiliser une méthode spéciale, appelée
System.loadLibrary pour demander à l'environnement d'exécution Java de
charger la bibliothèque à partir de l'étape 2.
Nous allons maintenant vous montrer comment exécuter ces étapes
différents exemples, en partant d'un cas spécial d'utilisation triviale de
printf pour finir avec un exemple réaliste impliquant la fonction base de
registres pour des fonctions dépendantes de la plate-forme Java.
Travailler avec la fonction printf
Nous allons commencer par la situation
virtuellement la plus simple possible d'utilisation de printf: l'appel
d'une méthode native qui imprime le message "Hello, Native World". En fait, nous
n'allons même pas exploiter les fonctionnalités de formatage de printf!
C'est toutefois une excellente façon pour vous de vérifier que votre compilateur
C fonctionne comme prévu avant de tenter l'implémentation de méthodes natives plus élaborées.
Comme nous l'avons dit précédemment, vous devez déclarer la méthode native dans
une classe. Le mot clé native avertit le compilateur que la méthode sera
définie externe. Bien entendu, les méthodes natives ne contiennent aucun code en
langage Java, et l'en-tête de la méthode est immédiatement suivi du
point-virgule de fin. Cela signifie, comme vous l'avez vu dans l'exemple plus
haut, que les déclarations de méthodes natives ont un aspect analogue à celui
des déclarations de méthodes abstraites. class HelloNative
{
public native static void greeting();
...
}
Dans cet exemple particulier, remarquez que nous déclarons également la méthode
native comme static. Les méthodes natives peuvent être statiques et non
statiques. Cette méthode ne prend pas d'arguments; nous ne voulons pas pour
l'instant nous préoccuper du passage de paramètres.
Ecrivez ensuite une fonction C correspondante. Vous
devez nommer cette fonction exactement comme l'attend l'environnement
d'exécution de Java c'est-à-dire:
- Utilisez le nom complet de méthode Java, comme
HelloNative.greeting.
- Remplacez chaque point par un caractère de
soulignement, et faites précéder du préfixe Java_. Par
exemple,Java_HelloNative_greeting.
- Si le nom de la classe contient des caractères
qui ne sont ni des chiffres ni des lettres ASCII, par exemple, '_', '$', ou
des caractères Unicode '\u007F', remplacez-les par _0xxxx, où xxxx est la
séquence de quatre chiffres hexadécimaux de la valeur Unicode du caractère.
En réalité, personne ne fait cela manuellement; exécutez plutôt l'utilitaire
javah, qui génère automatiquement les noms de fonctions.
Passage de paramètres et valeurs renvoyées
Passage de types primitifs
Pour le passage de valeurs numériques entre C et Java, vous devez savoir
quels types correspondent à quels autres. Pare exemple, si C possède des types
de données appelés int et long, leur implémentation est dépendante
de la plate-forme. sur certaines plates-formes, les types entiers font 16 bits,
alors que sur d'autres ils font 32 bits. Pour cette raison, la JNI(Java
Native Interface) définit les types jint, jlong, ect... Voici
un tableau regroupant les correspondances entres les types natifs(C) et Java.

Voyons un exemple pour mieux comprendre. On va définir et utiliser une méthode
native qui ajoute deux entiers et renvoie le résultat de l'addition.
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 des
paramètres.
JNIEXPORT jint JNICALL Java_TestJNI1_ajouter
(JNIEnv *, jobject, jint, jint);
Il suffit ensuite écrire l'implémentation du code natif.
#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. Il faut
définir le fichier .def : l'exemple ci dessous va construire une bibliothèque
qui va contenir les fonctions natives des deux classes Java précédemment
définies.
EXPORTS
Java_TestJNI2_ajouter
Les objets sont passés par référence en
utilisant une variable de type jobject. Plusieurs autres type sont
prédéfinis par JNI pour des objets fréquemment utilisés :

Voici un exemple qui concatène 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 :
/*
* 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 de type 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 :
- la méthode GetStringUTFChars() permet de convertir
une chaîne de caractères Java en une chaîne de caractères de type C.
- la méthode NewStringUTF() permet de demander la
création d'une nouvelle chaîne de caractères.
- la méthode GetStringUTFLength() permet de connaître
la taille de la chaîne de caractères.
- la méthode ReleaseStringUTFChars() permet de
demander la libération des ressources allouées pour la chaîne de caractères
dès que celle ci n'est plus utilisée. Son utilisation permet d'éviter des
fuites mémoire.
Les méthodes équivalentes pour les chaînes de caractères au
format Unicode sont : GetStringChars(), NewString(),
GetStringUTFLength() et ReleaseStringChars():
#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 résultat. Après la compilation des différents
éléments, l'exécution affiche le résultat escompté.
La gestion des erreurs
Les méthodes natives constituent un risque significatif au
niveau de la sécurité des programmes en langage Java. Le système d'exécution C
n'a aucune protection contre les erreurs de limites de tableau, l'indirection
via des pointeurs erronés,etc. Il est particulièrement important que les
programmeurs de méthodes natives gèrent toutes les conditions d'erreur afin de
préserver l'intégrité de la plate-forme Java. En particulier, lorsque votre
méthode native diagnostique un problème qu'elle ne peut gérer, elle doit le
signaler à une machine virtuelle de Java. Dans un tel cas, vous lanceriez alors
naturellement une exception. Cependant, il n'y a pas d'exceptions en C. A la
place, vous devez appeler la fonction Throw ou ThrowNew pour créer
un nouvel objet exception. Lors de la sortie de la méthode native, la
machine virtuelle de Java va lancer cette exception.
Pour utiliser la fonction Throw, appelez NewObject pour créer un
objet d'un sosu-type de Throwable. Par exemple, ici, nous allouons un
objet EOFException et nous le lançons.
jclass class EOFException=(*env)->FindClass(env,"java/io/EOFException");
jmethodID id_EOFException=(*env)->GetMethodID(env,class_EOFException,"<init>","()V);
/* ID du constructeur par défaut */
jthrowable obj_exc=(*env)->NewObject(env,classEOFException,id_EOFException);
(*env)->Throw(env,obj_exc);
Il est généralement plus pratique d'appeler ThrowNew,
qui construit un objet exception, à partir d'une classe et d'une chaîne UTF. Ces
deux méthodes n'interrompent pas le flux de la méthode native. Ce n'est que lors
du retour de la méthode que la machine virtuelle lance réellement l'exception.
Par conséquent, chaque appel à Throw et ThrowNew doit toujours
être suivi immédiatement d'une instruction return.
Conclusion
Bien que nous ayons vu l'impossibilité des méthodes natives à
être portables, nous avons par contre pu constater le caractère interopérable du
langage Java avec d'autres langages exécutables. Ainsi, le programmeur dispose
d'une certaine flexibilité quant à l'utilisation de Java avec un langage
exécutable même si en termes de syntaxe cela s'avère plus difficile à réaliser.
|