P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ ETHERNET (N): SOMETHING USED TO CATCH THE ETHERBUNNY Articles | Connexion
 
~Appels natifs avec JNI

Précédent  
  Java  
  Suivant
 Présentation

La plate-forme Java a prouvé ses capacités dans un large champ d'applications. Néanmoins, certaines opérations ne peuvent être effectuées à l'aide de cette seule technologie. Nous aurons donc recours à l'API JNI.
 Sommaire


 Introduction

L'API intitulée Java Native Interface (JNI) apporte une réponse au problème de l'insuffisance des services de la plate-forme Java. Il s'agit d'une interface de programmation native présente dans le Java2 SDK. L'écriture de programmes à l'aide de JNI vous garantira en outre, dans une certaine mesure, la portabilité de votre application. La bibliothèque JNI vous permet donc d'exécuter du code écrit dans un langage dit natif, comme le C, le C++ ou l'assembleur, depuis votre programme Java. Inversement, il vous sera permis d'accéder à l'ensemble des possibilités de Java depuis une application native. En tant que développeur, l'utilisation de méthodes natives au sein de vos produits se justifiera dans certaines situations particulières. Par exemple, une section de code critique en termes de performances pourra bénéficier de la puissance d'une bibliothèque native. On pourra ainsi accélérer de lourds calculs en se reposant sur du code assembleur. Il se peut également que vous souhaitiez employer des fonctionnalités offertes par le système d'exploitation ou toute bibliothèque native qui ne sont pas à votre disposition en Java. De cette manière, vous serez capable de rédiger des programmes de gestion du système avec une interface graphique Swing. Bien évidemment, le code natif que vous invoquerez pourra aussi bien être représenté par une application complète que par une solution écrite spécifiquement pour vos besoins. L'utilité de JNI pourrait se révéler particulièrement limitée s'il n'était possible d'employer des objets Java au sein du code natif. Ainsi, depuis un code source C, vous serez en mesure de créer des objets Java tels que des chaînes de caractères ou des objets graphiques puis de les manipuler en invoquant leurs méthodes. En attachant une méthode native à un objet Java, vous obtiendrez la capacité d'accéder à ce dernier. De fait, la partie native de votre programme pourra étendre vos classes sans aucune restriction.


 Optimisation des performances avec JNI

Notre premier exemple d'application basée sur JNI propose une amélioration importante des temps d'exécution lors du calcul de sinus et de cosinus. En effet, la version 1.4 de J2SE a dégradé les performances des calculs trigonométriques effectués par l'entremise de la classe java.lang.Math. Ainsi en invoquant les méthodes sin() et cos() de cette dernière, avec une JVM J2SE 1.4.2 et un processeur Pentium IV 1.6 Ghz, le calcul de 100 000 cosinus et sinus nécessite 0.12 secondes. A l'aide de JNI et d'un peu de code C, nous pouvons doubler les performances en effectuant les mêmes calculs en 0.06 secondes. La première étape de notre travail, la plus simple, consiste simplement à rédiger le code Java qui sera lié à JNI. Le listing 1 présente le code complet de la classe FastMath contenant deux méthodes cosinus() et sinus(). Vous constaterez que l'écriture de l'implémentation d'une méthode dans un langage natif impose l'utilisation du mot-clé native dans la déclaration associée. Ce mot-clé indique au compilateur Java que le corps de la méthode ne se trouve dans aucune classe du programme. Nous avons donc ici déclaré deux méthodes natives, publiques, acceptant et retournant un double. Préciser la nature des méthodes ne suffit toutefois pas pour effectuer le lien avec le code natif : il est impératif de charger une bibliothèque partagée contenant les implémentations. Pour ce faire, le programmeur doit exécuter l'instruction System.loadLibrary() depuis un bloc d'initialisation statique. L'unique argument accepté par cette méthode de chargement correspond au nom de la bibliothèque partagée contenant votre code. Puisque Java se doit d'être aussi abstrait que possible, ce nom se voit automatiquement complété par la JVM pour trouver le fichier à charger. Ainsi, sur une plate-forme Unix (Solaris, Linux.), le nom de bibliothèque fastmath correspondra à libfastmath.so. Sur un système d'exploitation Windows, se nom sera transformé en fastmath.dll. L'exécution de l'instruction loadLibrary() aura lieu lors du chargement de la classe FastMath et se révèle susceptible de générer une exception. Dans ce cas, par exemple si aucun fichier ne correspond au nom de bibliothèque native, nous pourrons tenter de limiter l'étendue des dégâts. Après écriture de notre classe, nous pouvons la compiler puis rédiger un point d'entrée pour la tester. La compilation du code Java déclarant des méthodes natives est nécessaire pour la suite des opérations.


 Côté natif

A ce stade du développement, un élément vital manque encore : le lien entre le code Java et le code C. Les environnements de développement J2SE proposent en standard un outil appelé javah. Celui-ci permet de générer des fichiers en-tête .h correspondant aux méthodes natives d'une classe Java. Bien que de nombreuses options soient disponibles, une commande très simple nous suffira :

$ javah FastMath
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Le seul argument requis correspond au nom de la classe à traiter. Après intervention de l'outil, nous obtenons un fichier FastMath.h, placé dans le même répertoire que le fichier FastMath.class. Notez bien que javah agit sur du bytecode compilé et non sur un code source. Le fichier généré pour notre exemple se trouve dans le listing 2. Comme nous pouvions le deviner, deux fonctions y sont déclarées : Java_FastMath_cosinus() et Java_FastMath_sinus(). La règle de qualification des fonctions suit un schéma très simple puisque au préfixe "Java_" est ajouté le nom complet, prenant donc en compte le paquetage, de la classe puis de la méthode Java correspondante. Le premier paramètre d'une fonction JNI correspond toujours à un pointeur vers l'interface JNIEnv. A travers elle, vous pourrez accéder aux méthodes des objets Java. Ce qui nous amène au deuxième paramètre, de type jobject, qui désigne l'objet sur lequel la fonction doit s'appliquer. Toutefois, si nous avions déclaré des méthodes de classes, statiques donc, ce paramètre correspondrait à une référence vers la classe courante, de type jclass. Le dernier paramètre, de type jdouble, pour sa part s'avère spécifique à notre implémentation et correspond à l'argument Java de type double. De la même manière, un argument int deviendrait jint dans notre en-tête. Ne vous affolez pas, la conversion de l'un à l'autre des types est automatique. Maintenant que nous possédons une interface de programmation précise, nous pouvons procéder à l'étape d'implémentation. Notre exemple, très simple, ne nécessite que deux lignes de code, de simples appels aux fonctions de l'API standard math.h. Le listing 3 contient le code complet du fichier FastMath.c qui reprend au caractère près les prototypes définis dans FastMath.h. Enfin, avant de pouvoir exécuter notre programme Java, nous devons compiler le code C sous forme de bibliothèque partagée. Voici la commande pour Linux :

gcc -I/usr/java/include -I/usr/java/include/linux -c FastMath.c
gcc -shared -o libfastmath.so FastMath.o
       
      
JextCopier dans Jext
Sous Windows, puisque vous devrez probablement compiler la bibliothèque pour chaque système supporté, vous pourrez utiliser le compilateur Visual C++ :

cl -Ic:\j2sdk_1.4.2\include -I c:\j2sdk_1.4.2\include\win32 -LD FastMath.c -FeFastMath.dll
       
      
JextCopier dans Jext
Il se peut que vous obteniez une erreur de type UnsatisfiedLinkError lors de l'exécution de l'application. Dans ce cas, assurez-vous que votre bibliothèque est accessible par la JVM (répertoire courant, variable d'environnement LD_LIBRARY_PATH.).



JNI fait le pont entre l'application Java et le code natif.


 Protéger le code

Pour protéger votre application contre une erreur de chargement de la bibliothèque native, nous allons étudier une solution particulièrement simple. Vous trouverez sur le CD-Rom accompagnant le magazine un outil intitulé Jype mettant en ouvre cette technique tandis que le listing 4 présente les quelques lignes de code à considérer. A l'aide d'une simple variable booléenne nous conservons le statut du chargement (true pour succès et false pour échec). De cette manière, chaque fois que nous essayerons d'invoquer une méthode de la classe FontTypeSupport nous devrons vérifier que isValid() renvoie la valeur vrai :

if (FontTypeSupport.isValid())
{
  if (FontTypeSupport.isTrueType(this.baseFont))
    // .
}
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Cette solution s'adapte parfaitement aux méthodes natives que vous n'invoquerez pas depuis de nombreux endroits dans votre code. Dans le cas contraire, le résultat pourra devenir assez lourd. Une autre solution s'offre donc à vous. Il suffit de déclarer une interface contenant les définitions des méthodes, puis de l'implémenter dans deux classes DummySupport et NativeSupport. La première proposera une implémentation minimale avec des valeurs par défaut qui permettront à votre application de tourner correctement, tandis que la seconde ne contiendra que des méthodes natives. Vous pourrez vous référer au listing 5 pour un exemple. Cette nouvelle approche, plus élégante, nécessite toutefois une structure de classes plus complexe, vous devrez donc choisir la méthode la plus appropriée à votre outil.


 Types de données

Nous sommes dorénavant familiers avec le type jdouble. Si vous avez eu la curiosité d'essayer les différents types primitifs de Java avec JNI vous n'avez du avoir aucune surprise : jboolean, jfloat, void, jchar, et ainsi de suite, parlent d'eux-mêmes. Quatre autres types particuliers existent et correspondent respectivement aux classes String et Class, aux vecteurs et à l'interface Throwable. Ces types s'intitulent jstring, jclass, jarray et jthrowable. Le cas de jarray est un petit peu particulier puisqu'il se décline lui-même en sous-types correspondants aux primitives. Nous pourrons ainsi manipuler jintArray ou jobjectArray. Seul les types jstring et jarray nous intéresseront dans un premier temps. Les vecteurs de données ne peuvent malheureusement pas s'employer directement avec l'opérateur d'indexation. Nous devons utiliser les services de JNIEnv pour obtenir un pointeur sur les données contenues dans le jarray. Ainsi, pour parcourir un vecteur de booléens nous procéderons de la sorte :

jsize len = (*env)->GetArrayLength(env, arr);
jboolean* bools = (*env)->GetBooleanArrayElements(env, arr, 0);

for (i = 0; i < len; i++)
  printf("%b", bools[i]);

(*env)->ReleaseBooleanArrayElements(env, arr, bools, 0);
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Note 1
Les utilisateurs du langage C++ devront être vigilants quant à l'emploi de JNIEnv. En effet, une instruction C (*env)->Fonction(env, .) deviendra env->Fonction(.) en C++.

Outre ces trois fonctions, vous pourrez invoquer GetObjectArrayElement(env, arr, index) et SetObjectArrayElement pour consulter ou modifier un élément précis d'un vecteur. Les chaînes de caractères posent le même problème puisque nous ne pouvons les utiliser telles quelles. Prenons l'exemple d'une fonction native supposée agir comme une invite de commande :

JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring invite)
       
      
JextCopier dans Jext
Nous devrons ici afficher la valeur de "invite" dans la console, puis renvoyer une chaîne lue depuis l'entrée standard. La conversion d'une String Java en chaîne C requière également les services de JNIEnv :

const char *str = (*env)->GetStringUTFChars(env, invite, 0);
printf("%s", str);
 (*env)->ReleaseStringUTFChars(env, invite, str);
       
      
JextCopier dans Jext
A l'instar des vecteurs, il est important de libérer les objets alloués pour prévenir toute fuite mémoire. La création d'une String Java se révèle pour sa part un peu plus simple :

char buffer[256];
scanf("%s", buffer);
return (*env)->NewStringUTF(env, buffer);
       
      
JextCopier dans Jext
L'interface JNIEnv vous permet de réaliser bien d'autres choses, comme les appels de méthodes ou la consultation de membres d'instance. Nous étudierons cet aspect de l'API JNI le mois prochain, en apprenant à inclure une machine virtuelle Java au sein d'un programme C ou C++. Pour compléter l'apprentissage des invocations de méthodes natives depuis du code Java, vous pourrez consulter le code source de JeXMMS, un plugin pour l'éditeur Jext écrit par Julien Ponge, permettant de piloter XMMS depuis le logiciel. Ce programme repose notamment sur l'invocation de fonctions implémentées par libxmms.so. Ceci pourra vous servir de point de départ pour réaliser de belles interfaces Swing pour vos programmes Linux préférés.

Note 2
Type JavaType natifTaille en bits
booleanjboolean8, non signé
bytejbyte8
charjchar16, non signé
shortjshort16
intjint32
longjlong64
floatjfloat32
doublejdouble64
voidvoid/


 Listings

Listing 1

public class FastMath
{
  public native double cosinus(double radians);
  public native double sinus(double radians);

  static
  {
    System.loadLibrary("fastmath");
  }
}
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Listing 2

#include <jni.h>
#include "FastMath.h"
#include <math.h>

JNIEXPORT jdouble JNICALL Java_FastMath_cosinus(JNIEnv *env, jobject obj, jdouble radians)
{
  return cos(radians);
}

JNIEXPORT jdouble JNICALL Java_FastMath_sinus(JNIEnv *env, jobject obj, jdouble radians)
{
  return sin(radians);
}
       
      
JextCopier dans Jext
Listing 3

#include <jni.h>
#include "FastMath.h"
#include <math.h>

JNIEXPORT jdouble JNICALL Java_FastMath_cosinus(JNIEnv *env, jobject obj, jdouble radians)
{
  return cos(radians);
}

JNIEXPORT jdouble JNICALL Java_FastMath_sinus(JNIEnv *env, jobject obj, jdouble radians)
{
  return sin(radians);
}
       
      
JextCopier dans Jext
Listing 4

public class FontTypeSupport
{
  private static boolean isValid = true;

  static
  {
    try
    {
      System.loadLibrary("JypeFontTypeSupport");
    } catch (Exception e) {
      isValid = false;
    } catch (Error err) {
      isValid = false;
    }
  }

  public static boolean isValid() { return isValid; }

  public static native boolean isTrueType(Font f);
  public static native boolean isType1(Font f);
}
       
      
JextCopier dans Jext
Listing 5

interface FontSupport
{
  public boolean isTrueType(Font f);
}

class DummyFontSupport implements FontSupport
{
  public boolean isTrueType(Font f) { return true; }
}

class NativeFontSupport implements FontSupport
{
  static { System.loadLibrary("fontsupport"); }
  public native boolean isTrueType(Font f);
}

class App
{
  public static FontSupport getFontSupport()
  {
    try
    {
      return new NativeFontSupport();
    } catch (Error e) {
      return new DummyFontSupport ();
    }
  }

  // .
}
       
      
JextCopier dans Jext

 Téléchargement

Vous pouvez télécharger de nombreux exemples pour suivre cet article.



par Romain Guy
romain.guy@jext.org
http://www.jext.org
Dernière mise à jour : 14/10/2006


Précédent  
  Java  
  Suivant

 
#ProgX©2005 Mathieu GINOD - Romain GUY - Erik LOUISE