Artisan Numérique

/développement/java/c/jni/ Java - Introduction aux JNI

Le manque de performance est le reproche régulièrement fait à la plate-forme Java. C'est l'inconvénient de l'âge et des précurseurs. Car s'il est vrai que les premières tentatives, datant de plus de dix ans (1995), ont été difficiles, les machines Java modernes n'ont elles plus à rougir face des applications natives, ni même, malgré l'emphase marketing de certains, face à une application .Net... Et tout cela a été rendu possible par la maturation de trois approches conjointes : La compilation temps-réel (JIT), l'optimisation des librairies et enfin, les JNI. Et c'est ce dernier point que nous allons apprendre à connaître...

Java ou le mythe de la performance

La faible vélocité historique de Java tient à un fait tout simple. La machine virtuelle Java est, d'une manière ou d'une autre, un interpréteur qui va "traduire" un psoeudo-code assembleur (appelé bytecode) dans la langue native du micro-processeur qui exécute le code. Cette traduction ne que prendre du temps et cette perte se traduisait bien, il y 10 ans, par une application globalement 10 fois plus lente que du natif. Et c'est de là que vient cette vilaine réputation d'un Java poussif qui tient jusqu'à nos jours. Notez .Net et sa CLR (équivalent de la Java VM) fonctionne exactement de la même manière et a donc, techniquement, les mêmes contraintes. Aujourd'hui, en regard de l'extraordinaire richesse du langage et des libraiies Java, ce problème peut être concidéré comme de l'histoire ancienne. Le rapport de vitesse entre du code natif n'est plus que de 8%.. en faveur de Java sur une compilation GCC non optimisée et 30% en faveur de GCC sur une optimisation type O3.

La Raison d'un tel miracle est triple. Tout d'abord il y a eu l'introduction du HotSpot. Cette technologie compile, à la volée (JIT ou Just In Time Compiler) le psoeudo code Java pour en faire du binaire natif. Cela a apporté un énorme gain de vitesse aux applications Java (x2 environ). C'est aussi pour cela que de nombreuses personne pensent que .Net est plus rapide que Java car cette dernière a débuté sa carrière directement avec un JITC... Ensuite, le JDK, les librairies de Java, sont sans cesse améliorées. Les algorithmes qui s'y trouve sont généralement bien meilleur que ceux qu'un développeur moyen trouverait. Une application Java rapide repose donc sur une bonne connaissance du JDK pour ne pas réinventer ce qui marche déjà très bien.

Enfin, la dernière raison est un peu plus brutale : les JNI. En effet, pour obtenir de telles performance, de nombreuses fonctions ont été simplement codées... en C, sous la forme de librairies qui sont spécifiques à chaque plate-forme. C'est pour cela qu'un JDK windows ne fonctionne pas sous Linux et qu'une version 64 bit ne tourne pas sur un CPU 32 bits. Au fur et à mesure de l'avancement de Java, le nombre de librairies natives n'a cessé d'augmenter pour couvrir aujourd'hui les entrées/sorties (Java Native IO), les effets graphiques 2D (Java2D), openGL (Java3D), etc... Ces librairies sont pour grande part dans les performances de l'ensemble. Certaines démonstration comme le projet Looking Glass permet de comprendre ce qu'en 3D par exemple, Java est capable de faire.

Quelle est l'utilité des JNI pour un développeur Java

Pour un développeur, utiliser les JNI peut donc se justifier par un besoin d'accès directe à du matériel, par une besoin de performance ou encore parce qu'il désire faire communiquer une application Java avec une application ou une librairie native. En effet, si je désire par exemple interagir avec mplayer, la librairie libmplayer va pouvoir être utilisée par les JNI et donc être intégrée très finement à une application Java. Voilà donc pour ce que l'on peut faire des JNI, passons maintenant à la pratique :)

Les JNI en action

L'objectif de ce tutorial est de permettre la mise en oeuvre très simple des JNI. Le classique "HelloWorld!!" de M. Ritchie. L'invocation de l'affichage se fera sous Java, et l'affichage, en natif. Ce n'est pas très utile mais cela permet de comprendre les mécanismes fondamentaux des JNI.

Première étape, construire la classe HelloWorld.java. Cette classe est une simple déclaration de la fonction que nous cherchons à rendre native, une sorte d'interface :

class HelloWorld
{
  // Déclaration de la méthode native
  public native void printHello();
}
HelloWorld.java

Nous allons maintenant compiler cette classe :

javac HelloWorld.java

A ce stade, nous avons deux fichiers, un .java et un .class. L'étape suivante consiste à exploiter à extraire un header C (fichier .h) qui va servir de base à la construction de la librairie native. Pour cela, le JDK fournit un outil, javah (java header), que l'on invoque comme suit :

javah -jni HelloWorld

Le fichier HelloWorld.h doit être maintenant créé et doit ressembler à cela

DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloWorld */

#ifndef _Included_HelloWorld
#define _Included_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class:     HelloWorld
* Method:    printHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloWorld_printHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Nous retrouvons dans ce fichier, une version C de la déclaration Java pour la fonction printHello. Il est à noté que javah a utilisé le fichier .class pour travailler, d'où la necesité de compiler le .java au préalable.

Nous allons maintenant pouvoir rédiger notre librairie native sous la forme d'un fichier .c

#include <jni.h>
#include <code>

// Appel au header généré par javah -jni HelloWorkd
#include "HelloWorld.h"

JNIEXPORT void JNICALL

Java_HelloWorld_printHello(JNIEnv *env, jobject obj)
{
  printf("Hello world!!\n");
  return;
}

Il est temps de compiler ce code C pour générer notre première librairie native :

gcc HelloWorld.c -o libHelloWorld.so -shared -I/usr/java/jdk/include -I/usr/java/jdk/include/linux

Le dossier /usr/java/jdk est évidement à changer en fonction de là où se trouve votre JDK. Le choix de préfixer la librairie par lib n'est pas innocent, en effet java s'attend à trouver un fichier libXXXX.so et pas XXXX.so. Sans cela, la librairies native ne serait pas chargée.

Il fait maintenant rendre cette librairie visible par java. A ce stade, deux solutions. Soit vous placez votre librairie en /usr/lib, soit vous définissez la variable LD_LIBRARY_PATH:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PWD/libHelloWorld.so

Maintenant nous allons construire la classe principale de notre "application" qui va à la fois charger la librairie en mémoire et exécuter la classe printHello :

class Main
{
  // Chargement de la librairie native
  static
  {
    System.loadLibrary("HelloWorld");
  }

  // Affichage comme si tout était en java
  public static void main(String[] args)
  {
    new HelloWorld().printHello();
  }
}

La méthode loadLibrairy("Identifier") va chercher dans LD_LIBRARY_PATH (ou dans la variable PATH system de windows) une librairie se nommant libIdentifier.so (ou identifier.dll pour Windows).Un autre moyen moins portable mais plus directe de charger une librairie est d'utiliser la méthode System.load("chemin/libIdentifier.so") qui prends donc en paramètre un chemin complet vers la librairie à charger.

Pour osculter une librarie et ainsi déterminer si elle contient bien les exports JNI, la fonction nm libMaLibJni.so est très utile. Elle permet d'obtenir la liste de toutes les fonctions exportées dans la librairie.

Voilà, il ne nous reste plus qu'à compiler Main et à lancer notre application :

javac Main
java HelloWorld

Votre premier "Hello World !!" en JNI devrait s'afficher.

Passer une chaîne de caractère en paramètre

Si nous avons maintenant un paramètre de type String dans notre fonction print Hello, la fonction prendrait la forme suivante :

JNIEXPORT void JNICALL Java_HelloWorld_printHello(JNIEnv *env, jobject obj, jstring message)
{
    // Fabrication de la chaîne
    const char *szMessage = env->GetStringUTFChars(message,0);
    // Affichage du message
    printf(szMessage);
    // Libération de la chaîne
    env->ReleaseStringUTFChars(message,szMessage);
}

Renvoyer une chaîne de caractère

Maintenant, à l'inverse, imaginon que la méthode sayHello renvois une chaîne de caractère :

JNIEXPORT jstring JNICALL Java_HelloWorld_sayHello(JNIEnv *env, jobject obj)
{
  char helloMessage[100];
  sprintf(helloMessage,"%s", "Hello world !!!");
  return env->NewStringUTF(helloMessage);
}

Déclancher une exception

Pour déclancher une exception à partir d'un code natif, la recette est la suivante :

env->ExceptionDescribe();
env->ExceptionClear();
jclass exception = env->FindClass("net/karmaLab/toolkit/shell/linux/LinuxErrorNumberException");
if (exception == NULL)
  return NULL;
env->ThrowNew(exception,"Unable to find a free loop device");