vendredi 28 mai 2010

Mettre des librairies natives dans un jar, et les utiliser

Certaines applications Java peuvent utiliser du code natif C ou C++ grâce au framework JNI. On parlera pas ici de la manière d'utiliser l'interface JNI, mais de la manière de l'emballer dans l'application finale pour que l'utilisateur n'ait pas à s'en soucier.

En général, une fois le développement terminé, on place les classes dans un fichier jar et on délivre l'application sous cette forme, avec ou sans enrobage supplémentaire tel qu'un installeur.
Avec JNI, outre les classes, on se retrouve avec des librairies natives: une par plateforme supportée.


La question est de savoir que faire de ces librairies. On peut bien sûr les mettre à côté, mais cela oblige à spécifier leur emplacement au moyen de paramètres sur la ligne de commande Java. exemple: java -Dava.library.path=lib_dir .....
Au delà de cette lourdeur, il reste la question de l'identification de la librairie associée à la plateforme. On va voir comment affranchir l'utilisateur de tous ces problèmes.

Dans un premier temps, comme la présence de librairies natives à coté des JAR fait désordre, on va profiter du fait que le jar n'est rien d'autre que tar compressé. On peut y mettre ce que l'on veut, on va donc y mettre nos librairies.

1) Bien nommer les librairies:
On va d'abord renommer nos librairies afin d'identifier clairement l'architecture à laquelle elles se rattachent.
Il existe 2 propriétés java indiquant l'architecture sur laquelle tourne la machine java:
  • os.name: retourne le nom de l'OS (Linux, Win ou Mac)
  • os.arch: retourne l'architecture matérielle (i386, universal...)
On va renommer nos librairies sous la forme suivante: os.name_os.arch.lib, le tout en minuscules afin d'éviter les problèmes de casse. Ainsi, sous Linux 32bits, ma librairie s'appellera linux_i686.lib.

2) Placer les libraires à la racine du fichier JAR.
Cela n'est pas obligatoire, mais ca facilite. Il faut comprendre que le fichier JAR est vu par la JVM comme un répertoire accessible pour rechercher des ressources. On peut donc y organiser nos fichiers à notre guise (sauf pour les .class qui doivent respecter la hiérarchie des paquetages).

exemple: jar cvf mon_application.jar *.lib .....

3) Intégrer dans le code java le chargement des librairies natives.
Avant de faire appel à des méthodes natives, il faut charger le code natif. Le loader de Java sais bien le faire à condition de lui indiquer où chercher.
Cela se fait par étapes:
  1. Construire le nom de la librairie à partir des ressources os.*
  2. Localiser la ressource.
  3. Extraire la bonne librairie
  4. Charger la bonne librairie.
En bon java cela donne (on suppose que la classe native est sqlite.tools.ASCIIDataFileLoader):

public static void loadNativeLibrary() throws Exception {
if( LIB_LOADED == false ) {
String libname = System.getProperty("os.name")
+ "_"
+ System.getProperty("os.arch")
+ ".lib";
ClassLoader cl = Class.forName("sqlite.tools.ASCIIDataFileLoader")
.getClassLoader();
InputStream in = cl.getResourceAsStream(libname);
if (in == null) {
throw new Exception("libname: "
+ libname
+" not found (supposed to be in sqliteimporter.jar)");
}
/*
* Extract the lib file and link it with the app
*/
File tmplib = File.createTempFile("libsqlitejdbc-", ".lib");
tmplib.deleteOnExit();
OutputStream out = new FileOutputStream(tmplib);
byte[] buf = new byte[1024];
for (int len; (len = in.read(buf)) != -1;) {
out.write(buf, 0, len);
}
in.close();
out.close();
System.load(tmplib.getAbsolutePath());

LIB_LOADED = Boolean.TRUE;
}
}


Il faut ensuite lier l'appel de cette méthode à l'appel de chaque méthode native:


class .......
.....
static int importTSV(String table, String file, String db_file)
throws Exception {
ASCIIDataFileLoader.loadNativeLibrary();
return importASCIIFile(table, file, "\t", db_file);
}

native static int importASCIIFile(String table, String file
, String separ, String db_file) throws Exception;
}

Cela ne marche bien que si notre application n'utilise qu'une seule librairie native. Dans le cas contraire, il faut un peu compliquer le nommage afin d'y inclure le nom de la classe ou du paquetage ou de n'importe quoi d'autre évitant toute ambiguïté.

jeudi 27 mai 2010

Faire du JNI sous Eclipse

Rappel
Un petit rappel: JNI (Java Native Interface) est une interface de programmation permettant à des classes JAVA d'inclure du code C ou C++. Il peut paraître paradoxal de vouloir ainsi perdre la portabilité du langage. Il y a toutefois de nombreux cas pour lesquels cette interface est très utile. D'une manière générale, elle permet de récupérer des codes éprouvés, difficile voire impossible à porter en JAVA.

JNI c'est bien mais..
La conséquence de l'utilisation de JNI est qu'il faut (re)mettre les mains dans la programmation C.
Il faut aussi se rapeller que la classe JAVA en question devra être accompagnée des librairies pour toutes les plateformes sur lesquelles elle est susceptible de tourner (MAC, Window, Linux, 32/64bits..). On verra dans un prochain post comment s'y prendre.

Je veux pas quitter Eclipse
Comme beaucoup des utilisateurs d'Eclipse ont du mal à revenir au bon vieux couple emacs/gcc, on va voir comment mettre au point un projet JNI dans Eclipe.

1) Installer CDT sur Eclipse
CDT est le plugin C/C++ d'Eclipse.
- Aller dans Help->Install New Software
- Cliquer sur Available Software Sites
- Cliquer sur add et remplir les champs suivants:
    name = CDT
    URL = http://download.eclipse.org/tools/cdt/releases/galileo

- Selectionner CDT dans le combobox Work With
- Continuer l'installation avec Next/Next/Finish

2) La partie Java
- Créer un projet Java dans Eclipse (JSQLITE dans mon cas).
- Déclarer les méthodes natives là ou bon vous semble. Tant que les classes possédant ce genre de méthodes ne sont pas appelées, tout va bien. Autrement, le linker vous gratifie d'un java.lang.UnsatisfiedLinkError signifiant qu'il n'a pas trouvé le code natif.
- Créer dans le projet un répertoire pour les futurs fichiers headers (jni_headers par exemple). Cela se fait par le menu File->New->Directory.

Il faut maintenant créer les fichiers header contenant les prototypes C des méthodes natives. Pour cela, on peut créer un tache d'exécution en cliquant sur le bouton run surmontant une petite valise rouge.
- Clique droit sur Program sélectionner New et remplir les champs de la manière suivante:
    name: javah
    location: /usr/lib/jvm/jdk1.6.0_16/bin/javah
Il s'agit du chemin d'accès à l'utilitaire Java créant les fichiers headers. Il se trouve dans votre JDK.
    Argument -classpath build -d jni_headers wrapper.SQLiteDataImporter. On suppose que wrapper.SQLiteDataImporter est notre classe avec des méthodes natives

L'exécution de cette tâche doit produire les fichiers d'entêtes dans le répertoire jni_header.

3) La partie C
- Créer un projet C de type Shared Library: menu File->New->C Project, puis, Shared Library->Empty Project. Dans note exemple, ce projet s'appelle JSQLITENative.
- Importer les headers JNI: Clique droit sur le projet, puis Properties->C/C++ General->Paths and Symbols.
- Sélectioner le langage GNU C du tab Includes
- Cliquer sur Add->Workspace, puis sélectionner JSQLITE->jni_headers, valider.
- De la même manière ajouter les répertoire JAVA_HOME/include et JAVA_HOME/include/platform afin d'inclure les headers JNI dans les règles de compilation.
- Vos headers JNI doivent apparaitre dans le dossier Includes
- Créer votre source C: menu File->New->Source File
- Ecriver et compiler votre code. Attention, CDT ne pratique pas la compilation à la volée comme en Java. Il vous faut faire un CTRL B pour compiler. En cas d'erreurs persistentes, un peu de nettoyage peut arranger les choses: menu Project->Clean....
- La librairie doit être placée dans le répertoire Binaries.

4) Faire tourner le code Java.
Il faut d'abord indiquer ou se trouve la librairie C.
- Ouvrir la configuration de lancement de votre application Java: Bouton Run puis Run Configuration.
- Sélectionner votre application.
- Ouvrir le tab Argument
- Mettre la ligne suivant dans la zone VM Arguments: -Djava.library.path="${workspace_loc:SQLITENative/Debug}".
Attention le répertoire Debug est le nom du répertoire de votre Workspace ou se trouve la librairie C. Il ne s'agit pas de son avatar figurant dans l'explorateur de projets qui est Binaries.
- N'oublier pas charger la librairie dans le main de votre application: System.loadLibrary("SQLITENative"); pour un libraire dont le nom est libSQLITENative.so.


Voilà, c'est sommaire, mais ca peut aider. N'oublier pas de refaire cette manip sur chacune des plateforme pour lesquelles vous voulez implémenter vos classes JNI.