Cet article est paru dans le magazine
francophone consacré à Linux et aux
logiciels libres n°46 (janvier 2003).

©2003
Editions Diamond


Bibliothèques natives

Cette série a pour but de démontrer la possibilité de construire un environnement utilisateur entièrement en Java/Swing au-dessus de Linux et XFree. Ce quatrième article s'intéresse à l'utilisation de bibliothèques natives.

Objectif

La qualité des logiciels du système GNU/Linux se retrouve notamment dans leur modularité. Les fonctionnalités principales sont en général séparées des interfaces utilisateur, regroupées dans une bibliothèque et accessibles au moyen d'une API bien définie. Il s'agit donc d'utiliser depuis Java de telles bibliothèques, souvent écrites en C et dont un bon exemple est libxmms.so.

Appel d'une fonction C

Java fournit un moyen standard (et assez élégant) d'appeler des fonctions C et dénommé JNI (Java Native Interface). L'idée est de déclarer les fonctions natives dans des classes Java. Celles-ci sont ensuite analysées pour en extraire les déclarations et un fichier d'en-tête est généré. L'implantation se fait donc de Java vers C.

Ceci va être illustré avec le contrôle du volume sonore du canal de sortie PCM du mixeur (périphérique /dev/mixer). On déclare donc une méthode native setVolume qui va permettre de fixer la valeur du volume stéréophonique. Les deux paramètres sont des entiers compris entre 0 et 100 et correspondant au niveau souhaité pour les canaux gauche et droit.

// MixerPcm.java
public class MixerPcm {
  public static native void setVolume
    (int _left, int _right);
}

Ce source est ensuite traité par la commande javah pour générer le header C. La traduction est assez immédiate. Le premier paramètre est un pointeur sur l'environnement (la MVJ), le second sur l'instance ou la classe courante, les suivants correspondent aux paramètres de la méthode Java.

/* MixerPcm.h */
#include <jni.h>
JNIEXPORT void JNICALL Java_MixerPcm_setVolume
  (JNIEnv *, jclass, jint, jint);

Les types utilisés sont des aliasses de manière à assurer la portabilité. Ainsi un jint correspond à un long en C sur une machine 32-bits. A ce stade, il ne reste plus qu'à implanter (si ce n'est déjà fait) le corps des fonctions C.

/* MixerPcm.c */

#include <sys/ioctl.h>
#include <fcntl.h>
#include <linux/soundcard.h>

const char *sound_device_names[]
  =SOUND_DEVICE_NAMES;

void set_volume(int _left,int _right)
{
  int fd,devmask,i,level;

  fd=open("/dev/mixer",O_RDONLY);
  ioctl(fd,SOUND_MIXER_READ_DEVMASK,&devmask);

  // Recherche du périphérique
  for(i=0;i<SOUND_MIXER_NRDEVICES;i++)
    if(  ((1<<i)&devmask)
       &&!strcmp("pcm",sound_device_names[i]))
      break;

  // Encodage des niveaux
  level=(_left<<8)+_right;
  ioctl(fd, MIXER_WRITE(i), &level);

  close(fd);
}

// Pour tester sous bash
main(int argc, char *argv[])
{
  set_volume(atoi(argv[1]),atoi(argv[2]));
}

// Pour JNI
#include "MixerPcm.h"

JNIEXPORT void JNICALL Java_MixerPcm_setVolume
(JNIEnv *_env, jclass _class, jint _left, jint _right)
{
  set_volume(_left,_right);
}

La compilation ne pose pas de problème particulier. Il faut simplement indiquer l'emplacement des déclarations JNI et activer l'option -shared pour créer une bibliothèque partagée. A l'exécution, il faut soit déplacer le fichier résultat dans un emplacement standard (/lib, /usr/lib), soit définir un chemin de recherche à l'aide de la variable d'environnement LD_LIBRARY_PATH.

javac MixerPcm.java
javah MixerPcm

gcc -o libjmixerpcm.so -shared \
  -I$JAVA_HOME/include \
  -I$JAVA_HOME/include/linux \
  MixerPcm.c

export LD_LIBRARY_PATH=.

Utilisation transparente

Rien ne distingue, dans son utilisation, une méthode native d'une méthode normale. Ci-dessous, voici le code d'un effet panoramique où le son passe d'un haut-parleur à l'autre.

public class TestPan {
  static {
    // Chargement du code objet dans la MVJ
    System.loadLibrary("jmixerpcm");
  }

  private static void sleep(int _d) {
    try { Thread.sleep(_d); }
    catch(InterruptedException ex) { }
  }

  public static void main(String[] _args) {
    final int MAX=70;
    while(true) {
      for(int i=0;i<MAX;i++)
        { MixerPcm.setVolume(i,MAX-i); sleep(5); }
      for(int i=0;i<MAX;i++)
        { MixerPcm.setVolume(MAX-i,i); sleep(5); }
    }
  }
}

Réutiliser une bibliothèque existante

La première partie a décrit la manière conventionnelle de coupler du code écrit en C et en Java. La seconde partie montre comment réutiliser rapidement une bibliothèque existante, en l'occurrence celle de XMMS, et comment automatiser autant que possible le processus. L'idée est d'inverser le sens du procédé et de partir des déclarations C pour reconstruire l'ensemble. Pour se faire, on utilise Alma - mon projet libre principal ;-) - qui dispose depuis la version 0.38 des cibles appropriées à cette tâche.

La première étape consiste à lire les déclarations de xmms.h. Comme ces déclarations utilisent les types de la glibc, il faut soit déclarer ceux-ci, soit préprocesser le fichier. Et comme Alma ne dispose pas encore de préprocesseur C intégré, on utilise celui du système:

cpp -I /usr/include/glibc-1.2 \
  -I/usr/lib/glib/include \
  /usr/include/xmms/xmmsctrl.h >xmms.I

Et on extrait la partie intéressante:

typedef char gchar;
typedef int gint;
typedef gint gboolean;
[...]
void xmms_remote_play(gint session);
void xmms_remote_pause(gint session);
[...]

A partir de ce fichier xmms.I, on peut générer le code Java correspondant. La variable reversejni.code.classname sert à indiquer la classe à générer et permet de simplifier le nom des méthodes. L'option -p indique la nature de la source (ici du code C/C++) et l'option -g la cible désirée.

alma-cl -p Cpp -g ReverseJniJ \
  -s "reversejni.code.classname=xmms_remote" \
  xmms.I >XmmsRemote.java

// Résultat
public final class XmmsRemote {
  public native static void play(int _session);
  public native static void pause(int _session);
  [...]
}

On utilise ensuite la commande javah ou la cible équivalente d'Alma pour obtenir l'en-tête C.

alma-cl -p Java -g JniH XmmsRemote.java \
  >XmmsRemote.h

// Résultat
#include <jni.h>
JNIEXPORT void JNICALL  Java_XmmsRemote_play
  (JNIEnv *, jclass, jint);
JNIEXPORT void JNICALL  Java_XmmsRemote_pause
  (JNIEnv *, jclass, jint);
[...]

Finalement, on génère le code C qui sert de passerelle, nécessaire en raison du nommage des fonctions imposé par Java et de certaines conversions. Et on compile le tout.

alma-cl -p Java -g ReverseJniC \
  -s "reversejni.code.classname=xmms_remote" \
  XmmsRemote.java >XmmsRemote.c

gcc -shared -I/usr/include/glib-1.2 \
  -I/usr/lib/glib/include \
  -I$JAVA_HOME/include \
  -I$JAVA_HOME/include/linux \
  XmmsRemote.c -lxmms -o libjni_xmmsremote.so

javac XmmsRemote.java

// Résultat
#include "XmmsRemote.h"
JNIEXPORT void JNICALL Java_XmmsRemote_play
  (JNIEnv *_env, jclass _class, jint _session)
  { xmms_remote_play(_session); }
JNIEXPORT void JNICALL Java_XmmsRemote_pause
  (JNIEnv *_env, jclass _class, jint _session)
  { xmms_remote_pause(_session); }
[...]

Et pour finir un petit exemple d'utilisation, qui boucle sur des échantillons de 400ms:

public class TestXmms {
  static {
    System.loadLibrary("xmms");
    System.loadLibrary("jni_xmmsremote");
  }

  public static void main(String[] _args) {
    while(true) {
      try {
	  int t=XmmsRemote.getOutputTime(0);
	  XmmsRemote.pause(0);
	  XmmsRemote.jumpToTime(0,Math.max(0,t-300));
	  XmmsRemote.play(0);
	  Thread.sleep(380);
      }
      catch(InterruptedException ex) { }
    }
  }
}

Conclusion

Cet article a introduit JNI et montré comment appeler de manière simple des fonctions C. La seconde partie a proposé une démarche pour automatiser l'intégration des bibliothèques de GNU/Linux dans la MVJ. L'ensemble, bien que dense, aura peut-être suscité votre intérêt et vous pourrez dans ce cas consulter le didacticiel JNI de Sun Microsystems.

____
Guillaume Desnoix
Gödöllö, août 2002
RÉFÉRENCES Ring
desktop ICONS PIXELS, Women Supermodels, Useful daily, binoculars, , ...