| Cet article est paru dans le magazine francophone consacré à Linux et aux logiciels libres n°46 (janvier 2003). ©2003Editions Diamond |
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.
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.
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=.
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); }
}
}
}
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) { }
}
}
}
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.
RÉFÉRENCES