février
2008
« Write once, run anywhere » : le slogan de Java a toujours mis en avant la portabilité du langage et de ses APIs, en promettant qu’un même code pourra être exécuter n’importe où. Et si on peut dire que cela est globalement vrai, ce n’est pas toujours un avantage !
En effet l’API se trouve ainsi dépourvu de certaine fonctionnalité qui peuvent sembler « basique » sur un système, mais qui ne sont pas forcément disponible sur d’autres. Et même si Java à récemment mis de l’eau dans son vin en incorporant des fonctionnalités « optionnelles » ou au fonctionnement dépendant du système hôte, il reste toujours nécessaire de s’attaquer au code natif dès que l’on s’approche un peu trop du système…
Au grand malheur des développeurs qui voient JNI comme une usine à gaz, qui utiliserait un bazooka pour tuer une mouche…
… et ils n’ont pas complètement tord !
Car si JNI est assez puissant et remplit bien son rôle, il souffre de plusieurs défauts qui le rendent assez complexe à mettre en œuvre. Il existe pourtant une alternative assez attirante, bien qu’elle ne soit pas standard : JNA.
Mais voyons cela un peu plus en détails…
Avant propos…
Nous allons prendre un exemple tout simple : on va appeler depuis une application Java une fonction native présente dans une librairie native dynamique. Pour l’exemple on utilisera la fonction strcmp() bien que son intérêt soit totalement limité.
L’objectif étant plutôt de se concentrer sur le travail que cela représente…
Avant propos sur le chargement des librairies dynamiques
Par défaut, Java respecte les conventions du système hôte pour le chargement des librairies natives, c’est à dire :
- Sous Windows, les librairies seront recherchées dans le PATH.
- Sous Unix/Linux, elles sont recherchées dans le LD_LIBRARY_PATH.
- Sous Mac OS, c’est la variable d’environnement DYLD_LIBRARY_PATH qui est utilisée.
Il est possible d’outre-passer cela en modifiant la variable système java.library.path (ou jna.library.path pour JNA que nous verrons un peu plus loin). Si la librairie ne fait pas partie d’un des répertoires spécifiés, l’exécution du programme génèrera une UnsatisfiedLinkError…
De même, chaque système possède ses propres conventions pour le nommage des fichiers représentant les librairies, par exemple pour une librairie nommé « hello » :
- Sous Windows, on lui ajoute simplement l’extension .dll, soit hello.dll.
- Sous Unix/Linux, on utilise le prefixe lib couplé à l’extension .so, soit libhello.so.
- Sous Mac OS, on utilise le prefixe lib couplé à l’extension .jnilib, soit libhello.jnilib.
Amusons nous avec JNI
Le principal défaut de JNI vient du fait qu’il n’est pas possible d’appeler n’importe quelle fonction native directement : il est ainsi obligatoire de définir une méthode native qui respectent un prototype bien précis. Ainsi on est obligé de passer par une méthode intermédiaire qui englobera cet appel.
1. Déclarer la méthode native
La première étape est toute simple et consiste donc à écrire le prototype Java de la méthode native, par exemple on pourrait avoir la classe suivante :
package jnidemo;
public class JNIDemo {
public native int strcmp(String s1, String s2);
}
Ce code peut être compilé normalement sans problème puisque le compilateur ne vérifie pas les liens vers les méthodes natives, par contre l’exécution génèrera une belle exception puisque la méthode native correspondante n’existe pas encore…
2. Générer le header C/C++
Une fois cette classe compilé, il faut utiliser l’outil javah (fourni avec le JDK) afin de générer un fichier d’entête C/C++. Ce dernier s’utilise comme la commande java et neccessite donc un nom de classe complet (c’est à dire avec le package) :
javah jnidemo.JNIDemo
Ce qui nous génèrera dans le cas présent un fichier nommé « jnidemo_JNIDemo.h » et contenant le code suivant :
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class jnidemo_JNIDemo */
#ifndef _Included_jnidemo_JNIDemo
#define _Included_jnidemo_JNIDemo
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: jnidemo_JNIDemo
* Method: strcmp
* Signature: (Ljava/lang/String;Ljava/lang/String;)I
*/
JNIEXPORT jint JNICALL Java_jnidemo_JNIDemo_strcmp
(JNIEnv *, jobject, jstring, jstring);
#ifdef __cplusplus
}
#endif
#endif
3. Implémenter le code natif
Il est maintenant nécessaire de coder la fonction native correspondant au prototype généré, ce qui pourrait donner en C :
#include /* pour strcmp() */
#include "jnidemo_JNIDemo.h"
JNIEXPORT jint JNICALL Java_jnidemo_JNIDemo_strcmp (
JNIEnv* env, /* Environnement JNI */
jobject thiz, /* Pointeur "this" de l'instance courante */
jstring s1, /* Argument #1 */
jstring s2 /* Argument #2 */
) {
/* On "transforme" les chaines Java en chaines C : */
const char* str1 = (*env)->GetStringUTFChars(env, s1, 0);
const char* str2 = (*env)->GetStringUTFChars(env, s2, 0);
/* On appelle la fonction strcmp() : */
int result = strcmp(str1, str2);
/* On libère la mémoire utilisée pour les chaines C : */
(*env)->ReleaseStringUTFChars(env, s1, str1);
(*env)->ReleaseStringUTFChars(env, s2, str2);
/* Et enfin on retourne le résultat : */
return result;
}
Premier constat : le code natif est assez lourd, puisqu’il nécessite des conversions de types de Java vers C et inversement ainsi qu’une gestion des allocations mémoires (puisqu’on sort du cadre d’utilisation du GC). Bref pour un simple appel de fonction on se retrouve dans un nids de guêpes…
4. Compiler et générer la librairie native
Il nous faut désormais compiler ce bout de code. Pour cela il faut spécifier au compilateur l’emplacement des headers natif de JNI, qui se trouvent dans le répertoire include du JDK, ce qui nous donne (la variable d’environnement JAVA_HOME pointant vers le chemin d’installation du JDK) :
Ce qui donne sous Linux avec gcc :
gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/linux -c jnidemo_JNIDemo.c
Et l’équivalent sous Windows avec mingw :
gcc -I %JAVA_HOME%\include -I %JAVA_HOME%\include\win32 -c jnidemo_JNIDemo.c
On peut enfin générer notre librairie native, que l’on nommera « compare« , ce qui donne pour Linux :
gcc -shared jnidemo_JNIDemo.o -o libcompare.so
Et pour Windows :
gcc -shared -Wl,--kill-at jnidemo_JNIDemo.o -o compare.dll
L’option supplémentaire -Wl,–kill-at indique au linkeur qu’il ne doit pas modifier le nom des fonctions exportées (qui sont « décorées » par défaut), car cela semble causer des problèmes à JNI.
Nota-bene : Si je ne donne pas d’exemple sous Mac OS, c’est tout simplement que je ne dispose pas de Mac, mais n’hésitez pas à m’envoyer les commandes correspondant pour l’OS à la pomme ! Ou sinon vous pouvez tout simplement m’acheter un Mac…
5. Charger la librairie native
Il reste une petite modification à effectuer sur le code source de notre classe Java : il est impératif de charger cette librairie pendant le chargement de la classe afin que la méthode native puisse être utilisée sans problème. Pour cela il suffit d’ajouter un bloc static dans le corps de la classe :
package jnidemo;
public class JNIDemo {
/* Bloc static : le code est exécuté une seule fois
* lors du chargement de la classe
*/
static {
/* On charge la librairie en utilisant son nom de base : */
System.loadLibrary("compare");
}
public native int strcmp(String s1, String s2);
}
On peut désormais utiliser notre méthode native de manière tout à fait standard :
package jnidemo;
public class Main {
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("Paramètre absent !");
System.exit(1);
}
JNIDemo demo = new JNIDemo();
System.out.printf("strcmp('%s', '%s') => %d %n",
args[0], args[1], demo.strcmp(args[0], args[1]) );
}
}
Le constat est assez rapide pour moi : Tout ça pour ça !
Pour un simple appel de méthode, on se retrouve à suivre un mode d’emploi en cinq étapes :
- Déclarer une méthode native
- Générer le header C/C++
- Implémenter le code natif
- Compiler et générer la librairie native
- Charger la librairie native
Tout ceci est d’autant plus rageant lorsqu’on se contente d’appeler une fonction existante comme dans le cas présent, et que ce type de code tient sur quelques lignes dans n’importe quel langage natif (et généralement totalement transparent). Sans compter que l’on devra générer et déployer une librairie par système supporté.
Tout cela alors que le code natif nécessaire pour JNI se contente de faire des conversions de type et des allocations/libérations de mémoire afin de pouvoir appeler la fonction native. Bref rien de très intéressant à coder, mais une source de problème potentiellement…
Et pour faire plus simple ?
JNA se présente heureusement comme une alternative beaucoup plus simple d’accès, en permettant d’accéder dynamiquement à n’importe quelle bibliothèque partagée du système sans utiliser JNI (pas directement en tout cas). En fait il s’agit d’une librairie Java/native qui se chargera du chargement des librairies, de l’appel des fonctions et de la conversion des types… si bien qu’il n’y a quasiment rien à faire.
1. Déclarer les méthodes natives dans une interface
Contrairement à JNI, la marche à suivre est assez différentes puisqu’il ne faut pas marquer les méthodes avec le mot-clef native, et qu’il est impératif d’utiliser une interface qui contiendra les définitions des fonctions natives (et seulement celles-ci). A l’exécution on récupérera une instance valide de notre interface qui sera automatiquement associé aux méthodes natives correspondante.
Il suffit donc de déclarer toutes les fonctions dans une interface particulière, ce qui donne dans notre cas :
package jnademo;
import com.sun.jna.Library;
public interface JNADemo extends Library {
public int strcmp(String s1, String s2);
}
La seule condition est d’étendre l’interface com.sun.jna.Library qui fait simplement office de marqueur…
2. Instancier dynamiquement notre interface
Il ne reste plus qu’à créer une instance de cette interface qui sera automatiquement lié à la librairie native. Pour cela il suffit d’utiliser la méthode Native.loadLibrary() en lui précisant le type Java de l’interface et le nom de la librairie dynamique native.
Le seul problème vient du fait que le nom de la librairie peut être différent d’un système à l’autre. Dans ce cas précis la fonction strcmp() fait partit de la librairie « c » sous Unix/Linux, alors qu’il faut utiliser « msvcrt » sous Windows…
Mais il n’y a rien de bien méchant puisqu’il suffit de vérifier le nom du système pour régler le problème :
// On détermine le nom de la librairie selon le système :
String libName = "c";
if (System.getProperty("os.name").contains("Windows")) {
libName = "msvcrt";
}
// On charge la librairie dynamique en l'associant avec l'interface :
JNADemo demo = (JNADemo) Native.loadLibrary(libName, JNADemo.class);
Et ? C’est tout ou presque. En effet il suffit ensuite d’utiliser l’instance ainsi créée pour appeler les fonctions natives.
Le programme équivalent deviendrait alors :
package jnademo;
import com.sun.jna.Native;
public class Main {
public static void main(String[] args) {
if (args.length != 2) {
System.err.println("Paramètre absent !");
System.exit(1);
}
// On détermine le nom de la librairie selon le système :
String libName = "c";
if (System.getProperty("os.name").contains("Windows")) {
libName = "msvcrt";
}
// On charge la librairie dynamique en l'associant avec l'interface :
JNADemo demo = (JNADemo) Native.loadLibrary(libName, JNADemo.class);
System.out.printf("strcmp('%s', '%s') => %d %n",
args[0], args[1], demo.strcmp(args[0], args[1]) );
}
}
La librairie s’occupe elle-même de faire toutes les conversions de type et de rechercher les fonctions natives a appeler dynamiquement selon la définition de la méthode Java, si bien qu’il n’y a pas besoin d’écrire une seule ligne de code native !
Même si je n’ai fait que survoler les possibilités qu’offre JNA, et bien qu’il n’y ait qu’une documentation succincte pour le moment, je dois dire que cela me semble vraiment très complet, car cela inclut entre autres :
- Mapping Java/natif automatique des types primitifs et des String.
- Mapping des struct et des union vers des types Java spécifiques (Structure et Union).
- Mapping des pointeurs vers un type Java (ByReference).
- Mapping des pointeurs de fonctions (ou callback) en utilisant une interface Java.
- Possibilité de définir un mapping personnalisé pour ses propres objets Java.
- Mapping automatique de la méthode Java vers la fonction native du même nom, mais en gardant la possibilité d’utiliser une classe qui se chargera de cela (par exemple pour utiliser des noms de méthodes Java différent afin de respecter les règles de nommages Java).
- Gestion des librairies Win32 qui utilise la convention d’appel __stdcall.
Le seule reproche que je pourrais faire, c’est que l’API n’utilise pas les « nouveautés » de Java 5.0, car je pense que les annotations se serait bien appliqué dans ce cas précis…
Pour plus d’information sur le sujet vous pouvez consulter les liens suivants :
- Java Native Interface, la documentation officielle.
- Java Native Access, la page du projet sur java.net, et sa javadoc online.
[Mise à jour] : Enfin pour plus de détail je vous invite à consulter ce tutoriel en français : Exécuter du code natif en Java : JNI VS JNA.
4 Commentaires + Ajouter un commentaire
Tutoriels
Discussions
- [ fuite ] memoire
- Possibilité d'accéder au type générique en runtime
- Difference de performances Unix/Windows d'un programme?
- [REFLEXION] Connaitre toutes les classes qui implémentent une interface
- Recuperation du nom des parametres
- Classes, méthodes private
- L'apparition du mot-clé const est-il prévu dans une version à venir du JDK?
- Définition exacte de @Override
- jre 1.5, tomcat 6.0 et multi processeurs
Merci, mais les articles demandent beaucoup plus de travail en général (rédaction, relecture, correction, etc.) et donc beaucoup plus de temps…
Alors que le blog est plus « rapide » et moins contraignant ! Et déjà que je ne poste pas très souvent sur le blog…
J’ai quand même quelques idées d’articles et même un brouillon en cours mais je n’ai pas suffisamment de temps à y consacrer… mais petit à petit ca viendra
Mais sinon rien ne vous empêche de proposer un article
Tu me surprendras toujours, Adibuga
d’accord pour en faire un tutoriel
+1 pour le commentaire précédent …
Excellent billet, tu pourrais en faire un article non ?