P
'
t
i
t
e
C
h
a
t
t
e
 
spacer~ THE TROUBLE WITH LIFE IS THERE'S NO BACKGROUND MUSIC Articles | Connexion
 
~Java embarqué avec JNI

Précédent  
  Java  
  Suivant
 Présentation

Nous avons appris à manipuler la Java Native Interface pour permettre aux applications Java d'améliorer leurs performances en faisant appel à du code natif. Nous allons ce mois-ci découvrir un autre aspect de JNI, particulièrement méconnu.
 Sommaire


 Introduction

La plupart des développeurs comprennent aisément l'intérêt résidant dans l'intégration de code natif au sein d'une application Java, particulièrement lorsqu'un logiciel doit accéder à des ressources de bas niveau (par exemple un port série). Toutefois, rares sont ceux qui auront l'idée d'exécuter du code Java depuis leur programme C ou C++. Certaines applications se servent de cette technique avec succès. Matlab, un outil de calculs mathématiques, fait figure d'exemple très répandu d'un tel emploi de la technologie JNI. Celui-ci fait en effet appel à Java pour réaliser son interface graphique ainsi que son éditeur de texte. Nous pouvons ici apprécier l'intérêt d'un tel choix puisque toute la partie graphique du logiciel sera immédiatement disponible pour différents systèmes d'exploitation, et surtout, offrira le même aspect à l'utilisateur.


 Intégration d'une JVM dans un programme C++

Notre premier exemple concerne l'écriture d'un petit exécutable C++ servant à démarrer un outil Java. Le projet se trouve sur le CD-Rom accompagnant ce magazine dans le sous-répertoire xar/. Deux fichiers sources le composent : HDMirror.java et xar.cpp que vous pourrez compiler à l'aide de la commande make. La section Java de l'application ne recèle aucune particularité. Il s'agit d'un petit outil d'archivage de répertoires nécessitant quelques bibliothèques situées dans le répertoire xar/lib. Le code qui nous intéresse réside donc dans le fichier xar.cpp. Ce dernier se contente de créer une nouvelle machine virtuelle puis d'exécuter la méthode matin() de la classe HDMirror. Si nous pouvons réaliser cette opération en exécutant de simples commandes systèmes, nous nous baserons ici sur les fonctionnalités de l'API JNI. Ainsi, nous pourrons par la suite manipuler n'importe quel objet ou méthode Java directement depuis notre code C++. L'ensemble des actions relatives à Java s'effecte dans la fonction invokeJVM() qui réalise elle-même les étapes suivantes : création de la machine virtuelle, recherche de la classe HDMirror et invocation de la méthode main() avec transfert des paramètres de la ligne de commande. Les lignes de code suivantes désignent les déclarations indispensables à ce travail :

#include <jni.h>

static JNIEnv  *env;
static JavaVM  *jvm;
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Les deux variables définies ici seront renseignées lors de l'invocation de la fonction JNI_CreateJavaVM(). Nous aurons alors à notre disposition un pointeur vers la machine virtuelle elle-même (variable jvm) et vers l'environnement d'exécution du code Java (variable env). Ce dernier nous permettra d'accéder aux différentes caractéristiques des classes. Le listing 1 présente la création d'une JVM embarquée avec utilisation de paramètres. Ces derniers apparaissent dans le code sous la forme de chaînes de caractères membres d'une structure JavaVMOption. Celles-ci sont quant à elles contenues dans un tableau contenu dans la structure JavaVMInitArgs. Comme vous pouvez le constater, ces options nous permettent notamment d'indiquer à la machine virtuelle où se situent notre classe et les bibliothèques dont elle dépend. Nous avons ici préféré exploiter le paramètre -Djava.class.path (-D permettant de créer une nouvelle variable d'environnement pour la JVM) en lieu et place de -classpath dont le comportement se révélait erratique d'une plateforme à l'autre. Enfin, sachez que les attributs version et ignoreUnrecognized de vm_args servent à définir la version de JNI utilisée (le J2SE DK 1.4 propose la nouvelle version 1.4) et à ignorer les paramètres inconnus de la JVM. L'étape suivante de notre programme nécessite l'obtention d'une référence vers la classe HDMirror. Nous pouvons y parvenir par l'entremise de la méthode FindClass() qui accepte en paramètre le nom de la classe à rechercher. Faites attention cependant, le chargement d'une classe Login contenue dans le paquetage com.posse-press.mags demandera l'utilisation du nom "com/posse-press/mags/Login". Nous obtenons alors une valeur de type jclass ainsi que le démontre le listing 2. Rechercher une méthode nécessite un petit peu plus de travail. Tout d'abord, nous devons distinguer les méthodes de classes des méthodes d'instances. Les premières peuvent être découvertes en exécutant env->GetStaticMethodID() tandis que les secondes requièrent l'invocation de env->GetMethodID(). Néanmoins, nous utiliserons des paramètres identiques et récupérerons dans les deux cas une valeur de type jmethodID permettant à la JVM d'identifier la méthode ultérieurement. Le listing 3 vous présente les instructions nécessaires à l'obtention de l'identificateur de la méthode main() de notre classe. Ce listing utilise trois arguments désignant la classe cible, le nom de la méthode à trouver et surtout sa signature car il s'agit de la seule manière de la distinguer parmi ses surcharges. Pour JNI, une signature se définit de manière générale : (types des arguments;)type de retour. Le tableau 1 définit tous les types que vous pouvez employer pour rédiger une signature. D'après celui-ci, une méthode JFrame getWindow(int x, int y) possédera la signature "(II)Ljavax/swing/JFrame". Ainsi, le main() d'une classe correspond à ([Ljava/lang/String;)V. Seuls les constructeurs bénéficient d'un cas particulier puisque leur nom doit être et leur valeur de retour V. Maintenant que nous disposons d'une référence vers notre méthode, nous pouvons enfin l'exécuter. JNI nous propose à cet effet un grand nombre de solutions que nous pouvons classer en deux familles, CallMethod() et CallStaticMethod(). Vous l'aurez compris, les méthodes d'instance et de classe ne bénéficient pas du même traitement. En outre, l'appel JNI dépend de la valeur de retour de la cible. Ainsi, nous pourrons utiliser env->CallIntMethod() pour traiter l'invocation de size() sur un objet Vector, tandis que nous préférerons env->CallStaticVoidMethod() pour traiter le cas du main() :

env->CallStaticVoidMethod(cls, mid, args);
       
      
JextCopier dans Jext


Gestion de la communication entre les modules Java et C++ de qoj.

Si vous essayez de lancer le programme HDMirror, vous constaterez que celui-ci requiert une liste d'arguments passés depuis la ligne de commande. Nous allons donc transférer ceux fournis au programme C++ à notre méthode Java main() par l'intermédiaire de la variables args que nous construisons sur le principe évoqué le mois dernier (voir listing 4). Tel quel notre programme peut parfaitement fonctionner. Un détail subsiste toutefois : nous devons libérer les ressources allouées pour la JVM lorsque nous en avons terminé avec elle :

jvm->DestroyJavaVM();
       
      
JextCopier dans Jext

 Compilation, exécution et amélioration

La dernière phase délicate du développement d'une application C/C++ embarquant une machine virtuelle Java consiste en l'étape de compilation. Une application simple telle que la nôtre requière une liaison avec les bibliothèque libjava.so libverify.so et libjvm.so que vous pourrez retrouver dans vos répertoires $JAVAHOME/jre/lib/i386 et $JAVAHOME/jre/lib/i386/client. Le Makefile correspondant à notre exemple se trouve dans le listing 5. Malheureusement, toutes ces précautions ne suffiront pas à exécuter le binaire final. En effet, pour lancer un tel hybride, la variable d'environnement LD_LIBRARY_PATH doit impérativement contenir les chemins des bibliothèques utilisées lors de compilation puisque nous avons réalisé une liaison dynamique. Si vous désirez vous exercer, vous pouvez essayer d'ajouter une interface graphique Swing à xar sans modifier le code source Java. Ce dernier expose publiquement toutes les méthodes nécessaires à son fonctionnement. Vous devrez donc employer JNI à bon escient. Le listing 6 présente un petit exemple qui permet d'afficher une nouvelle fenêtre Swing vide à l'écran. Ce nouvel extrait de code met en évidence la fonction NewObject() de l'environnement d'exécution, permettant d'obtenir une instance d'une classe à partir d'une méthode (un constructeur donc). La référence obtenue se présente sous la forme d'un type jobject. Nous pourrons ainsi le passer en paramètre aux fonctions CallMethod() pour en exécuter ses méthodes. Le listing 6 peut donc se traduire ainsi en Java :

JFrame f = new JFrame();
f.setVisible(true);
       
      
JextCopier dans Jext | Jext | Plugin Codegeek

 Qt, C++, OpenGL et. Java

Notre second exemple propose une utilisation un peu plus concrète de JNI en environnement C++. L'application qoj, basée sur l'exemple sharedbox de la distribution Qt 3.2, se propose d'afficher une belle boîte à l'aide d'OpenGL au sein d'une fenêtre Qt. Et pour manipuler notre objet 3D, le programme dispose d'un menu écrit en Java donnant la possibilité à l'utilisateur de le faire tourner. Cet exemple soulève le problème de la communication entre une interface graphique Java et l'application C++. Une solution évidente consiste à utiliser des méthodes natives pour réagir aux événements. Le code C++ invoquera donc du code Java qui exécutera lui-même du code C++. Nous pouvons également nous reposer sur des systèmes de client/serveur, comme le fait qoj en créant un tube nommé. Ainsi, lorsque l'utilisateur modifie la valeur des glissières dans l'interface Swing, nous envoyons des données dans un tube nommé qoj.pipe. Par exemple, pour indiquer une rotation de 92 degrés sur l'axe X nous envoyons le caractère 'x' suivi de l'entier 92, codé sur 16 bits. De son côté, le programme Qt exécute un thread qui, toutes les 20 millisecondes, essaye de lire le contenu du tube pour en retirer ses directives. Nous devons alors faire face à un nouveau problème : le contexte OpenGL de Qt n'est pas "thread-safe". Autrement dit, nous ne pouvons pas demander une mise à jour de l'affichage OpenGL à partir d'un thread autre que le principal. L'astuce consiste ici à placer les directives de rotation dans des queues (FIFO) plutôt que d'effectuer directement les rotations. En outre, un objet QTimer doit être mis à contribution. Un tel objet permet d'exécuter une action toutes les 20 millisecondes par exemple. L'avantage du QTimer réside dans son exécution au sein du thread principal de l'application. Son rôle consistera donc à récupérer les données présentes dans les queues (nous en utilisons une par axe de rotation) et à invoquer les méthodes de rotation en conséquence. Vous trouverez l'ensemble de ces solutions dans le fichier source qlobjwin.cpp du projet qoj/ sur le CD-Rom. Un autre problème important relatif à l'utilisation de Java dans une application X11 intervient lorsque le développeur souhaite dessiner sur la fenêtre Swing directement depuis le code C++. Ceci est tout à fait réalisable mais nécessite l'utilisation d'une nouvelle API appelée JAWT, intégrée à JNI. Le principe consiste à obtenir la surface X11 correspondant à un canevas AWT (classe java.awt.Canvas) puis d'y apposer un verrou. Ce dernier est indispensable pour empêcher un plantage pur et simple de l'application. Il serait en effet désastreux que deux threads tentent de dessiner en même temps sur la même surface. Ainsi, une fois ce verrou posé, nous pouvons récupérer les informations nécessaires pour l'exécution de primitives de dessin X11. Le listing 7 contient une méthode lockAndDrawAWT() prenant en paramètre un canevas AWT. Cette méthode a été intégrée au programme qoj pour vous permettre d'essayer facilement ce système de dessin assez particulier. N'oubliez pas que vous pouvez parfaitement utiliser cette technique pour surcharger nativement les méthodes paint() des objets Java. Enfin, sachez que pour compiler un programme JAWT, vous devrez inclure la bibliothèque libjawt.so lors de la phase d'édition des liens Ces exemples relativement simples devraient vous permettre d'imaginer quantité d'applications possible de JNI. Combiner l'efficacité du C ou du C++ à la clarté et à la portabilité de Java pourrait vous permettre de réaliser de beaux projets dont aucun aspect n'aura été négligé.

Note 1
Les signatures et leurs équivalents en Java :
  • V pour void
  • Z pour boolean
  • B pour byte
  • C pour char
  • S pour short
  • I pour int
  • J pour long
  • F pour float
  • D pour double
  • LMaClasse; pour instance de MaClasse
  • [type pour un tableau de type (type[])


 Listings

Listing 1

jint res;
char classpath[1024];

JavaVMInitArgs  vm_args;
JavaVMOption options[1];

sprintf(classpath, "%s", "-Djava.class.path=lib/:lib/soap.jar:lib/xerces.jar:lib/xml.jar");
options[0].optionString = classpath;
vm_args.version = JNI_VERSION_1_2;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = 1;

res = JNI_CreateJavaVM(&jvm, (void **) &env, &vm_args);
if (res < 0)
{
  cout << "Impossible de créer la JVM." << endl;
  return;
}
       
      
JextCopier dans Jext | Jext | Plugin Codegeek
Listing 2

jclass cls;
cls = env->FindClass("HDMirror");
if (!cls)
{
  cout << "Impossible de charger HDMirror.class" << endl;
  return;
}
       
      
JextCopier dans Jext
Listing 3

jmethodID mid;
mid = env->GetStaticMethodID(cls, "main", "([Ljava/lang/String;)V");
if (!mid)
{
  cout << "Impossible de trouver main()." << endl;
  return;
}
       
      
JextCopier dans Jext
Listing 4

jstring jstr;
jobjectArray args;

args = env->NewObjectArray(argc - 1, env->FindClass("java/lang/String"), NULL);
if (args)
{
  for (int i = 1; i < argc; i++)
  {
    jstr = env->NewStringUTF(argv[i]);
    env->SetObjectArrayElement(args, i - 1, jstr);
  }
}
       
      
JextCopier dans Jext
Listing 5

JAVAHOME=/usr/java/j2sdk1.4.2_01
INC=-I${JAVAHOME}/include -I${JAVAHOME}/include/linux
LIB=-L${JAVAHOME}/jre/lib/i386 -L${JAVAHOME}/jre/lib/i386/client -ljava -ljvm -lverify

all: native

lib/HDMirror.class: src/HDMirror.java
  javac -classpath lib/xerces.jar:lib/soap.jar:lib/xml.jar -d lib src/*.java

native: lib/HDMirror.class src/xar.cpp
  g++ ${INC} ${LIB} -o xar src/xar.cpp
       
      
JextCopier dans Jext
Listing 6

jobject obj;
cls = env->FindClass("javax/swing/JFrame");
mid = env->GetMethodID(cls, "<init>", "()V");
obj = env->NewObject(cls, mid);
mid = env->GetMethodID(cls, "setVisible", "(Z)V");
env->CallVoidMethod(obj, mid, JNI_TRUE);
       
      
JextCopier dans Jext
Listing 7

#include <jawt.h>
#include <jawt_md.h>
#include <X11/Xlib.h>

void GLObjectWindow::lockAndDrawAWT(jobject &canvas)
{
  JAWT awt;
  JAWT_DrawingSurface *ds;
  JAWT_DrawingSurfaceInfo *dsi;
  JAWT_X11DrawingSurfaceInfo *dsi_X11;
  jint lock;
  Window  win;
  Display *dpy;

  awt.version = JAWT_VERSION_1_3;
  if (JAWT_GetAWT(env, &awt) == JNI_FALSE)
  {
     exit(11);
  }

  ds = awt.GetDrawingSurface(env, canvas);
  if (ds == NULL)
  {
     exit(12);
  }

  lock = ds->Lock(ds);
  if ((lock & JAWT_LOCK_ERROR) != 0)
  {
    exit(13);
  }

  dsi = ds->GetDrawingSurfaceInfo(ds);
  if (dsi == NULL)
  {
    exit(14);
  }

  dsi_X11 = (JAWT_X11DrawingSurfaceInfo*) dsi->platformInfo;
  dpy = dsi_X11->display;
  win = dsi_X11->drawable;
  // . dessiner .

  ds->FreeDrawingSurfaceInfo(dsi);
  ds->Unlock(ds);
  awt.FreeDrawingSurface(ds);
}
       
      
JextCopier dans Jext

 Téléchargements

Vous pouvez télécharger notre exemple QOJ.



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