1 

Les méthodes natives

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:

  1. 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é.

  2. 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.

  3. 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:

  1. 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.
  2. Créer une bibliothèque partagée spéciale et exporter le pseudo-code à partir de celle-ci.
  3. 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:

  1. Utilisez le nom complet de méthode Java, comme HelloNative.greeting.
  2. Remplacez chaque point par un caractère de soulignement, et faites précéder du préfixe Java_. Par exemple,Java_HelloNative_greeting.
  3. 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
 

Passage d'objets

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.


1 

Retrouvez ci-dessous les autres sections du Laboratoire Sun
Evènements
Java Sun Net Talk LIVE CHAT le 2 Avril à 16h303/29/08
SolarisSunDécouvrez les nouveaux Sun Fire sous Intel10/11/07
JavaValtech Days10/9/07
JavaApacheCon du 1 au 4 mai à Amsterdam2/13/07

Exemples de code
JavaManipuler les looks and feel (lister et affecter)10/15/07
JavaFaire sa propre injection de dépendance avec les annotations5/9/06
JavaSplash screen avec progress Bar5/5/06
JavaFaire un splash screen en swing5/5/06

Actualités
SunProjet Kenai: une nouvelle forge open source10/3/08
SunSun Microsystems en forme !8/4/08
SunOpenDS un ldap 100% java7/24/08
SunSun et Fujitsu annoncent un nouveau Sparc647/16/08
SunVisualVM, un outil de surveillance des applications Java7/10/08

Tips du laboratoire
EclipseVisual Editor avec Eclipse Europa, c'est possible3/28/08
EclipseGérer les projets dans un workspace.10/16/07
JavaManager votre server d'application avec Eclipse4/21/07
JavaVue des sub-packages avec Eclipse4/21/07
JavaGlisser-déposer avec Eclipse4/21/07

Laboratoire SUPINFO des technologies Sun
labo-sun@supinfo.com


Conditions d'utilisation et © Copyright SUPINFO International University
23, rue de Château Landon - 75010 PARIS - Tél : +33 (0) 153359700 Fax : +33 (0) 153359701
Respect de la vie privée