Liens vers d’autres formalismes

Une application n’est pas toujours réalisée avec l’aide d’un seul formalisme (Java en l’occurrence).

Il arrive qu’on ait besoin d’utiliser un autre langage:

Ce chapitre ne peut être exhaustif de tous ces points de vue. Son rôle est plus de mentionner des possibilités.

Utilisation de code natif

images/blackbelt.png 2° Dan

Il existe deux outils pour inclure du code "natif" dans Java: JNI et JNA. JNI sera présenté ici mais JNA est plus simple et donc le lecteur pourra chercher sur le WEB des documents explicatifs.

Pourquoi réaliser du code natif?

Il y a des situations dans laquelle le code ne peut pas être complétement écrit en JAVA : - Une grande quantité de code compilé existe déja et fonctionne de manière satisfaisante. La fourniture d’une interface avec JAVA peut être plus intéressante qu’une réécriture complète. - Une application doit utiliser des services non fournis par JAVA (et en particulier pour exploiter des spécificités de la plate- forme d’exécution. Exemple : accès à des cartes). - Le système JAVA n’est pas adapté à une partie critique de l’application et la réalisation dans un code natif serait plus efficiente.

Il est possible d’implanter en JAVA des méthodes natives réalisées typiquement en C ou C\++.

Une classe comprenant des méthodes natives ne peut pas être téléchargée au travers du réseau de manière standard: il faudrait que le serveur ait connaissance des spécificités de la plate-forme du client. De plus une telle classe ne peut faire appel aux services de sécurité de JAVA (en 1.1)

Bien entendu pour toute application JAVA qui s’appuie sur des composants natifs on doit réaliser un portage du code natif sur chaque plate-forme spécifique. De plus c’est un code potentiellement plus fragile puisque les contrôles (pointeurs, taille, etc.) et la récupération d’erreurs sont entièrement sous la responsabilité du programmeur.

Il existe diverses manières d’assurer cette liaison code compilé-Java. Depuis la version JAVA 1.1 le protocole JNI a été défini pour rendre cette adaptation plus facilement indépendante de la réalisation de la machine virtuelle sous-jacente.

D’autre part il est également possible d’exécuter du code JAVA au sein d’une application écrite en C/C\++ en appelant directement la machine virtuelle ( JAVA "enchassé" dans C).

Un exemple : "Hello World" en C

Résumé des phases :

  • Ecriture du code JAVA :

    • Création d’une classe "HelloWorld" qui déclare une méthode (statique) native.
  • Création des binaires JAVA de référence :

    • Compilation du code ci-dessus par javac .
  • Génération du fichier d’inclusion C/C\++ :

    • Ce fichier est généré par l’option javac -h . Il fournit une définition d’un en-tête de fonction C pour la réalisation de la méthode native getGreetings() définie dans la classe Java "HelloWorld".
  • Ecriture du code natif :

    • Ecriture d’un fichier source C (".c") qui réalise en C le code de la méthode native. Ce code fait appel à des fonctions et des types prédéfinis de JNI.
  • Création d’une librairie dynamique:

    • Utilisation du compilateur C pour générer une librairie dynamique à partir des fichiers .c et .h définis ci-dessus. (sous Windows une librairie dynamique est une DLL)
  • Exécution:

    • Exécution du binaire JAVA (par java) avec chargement dynamique de la librairie.

Ecriture du code JAVA

Le code de l’exemple définit une classe JAVA nommée "HelloWorld" et faisant partie du package "hi".

package hi ;

class HelloWorld {
   static {
      System.loadLibrary("hello");
   }

   public static native String getGreetings();

   public static void main (String[] tArgs) {
      for (int ix = 0 ; ix < tArgs.length; ix++) {
         System.out.println(getGreetings() + tArgs[ix]) ;
      }
   }// main
}

Cette classe pourrait contenir également d’autres définitions plus classiques (champs, méthodes, etc.).

On remarquera ici : - La présence d’un bloc de code static exécuté au moment du chargement de la classe. A ce moment il provoque alors le chargement d’une bibliothèque dynamique contenant le code exécutable natif lié à la classe. Le système utilise un moyen standard (mais spécifique à la plate-forme) pour faire correspondre le nom "hello" à un nom de bibliothèque ( "libhello.so" sur certains UNIX, "hello.dll" sur Windows,…) - La définition d’un en-tête de méthode native. Une méthode marquée native ne dispose pas de corps. Comme pour une méthode abstract le reste de la "signature" de la méthode (arguments, résultat,…) doit être spécifié. (ici la méthode est static mais on peut, bien sûr, créer des méthodes d’instance qui soient natives).

Création des binaires JAVA de référence

La classe ainsi définie se compile comme une autre classe :

javac -d . HelloWorld.java

(autre exemple sous UNIX : au lieu de "-d ." on peut faire par exemple "-d $PROJECT/javaclasses")

Le binaire JAVA généré est exploité par les autres utilitaires employés dans la suite de ce processus.

Dans l’exemple on aura un fichier "HelloWorld.class" situé dans le sous- répertoire "hi" du répertoire ciblé par l’option "-d"

Génération du fichier d’inclusion C/C\++

Dans les versions antérieures de Java on utilisait l’utilitaire javah pour générer à partir du binaire JAVA un fichier d’inclusion C/C++ ".h". Ce fichier définit les prototypes des fonctions qui permettront de réaliser les méthodes natives de HelloWorld. A partir de java 10 c’est une option de javac qui permet de générer ces fichiers.

javac -h .   hi/HelloWorld.java

On obtient ainsi un fichier nommé hi_HelloWorld.h:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class hi_HelloWorld */
#ifndef _Included_hi_HelloWorld
#define _Included_hi_HelloWorld
#ifdef __cplusplus
extern "C" {
#endif
/*
  * Class:   hi_HelloWorld
  * Method:   getGreetings
  * Signature: ()Ljava/lang/String;
  */

JNIEXPORT jstring JNICALL Java_hi_HelloWorld_getGreetings
   (JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

Des règles particulières régissent la génération du nom de fichier d’inclusion et des noms de fonctions réalisant des méthodes natives. On notera que la fonction rend l’équivallent d’un type JAVA (jstring) et, bien qu’étant définie sans paramètres en JAVA, comporte deux paramètres en C. Le pointeur d’interface JNIEnv permet d’accéder aux objets JAVA, jclass référence la classe courante (on est ici dans une méthode statique: dans une méthode d’instance le paramètre de type jobject référencerait l’instance courante).

Ecriture du code natif

En reprenant les prototypes définis dans le fichier d’inclusion on peut définir un fichier source C : "hi_HelloWorldImp.c" :

#include <jni.h>
#include "hi_HelloWorld.h"
/*
  * Class:   hi_HelloWorld
  * Method:   getGreetings
  * Signature: ()Ljava/lang/String;
  * on a une methode statique et c’est la classe
  * qui est passée en paramètre
  */

   JNIEXPORT jstring JNICALL Java_hi_HelloWorld_getGreetings
      (JNIEnv * env , jclass curclass) {
      return (*env)->NewStringUTF(env, "Hello ");
   }

env nous fournit une fonction NewStringUTF qui nous permet de générer une chaîne JAVA à partir d’une chaîne C.

NOTA : en C\++ les fonctions JNI sont "inline" et le code s’écrirait :

JNIEXPORT jstring JNICALL Java_hi_HelloWorld_getGreetings
   (JNIEnv * env , jclass curclass) {
   return env->NewStringUTF("Hello ");
}

Création d’une librairie dynamique

Exemple de génération sous UNIX (le ".h" est dans le répertoire courant)

#!/bin/sh
# changer DIR en fonction des besoins
DIR=/usr/local/java
cc -G -I$DIR/include -I$DIR/include/solaris  hi_HelloWorldImp.c -o libhello.so

Exécution

java hi.HelloWorld World underWorld
Hello World
Hello underWorld

Si, par contre, vous obtenez une exception ou un message indiquant que le système n’a pas pu charger la librairie dynamique il faut positionner correctement les chemins d’accès aux librairies dynamiques (LD_LIBRARY_PATH sous UNIX)

Présentation de JNI

JNI est une API de programmation apparue à partir de la version 1.1 de JAVA. Il existait auparavant d’autres manières de réaliser des méthodes natives. Bien que ces autres APIs soient toujours accessibles elles présentent quelques inconvénients en particulier parce qu’elles accèdent aux champs des classes JAVA comme des membres de structures C (ce qui oblige à recompiler le code quand on change de machine virtuelle) ou parcequ’elles posent quelques problèmes aux glaneurs de mémoire (garbage collector).

Les fonctions de JNI sont adressables au travers d’un environnement (pointeur d’interface vers un tableau de fonctions) spécifique à un thread. C’est la machine virtuelle elle même qui passe la réalisation concrète de ce tableau de fonctions et on assure ainsi la compatibilité binaire des codes natifs quel que soit la machine virtuelle effective.

Les fonctions proposées permettent en particulier de :

  • Créer, consulter, mettre à jour des objets JAVA, (et opérer sur leurs verrous). Opérer avec des types natifs JAVA.
  • Appeler des méthodes JAVA
  • Manipuler des exceptions
  • Charger des classes et inspecter leur contenu

Le point le plus délicat dans ce partage de données entre C et JAVA et celui du glaneur de mémoire (garbage collector): il faut se protéger contre des déréférencements d’objets ou contre des effets de compactage en mémoire (déplacements d’adresses provoqués par gc), mais il faut savoir aussi faciliter le travail du glaneur pour recupérer de la mémoire.

JNI: types, accès aux membres, création d’objets

Soit l’exemple de classe :

package hi ;
class Uni {
   static {
      System.loadLibrary("uni");
   }
   public String [] tb ;// champ "tb"
   public Uni(String[] arg) {
      tb=arg;
   }// constructeur
   public native String [] getMess(int n, String mess);
   public static native Uni dup(Uni other);
   public String toString() {
      String res = super.toString() ;
      for(int ix=0;ix<tb.length;ix++){  // pas efficient remplacer par un StringBuilder
         res=res+’\n’+tb[ix];
      }
      return res ;
   }

   public static void main (String[] tArgs) {
      Uni you = new Uni(tArgs) ;
      System.out.println(you) ;
      String[] mess = you.getMess(tArgs.length, " Hello") ;
      for (int ix = 0 ; ix < mess.length; ix++) {
         System.out.println(mess[ix]) ;
      }
      Uni me = Uni.dup(you) ;
      System.out.println(me) ;
   }// main
}

Exemple d’utilisation :

java hi.Uni World
  hi.Uni@1dce0764
  World
    Hello
  hi.Uni@1dce077f
  World

La méthode native getMess(int nb, String mess) génère un tableau de chaînes contenant "nb" fois le même message "mess" :

/* Class:   hi_Uni
 * Method:   getMess
 * Signature: (ILjava/lang/String;)[Ljava/lang/String;
 */
JNIEXPORT jobjectArray JNICALL
    Java_hi_Uni_getMess (JNIEnv * env , jobject curInstance,
   jint nb , jstring chaine) { /* quelle est la classe de String ? */
      jclass stringClass = (*env)->FindClass(env, "java/lang/String") ;
   /* un tableau de "nb" objet de type "stringClass"
    * chaque element est initialise a "chaine"
    */
      return (*env)->NewObjectArray(env, (jsize)nb,stringClass,chaine) ;
}
  • La fonction NewObjectArray est une des fonctions de création d’objets JAVA. Elle doit connaître le type de ses composants (ici fourni par "stringClass"). L’initialisation de chaque membre d’un tel tableau se fait par l’accesseur SetObjectArrayElement() - mais ici on profite du paramètre d’initialisation par défaut-
  • JNI fournit des types C prédéfinis pour représenter des types primitifs JAVA (jint) ou pour des types objets (jobject, jstring,..)
  • La fonction FindClass permet d’initialiser le bon paramètre désignant la classe "java.lang.String" (la notation utilise le séparateur "/"!). Noter également la représentation de la signature de la fonction "getMess":(ILjava/lang/String;) indiqueunpremierparamètre de type int (symbolisé par la lettre I) suivi d’un objet (lettre L+ type + ; ) . De même [Ljava/lang/String; désigne un résultat qui est un tableauàunedimension(lettre[)contenantdes chaînes.

La méthode statique "dup" clone l’instance passée en paramètre :

/* Class:   hi_Uni; Method:   dup
 * Signature: (Lhi/Uni;)Lhi/Uni;
 */
JNIEXPORT jobject JNICALL
   Java_hi_Uni_dup (JNIEnv * env,
       jclass curClass , jobject other) {
      jfieldID idTb ;
      jobjectArray tb ;
      jmethodID idConstr ; /* en fait inutile puisque c’est curClass !*/
      jclass uniClass = (*env)->GetObjectClass(env, other) ;
      if(! (idTb = (*env)->GetFieldID (env,uniClass, "tb","[Ljava/lang/String;")))
         return NULL ;
      tb = (jobjectArray) (*env)->GetObjectField(env, other,idTb) ;
      /* on initialise un nouvel objet */
      if(!(idConstr = (*env)->GetMethodID(env, curClass, "<init>", "([Ljava/lang/String;)V")))
         return NULL ;
      return (*env)->NewObject(env, curClass,idConstr,tb) ;
}
  • La récupération du champ "tb" (de type tableau de chaîne) sur l’instance passée en paramètre se fait par la fonction GetObjectField. On a besoin de la classe de l’instance consultée et de l’identificateur du champ qui est calculé par GetFieldID.
  • De la même manière l’appel d’une méthode nécessite une classe et un identificateur de méthode calculé par GetMethodID. Ici ce n’est pas une méthode qui est appelée mais un constructeur et l’identifiant est calculé de manière particulière ("<init>"), le type indique un paramètre de type tableau de chaîne et un "résultat" qui est void (lettre V ) .

JNI fournit ainsi des accesseurs à des champs (Get<static><type>Field, Set<static><type>Field) et des moyens d’appeler des méthodes (Call<statut><type>Method : exemple CallStaticBooleanMethod). Il existe, en plus, des méthodes spécifiques aux tableaux et aux Strings

Références sur des objets JAVA:

Le passage du glaneur de mémoire sur des objets JAVA pourrait avoir pour effet de rendre leur référence invalide ou de les déplacer en mémoire. Les reférences d’objet JAVA transmises dans les transitions vers le code natif sont protégées contre les invalidations (elles redeviennent récupérables à la sortie du code natif). Toutefois JNI autorise le programmeur à explicitement rendre une référence locale récupérable. Inversement il peut aussi se livrer à des opérations de "punaisage" (pinning) lorsque, pour des raisons de performances, il veut pouvoir accéder directement à une zone mémoire protégée :

const char * str = (*env)->GetStringUTFChars(env, javaString,0) ;
.... /* opérations sur "str" */
(*env)->ReleaseStringUTFChars(env, javaString, str) ;

Des techniques analogues existent pour les tableaux de scalaires primitifs (int, float, etc.). Bien entendu il est essentiel que le programmeur C libère ensuite la mémoire ainsi gelée. Si on veut éviter de bloquer entièrement un tableau alors qu’on veut opérer sur une portion de ce tableau , on peut utiliser des fonctions comme :

void GetIntArrayRegion(JNIenv* env, jintArray tableau, jsize debut, jsize taille, jint * buffer) ;
void SetIntArrayRegion(....

Le programmeur a aussi la possibilité de créer des reférences globales sur des objets JAVA. De telles références ne sont pas récupérables par le glaneur à la sortie du code natif, elles peuvent être utilisées par plusieurs fonctions implantant des méthodes natives. Ici aussi il est de la responsabilité du programmeur de libérer ces références globales.

Exceptions

JNI permet de déclencher une exception quelconque ou de recupérer une exception JAVA provoquée par un appel à une fonction JNI. Une exception JAVA non récupérée par le code natif sera retransmise à la machine virtuelle .

.... jthrowable exc;
(*env)->CallVoidMethod(env, instance , methodID) ;
/* quelque chose s’est-il produit? */
exc = (*env)->ExceptionOccurred(env);
if (exc) {
   jclass NouvelleException ;
   ... /* diagnostic */
   /* on fait le ménage */
   (*env)->ExceptionClear(env) ;
   /*etonfaitduneuf!*/
   NouvelleException = (*env)->FindClass(env, "java/lang/IllegalArgumentException") ;
   (*env)->ThrowNew(env,NouvelleException, message);
}
...

Liens vers d’autres langages

Un langage interprété plus simple peut être utile pour écrire, par exemple, du code de déploiement/configuration (mais aussi pour utiliser certaines bibliothèques de codes utilitaires). Il est possible de faire collaborer ces codes avec Java.

Quelques cas:

Code JShell
ici le code java peut faire appel au module jdk.jshell. Voir la documentation du package jdk.jshell
ScriptEngine
Ici on peut appeler un script ECMA (javascript) voir le module jdk.scripting.nashorn. De manière plus générale voir le module java.scripting et les ScriptEngine (mais il faut disposer d’un outil conforme).
Groovy
Groovy est une sur-couche de java: on peut invoquer directement des classes Groovy ou des scripts. Voir GroovyClassLoader, GroovyScriptEngine, et GroovyShell.
Scala, Kotlin
ces deux langages utilisent les pseudo-codes java; les transitions demandent quelques précautions mais ne sont pas trop difficiles: voir les manuels correspondants.
Python
Il existe différents modes opératoires depuis Jython (Python écrit en java) jusqu'à des échanges inter-processus.
Go
Ici on sort complètement des modes assimilés à des interprètes: il faut à la fois créer une librairie binaire shared depuis le compilateur puis utiliser JNA pour le lien avec Java.

Liens vers des formalismes textuels

Les formalismes textuels permettent de décrire des données java d’une manière qui soit à la fois analysable par un programme et lisible par un être humain. On peut utiliser ces formalismes par exemple pour des configurations (lisibilité) ou pour des échanges inter plateformes (portabilité).

Pour une comparaison des formats voir l’article JSON dans le wikipedia en anglais: https://en.wikipedia.org/wiki/JSON

XML
formalisme textuel à "balises". Avantages: très normalisés; inconvénients: lourds et bavards. Voir le module java.xml (note: les codes XMLEncoder/Decoder du package java.beans sont pratiques mais peu utilisés - et malheureusement enfouis dans le module java.desktop - )
JSON
issu de javascript. Plus léger, plus "lisible" (?). Voir le moteur Nashorn
YAML
JSON plus "lisible" (voir également HOCON un format encore plus travaillé de ce point de vue). Voir les bibliothèques sur yaml.org